Saturday 18 March 2017

Wemos ESP8266 based (WiFi) Stick-A-Switch Arduino Compatible




I got this guy off eBay for $4, I'm pretty sure it's a Chinese clone of the actual thing and it _might_ be an older revision but it seems to work...

The board is a Wemos D1, I believe in this boards case you are programming the ESP chip itself and it does not have the usual Atmega processor.  It works with the Arduino IDE however and programs in the same way so  there is no learning curve.

You will need to load the ESP boards into your Arduino IDE, so follow these instructions to do that.

This unit seems to take longer to actually upload sketches to, but it seems to be much more reliable as far as staying on and responding on the WiFi and being less location sensitive than the WiFi Shield I used in my last project.

I believe it has less inputs (for sure on the analog side it only has 1). It should have 11 input pins but apparently they don't match the standard pin numbering. They are also labeled with specific purposes (SPI it looks like) so I'm not sure if you are required to use them for that or not, I put this one into a project so I can't test it, I have another on the way and I'll add some more details in the future.

According to the datasheet:
11 digital input/output pins, all pins have interrupt/pwm/I2C/one-wire supported(except for D0)
1 analog input(3.2V max input)
Power jack, 9-24V power input.

You can find more information on it here:

It seems to have internal pullup/pulldown on some pins, again according to the datasheet.

Anyway enough jibber-jabbering... Let's do something fun with it!

I decided to try and make a switch that could send messages to control something with this...
I decided I wanted to get MQTT working so this will tell you how to make this.

I also today learned about this library called WiFiManager which mimics the usual behavior of IoT devices providing an AP to configure it's WiFi parameters so I have added this to the project as well.

It is possible to add additional settings such as MQTT server to WiFi manager so you can set everything up on the fly via your smartphone, but I haven't done that yet, I have just put the servers IP into the code.

So I took a box (which happened to be from a WiFi Controller for some connected bulbs) that was the perfect size for the switch and somewhat rigid.  I cut a hole in it so the back of the standard light switch would slip in there and bolted it to the box.

I put a resistor into GND and pin D2 on the Wemos board, and the plugged the switch into 5v and the D2 side of the resistor and wrapped it all in electical tape to keep it from shorting out.  I then put some tape on the bottom of the board to make sure it wouldn't short out against the switch and jammed it into the box.   A cover plate for the switch completed the build.

I then loaded the following sketch onto the board, and added a new topic to my X10 controller's MQTT receiver (that currently controls the lights in the bathroom)..

You will need to load the PubsubClient (MQTT Client)
You will also need  WiFi Manager if you plan on using it as I did in this sketch
The rest should already be installed with the board.

============Start Sketch ==============

/*
 *  MQTT Light switch by Guyfromhe
 */

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>

const char* ssid = ".....";   // Not used if using WiFiManager
const char* password = "......"; // Not used if using WiFiManager





const int swpin = 16;  // This is pin D2 on the board... Don't ask me :P
int sw = 0;
int lastsw = 0;
int buttonTime = 0;

//const char* mqtt_server = "broker.mqtt-dashboard.com";    // Public Broker. Can use if you want
const char* mqtt_server = "192.168.0.xxx";  // Mosquito on my Raspberry Pi (apt-get install mosquitto)
WiFiClient espClient;   // Init WiFi
PubSubClient client(espClient);  //Init MQTT client
long lastMsg = 0;
char msg[50];
int value = 0;

void setup_wifi() {

  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  //WiFi.begin(ssid, password);  // Uncomment this if you want to use the credentials at the top
   WiFiManager wifiManager;   // or use WiFi manager for credentials
   wifiManager.autoConnect("AutoConnectAP");

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  randomSeed(micros());

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void callback(char* topic, byte* payload, unsigned int length) {
// This will print out messages from topics we are subbed to
// This is the default code from the PubSub Example, it will control the LED
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  // Switch on the LED if an 1 was received as first character
  if ((char)payload[0] == '1') {
    digitalWrite(BUILTIN_LED, LOW);   // Turn the LED on (Note that LOW is the voltage level
    // but actually the LED is on; this is because
    // it is acive low on the ESP-01)
  } else {
    digitalWrite(BUILTIN_LED, HIGH);  // Turn the LED off by making the voltage HIGH
  }

}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Create a random client ID
    String clientId = "ESP8266Client-";
    clientId += String(random(0xffff), HEX);
    // Attempt to connect
    if (client.connect(clientId.c_str())) {
      Serial.println("connected");
      client.subscribe("inTopic");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void setup() {
  pinMode(swpin, INPUT);
  sw = digitalRead(swpin);
  lastsw = digitalRead(swpin);
  Serial.begin(115200);

  // Bring up MQTT
  pinMode(BUILTIN_LED, OUTPUT);     // Initialize the BUILTIN_LED pin as an output
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);




}

void loop() {
  // MQTT
 if (!client.connected()) {
    reconnect();
  }
  client.loop();



  // Button



  sw = digitalRead(swpin);
  if (sw != lastsw) {
    if ((millis() - buttonTime) > 50)        // Number of mills for debounce counter
      Serial.println(sw);
      lastsw = sw;
      buttonTime = millis();
      if (sw == 0) { client.publish("MySwitchTopic", "Switch Off!"); }
      if (sw == 1) { client.publish("MySwitchTopic", "Switch On!"); }
   
  }

}

============ End Sketch ==============

After running a short USB cable to it (since it needs constant power) I stuck it to the wall with some mounting tape.  Works great and is simple to use.

EDIT: Recently upgraded from X10 to Lutron for control of the actual fixture and now the delay between flipping the switch and the light changing states is gone.  See my post on Lutron for details.

In the future I would like to make it much thinner by modifying the switch and run it on batteries by building an Arduino from scratch and using ultra low power sleep mode along with an nRF24 in sleep mode.

Total cost $4.



Stay tuned for more projects, and cheap hacks.

Wednesday 15 March 2017

Arduino Light Switch -- Making it WiFi (AKA Look maw, no PC)



This entry will detail my bridge step in making a WiFi to RF24 self contained bridge without a Pi or any other computer.  The final step will be to get rid of the RF24 and just have the WiFi module control the servo directly.


So my WiFi Shield (or in this case Shia1d) showed up today in the mail even though I wasn't expecting it for another month or so so I figured I would see what info I could find about it, and it didn't look good...

This $6 eBay special (ESP-12E ESP8266 UART WIFI Wireless Shield for Arduino UNO R3) had all sorts of tales of woe from lack of documentation to firmware issues to pins not connecting to the right place, but one site aggregated all the info and really helped me get this thing going.

These will be labeled as follows:
ESP8266 WiFi Sheild Version 1.0 by WangTongze
elecshop.ml
Designed in Beijjing P.R. China


First off, you need to know there are at least 4 different versions of the board and there are no promises which one you will get.

DIP 1 and 2 are _supposed_ to connect and disconnect the ESP from pins 0/1 (hardware serial) but they often DON'T DO ANYTHING...
DIP 2 and 3 are used to enable firmware flashing mode on the board on all revisions

The version I got is unstable running as a web server.  When placed in the right location I can get 0% packet loss (it is very picky about location) but I often don't get a page back. It does seem to always read my request and execute my code, so my lights always switch correctly but 40% of the time I don't get the web page back from it, which is no big deal for my current application but could be a deal breaker for someone trying to get status or other information from the device.
While I had the serial interface connected I would sometimes see TIMEOUT from the library talking to the ESP usually after a web request so this would probably explain it... You may be able to add some error checking in the sketch if it really bugs you, or just reload a few times and it will eventually work...

According to Claus, the new version (with the maintenance pins not populated, the word shield spelled correctly and a different shape on the antenna on the ESP module) provides a fast and stable web interface so try and get that version if you can.


To start you want to flash the latest firmware on the device, we will use the Uno as a TTL to serial converter. (You can use a TTL to serial cable if you want)

Put a jumper between RST (pin 3 on the power rail) and GND. This will keep the MCU out of the picture.

Connect your shield as follows (not plugged into the top of your Uno yet, but next to it).

Debug Port RX => Uno Pin 0 (Rx)
Debug Port TX => Uno Pin 1 (Tx)
Debug Port 5v => Uno 5v
Debug Port GND => Uno GND

The ESP is meant to use 3.3v logic but the Uno uses 5v.  You can use a level shifter if you want but I have yet to hear about anyone wrecking their device on 5v (me included on 2 different ones so far).  If you do happen to let the smoke though, don't blame me.

Open your serial monitor and set the baud to 115200... When you reset your ESP with the button it should show some garbage (the garbage is actually the boot loader at a different baud rate) and then say ready.

Set your terminal to CR & LF then send AT.  You should get an OK back from the ESP... If you don't make sure you didn't plug the ESP into the top of your Uno, and make sure you wires are secure...

Download this flasher utility from  http://www.xess.com/blog/esp8266-reflash/
And the Firmware from https://github.com/sleemanj/ESP8266_Simple/tree/master/firmware
You want the 1.1.1 Version as of this writing and the 115200 subset.  Make sure the file is about a meg (I got a bunk file somehow and it took me a while to figure out what was wrong)
1,044,480 ai-thinker-v1.1.1-115200.bin

Unplug your Uno and set dip switches 3 and 4 to ON on the shield, this will put your ESP into programming mode, then plug it back it... Make sure your serial monitor is closed.

Open the flash utility:
Click on Bin and locate the bin file you downloaded (don't bother with the included one in the zip)
Change the COM to the com port your Arduino is using (you can check this under Tools -> Port in the IDE)
You can leave the 0x0000000 defaults alone and hit Download...
It will say Erasing and then Writing, this process should take around a minute or so... When it gets to Leaving at the end it WILL return an error you can ignore this, it's normal.
Close the flasher, unplug your Arduino, turn switches 3 and 4 off and then plug er back in.

Open the serial monitor again and check your device boots still and that you can still get the OK.

If it doesn't give you the ready anymore, you can change the baud rate to 78 something and you can see the boot loader messages... Usually this means you flashed bad firmware... Check everything and then repeat the flashing steps, I don't think you can actually brick it... You can always flash the included firmware in the zip to see if you can flash working firmware on the device, but you need to update to the latest version still...

Finally since we have to use software serial we will need to slow the ESP down to 9600 BPS or we will  have communication issues.

In your terminal enter the following command: AT+UART_DEF=9600,8,1,0,0
This should return OK and will be stored in NVRam so you only need to do this once.  Reboot it and change your serial console to 9600 to make sure it took effect.

Phew that's finally over, now we can start the fun part!

Remove all the jumpers from the Uno side, and remove the wires from +5v and GND from the debug port.  We will need the RX and TX still though don't pull them off...

Now stick the shield onto your Uno.. The first pin on the power rail will hit the Reset pin on the Arduino, the first 2 aren't populated. The last pins should all line up properly.  Make sure you don't have any bent or missing pins..

Now plug the Debug RX into the pass through for pin 2 and the Debux TX into the pass through for pin 3.     Since we need the hardware serial for programming the Uno and Serial debug we will need to use software serial.

Note: If you have issues programming your Uno or using the serial console while the shield is plugged in make sure DIP 1 and 2 are off.. If it still won't work, try bending pins 0 and 1 so they don't plug into the Uno, this is a problem on some revisions.

Don't forget to switch your serial console back to 115200 at this point.

Grab the WiFiESP library from here and install the zip file (see my last post if you don't know how to download and install libraries)

Load the example code WiFiESP -> ScanNetworks
You will need to update the pin numbers with the following line: "SoftwareSerial Serial1(6,7)" to SoftwareSerial Serial1(3,2) -- The 3,2 being the only change!
Compile and run that and your ESP should come to life and show a list of networks in range and their power levels in the serial monitor.  If this doesn't work check your connections and make sure your PC is able to supply enough power (at least 350mA).

If all goes well, move on to the webserverled example... You need to put your SSID and password into the sketch before running it, then follow the instruction in the serial monitor to connect to your device with your web browser!

If all is well you should see the status and be given links to toggle LED 13 (you need to peek inside as this LED is covered by the shield).  Click the links and make sure the LED toggles and the status updates in your browser.  It may occasionally not load, just hit refresh... I think this just happens sometimes but it seems pretty rare if you have good power.

Great now for the "bridge" part of this project, we will connect the RF24 module like before and then run a modified version of the webserverled sketch to send out commands...

See my last post for detailed instructions on the RF24 but for quick reference:
Brown Wire -> GND
Red Wire -> 3.3v
Orange Wire -> Pin 7
Yellow Wire -> Pin 8
Blue Wire -> Pin 11
Purple Wire -> Pin 12
Green Wire -> Pin 13

Load up the following sketch substituting your SSID and password where requested...

Most of the code for the RF24 is copied from the previous sketch, see it if you want more comments..

----------------------Start Arduino Sketch----------------------------

/*
 WiFiEsp example: WebServerLed
 MODIFIED BY GUYFROMHE TO BE A CLIENT FOR RF24 LIGHT SWITCH
 A simple web server that lets you turn on and of an LED via a web page.
 This sketch will print the IP address of your ESP8266 module (once connected)
 to the Serial monitor. From there, you can open that address in a web browser
 to turn on and off the LED on pin 13.

 For more details see: http://yaab-arduino.blogspot.com/p/wifiesp.html
*/

// RF24 to WiFi bridge

#include "WiFiEsp.h"

#include <SPI.h>
#include "RF24.h"

// Setup RF24
RF24 radio(7,8); // Stand Alone

bool radioNumber = 1; // Stand Alone
bool role = 1; // Stand alone
unsigned long cmd=255;                             // Do not send anything by default

byte addresses[][6] = {"1Node","2Node"};  // You can change the node names here if you'd like, I left them alone

// Emulate Serial1 on pins 3/2 if not present
#ifndef HAVE_HWSERIAL1
#include "SoftwareSerial.h"
SoftwareSerial Serial1(3, 2); // RX, TX
#endif

char ssid[] = "YOURSSID";            // your network SSID (name)
char pass[] = "YOUR-WIFI-PASSWORD";        // your network password
int status = WL_IDLE_STATUS;

//int ledStatus = LOW;

WiFiEspServer server(80);

// use a ring buffer to increase speed and reduce memory allocation
RingBuffer buf(8);

void setup()
{
  // Setup RF24
  radio.begin();
  // It's now really close to my device and I want to conserve power. If you need more range change to MAX
  radio.setPALevel(RF24_PA_MIN);
  radio.openWritingPipe(addresses[1]);
  radio.openReadingPipe(1,addresses[0]);

  radio.startListening();

  // WiFi Stuff
  //pinMode(LED_BUILTIN, OUTPUT); // initialize digital pin LED_BUILTIN as an output.
  Serial.begin(115200);   // initialize serial for debugging
  Serial1.begin(9600);    // initialize serial for ESP module
  WiFi.init(&Serial1);    // initialize ESP module

  // check for the presence of the shield
  if (WiFi.status() == WL_NO_SHIELD) {
    Serial.println("WiFi shield not present");
    // don't continue
    while (true);
  }

  // attempt to connect to WiFi network
  while (status != WL_CONNECTED) {
    Serial.print("Attempting to connect to WPA SSID: ");
    Serial.println(ssid);
    // Connect to WPA/WPA2 network
    status = WiFi.begin(ssid, pass);
  }

  Serial.println("You're connected to the network");
  printWifiStatus();

  // start the web server on port 80
  server.begin();
}


void loop()
{
  WiFiEspClient client = server.available();  // listen for incoming clients

  if (client) {                               // if you get a client,
    Serial.println("New client");             // print a message out the serial port
    buf.init();                               // initialize the circular buffer
    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
        buf.push(c);                          // push it to the ring buffer

        // printing the stream to the serial monitor will slow down
        // the receiving of data from the ESP filling the serial buffer
        //Serial.write(c);
     
        // you got two newline characters in a row
        // that's the end of the HTTP request, so send a response
        if (buf.endsWith("\r\n\r\n")) {
          sendHttpResponse(client);
          break;
        }

        // Check to see if the client request was "GET /H" or "GET /L":
        if (buf.endsWith("GET /1")) {
          Serial.println("Turn light ON");
          //ledStatus = HIGH;
       
          // Send RF24 command
          radio.stopListening();                                    // First, stop listening so we can talk.
          cmd = 1;
          Serial.println("Trying to send to radio");
          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }
          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }

          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }

          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }

          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }
       
       
          Serial.println("Thats it...");
          radio.startListening();                                    // Listen again

       
          //digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
        }
        else if (buf.endsWith("GET /0")) {
          Serial.println("Turn light OFF");
       
          // Send RF24 command
          radio.stopListening();                                    // First, stop listening so we can talk.
          cmd = 0;
          Serial.println("Trying to send to radio");
          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }
          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }

          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }

          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }

          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }
       
       
          Serial.println("Thats it...");
          radio.startListening();                                    // Listen again

          //ledStatus = LOW;
          //digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
        }
        else if (buf.endsWith("GET /T")) {
          Serial.println("Toggle Light");
       
          // Send RF24 command
          radio.stopListening();                                    // First, stop listening so we can talk.
          cmd = 2;
          Serial.println("Trying to send to radio");
          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }
          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }

          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }

          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }

          if (!radio.write( &cmd, sizeof(unsigned long) )){
              Serial.println(F("failed"));
          }
       
       
          Serial.println("Thats it...");
          radio.startListening();                                    // Listen again

          //ledStatus = LOW;
          //digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
        }
      }
    }
 
    // close the connection
    client.stop();
    Serial.println("Client disconnected");
  }
}


void sendHttpResponse(WiFiEspClient client)
{
  // 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();

  // the content of the HTTP response follows the header:
  client.print("<b>Office Lights Control</b>");
  //client.print("The LED is ");
  //client.print(ledStatus);
  client.println("<br>");
  client.println("<br>");

  client.println("Click <a href=\"/1\">here</a> turn the lights on<br>");
  client.println("Click <a href=\"/0\">here</a> turn the lights off<br>");
  client.println("Click <a href=\"/T\">here</a> toggle the lights<br>");

  // The HTTP response ends with another blank line:
  client.println();
}

void printWifiStatus()
{
  // print the SSID of the network you're attached to
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  // print your WiFi shield's IP address
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  // print where to go in the browser
  Serial.println();
  Serial.print("To see this page in action, open a browser to http://");
  Serial.println(ip);
  Serial.println();
}
------------------------------ END SKETCH --------------------------------

Upload the sketch and open the serial monitor... If your Uno starts disconnecting from USB you aren't providing enough power to keep it running.. You can probaly move it to a plug or USB charger now though assuming it's working and you have written down it's IP.

You can now send commands by browsing to your devices IP and clicking the links or accessing /1 /2 or /T URLs.   This should in turn control your servo half of the project without any changes.

You should get the following page in your browser:

The strings are obvious in the sketch, feel free to make them your own!!

Though the browser works fine, it will make spurious requests (like favicon) and send a ton of extra data which can slow down and overwhelm your little ESP so it's not ideal to access it this way all the time.. For best results use the script below...

Now all that's left is a super simple Python script to control the lights from any machine on your network for easy integration into your automation system.  


---------------------Start remote.py ----------------------------------
import urllib2, sys

ip = "192.168.0.xxx"     # This is the IP of your ESP

cmd = sys.argv[1]

out = ""
if cmd == "1": out = "1"
if cmd == "0": out = "0"
if cmd.lower() == "t": out = "T"


try:
x = urllib2.urlopen('http://%s/%s' % (ip,out), timeout=1).read()
print x
except:
try:
print "Retrying..."
x = urllib2.urlopen('http://%s/%s' % (ip,out), timeout=1).read()
print x
except:
pass

-----------------------End-----------------------------------------

Then just run python remote.py 0 for off, python remote.py 1 for on and python remote.py t for toggle.

Let me know if you have any questions or found this information helpful in the comments.

Thursday 9 March 2017

$16 Wireless Lightswitch Mark II


                                        BEFORE                                                    AFTER 

Update 2: I made a WiFi to RF24 bridge and can now directly access this without it being plugged into a Pi.  This new article is in addition to this one and explains how to modify this build to run on WiFi directly instead of serial.

Update: Due to all the complaining about the sheer ugliness of this build, I cleaned up the extra tape and glue, removed the black tape from the wall, replaced the servo screw with a properly sized one and made a little paper box that matches the wall plate (if I had some white paint I'd paint it white but I don't so I won't) I hope your happy :P  I realize it's still not pretty but it's much nicer looking than it was.

I have a first world problem.... My light switch is too far away from my desk and sometimes I want the lights off to watch a movie but then I need them on again to work on a project or even to make it through the mindfield of bits and bobs on my floor to the way to the light switch...  Sure I could stop being lazy, or heaven forbid clean up the crap on the floor but that isn't the hacker way!

First I though I'll just get a Fing-longer (TM) but Amazon doesn't ship them to Canada, those bastards!

Image result for fing longer

So I ordered a couple Arduino's to play with and a servo and built my first serial controlled light switch, but that required a laptop (or a trip-wire across my already cluttered floor to my PC) and that was super overkill to run a light switch, so I searched for the right solution..

I tried an ESP8266 but alas I didn't have the right power source or much else beyond a pack a resistors (I am just starting out hardware hacking, I don't have a ton of parts yet) and though this worked it would only get on the network about 20% of the time, this was much worse than the laptop, alas...

I didn't have enough time/interest at this point to do anymore with this and just left it running on the laptop....

Then I read about someone using the NRF24 chips on Hack A Day in a project and figured I would give them a shot as it seemed much simpler and lower power than a ESP, and this seemed to do the trick! (Though I do intend for the final thing to be on WiFi and connect to my existing MQTT network.. I have parts coming)

The idea is to very simply be able to send a single character from an automation system to either turn my lights on, off or toggle them and possibly have a switch close to my desk, a-la 3-way switch but without ripping up walls, dealing with high voltages or running wires across my office.

Today I will describe how you can make a remotely controlled (via RF) light switch (that can be controlled via anything that can talk serial) using 2x Arduino Uno clones, 2x NRF24L01+ modules, 1x Servo motor and an optional switch (which requires a resistor as well).

Build of Materials:
2 x Arduino Uno clones $7.10
2 x nRF24L01+ modules $1.66
1 x 9G SG90 Servo $1.88
1 x 100k Ohm Resistor (brown-black-yellow-gold) $0.01
1 x Decora light switch $3.95
2 x USB A to B cables $1.48 (Included with the above Arudino's)
========================
Total Cost: $16.08 though this actually cost me much less since I had a lot of the parts lying around from other projects and you probably do too!

You will also need some Dupont hookup wire or some regular wire and a soldering iron, and a hot glue gun. You'll also need a PC to program it with and if you want a Raspberry Pi to control it, or you can even use your PC if you prefer. The point is you don't need anything fancy or expensive to do this project.

This build isn't super pretty or anything but it can be mounted on any Decora style switch, and it mounts to the plate so you can even do this if you don't own the property.  You can get a switch plate for 50 cents at Home Depot.  It does not require modifying any mains wiring, it's non permanent and doesn't interfere with regular operation of the switch!

Note: If you have an old style toggle switch you can very easily change it out for a Decora style switch, make sure to turn off the breaker if you decide to do this, this is not part of my project!

 There are also other projects that adapt the servo to a toggle style switch, here is one on Hack A Day but you need to 3D print a special wall plate and arm for it... You can still follow my instructions to get the wireless and local switch part through while using his servo mount.




So I've got some parts, made a revision to my switch...
I got 10 x NRF24L01+ 2.4 GHz transceivers on eBay for about 6 bucks...
These are basically over the air serial devices, they run in 3.3v and are 5v logic tolerant and use MUCH less current than an ESP WiFi board... (15 mA I believe).  The majority of stability issues are resolved by putting a cap between VCC and GND if you have any.

These can take up to a few months to get to you from China so make sure you order them well in advance of your project... Fortunately you get a lot of them in a package..

So I have 2 knockoff Arduino Uno clones and my sketches are based on the simplest example code with the RF24 module...

I spent about a day trying to get the Raspberry Pi to talk with the NRF module directly with SPI but I was having major software issues and decided just to plug a spare $4 Arduino into the Pi instead...

Wiring the devices is super simple...
Image result for nrf24l01+ pinoutNRF: (GND has a square around it)
~~~~~~~~~~~~~~~~~~|
           ~~~~~~~~~~~~~~~|

[VCC]  [CSN] [MOSI] [UNUSED]
[GND] [CE]    [SCK]    [MISO]

VCC -> 3.3v  Arduino (Red wire)
GND -> GND Arduino (Brown or Black wire)
CE -> Pin 7 Arduino (Orange wire) [Chip Enable]
CSN -> Pin 8 Arduino (Yellow wire)  [Slave Select]
MOSI -> Pin 11 Arduino (Blue wire) [Master Output, Slave Input]
MISO -> Pin 12 Arduino (Purple wire) [Master Input, Slave Output]
SCK -> Pin 13 Arduino (Green wire) [Serial Clock]

The nRF24 talks SPI serial.

Here is a picture I found on Google (slightly modified) for those who like a visual aide:




I also have a switch which allows me to control the light locally, this is using a pull-down resistor and a regular light switch....(This allows me to control the light switch by the door from my chair without interacting with a computer, I've also got a hacked Dash button that will do the same)

You can choose in the code if you want it to toggle every time you flip it, or have one position be on and one position be off, or if you are using a momentary button you can set it up for that so it won't switch twice or you can take the switch out completely if you don't need it.
There are comments on the lines you need to change for this purpose.

The resistor is connected between GND and pin 6, and the switch is connected to 5v on the Arduino and the leg of the resistor that is connected to pin 6 (not the GND side).

Finally your light switch 'Duino has a servo connected to 5v and GND on the power wires and signal conneced to pin 9.  The servo is then hot glued to the switch plate with the horn perpendicular to the light switch at idle. (with the shaft of the servo lined up about with the center of the switch).



The following sketch which isn't perfect (I've got some kind of issue with my character buffers and I commented out the response for now) but for the simple task of sending on, off and toggle it works fine...

The sketch works for both the server and the client device, you just need to change 2 lines near the top to tell it which side it is.

The side that connects to the Pi only needs the 7 wires connected to the NRF module and a USB cable.

I will probably try and clean this up another time, but for anyone interested here's the work in progress:

It would stop responding to the wireless after a few messages and I don't know why yet, it always worked after a reset so I added code to reset the program in software and to keep the state in an uninitialized area of ram so it would survive a reboot (but not a power loss).  This has been working perfectly for 2 days now.

For those of you new to Arduino like I was still am, I'll save you the trouble of Googling how to install the RF24 library you will need to install for this sketch...

First download the zip from here: https://github.com/maniacbug/RF24 (You click the green Clone or Download and choose download as Zip)

Then in Arduino click on Sketch -> Include Library -> Add Zip Library then locate the Zip file from above... That's it, it's that simple!


----ARDUINO SKETCH------


/*
* Getting Started example sketch for nRF24L01+ radios
* MODIFIED BY GUYFROMHE FOR LIGHT SWITCH CONTROL
* This is a very basic example of how to send data from one node to another
* Updated: Dec 2014 by TMRh20
*/

// THIS PROGRAM IS CURRENTLY SETUP FOR THE SERVO END!!!

#include <SPI.h>
#include "RF24.h"
#include <Servo.h>

void(* resetFunc2) (void) = 0;//declare reset function at address 0  // Calling this will reset the Arduino without needing any wires

void resetFunc() {
  // This will make us wait a second before restarting so that only 1 command is processed even though it's sent a few times
  //Serial.println("Restart Requested...");
  delay(1000);  // Wait 1 second
  //Serial.println("REBOOT!");
  resetFunc2();
}

Servo myservo;  // create servo object to control a servo
int sw = 6;  // Switch pin number
int servopin == 9; // Servo control pin
int pos = 0;    // variable to store the switch position
int val; // value to store current switch state in
int lastval = 0; // Last value from switch
int scmd; // Special command -- Used to tell the program the switch was flipped
unsigned long buttonTime = 0; // Used for debouncing


//non-initialized value
// This structure will never be initialized by the compiler so even when the device is reset this value will remain
// On first power up there will be random garbage in here and that is deal with in setup.
union configUnion{
  uint8_t    byte[1]; // match the below struct...
  struct {
    uint16_t value1;
  } val ;
} config  __attribute__ ((section (".noinit")));


/****************** User Config ***************************/
/***      Set this radio as radio number 0 or 1         ***/

// THIS IS WHERE YOU SET IF IT'S THE SERVO OR THE CLIENT SIDE

bool radioNumber = 0; // Servo Board
//bool radioNumber = 1; // Stand Alone / Raspi

/* Hardware configuration: Set up nRF24L01 radio on SPI bus plus pins 7 & 8 */
RF24 radio(7,8); // Stand Alone
/**********************************************************/

byte addresses[][6] = {"1Node","2Node"};  // You can change the node names here if you'd like, I left them alone

// Used to control whether this node is sending or receiving
bool role = 0; // Servo board
//bool role = 1; // Stand alone



unsigned long cmd=255;                             // Do not send anything by default

void setup() {
  pinMode(sw, INPUT);    // declare switch as input
  // Initialize the switch in it's current position, we don't want to toggle it on bootup!!
  val = digitalRead(sw);
  lastval = val;


  // See if our presistant var has been initialized already
  int un = 1;   // Assume it's not...

  // If it has a valid value in it, assume it is (It's highly unlikely it will ever boot with just a 0 or 1 in that location)
  if (config.val.value1 == 0) { un = 0; }
  if (config.val.value1 == 1) { un = 0; }

  if (un == 1) { config.val.value1 = 0; } // Initialize the presistant state (this is probably first boot)
  if (un == 0) { pos = config.val.value1; } // It's already good, update position to match the stored value


  myservo.attach(servopin);  // attaches the servo on pin 9 to the servo object


  Serial.begin(115200);   // Speed for serial communications
  Serial.println("RF24-Point-to-Point Light Switch....");   // Startup/welcome message
  //Serial.println(config.val.value1);  // Some debugging stuff
  //Serial.println(lastval); // Some debugging stuff
 
  radio.begin();

  // Set the PA Level low to prevent power supply related issues since this is a
 // getting_started sketch, and the likelihood of close proximity of the devices. RF24_PA_MAX is default.
 // Living dangerously! Power to the MAX!
  radio.setPALevel(RF24_PA_MAX);

  // Open a writing and reading pipe on each radio, with opposite addresses
  if(radioNumber){
    radio.openWritingPipe(addresses[1]);
    radio.openReadingPipe(1,addresses[0]);
  }else{
    radio.openWritingPipe(addresses[0]);
    radio.openReadingPipe(1,addresses[1]);
  }

  // Start the radio listening for data
  radio.startListening();
}

void loop() {
 
 
    val = digitalRead(sw); // Check that state of the switch  
    if (val != lastval) // Only run this code if the state of the switch changed
    {
      if ((millis() - buttonTime) > 50)        // Number of mills for debounce counter
        {    

          // scmd: 1 - Toggle, 2 - Off, 3 - On
          // For a Forced on/off light switch Pressed scmd = 3, Released scmd = 2
          // For a Toggle light switch Pressed scmd = 1, Released scmd = 1
          // For a momentary pushbutton Pressed scmd = 1, remove scmd from Released

          // Setup for a standard light switch to toggle.
          if (val == 1) { Serial.println("Switch Pressed!");  scmd = 1; }
          if (val == 0) { Serial.println("Switch Relased!");  scmd = 1;} // Don't put scmd here for a momentary switch
          lastval = val;  // Store the last changed value
          buttonTime = millis();    // Store the last time the button was pressed so we can debounce it
        }
    }

/****************** Ping Out Role ***************************/
// Transmitter Part....
if (role == 1)  {
 
    if (cmd != 255) // We only want to sent if it's not 255...
    {
    radio.stopListening();                                    // First, stop listening so we can talk.
 
 
    Serial.println(F("Now sending..."));

    unsigned long start_time = micros();                             // Take the time, and send it.  This will block until complete
    //unsigned long cmd = 7;                             // Command to send
     //if (!radio.write( &start_time, sizeof(unsigned long) )){

     if (!radio.write( &cmd, sizeof(unsigned long) )){
       Serial.println(F("failed"));
     }
     
    radio.startListening();                                    // Now, continue listening
 
    unsigned long started_waiting_at = micros();               // Set up a timeout period, get the current microseconds
    boolean timeout = false;                                   // Set up a variable to indicate if a response was received or not
 
    while ( ! radio.available() ){                             // While nothing is received
      if (micros() - started_waiting_at > 200000 ){            // If waited longer than 200ms, indicate timeout and exit while loop
          timeout = true;
          break;
      }    
    }
     
    if ( timeout ){                                             // Describe the results
        Serial.println(F("Failed, response timed out."));
    }else{
        unsigned long got_time;                                 // Grab the response, compare, and send to debugging spew
        char resp[10];
        //radio.read( &got_time, sizeof(unsigned long) );
        radio.read( &resp, 10 );
        unsigned long end_time = micros();
     
        // Spew it
        //Serial.print(F("Sent "));
        //Serial.print(start_time);
        Serial.print(F("Response: "));
        //Serial.print(got_time);
        Serial.println(resp);
        //Serial.print(F(", Round-trip delay "));
        //Serial.print(end_time-start_time);
        //Serial.println(F(" microseconds"));
    }

    // Try again 1s later  
    //delay(1000);
   //This code sends the message over and over again until reset, turns out this is good for packet loss
  //(the resending for a bit) but I think adding a cmd=255 here would stop it from doing so... I'm not
//concerned enough at the moment to grab the unit from the basement and test though!
    }
  }



/****************** Pong Back Role ***************************/
// Receiver part -- Servo module
// NOTE: When I had some loose or broken wires on my nRF24 I was getting random 0's out of it
// If it starts turning your lights off randomly, check your wiring
// It also may be advisable to replace the 0 radio command with something else
// My transmitter is already installed and I don't care to get it to modify the code
// 2 days so far and I haven't had a problem since replacing and re-soldering the wires


  if ( role == 0 )
  {

    if (scmd == 3) // Local switch ON
    {
      scmd = 0;
      Serial.println("SWITCH ON");
      myservo.write(140);    // ON    
      delay(200);
      Serial.println("Lights On!");
      pos = 1;
      config.val.value1 = 1; // Set the persistant value
      myservo.write(90);    // Neutral    
      resetFunc2(); // Reset the thing instantly

    }
 
      if (scmd == 2) // Local switch OFF
    {
      scmd = 0;
      Serial.println("SWITCH OFF");
      myservo.write(50);    // ON    
      delay(200);
      Serial.println("Lights Off!");
      pos = 0;
      config.val.value1 = 0; // Set the persistant value
      myservo.write(90);    // Neutral    
      resetFunc2(); // Reset the thing instantly

    }
 
 
    if (scmd == 1 ) // For local commands from switch - Will toggle.
    {
      scmd = 0;
      Serial.println("Toggling Lights");
      if (pos == 0) {                
            myservo.write(140);    // ON    
            delay(200);
            Serial.println("Lights On!");
            pos = 1;
            config.val.value1 = 1; // Set the persistant value
     } else {
          myservo.write(50);    // OFF    
            delay(200);
            Serial.println("Lights Off!");
            pos = 0;
            config.val.value1 = 0; // Set the persistant value
        }        

         
          myservo.write(90);    // Neutral    
          resetFunc2(); // Reset the thing instantly

    }
 
 
    unsigned long got_time;
     unsigned long rec;
 
 
    if( radio.available()){  
    // Trouble happens down here...
    // Let's try not responding at all - This didn't fix anything but this isn't too imporant so it's still commented out...
      while (radio.available()) {                                   // While there is data ready
        //radio.read( &got_time, sizeof(unsigned long) );             // Get the payload
        radio.read( &rec, sizeof(unsigned long) );             // Get the payload
     
        Serial.println(rec);

        if (rec == 1) {
          pos = 1;
          config.val.value1 = 1; // Set the persistant value
          myservo.write(140);    // ON    
          delay(200);
          myservo.write(90);    // Neutral    
          resetFunc(); // Reset the thing
          //radio.stopListening();                                        // First, stop listening so we can talk
          //char resp[] = "Lights On";
          //radio.write( &resp, 9 );              // Send the final one back.            
          //radio.startListening();                                       // Now, resume listening so we catch the next packets.  
        }

 
        if (rec == 0) {
          pos = 0;
          config.val.value1 = 0; // Set the persistant value
          myservo.write(50);    // OFF    
          delay(200);
          myservo.write(90);    // Neutral    
          resetFunc(); // Reset the thing
          //radio.stopListening();                                        // First, stop listening so we can talk
          //char resp[] = "Lights Off";
          //radio.write( &resp, 10 );              // Send the final one back.            
          //radio.startListening();                                       // Now, resume listening so we catch the next packets.  

        }
     

    if (rec == 2) // Radio toggle
    {    
      Serial.println("Toggling Lights");
      if (pos == 0) {                
            myservo.write(140);    // ON    
            delay(200);
            Serial.println("Lights On!");
            pos = 1;
            config.val.value1 = 1; // Set the persistant value
     } else {
          myservo.write(50);    // OFF    
            delay(200);
            Serial.println("Lights Off!");
            pos = 0;
            config.val.value1 = 0; // Set the persistant value
        }        

       
          myservo.write(90);    // Neutral    
          resetFunc(); // Reset the thing

    }
 

       




     
      }
   
      //radio.stopListening();                                        // First, stop listening so we can talk
      //radio.write( &got_time, sizeof(unsigned long) );              // Send the final one back.    
      //radio.startListening();                                       // Now, resume listening so we catch the next packets.  
      //Serial.print(F("Sent response "));
      //Serial.println(got_time);
   }
 }




/****************** Change Roles via Serial Commands ***************************/

  if ( Serial.available() )
  {
    //char c = toupper(Serial.read());
    // Read data from the serial (for the client / Raspi side)
    // To talk to this just open the serial port, send 0 for off, 1 for on or t for toggle then close the port
 
    unsigned long c  = toupper(Serial.read());


    int match  = 0;
 
    //Serial.println(c);
    if (c == 48) { cmd = 0; match = 1; Serial.println("Lights Off Request..."); }
    if (c == 49) { cmd = 1; match = 1;  Serial.println("Lights On Request..."); }
    if (c == 84) { cmd = 2; match = 1;  Serial.println("Lights Toggle Request..."); }

    if (match == 0) { cmd = 255; } // Switc the command back to -don't send anything mode-
 
//    if ( c == 'T' && role == 0 ){    
  //    Serial.println(F("*** CHANGING TO TRANSMIT ROLE -- PRESS 'R' TO SWITCH BACK"));
    //  role = 1;                  // Become the primary transmitter (ping out)
 
   //}else
    //if ( c == 'R' && role == 1 ){
      //Serial.println(F("*** CHANGING TO RECEIVE ROLE -- PRESS 'T' TO SWITCH BACK"));    
       //role = 0;                // Become the primary receiver (pong back)
       //radio.startListening();
     
//    }
  }


} // Loop



----END SKETCH------




Finally this simple python script to control the whole thing:

Requires the pyserial library be installed to work.

----PYTHON CODE------
import serial, time, sys
ser = serial.Serial('/dev/ttyUSB0', 115200)  # op

#print ser
time.sleep(1) # wait for duino to wake up
x = ser.read(36) # Read the header
try:
        cmd = sys.argv[1]
except:
        print "Remote Controlled Lightswitch"
        print
        print "Command Line Arguments:"
        print "0\tOff"
        print "1\tOn"
        print "t\tToggle"
        sys.exit(0)

cmd = cmd.lower()
if cmd == "0": print "Turning Lights Off"
if cmd == "1": print "Turning Lights On"
if cmd == "t": print "Toggling Lights"

ser.write(sys.argv[1])
time.sleep(1)
# It seems to keep sending until reset, so reset it...
# Send the command a few times, the Servo will sleep for a second after the first command so it will only take this once but if there's any packet loss OTA one of the re-sends will hit...

ser.close()
ser = serial.Serial('/dev/ttyUSB0', 115200)  # op
ser.close()

sys.exit(0)
----END CODE------

This is all very rough, it was built in a few hours with example code...  This is probably a temporary setup since I will be getting some ESP based WiFi boards in the next month or so and that's where this project will probably end up (and I will do another write up on setting that up then)

I would like to work more with these NRF modules to make a low power stick up toggle switch that will cost under $15 and run on batteries for a long time.

Add on wireless switches that could through a gateway talk to an MQTT server are a pretty big need when retrofitting a home with automation gear when you can't run wires... I have yet to find a really good solution that's easy to use (doesn't have like 6-10 buttons on a panel), works reliably and quickly, doesn't eat battieres and doesn't cost $30+ per switch...

If you know of a product like this on the market already do please let me know.

If you decide to try this yourself please post a comment or let me know how it works and if you have any questions just ask!

Video of it in action: