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:



4 comments:

  1. Great job, and clever non-invasive solution.

    Also, I dig the Meow Mix remote, classy. ;)

    ReplyDelete
    Replies
    1. Thanks, the best part is that it works reliably and has multiple sources of control. I do love me some meow mix :P

      Delete