How to Build a Remote Kill Switch

Pages
Contributors: Nate
Favorited Favorite 18

Code

Note: This example assumes you are using the latest version of the Arduino IDE on your desktop. If this is your first time using Arduino, please review our tutorial on installing the Arduino IDE.

If you have not previously installed an Arduino library, please check out our installation guide.

You can find the latest code for the RCU and VCU in this github repo.

language:c
/*
 Remote Kill Switch - On the Car
 By: Nathan Seidle
 SparkFun Electronics
 Date: March 23rd, 2016
 License: This code is public domain but you buy me a beer if you use this and we meet someday (Beerware license).

 This is the part of the remote kill switch that lives on the car.

 If we receive 'R' (kill) system turns off the relay.
 If we receive 'Y' (pause) from the remote then set pin PAUSE high to signal master
 computer to pause.
 If we receive 'G' (go) system turns on relay, sets PAUSE pin to low.
 If we don't receive a system status from the remote every MAX_TIME_WITHOUT_OK ms, system goes into safety shutdown (relay off).

 Locomotion controller requires a 5V FTDI. The kill switch requires a 3.3V FTDI.
*/

#include <SPI.h>
#include <RH_RF69.h> //From http://www.airspayce.com/mikem/arduino/RadioHead/
#include <SimpleTimer.h> //https://github.com/jfturcot/SimpleTimer
#include <avr/wdt.h> //We need watch dog for this program

//If we don't get ok after this number of milliseconds then go into safety-shutdown
//This must be longer than MAX_DELIVERY_FAILURES * CHECKIN_PERIOD
//250ms is good
//L defines the value as a long. Needed for millisecond times larger than int (+32,767) but doesn't hurt to have.
#define MAX_TIME_WITHOUT_OK 250L

unsigned long lastCheckin = 0;

RH_RF69 rf69;

#define RELAY_CONTROL 9
#define PAUSE_PIN 6

#define LED_RED 5
#define LED_YLW 4
#define LED_GRN 3

//Define the various system states
#define RED 'R'
#define YELLOW 'Y'
#define GREEN 'G'
#define DISCONNECTED 'D'

char systemState;

void setup()
{
  wdt_reset(); //Pet the dog
  wdt_disable(); //We don't want the watchdog during init

  Serial.begin(9600);

  pinMode(RELAY_CONTROL, OUTPUT);
  turnOffRelay(); //During power up turn off power

  pinMode(LED_RED, OUTPUT);
  pinMode(LED_YLW, OUTPUT);
  pinMode(LED_GRN, OUTPUT);

  pinMode(PAUSE_PIN, OUTPUT);
  digitalWrite(PAUSE_PIN, LOW); //Resume

  if (!rf69.init())
    Serial.println("init failed");
  // Defaults after init are 434.0MHz, modulation GFSK_Rb250Fd250, +13dbM
  if (!rf69.setFrequency(915.0))
    Serial.println("setFrequency failed");

  // If you are using a high power RF69, you *must* set a Tx power in the range 14 to 20 like this:
  rf69.setTxPower(20);

  //This key is the same on the remote. Pick your own random sequence.
  uint8_t key[] = { 0xAB, 0x1C, 0x0E, 0x39, 0xF8, 0xFF, 0xA6, 0xFC,
                    0x7B, 0x44, 0xC3, 0xC0, 0x2D, 0x2D, 0x2D, 0xD2
                  };
  rf69.setEncryptionKey(key);

  systemState = RED; //On power up start in red state
  setLED(LED_RED);

  Serial.println("Power Wheels Kill Switch Online");

  wdt_reset(); //Pet the dog
//  wdt_enable(WDTO_1S); //Unleash the beast
}

void loop()
{
  if (millis() - lastCheckin > MAX_TIME_WITHOUT_OK)
  {
    if (systemState != DISCONNECTED)
    {
      setLED(LED_RED); //Turn on LED
      turnOffRelay();
      systemState = DISCONNECTED;

      Serial.println("Remote failed to check in! Turn off relay!");
    }
  }

  if (rf69.available())
  {
    uint8_t buf[RH_RF69_MAX_MESSAGE_LEN];
    uint8_t len = sizeof(buf);

    if (rf69.recv(buf, &len))
    {
      sendResponse(); //Respond back to the remote that we heard it loud and clear

      Serial.print("Received: ");
      Serial.println((char*)buf);

      if (buf[0] == RED || buf[0] == YELLOW || buf[0] == GREEN || buf[0] == DISCONNECTED) lastCheckin = millis(); //Reset timeout

      if (buf[0] == RED)
      {
        if (systemState != RED)
        {
          setLED(LED_RED); //Turn on LED
          turnOffRelay();
          systemState = RED;

          Serial.println("Kill!");
        }
      }
      else if (buf[0] == YELLOW)
      {
        if (systemState != YELLOW)
        {
          setLED(LED_YLW); //Turn on LED
          digitalWrite(PAUSE_PIN, HIGH);
          systemState = YELLOW;

          Serial.println("Pause!");
        }
      }
      else if (buf[0] == GREEN)
      {
        if (systemState != GREEN)
        {
          digitalWrite(PAUSE_PIN, LOW); //Turn off pause
          setLED(LED_GRN); //Turn on LED
          turnOnRelay();
          systemState = GREEN;

          Serial.println("Go!");
        }
      }
      else if (buf[0] == DISCONNECTED)
      {
        //If we've received a 'D' from the remote it means
        //it is trying to get back in touch
        setLED(LED_RED); //Turn on LED
        turnOffRelay();
        systemState = DISCONNECTED; //Remote will move the state from disconnected

        Serial.println("Reconnecting!");
      }

      Serial.print("RSSI: ");
      Serial.println(rf69.lastRssi(), DEC);
    }
  }
}

//If we receive a system state we send a response
void sendResponse()
{
  uint8_t response[] = "O"; //Send OK
  rf69.send(response, sizeof(response));
  rf69.waitPacketSent(50); //Block for 50ms before moving on

  Serial.println("Sent a reply");
}

//Turns on a given LED
void setLED(byte LEDnumber)
{
  digitalWrite(LED_RED, LOW);
  digitalWrite(LED_YLW, LOW);
  digitalWrite(LED_GRN, LOW);

  digitalWrite(LEDnumber, HIGH);
}

//Turn on the relay
void turnOnRelay()
{
  digitalWrite(RELAY_CONTROL, HIGH);
}

//Turn off the relay
void turnOffRelay()
{
  digitalWrite(RELAY_CONTROL, LOW);
}

Above is the code for the VCU

language:c
/*
 Remote Kill Switch - Hand Held Controller
 By: Nathan Seidle
 SparkFun Electronics
 Date: March 23rd, 2016
 License: This code is public domain but you buy me a beer if you use this and we meet someday (Beerware license).

 This is the part of the remote kill switch that the user holds in their hand.

 If red button pressed we send 'R' (kill). Turn on red LED.
 If yellow button pressed we send 'Y' (pause). Turn on yellow LED.
 If green button pressed we send 'G' (go). Turn on green LED.
 Send the system state every CHECKIN_PERIOD ms. Car turns off if nothing is received after MAX_TIME_WITHOUT_OK ms.

 Indicator LEDs: Green/Yellow/Red

 Measured current: 42mA roughly

*/

#include <SPI.h>
#include <RH_RF69.h> //From: http://www.airspayce.com/mikem/arduino/RadioHead/s
#include <SimpleTimer.h> //https://github.com/jfturcot/SimpleTimer
#include <avr/wdt.h> //We need watch dog for this program

RH_RF69 rf69;

SimpleTimer timer;
long secondTimerID;

#define BUTTON_RED 9
#define BUTTON_GND 8
#define BUTTON_GRN 7
#define BUTTON_YLW 6

#define LED_RED 5
#define LED_YLW 4
#define LED_GRN 3

//Define the various system states
#define RED 'R'
#define YELLOW 'Y'
#define GREEN 'G'
#define DISCONNECTED 'D'

//Number of milliseconds between broadcasting our system state to the vehicle
//L defines the value as a long. Needed for millisecond times larger than int (+32,767) but doesn't hurt to have.
//25ms is good.
#define CHECKIN_PERIOD 25L

//Number of milliseconds to block for sending packets and waiting for the radio to receive repsonse packets
//This should be not be longer than the CHECKIN_PERIOD
//10ms is good. 
#define BLOCKING_WAIT_TIME 10L

//How many failed responses should be allowed from car until we go into disconnect mode
#define MAX_DELIVERY_FAILURES 3
byte failCount = 0;

char systemState;

unsigned long lastBlink = 0;
#define BLINK_RATE 500 //Amount of milliseconds for LEDs to toggle when disconnected

void setup()
{
  wdt_reset(); //Pet the dog
  wdt_disable(); //We don't want the watchdog during init

  Serial.begin(9600);

  pinMode(BUTTON_RED, INPUT_PULLUP);
  pinMode(BUTTON_YLW, INPUT_PULLUP);
  pinMode(BUTTON_GRN, INPUT_PULLUP);
  pinMode(BUTTON_GND, OUTPUT);
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_YLW, OUTPUT);
  pinMode(LED_GRN, OUTPUT);

  digitalWrite(BUTTON_GND, LOW);

  secondTimerID = timer.setInterval(CHECKIN_PERIOD, checkIn); //Call checkIn every 500ms

  if (!rf69.init())
    Serial.println("init failed");
  // Defaults after init are 434.0MHz, modulation GFSK_Rb250Fd250, +13dbM
  // No encryption

  if (!rf69.setFrequency(915.0))
    Serial.println("setFrequency failed");

  // If you are using a high power RF69, you *must* set a Tx power in the range 14 to 20 like this:
  rf69.setTxPower(20);

  //This key is the same on the car. Pick your own random sequence.
  uint8_t key[] = { 0xAB, 0x1C, 0x0E, 0x39, 0xF8, 0xFF, 0xA6, 0xFC,
                    0x7B, 0x44, 0xC3, 0xC0, 0x2D, 0x2D, 0x2D, 0xD2
                  };
  rf69.setEncryptionKey(key);

  systemState = RED; //On power up start in red state
  setLED(LED_RED);

  Serial.println("Remote Controller Online");

  wdt_reset(); //Pet the dog
//  wdt_enable(WDTO_1S); //Unleash the beast
}

void loop()
{
  timer.run(); //Update any timers we are running
  wdt_reset(); //Pet the dog

  if (digitalRead(BUTTON_RED) == HIGH) //Top priority (Red is NC to ground so high = pressed)
  {
    systemState = RED;
    setLED(LED_RED); //Turn on LED

    //Check the special case of hitting all three buttons
    if (digitalRead(BUTTON_YLW) == LOW && digitalRead(BUTTON_GRN) == LOW) shutDown(); //Go into low power sleep mode
  }
  else if (digitalRead(BUTTON_YLW) == LOW)
  {
    systemState = YELLOW;
    setLED(LED_YLW); //Turn on LED
  }
  else if (digitalRead(BUTTON_GRN) == LOW)
  {
    systemState = GREEN;
    setLED(LED_GRN); //Turn on LED
  }
}

//Powers down all LEDs, radio, etc and sleeps until button interrupt
void shutDown()
{
  //Turn off LEDs
  digitalWrite(LED_RED, LOW);
  digitalWrite(LED_YLW, LOW);
  digitalWrite(LED_GRN, LOW);

  //Turn off radio

  //Sleep microcontroller and wake up on button interrupt

  Serial.println("Powering down");
}

//Send the system status notification every CHECKIN_PERIOD number of ms
void checkIn()
{
  wdt_reset(); //Pet the dog

  if(systemState == RED)
  {
    sendPacket("R");
  }
  else if (systemState == YELLOW)
  {
    sendPacket("Y");
  }
  else if (systemState == GREEN)
  {
    sendPacket("G");
  }
  else if (systemState == DISCONNECTED)
  {
    if(millis() - lastBlink > BLINK_RATE)
    {
      lastBlink = millis();
      digitalWrite(LED_RED, !digitalRead(LED_RED));
      digitalWrite(LED_YLW, !digitalRead(LED_YLW));
      digitalWrite(LED_GRN, !digitalRead(LED_GRN));

      sendPacket("D"); //Attempt to re-establish connection
    }
  }
}

//Sends a packet
//If we fail to send packet or fail to get a response, time out and go to DISCONNECTED system state
void sendPacket(char* thingToSend)
{
  Serial.print("Sending: ");
  Serial.println(thingToSend);

  rf69.send((uint8_t*)thingToSend, sizeof(thingToSend));

  rf69.waitPacketSent(BLOCKING_WAIT_TIME); //Wait for a bit of time

  //Wait for a "O" response from the car
  boolean responseFromCar = rf69.waitAvailableTimeout(BLOCKING_WAIT_TIME); //Wait some ms time to get a response

  //Read in any response from car
  uint8_t buf[RH_RF69_MAX_MESSAGE_LEN];
  uint8_t len = sizeof(buf);
  if (rf69.recv(buf, &len))
  {
    Serial.print("Heard from car: ");
    Serial.println((char*)buf);
  }

  if(responseFromCar == true) //We got a response
  {
    failCount = 0; //Reset the count

    if(systemState != DISCONNECTED)
    {
      Serial.println("System status delivered");
    }
    else if(systemState == DISCONNECTED)
    {
      //We are back online!
      Serial.println("Back online!");
      setLED(LED_RED);
      systemState = RED; //Default to stop if we are regaining connection
    }
  }
  else if (responseFromCar == false)
  {
    Serial.println("No response from car");

    //Go into triple blink mode indicating disconnect
    if(systemState != DISCONNECTED)
    {
      if(failCount++ > MAX_DELIVERY_FAILURES)
      {
        failCount = MAX_DELIVERY_FAILURES; //Don't let it increase and roll over
        digitalWrite(LED_RED, HIGH);
        digitalWrite(LED_YLW, HIGH);
        digitalWrite(LED_GRN, HIGH);
        systemState = DISCONNECTED;
      }
    }

    rf69.setModeIdle(); //This clears the buffer so that rf69.send() does not lock up
  }
}

//Turns on a given LED
void setLED(byte LEDnumber)
{
  digitalWrite(LED_RED, LOW);
  digitalWrite(LED_YLW, LOW);
  digitalWrite(LED_GRN, LOW);

  digitalWrite(LEDnumber, HIGH);
}

Above is the code for the RCU

The RCU waits for the user to press a button and monitors the link to the VCU. If the link is not detected, the RCU flashes all three LEDs letting the user know the VCU is offline (either unpowered or out of range). Once the user presses the green button, the RCU turns the LED to green and begins transmitting the ‘Ok, go for it’ signal. Once received, the VCU sets the KILL_LOW pin to high, thus activating both relays and connecting the motor to the motor controller.

Certain settings can be changed in the code for your specific application:

  • MAX_DELIVERY_FAILURES: It turns out that the RF link will drop a packet every once and a awhile, so the MAX_DELIVERY_FAILURES is set to three. If we don’t get a valid send/response after 3 tries, the system goes into disconnected mode. Setting this lower than 3 means the car may shutdown prematurely because we had some RF traffic/collisions.
  • CHECKIN_PERIOD: The remote will broadcast the system state every 25ms.
  • MAX_TIME_WITHOUT_OK: The vehicle will shut down if it doesn’t hear from the remote after 250ms.

These settings worked well for me and created a responsive, dependable link that shut the vehicle down within 250ms if things went pear-shaped. You may need to adjust these settings for other, longer-range applications.