Introduction: Easy IOT - Remote Controlled Relay Node

This tutorial is part of a series of tutorials, building a network of devices that can be controlled via a radio link from a central hub device. For the tutorial series overview please follow this link:

https://www.instructables.com/id/Easy-IOT-App-Cont...

For our first controllable device, we will use an Arduino

Nano connected to a relay module and another HC-12 Module to receive the radio data from our ESP32 Hub. Once again, these parts are very cheaply obtainable from Amazon and eBay and the whole node can be built for well under 10 dollars from the right sellers.

The diagram shows the components and connections we will be using. An example use of this kind of setup could be to control a garage door opener, an outdoor light or heater.

Supplies

· Arduino Nano (or clone)

· HC-12 Module

· Relay Module

· Female to Female Dupont Wires

· Mini USB cable (or micro depending on the board)

Step 1: Connect the HC-12 Module

If you didn’t make multiple of the HC-12 modules in Part 1, go back now and build another one. The relay module we used is a cheap opto-isolated module from amazon and runs from the 5V rail of the Arduino.

Connect your HC-12 module to the Arduino as follows:

VCC = RED => 3.3V

GND = BROWN => GND

RXD = ORANGE => A3

TXD = YELLOW => A2

SET = GREEN => A1

Step 2: Connecting the Relay Module

Next, connect the relay module:

VCC => 3.3V

GND => GND

IN => D5

Your hardware is now assembled and should look like the image above.

Step 3: Testing the Node

As shown in the previous tutorial, the first thing we will do is check that the board and our connections are working properly with the Blink sketch and an HC-12 Test sketch. This time we are using an Arduino Nano however and so the code will be slightly different.

Plug the Arduino into your computer, go to File, Examples, 01 Basics, Blink. Leave the sketch as it is then upload the code. Don’t forget to change the board type to “Arduino Nano” and choose the correct COM port under Tools. If it is all working, you should see the onboard LED blinking.

You could also take this opportunity to test the relay module by changing the words “LED_BUILTIN” to “5”. This should make the relay turn on and off every second.

You should also check the HC-12 module using a slightly modified version of the code shown in Part 1. The changes are needed because the HC-12 module is connected to different pins on the Arduino Nano and also uses SoftwareSerial as the Nano does not have more than one hardware serial port.

Upload the following sketch and like before, make sure that the HC-12 module responds with “OK“ and that the settings match that of the other HC-12 modules you are using.

#include <SoftwareSerial.h>

SoftwareSerial HC12(A2, A3); // HC-12 TX Pin, HC-12 RX Pin

void setup() 
{
  // Set pin A1 as an output and pull low for HC-12 settings mode
  pinMode(A1, OUTPUT);
  digitalWrite(A1, LOW);

  // Start serial ports
  Serial.begin(9600);     
  HC12.begin(9600);
}

void loop() 
{
  // While there are data bytes available in HC-12 serial buffer
  // write those bytes to the serial monitor.
  while (HC12.available()) 
  {        
    Serial.write(HC12.read());
  }
  
  // While there are data bytes available from serial monitor
  // write those bytes to the HC-12 radio.
  while (Serial.available()) 
  {      
    HC12.write(Serial.read());
  }
}

The final thing we need to do before writing the actual
relay control program is give the node an ID code and store it in the EEPROM memory. This is so that the module will only respond to commands sent to it with the correct ID.

To set an ID, the following sample sketch can be used. This will only need to be run once, the first time you program each node.

The ID we will give the relay node in this case is “RM0002”, but this can be any string of 6 characters you like. The first two characters can be used as an identifier for the node later on and so we chose “RM” to represent the Relay Module.

Once you have run the program, open the serial monitor, and if it ran successfully you should see the ID that you set.

#include <EEPROM.h>

char progID[7] = "RM0002";   // Device ID
char sID[7];                 // Buffer to store read ID
String nodeId;               // String to store ID

void setup()
{
 // Start serial port to computer
 Serial.begin(9600);        
 
 //For each character of the ID write to EEPROM position 0-5
 for (int i=0; i<6; i++)
 {
   EEPROM.write(i,progID[i]);
 }

  //Read back ID from EEPROM
  for (int i=0; i<6; i++)         
  {
    sID[i] = EEPROM.read(i);     
  }

  //Save read ID to string and print to Serial
  nodeId = String(sID);
  Serial.println(nodeId);
}

// Do nothing.
void loop() {}

Step 4: Relay Control Code

Now we have proven that the hardware is working correctly, we can put it together so that the node switches the relay on and off when it receives commands from the hub.

To keep the code easy to understand, we will break it down into sections.

The first part of the code is where we include libraries and declare the global variables that are going to be used in the program. Here we include the SoftwareSerial library used to communicate with the HC-12 Module and the EEPROM library, used to read the device ID from the memory.

#include <SoftwareSerial.h>
#include <EEPROM.h>
SoftwareSerial HC12(A2, A3); //HC-12 TX Pin, HC-12 RX Pin.

String nodeId;               //String to store device ID.
char sID[7];                 //Char array ID is read into.
String readString;           //String to store incoming radio message.
String msgMode;              //String to store message mode.
String msgFld1;              //String to store message data field 1.

The next part is the setup code:

void setup()
{
  //Set pin 5 as output and pull high (relay is active low).
  pinMode(5, OUTPUT);
  digitalWrite(5, HIGH);
  //Pull the HC12 SET pin high (LOW for AT Commands).       
  pinMode(A1, OUTPUT);
  digitalWrite(A1, HIGH);         
  //Start serial ports.
  Serial.begin(9600);
  HC12.begin(9600);
  //Read 7 digit ID from EEPROM.
  for (int i=0; i<6; i++)         
  {
    sID[i] = EEPROM.read(i);    
  }
  //Save read ID to string and print to Serial.
  nodeId = String(sID);
  Serial.println(nodeId);
}

This is the code that initialises the pins and serial ports, then reads its ID from the EEPROM memory.

Once setup has finished, the main loop begins, which repeatedly checks for new radio messages, and if a message is received, executes the “HandleCommand()” function.

//Main program (loops forever).
void loop() 
{
  //Check for messages
  bool gotMessage = checkRadio();
  //If message is received and mode is "CMD" 
  if( gotMessage )
  {
    if( msgMode == "CMD")  HandleCommand();
  }  
}

The “HandleCommand()” function checks the first two characters its own ID in order to know how to handle the command (more lines will be added here in future). If the ID starts with “RM” then the program knows that it is a relay module and it executes the “RelayControl()” function.

The “RelayControl()” takes the relevant part of the incoming message and responds to it. It also sends a reply back to the Hub to show that the command has been received.

//Process commands.
void HandleCommand()
{
    if( nodeId.startsWith("RM")) RelayControl();
}

//Control relay module.
void RelayControl()
{
  //If received command is "RELAYON", 
  //switch the relay on and send a reply to confirm.
  if( msgFld1.length() && msgFld1 == "RELAYON")
  {
    digitalWrite(5, LOW);
    Serial.println("RELAY Remotely Switched ON");
    HC12.print(nodeId);
    HC12.println("|CMD|OK");
  }
  
  //If received command is "RELAYOFF", 
  //switch the relay off and send a reply to confirm.
  else if( msgFld1.length() && msgFld1 == "RELAYOFF") 
  {
    digitalWrite(5, HIGH);
    Serial.println("RELAY Remotely Switched OFF");
    HC12.print(nodeId);
    HC12.println("|CMD|OK");
  }
}

The next function is the “checkRadio()” function used in the
main loop.

Each time this is called, it checks for new characters from the HC-12 serial port then adds them to a string. If it sees a “\r” or “\n” line ending, it knows that the message is complete and then checks if the message is meant for it by checking the ID included in the message to its own ID.

If the ID is a match then the different message fields are stored and the function returns “true” signalling that a valid message for the device has been received.

bool checkRadio()
{
 //Each time the radio receives data, 
 //add to an array until a line ending is received.
 if (HC12.available())  
 {
    char c = HC12.read();
    //If a line ending is received signalling end of message,
    //read the ID in the first part of the message, if the ID does
    //not match the device ID clear the message buffer and return.
    if (c == '\r'||c == '\n') 
    {
      String cmdId = getValue(readString, '|', 0);
      if (cmdId != nodeId) 
      {
        readString=""; 
        return false;
      }
      //If ID match, read each field of the message into global variables.
      msgMode = getValue(readString, '|', 1);
      msgFld1 = getValue(readString, '|', 2);
      //Clear the buffer. 
      readString="";
      return true;  
     }
     
    //If no line ending received yet, add the character to the message string.
    else  readString += c;
  }  
  //If no data available, return false.
  return false;
}

The final function is “getValue()” and is used to break up the messages on a certain character. In this program we use the “|” character to split up the different parts of a message. For example, if the incoming string was “RM0002|CMD|RELAYON” you would call “getValue(readString, '|', 0);” to get the node ID or “getValue(readString, '|', 1);” for the message type.

//Function to break a "|" separated string into separate fields.
String getValue(String data, char separator, int index)
{
  int found = 0;
  int strIndex[] = {0, -1};
  int maxIndex = data.length()-1;

  for(int i=0; i<=maxIndex && found<=index; i++)
  {
    if(data.charAt(i)==separator || i==maxIndex)
    {
        found++;
        strIndex[0] = strIndex[1]+1;
        strIndex[1] = (i == maxIndex) ? i+1 : i;
    }
  }
  return found>index ? data.substring(strIndex[0], strIndex[1]) : "";
}

Here is the full code. Upload this to your Arduino and make sure there are no errors. Check the Serial terminal where you should the ID.

#include <SoftwareSerial.h>
#include <EEPROM.h>
SoftwareSerial HC12(A2, A3); //HC-12 TX Pin, HC-12 RX Pin.

String nodeId;               //String to store device ID.
char sID[7];                 //Char array ID is read into.
String readString;           //String to store incoming radio message.
String msgMode;              //String to store message mode.
String msgFld1;              //String to store message data field 1.

void setup() 
{
  //Set pin 5 as output and pull high (relay is active low).
  pinMode(5, OUTPUT);
  digitalWrite(5, HIGH);     //Pull the HC12 SET pin high (LOW for AT Commands).       
  pinMode(A1, OUTPUT);
  digitalWrite(A1, HIGH);    //Start serial ports.
  Serial.begin(9600);
  HC12.begin(9600);          //Read 7 digit ID from EEPROM.
  for (int i=0; i<6; i++)         
  {
    sID[i] = EEPROM.read(i);    
  }
  //Save read ID to string and print to Serial.
  nodeId = String(sID);
  Serial.println(nodeId);
}

//Main program (loops forever).
void loop() 
{
  //Check for messages
  bool gotMessage = checkRadio();

  //If message is received and mode is "CMD" 
  if( gotMessage )
  {
    if( msgMode == "CMD")  HandleCommand();
  }  
}

//Process commands.
void HandleCommand()
{
    if( nodeId.startsWith("RM")) RelayControl();
}

//Control relay module.
void RelayControl()
{
  //If received command is "RELAYON", 
  //switch the relay on and send a reply to confirm.
  if( msgFld1.length() && msgFld1 == "RELAYON")
  {
    digitalWrite(5, LOW);
    Serial.println("RELAY Remotely Switched ON");
    HC12.print(nodeId);
    HC12.println("|CMD|OK");
  }
  
  //If received command is "RELAYOFF", 
  //switch the relay off and send a reply to confirm.
  else if( msgFld1.length() && msgFld1 == "RELAYOFF") 
  {
    digitalWrite(5, HIGH);
    Serial.println("RELAY Remotely Switched OFF");
    HC12.print(nodeId);
    HC12.println("|CMD|OK");
  }
}

bool checkRadio()
{
 //Each time the radio receives data, 
 //add to an array until a line ending is received.
 if (HC12.available())  
 {
    char c = HC12.read();
    //If a line ending is received signalling end of message,
    //read the ID in the first part of the message, if the ID does
    //not match the device ID clear the message buffer and return.
    if (c == '\r'||c == '\n') 
    {
      String cmdId = getValue(readString, '|', 0);
      if (cmdId != nodeId) 
      {
        readString=""; 
        return false;
      }

      //If ID match, read each field of the message into global variables.
      msgMode = getValue(readString, '|', 1);
      msgFld1 = getValue(readString, '|', 2);

      //Clear the buffer. 
      readString="";
      return true;  
     }
     
    //If no line ending received yet, add the character to the message string.
    else  readString += c;
  }
  
  //If no data available, return false.
  return false;
}

//Function to break a "|" separated string into separate fields.
String getValue(String data, char separator, int index)
{
  int found = 0;
  int strIndex[] = {0, -1};
  int maxIndex = data.length()-1;</p><p>  for(int i=0; i<=maxIndex && found<=index; i++)
  {
    if(data.charAt(i)==separator || i==maxIndex)
    {
        found++;
        strIndex[0] = strIndex[1]+1;
        strIndex[1] = (i == maxIndex) ? i+1 : i;
    }
  }
  return found>index ? data.substring(strIndex[0], strIndex[1]) : "";
}

Step 5: Adding Relay Control to the Hub

Now we have a node ready and waiting for commands, we need to add some code to the ESP32 Hub to pass on messages received from our Droidscript App to the appropriate nodes.

There are several changes we need to make. First, we need to add the HC-12 setup code then add a new endpoint to the webserver to handle messages we want to transmit. We will also rearrange the code a bit to put the webserver setup code in its own function, making the sketch a bit neater and easier to understand.

The code should now look like this:

#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#define HC12 Serial2  		             //Hardware serial 2 on the ESP32
const char* ssid = "FARMERSCLUB 3";          //Change this to your router SSID.
const char* password =  "farmersclub";       //Change this to your router password.
AsyncWebServer server(80);                   //Initialise server on port 80.

void setup() 
{
  //Pull the HC12 SET pin high (LOW for AT Commands).
  pinMode(5, OUTPUT);
  digitalWrite(5, HIGH); 

  //Start serial ports.
  Serial.begin(115200);
  HC12.begin(9600);

  //Start wifi and try to connect to router.
  Serial.printf("Connecting to %s ", ssid);
  WiFi.begin(ssid, password);

  //While not connected write "." to serial monitor.
  while (WiFi.status() != WL_CONNECTED) 
  {
      delay(500);
      Serial.print(".");
  }
    
  //Once WIFI is connected print the hub IP to serial
  Serial.println(" CONNECTED");
  Serial.println(WiFi.localIP());
  
  //Set up and launch webserver.
  AsyncServerSetup();
  server.begin();
}

//Main loop (does nothing).
void loop() {}

//Function to transmit radio messages
void SendHubMessage(String hubMessage)
{
      HC12.print(hubMessage);      // Send that data to HC-12
      HC12.print("\r");      // Send that data to HC-12
      hubMessage = "";
      Serial.println("Message Sent...");
}

//Setting up webserver end points
void AsyncServerSetup()
{
  //Status check end point (used to check connection to app).
  server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send(200, "text/plain", "HUB|OK");
  });

  //End point to listen for messages to be transmitted on HC-12 radio.
  server.on("/tx", HTTP_GET, [] (AsyncWebServerRequest *request) {
  HandleMessage(request);  
  //Reply to GET with message that was received.
  request->send(200, "text/plain", "Message Sent");
  });
}

//Handle the message sent to end point.
void HandleMessage(AsyncWebServerRequest *request)
{
    String message;
    const char* PARAM_TXNOW = "txnow";
    
    //If received message has the parameter "txnow", transmit immediately.
    if (request->hasParam(PARAM_TXNOW)) 
    {
      message = request->getParam(PARAM_TXNOW)->value();
      String msgtoConvert = message;
      msgtoConvert.replace(":","|");
      SendHubMessage(msgtoConvert); 
    } 
    else 
    {
      message = "No message sent";
    }
}

Upload this to your ESP32 hub, make sure you have changed
the board back to ESP32 and the correct COM Port (Tip: If you open Arduino from the start menu twice, it will allow you to work on two instances of the IDE at the same time, whereas if you open files from within the IDE the Board and COM port will change for all open sketches).

Open the serial monitor and you should see, like before your ESP32 connect to your wifi and give you an IP. If this has not changed you should be able to run your DroidScript app and the hub should show as “Connected”.

Step 6: Droidscript Code

The final stage to controlling our relay module is to add a couple of buttons to the app we built in the first tutorial that will send the commands to the hub that will be forwarded on to the relay node.

Add the following to OnStart() before the “app.AddLayout()” line:

//Create a button to send request.
btnRelayOn = app.CreateButton( "Relay On", 0.3 ); 
btnRelayOn.SetMargins( 0, 0.03, 0, 0 ); 
btnRelayOn.SetOnTouch( btnRelayOn_OnTouch ); 
lay.AddChild( btnRelayOn ); 
	
//Create a button to send request.
btnRelayOff = app.CreateButton( "Relay Off", 0.3 ); 
btnRelayOff.SetMargins( 0, 0.01, 0, 0 ); 
btnRelayOff.SetOnTouch( btnRelayOff_OnTouch ); 
lay.AddChild( btnRelayOff );

Then add the following two functions to the end of your code:

//Send relay on command for hub to transmit.
function btnRelayOn_OnTouch()
{
    //Send request to remote server.
    var cmd = "txnow=RM0002:CMD:RELAYON";
    app.HttpRequest( "get", url, "/tx", cmd , HandleReply );
}

//Send relay off command for hub to transmit.
function btnRelayOff_OnTouch()
{
    //Send request to remote server.
    var cmd = "txnow=RM0002:CMD:RELAYOFF";
    app.HttpRequest( "get", url, "/tx", cmd, HandleReply );
}

Now when the buttons are pressed, the command to be transmitted by the HC-12 is sent to the hub. In DroidScript we have to use “:” instead of “|” as the pipe symbol is used internally and will not work.

The full code is given below:

var url = ""; //variable to store url
var ipfile = "/sdcard/hubip.txt";

//Called when application is started.
function OnStart()
{
    //Get last hub IP saved from text file.
    url = "http://" + app.ReadFile( ipfile );
    
    //Create a layout with objects vertically centered.
    lay = app.CreateLayout( "linear", "VCenter,FillXY" );

    //Create a text label and add it to layout.
    txt = app.CreateText( "HUB IP:" );
    txt.SetTextSize( 16 );
    lay.AddChild( txt );
    
    //Create an text edit box.
    var txt = app.ReadFile( ipfile );
    edtIP = app.CreateTextEdit( txt, 0.6 );
    edtIP.SetMargins( 0, 0.02, 0, 0 );
    lay.AddChild( edtIP );
    
    //Create a button to send IP.
    btnIPSave = app.CreateButton( "Save", 0.3, 0.1 ); 
    btnIPSave.SetMargins( 0, 0.05, 0, 0 ); 
    btnIPSave.SetOnTouch( btnIPSave_OnTouch ); 
    lay.AddChild( btnIPSave ); 
    
    //Create a text label.
    txtLabel = app.CreateText( "HUB STATUS: " );
    txtLabel.SetTextSize( 15 );
    txtLabel.SetMargins(0, 0.03);
    lay.AddChild( txtLabel );
    
    //Create text to show if connected or not.
    txtStatus = app.CreateText( "" );
    txtStatus.SetTextSize( 15 );
    txtStatus.SetMargins(0, 0.01);
    txtStatus.SetTextColor("red");
    txtStatus.SetText("Disconnected");
    lay.AddChild( txtStatus );
    
    //Create a button to send request.
    btnRelayOn = app.CreateButton( "Relay On", 0.3 ); 
    btnRelayOn.SetMargins( 0, 0.03, 0, 0 ); 
    btnRelayOn.SetOnTouch( btnRelayOn_OnTouch ); 
    lay.AddChild( btnRelayOn ); 
    
    //Create a button to send request.
    btnRelayOff = app.CreateButton( "Relay Off", 0.3 ); 
    btnRelayOff.SetMargins( 0, 0.01, 0, 0 ); 
    btnRelayOff.SetOnTouch( btnRelayOff_OnTouch ); 
    lay.AddChild( btnRelayOff ); 
    
    //Add layout to app.	
    app.AddLayout( lay );
    
    //Check for connection to hub, then again every 10s.
    HubStatus();
    setInterval(HubStatus ,10000);
}

//Saves current hub IP to text file and checks for connection.
function btnIPSave_OnTouch()
{
    //Get IP from text box and save to file.
    var s = edtIP.GetText();
    app.WriteFile( ipfile, s );
    app.ShowPopup("Saved: " + app.ReadFile( ipfile ));
    url = "http://" + app.ReadFile( ipfile );
    
    //Show progress wheel until response received
    //then check status after half a second.
    app.ShowProgress();
    setTimeout(HubStatus, 500);
}

//Check if Hub is connected by asking for status.
function HubStatus()
{
    //Send request to remote server.
    var path = "/status";
    app.HttpRequest( "get", url, path, "", HandleReply );
}

//Handle the hub's webserver reply.
function HandleReply( error, response )
{
    console.log(response);
    
    //Splits message on "|" into array.
    var res = response.split("|");
    
    //If timeout or other error show disconnected.
    if (error)
    {
        console.log(error);
        txtStatus.SetTextColor("red");
        txtStatus.SetText("Disconnected");
    }
    
    //If message reads "HUB|OK" show connected else show disconnected.
    else if (res[0] == "HUB" ) 
    {
        if (res[1] == "OK")
        {
            txtStatus.SetTextColor("green");
            txtStatus.SetText("Connected");
        }
        else
        {
            txtStatus.SetTextColor("red");
            txtStatus.SetText("Disconnected");
        }
    }
    
    //Response received so hide progress wheel.
    app.HideProgress();
}

//Send relay on command for hub to transmit.
function btnRelayOn_OnTouch()
{
    //Send request to remote server.
    var cmd = "txnow=RM0002:CMD:RELAYON";
    app.HttpRequest( "get", url, "/tx", cmd , HandleReply );
}

//Send relay off command for hub to transmit.
function btnRelayOff_OnTouch()
{
    //Send request to remote server.
    var cmd = "txnow=RM0002:CMD:RELAYOFF";
    app.HttpRequest( "get", url, "/tx", cmd, HandleReply );
}

Step 7: Testing

Run your app, check that it still says “Connected” (If not, check your Hub IP address), then try pressing the buttons. The relay should turn on and off. Be aware that there may be some delay due to the way the router is handling the messages.

The full SPK can be downloaded here:

http://androidscript.org/tutorials/Easy-IOT/Easy-I...

STEM Contest

Participated in the
STEM Contest