Imagine being able to control an electrical device located far away—maybe in another building, across a field, or even several kilometers away—just by tapping a button on your smartphone. No internet connection, no complicated network setup, and no long wires running between locations. Sounds interesting, right? This type of wireless control becomes possible with LoRa (Long Range) communication technology, which is used specifically to transmit data reliably over very large distances while consuming very little power. 

In this project, we will build a long-range appliance control system using Arduino Nano and RYLR999 LoRa module. We will learn how to control electrical appliances such as a bulb and a fan remotely using LoRa wireless communication and an Android mobile application. To receive the commands from the app, we will also use Bluetooth functionality of the module. To monitor communication status, 16×2 I2C LCD displays are used to show the received commands and response messages. Let’s get started!

Reyax RYLR999 Module Overview

At the core of this system lies the Reyax RYLR999, a wireless communication module that integrates Bluetooth Low Energy (BLE) and LoRa technology within a single compact hardware module.

In this project, the module operates in two communication modes: BLE for short-range interaction and LoRa for long-range transmission.

  • It acts as a BLE interface, allowing a smartphone to connect and send commands.
  • It functions as a LoRa transceiver, forwarding those commands over long distances to another RYLR999 module.

BLE is used as the user interface layer. A smartphone connects to the module via BLE and sends appliance control commands. Once the module receives the command through its BLE interface, it processes the data and forwards those commands over long distances to another RYLR999 module.

This dual capability of the Reyax module makes it possible to transform short-range BLE commands into long-range LoRa transmissions.

RYLR999 Pinout

The pinout of Reyax RYLR999 module is shown as follows:

RYLR999 Pinout

PinNameDescription
1VDDPower supply pin. The module is powered with a 5V supply.
2RSTReset pin. Pulling this pin LOW resets the module.
3TXD_BLEBLE UART transmit pin. Sends data from the BLE interface to an external device or microcontroller.
4RXD_LoRaLoRa UART receive pin. Used by a microcontroller to send commands to the LoRa module.
5TXD_LoRaLoRa UART transmit pin. LoRa module transmits data to the Arduino through this pin.
6RXD_BLEBLE UART receive pin. Used to send data from a microcontroller to the BLE interface.
7GNDGround pin

  💡Must Read

How to Interface Reyax RYLR999 LoRa Module with Arduino

How to Interface Reyax RYLR999 LoRa Module with Arduino

Explore this tutorial to learn about the RYLR999 Module and how to interface it with Arduino UNO.

Why is Relay Required?

In this project, the microcontroller receives control commands from the RYLR999 module and must physically switch electrical appliances ON or OFF. However, microcontrollers like Arduino operate at low voltages (5V) and can only supply a small amount of current through their GPIO pins.

On the other hand, household electrical appliances operate at high-voltage AC (110V or 230V, depending on region) and may draw several amps of current. Directly connecting such loads to a microcontroller would not only damage the board but would also pose a serious safety risk.

This is where the relay becomes important. A relay acts as an electrically controlled switch. It allows a low-voltage control signal from the microcontroller to safely switch high-voltage AC or high-current DC loads. In this project, the relay forms the critical interface between the digital control system and real-world electrical devices.

Hardware Setup and Connections

The system is divided into two main parts: a controller and a target

On the controller side, an Android smartphone acts as the user interface. It connects to the RYLR999 module via Bluetooth Low Energy (BLE) using the LightBlue mobile application. When a user presses a button in the mobile app, a command such as turning a bulb or fan ON or OFF is transmitted to the RYLR999 module through BLE.

The Arduino Nano connected to the LoRa module reads and parses these incoming BLE commands. The Arduino then retransmits the same data to the LoRa module, which transmits the command over LoRa wireless communication.

At the target side, another RYLR999 LoRa module receives the transmitted data. This module is connected to a second Arduino Nano, which reads the incoming LoRa message and interprets the command. Based on the received instruction, the Arduino controls electrical appliances such as a 220 V AC bulb (lamp) or a 12V DC fan using a relay module.

Once the command is executed, the target also sends a confirmation message back to the controller to indicate successful operation.

The LCD provides real-time feedback about the system status during operation. It displays messages such as module initialization status, received commands from the mobile application, transmission of commands over LoRa, and confirmation responses from the target side. If a command is successfully transmitted and acknowledged, the LCD shows a confirmation message; otherwise, it displays alerts such as invalid command. 

Hardware Requirement

ComponentsQuantityRemarksWhere to Buy
Arduino Nano2Microcontroller boardAmazon
RYLR999 Module2Converts logic levels between 5V Arduino and 3.3V LoRa moduleAmazon / Digikey
Jumper WiresMultipleFor connections between modules, LCD and Arduino NanoAmazon
USB Cable Type A to B Mini1For providing power to 12V FanAmazon
12V Supply Adapter1For providing power to Arduino NanoAmazon
5V Bidirectional Voltage Shifter Module2Converts logic levels between 5V Arduino and 3.3V LoRa moduleAmazon
LCD 16x22I2C-based LCD used to display system status and communication messagesAmazon
2 Channel Relay Module2Used to control external loads such as a 12V fan and 240V AC bulbAmazon

Software Requirement

  • Arduino IDE, Version 2.3.4 or above installed on your PC.
  • LiquidCrystal_I2C Library by Frank de Brabander V 1.1.2

Wiring Connections for the Controller Setup

Wiring Connections for the Controller (Transmitter) Setup

The above image shows the circuit diagram for the controller setup. The RYLR999 LoRa + BLE module is responsible for wireless communication. It communicates with the Arduino using serial communication lines. The module requires 3.3V logic levels, whereas the Arduino Nano operates at 5V logic levels. Because of this difference, a voltage level shifter is placed between the Arduino Nano and the module.

The power connections are straightforward. VDD pin of the RYLR999 is connected to the 5V output of the Arduino Nano. The GND pin of the module is connected to the Arduino’s GND. 

The TXD_LORA pin of LoRa module is connected to the LV2 pin of voltage shifter and the corresponding HV2 output of voltage shifter module is connected to the Arduino’s Rx pin, while the TXD pin of Arduino is connected to the HV1 pin of voltage shifter and corresponding LV1 output is connected to RXD_LORA pin.

As Arduino has only one Serial port another serial port is created using Software, and using that we make Pin 2 and Pin 3 as BLE_TX and BLE_RX for BLE Communication.

The TXD_BLE pin of the module is connected to the LV4 pin of the voltage shifter and the corresponding HV4 output of the voltage shifter is connected to the Arduino’s BLE_RX (pin3). The BLE_TX (pin 2) of the Arduino is connected to the HV3 pin of the voltage shifter and corresponding LV3 output is connected to the RXD_BLE pin of the module.

The 5V pin of the Arduino is connected to the HV pin of the voltage shifter and 3.3V pin of Arduino is connected to the LV pin of the voltage shifter module. These power connections are important to make voltage shifter work.

The pinout of the voltage shifter module is given below:

3.3V to 5V Bi-directional Voltage Shifter Pinout

The connections between Arduino Uno and RYLR999 Module are as follows:

Arduino Nano PinRYLR999 PinPurpose
5VVDDPower supply to module
GNDGNDCommon ground
Rx (pin 0)TXD_LoRa (via voltage shifter)Arduino receives data from module through LORA
Tx (pin 1)RXD_LoRa (via voltage shifter)Arduino sends data to module through LORA
BLE_TX (pin 2)RXD_BLE (via voltage shifter)Arduino sends data to module through BLE
BLE_RX (pin 3)TXD_BLE (via voltage shifter)Arduino receives data from module through BLE

The wiring of the I2C LCD with Arduino is simple. Connect the Vcc and GND pins of I2C LCD with the Vcc and GND pins of the Arduino. SCL(clock) and SDA(data) pins of I2C LCD are connected to SCL and SDA pins of Arduino.

SCL and SDA pins are the same as pins A5 and A4 Analog input pins of Arduino where pink and blue wires are connected with LCD. These header pins are also connected to A5 and A4 pins of Arduino.

Make sure A0, A1, A2 address jumpers are not shorted when using I2C LCD. As in code we will be using Address 0x27 for the I2C LCD which will only be formed when these jumpers are not shorted in the I2C LCD.

I2C LCD back panel

Wiring Connections for the Target Setup

Wiring Connections for the Target (Receiver) Setup

The target circuit is similar to the controller circuit, except that it doesn’t use BLE Functionality and includes an additional relay module that controls the electrical appliances fan and bulb.

The RYLR999 LoRa module communicates with the Arduino Nano using serial communication. Just like in the controller, the communication lines pass through a voltage level shifter to safely convert voltage levels between the 5V Arduino and the 3.3V LoRa module.

Here, the target module continuously listens for incoming LoRa messages transmitted by the controller through the RYLR999 module. When a message arrives, the module forwards the received data to the Arduino through the serial communication lines. The Arduino Nano interprets the received message and determines which appliance should be controlled. The Arduino then sends control signals to the relay module. 

The relay module is connected to the Arduino through digital output pins. Each relay channel has an input pin that receives a control signal from the Arduino. When the Arduino activates a relay input pin, the relay switches its internal contacts, allowing it to turn an electrical appliance ON or OFF.

In the above circuit, relay channel 1, which is controlled using the D11 pin of Arduino, is used to control a bulb, while relay channel 2, which is controlled using the D12 pin of Arduino, controls a bulb

The relay module is powered from the Arduino Nano, where the VCC pin of the relay is connected to the 5V pin of the Arduino, and the GND pin of the relay is connected to the Arduino’s GND pin. For 240V AC bulb, the relay is used to switch the live wire of the AC supply. The line from the AC source is connected to the relay’s common terminal, and the output from the normally open terminal goes to the bulb. When relay channel 1 is activated, the relay closes the circuit and allows the AC voltage to reach the bulb, turning it ON. When the relay is deactivated, the circuit opens and the bulb turns OFF.

For the 12V DC fan, the relay is placed in series with the 12V power line going to the fan. When the Arduino activates relay channel 2, the relay contacts close and allow the 12V supply to reach the fan, turning it ON. When the relay is deactivated, the circuit opens and the fan turns OFF.

This configuration allows the Arduino to control both DC and AC electrical loads safely using the relay module while keeping the high-voltage circuits isolated from the main circuit.

Just like the controller setup, the target setup also includes a 16×2 I2C LCD connected to the Arduino. This LCD displays information such as received commands and the status of appliance control. 

Note

Since the relay controls a 240V AC load, proper electrical safety precautions must be followed while wiring the circuit. Always ensure that the AC power supply is disconnected during wiring to avoid electric shock.

Command Structure

Controller Side

On the controller side, we write the code to parse the commands sent from the BLE Mobile application.

  1. CMD: *L1#

To Turn Bulb ON

  1. CMD: *L0#

To Turn Bulb OFF

  1. CMD: *F1#

To Turn Fan ON

  1. CMD: *F0#

To Turn Fan OFF

Please note that we are not sending any response to the Light Blue BLE application, as this application is not created by us. Since the application does not receive or display responses from our system, sending a reply would serve no practical purpose. Therefore, the Arduino only reads and processes the commands sent from the app without transmitting any response back to it.

Target Side

The target will receive commands from the controller over the LoRa interface. In the target, we write the code to parse the commands sent using the LoRa.

  1. CMD: L1

REPLY: DONE

To Turn Bulb ON

  1. CMD: L0

REPLY: DONE

To Turn Bulb OFF

  1. CMD: F1

REPLY: DONE

To Turn Fan ON

  1. CMD: F0

REPLY: DONE

To Turn Fan OFF 

Please note that here we are showing only the actual command payload and response payload for simplicity. However, the data transmitted by the LoRa module is not limited to just this payload.

When using the RYLR999 LoRa module, additional information must be included in the transmission command, such as the destination device address, payload length, and other protocol-specific characters required by the module’s AT command format. These fields are necessary for the module to correctly identify the target and transmit the data properly over LoRa communication.

Arduino Code for Controller

This program receives commands from a mobile application via Bluetooth using the Bluetooth functionality of the RYLR999 LoRa module. Once a command is received, the Arduino validates the command and only after validation, it forwards that command to another Arduino (Target) using LoRa communication through the same module. The complete communication process is displayed on a 16×2 I2C LCD.

 /*
Code to receive commands from Mobile device using bluetooth functionality of RLY999 module and send these commands from one Arduino to another Arduino using LORA functionality of RYL999 Module and display complete communication on I2C LCD
by playwithcircuit.com
*/
#include <LiquidCrystal_I2C.h>
#include <SoftwareSerial.h>

// below MACROS are for LORA communication
#define REPLY_TIMEOUT_IN_MS 300
#define REPLY_END_CHAR '\n'
#define SELF_ADDRESS 0
#define TARGET_ADDRESS 1
#define MIN_CHAR_TO_RCV 1
#define WAIT_FOR_TARGET_REPLY 3000

// below MACROS and strings are for Bluetooth communication
#define START_CHAR_BT_COMM '*'
#define END_CHAR_BT_COMM '#'
#define START_CHAR_TIME_OUT_BT_COMM (3000U)  // time in ms
#define END_CHAR_TIME_OUT_BT_COMM (300U)      // time in ms

// these four strings can be received from bluetooth Rx pin which are transmitted by the mobile application
String sCMDLampON = "L1";
String sCMDLampOFF = "L0";
String sCMDFanON = "F1";
String sCMDFanOFF = "F0";

// save received command in this global string object
String receivedCommand;

// initialize softserial port at pin 3(Rx) and 2(Tx)
SoftwareSerial btSerial(3, 2);

// Init LCD at 0x27, 16x2
LiquidCrystal_I2C lcd(0x27, 16, 2);

void setup() {
  boolean boRetVal = false;
  // begin serial communication at baud 115200,n,8,1
  // to communicate with the LORA functionality of module
  Serial.begin(115200);
  // begin soft serial communication at baud 115200,n,8,1
  // to communicate with the Bluetooth functionality of module
  btSerial.begin(115200);
  btSerial.setTimeout(END_CHAR_TIME_OUT_BT_COMM);  // set time out for readStringUntil() function

  // Initialize the LCD
  lcd.init();
  // Turn ON the Backlight
  lcd.backlight();
  // Clear the display buffer
  lcd.clear();

  receivedCommand.reserve(50);  // prevents fragmentation, as multiple times data shall be received in this string

  // clear receive buffer of LORA
  flushBuffer();  // clear rx data

  // Reset settings to factory defaults for LORA functionality
  boRetVal = boRestoreFactoryDefaults();

  // setting the address of LORA
  if (boRetVal == true) {
    flushBuffer();  // clear rx data
    boRetVal = boSetAddress();
  }

  if (boRetVal == true) {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Module Init");
    lcd.setCursor(0, 1);
    lcd.print("Successful");
    delay(1000);
  } else {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Module Init");
    lcd.setCursor(0, 1);
    lcd.print("Failed");
    while (1)
      ;
  }
}

void loop() {
  String expected_reply = "DONE";
  bool boRetVal = false;

  flushBuffer();  // clear rx data

  // get commands using BT functionality using Mobile Application
  boRetVal = rcvCommand(START_CHAR_TIME_OUT_BT_COMM);

  if (boRetVal == false) {
    // Displaying Failed Msg
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("No BT Command");
    lcd.setCursor(0, 1);
    lcd.print("Received");
  } else {
    // check if valid command is received
    if ((receivedCommand == sCMDLampON) || (receivedCommand == sCMDLampOFF) || (receivedCommand == sCMDFanON) || (receivedCommand == sCMDFanOFF)) {
      // transmits receivedCommand
      boRetVal = boSendData(receivedCommand);
      if (boRetVal == true) {
        // Displaying Sent Msg
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Cmd Rcvd & Sent");
        lcd.setCursor(0, 1);
        lcd.print(receivedCommand);
        delay(1000);
        boRetVal = chkReply(expected_reply, REPLY_END_CHAR, WAIT_FOR_TARGET_REPLY);

        if (boRetVal == true) {
          // Displaying received Msg
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("Reply Received:");
          lcd.setCursor(0, 1);
          lcd.print(expected_reply);
        } else {
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("No Reply");
          lcd.setCursor(0, 1);
          lcd.print("Received");
        }
      } else {
        // Displaying Failed Msg
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Command");
        lcd.setCursor(0, 1);
        lcd.print("Sending Failed");
      }
    } else {
      // Displaying Invalid Msg
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Invalid:");
      lcd.print(receivedCommand);
      lcd.setCursor(0, 1);
      lcd.print("Command Received");
    }
  }
}
/******************************************** Function Definition Related to LORA Functionality ***********************/

void sendCrLf(void) {
  Serial.write(0x0D);  // Carriage Return
  Serial.write(0x0A);  // Line Feed
}

// clear receive buffer of LORA
void flushBuffer(void) {
  while (Serial.available() > 0) {
    Serial.read();
  }
}

// check data on rx pin of LORA functionality
bool chkReply(String chkString, char receiveUntil, unsigned int timeout) {
  String receivedString;       // save received data in this string object
  bool boReturnValue = false;  // function's return value

  // wait for reply
  do {
    timeout--;
    delay(1);  // delay of 1 ms
  } while ((Serial.available() < MIN_CHAR_TO_RCV) && (timeout > 0));

  if (timeout) {
    // if timeout is left then a reply is received check for the string in the reply
    receivedString = Serial.readStringUntil(receiveUntil);
    if (receivedString.indexOf(chkString) != -1) {
      boReturnValue = true;
    } else {
      boReturnValue = false;
    }
  } else {
    boReturnValue = false;
  }

  // return result
  return boReturnValue;
}

// Reset settings to factory defaults for LORA functionality
bool boRestoreFactoryDefaults(void) {
  const char factoryDefaultCmd[] = "AT+FACTORY";  // command to be sent
  bool boReturnValue = false;                     // function's return value
  char downCounter = 100;                         // Down counter to wait for reply
  String receivedString;                          // save received data in this string object

  String chkRcvString1 = "+FACTORY";
  String chkRcvString2 = "+READY";

  // send command
  Serial.print(factoryDefaultCmd);
  sendCrLf();

  // check first string in reply
  boReturnValue = chkReply(chkRcvString1, REPLY_END_CHAR, REPLY_TIMEOUT_IN_MS);
  if (boReturnValue == true) {
    // check second string in reply
    boReturnValue = chkReply(chkRcvString2, REPLY_END_CHAR, REPLY_TIMEOUT_IN_MS);
  }

  // return result
  return boReturnValue;
}

// setting the address of LORA
bool boSetAddress(void) {
  const char setAddressCmd[] = "AT+ADDRESS=";  // command to be sent
  bool boReturnValue = false;                  // function's return value
  String chkRcvString = "+OK";

  // send command
  Serial.print(setAddressCmd);
  Serial.print(SELF_ADDRESS);
  sendCrLf();

  // check reply
  boReturnValue = chkReply(chkRcvString, REPLY_END_CHAR, REPLY_TIMEOUT_IN_MS);

  // return result
  return boReturnValue;
}

// Send data to LORA in command form
bool boSendData(String data) {
  const char sendDataCmd[] = "AT+SEND=";  // command to be sent
  bool boReturnValue = false;             // function's return value
  String chkRcvString = "+OK";

  // send command
  Serial.print(sendDataCmd);
  Serial.print(TARGET_ADDRESS);
  Serial.print(',');
  Serial.print(data.length());
  Serial.print(',');
  Serial.print(data);
  sendCrLf();

  // check reply
  boReturnValue = chkReply(chkRcvString, REPLY_END_CHAR, REPLY_TIMEOUT_IN_MS);

  // return result
  return boReturnValue;
}

/******************************************** Function Definition Related to BT Functionality *************************/

// Receive Command from Rx pin of Bluetooth functionality
bool rcvCommand(unsigned int timeout) {
  bool boReturnValue = false;  // function's return value
  unsigned long startTime;

  // wait for reply
  do {
    timeout--;
    delay(1);  // delay of 1 ms
    if (((btSerial.available() >= MIN_CHAR_TO_RCV) && (btSerial.read() == START_CHAR_BT_COMM)) || (timeout == 0)) {
      break;
    }
  } while (1);

  // if start character is received within time then timeout will be greater than or equal to 1
  if (timeout) {
    startTime = millis();
    receivedCommand = btSerial.readStringUntil(END_CHAR_BT_COMM);
    if ((millis() - startTime) <= END_CHAR_TIME_OUT_BT_COMM) {
      boReturnValue = true;  // it means in readStringUntil() function '#' is received
    } else {
      boReturnValue = false;  // it means in readStringUntil() function timeout has occurred
    }
  } else {
    boReturnValue = false;  // it means get character timeout has occured
  }

  // return result
  return boReturnValue;
}

Code Explanation

Including Required Libraries

The program begins by including two libraries. The LiquidCrystal_I2C.h library is used to control the I2C LCD display.

The SoftwareSerial library creates a secondary serial interface that allows the Arduino to communicate with the Bluetooth functionality of the RYLR999 module using custom digital pins.

#include <LiquidCrystal_I2C.h>
#include <SoftwareSerial.h>
Defining LoRa Communication Parameters

Now we define parameters used for LoRa communication. These parameters control how the Arduino communicates with the LoRa module.

  • REPLY_TIMEOUT_IN_MS defines the maximum time (in milliseconds) the Arduino will wait for a reply from the LoRa module after sending a command. In this case, the Arduino will wait 300 milliseconds for a response.
  • REPLY_END_CHAR defines the character that marks the end of a message received from the LoRa module.
  • SELF_ADDRESS defines the unique address of the current LoRa node. Here, the current device has the address 0.
  • TARGET_ADDRESS specifies the address of the remote LoRa node that will receive the transmitted message. Here the target node has the address 1.
  • MIN_CHAR_TO_RCV defines the minimum number of characters required before reading serial data. Setting this value to 1 ensures that the program waits until at least one character is received before attempting to read the message.
  • WAIT_FOR_TARGET_REPLY determines how long the Arduino waits for a response from the target node. 

Each LoRa node must have a unique address to ensure proper communication.

// below MACROS are for LORA communication
#define REPLY_TIMEOUT_IN_MS 300
#define REPLY_END_CHAR '\n'
#define SELF_ADDRESS 0
#define TARGET_ADDRESS 1
#define MIN_CHAR_TO_RCV 1
#define WAIT_FOR_TARGET_REPLY 3000
Defining Bluetooth Communication Parameters

Now we define macros that configure how commands are received through Bluetooth. The mobile application sends commands in a specific format.

For example: *L1#, where * marks the start of the command and # marks the end of the command

The timeout values ensure that the Arduino does not wait indefinitely if the command is incomplete or corrupted.

// below MACROS and strings are for Bluetooth communication
#define START_CHAR_BT_COMM '*'
#define END_CHAR_BT_COMM '#'
#define START_CHAR_TIME_OUT_BT_COMM (3000U)  // time in ms
#define END_CHAR_TIME_OUT_BT_COMM (300U)      // time in ms
Defining Supported Commands

Here we define four valid commands that can be received from the mobile application. These commands represent device control operations:

L1 → Turn Bulb ON

L0 → Turn Bulb OFF

F1 → Turn Fan ON

F0 → Turn Fan OFF

The received command is then stored in the global string.

// these four strings can be received from bluetooth Rx pin which are transmitted by the mobile application
String sCMDLampON = "L1";
String sCMDLampOFF = "L0";
String sCMDFanON = "F1";
String sCMDFanOFF = "F0";
// save received command in this global string object
String receivedCommand;
Creating Communication Objects

Here we create a SoftwareSerial object named btSerial. The SoftwareSerial library allows the Arduino to create an additional serial communication port using digital pins instead of the hardware serial pins. Here, this software serial port is used for BLE communication.

Here we define Pin 3 as RX (Receive pin of Arduino) and Pin 2 as TX (Transmit pin of Arduino).

This allows us to keep the hardware serial pins (D0 and D1) free for communicating with the RYLR999 module.

Next, we create an object named lcd for controlling the 16×2 I2C LCD display using the LiquidCrystal_I2C library. The configuration of the display is:

  • I2C address: 0x27
  • Display size: 16 columns × 2 rows
// initialize softserial port at pin 3(Rx) and 2(Tx)
SoftwareSerial btSerial(3, 2);

// Init LCD at 0x27, 16x2
LiquidCrystal_I2C lcd(0x27, 16, 2);
Setup Function

The setup() function initializes all hardware components and configures the LoRa module.

First, a Boolean variable boRetVal is declared and initialized to false. This variable is later used to store the success or failure status of certain operations performed during initialization.

Next, the Arduino begins serial communication at a baud rate of 115200 using the Serial.begin() command. This serial interface is used to communicate with the LoRa functionality of the RYLR999 module, allowing the Arduino to send AT commands and receive responses from the module.

After that, the SoftwareSerial communication is started using btSerial.begin(). Next, we set a timeout for the readStringUntil() function, ensuring that the Arduino does not wait indefinitely if the end character of a command is not received.

void setup() {
  boolean boRetVal = false;
  // begin serial communication at baud 115200,n,8,1
  // to communicate with the LORA functionality of module
  Serial.begin(115200);
  // begin soft serial communication at baud 115200,n,8,1
  // to communicate with the Bluetooth functionality of module
  btSerial.begin(115200);
  btSerial.setTimeout(END_CHAR_TIME_OUT_BT_COMM);  // set time out for readStringUntil() function

Next, we initialize the 16×2 I2C LCD. The lcd.init() function prepares the LCD for operation, lcd.backlight() turns on the display backlight, and lcd.clear() clears any previous data from the screen. This ensures that the LCD starts with a clean display.

  // Initialize the LCD
  lcd.init();
  // Turn ON the Backlight
  lcd.backlight();
  // Clear the display buffer
  lcd.clear();

Next we allocate memory for the receivedCommand string in advance. Since this string will be used repeatedly to store commands received from the BLE mobile application, reserving memory helps prevent memory fragmentation in the Arduino’s limited RAM.  By reserving space for up to 50 characters, the program avoids repeatedly reallocating memory during runtime.

Next, the function flushBuffer() is called to clear any leftover data in the serial receive buffer of the LoRa module. Clearing the buffer helps prevent incorrect data from interfering with the next command or response.

Next, we reset the LoRa functionality of the RYLR999 module to its factory default settings. This step ensures that the module starts with a known and stable configuration before further setup commands are executed. The result of this operation is stored in the variable boRetVal, which indicates whether the reset was successful or not.

  receivedCommand.reserve(50);  
// prevents fragmentation, as multiple times data shall be received in this string

  // clear receive buffer of LORA
  flushBuffer();  // clear rx data

  // Reset settings to factory defaults for LORA functionality
  boRetVal = boRestoreFactoryDefaults();

The program now checks whether the reset operation was successful by evaluating the value of boRetVal. If the reset was successful (boRetVal == true), the serial buffer is cleared again using flushBuffer().

Next, the function boSetAddress() is called to assign a unique address to the LoRa module. This address is used to identify the device during communication.

The program again checks the value of boRetVal to determine whether the initialization process was completed successfully.

If the initialization is successful, the LCD displays “Module Init Successful” to inform the user that the LoRa module has been configured correctly. A short delay is added so that the message remains visible on the screen.

If the initialization fails, the LCD displays “Module Init Failed”. The program then enters an infinite loop using while(1); which stops further execution of the program. 

// setting the address of LORA
  if (boRetVal == true) {
    flushBuffer();  // clear rx data
    boRetVal = boSetAddress();
  }

  if (boRetVal == true) {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Module Init");
    lcd.setCursor(0, 1);
    lcd.print("Successful");
    delay(1000);
  } else {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Module Init");
    lcd.setCursor(0, 1);
    lcd.print("Failed");
    while (1)
      ;
  }
}
loop() Function

The loop() function is responsible for receiving commands from the mobile application through BLE, validating them, transmitting them over LoRa, and updating the LCD with the system status.

At the beginning of the loop, we define a string variable expected_reply with the value “DONE”. This is the confirmation message expected from the target side after it successfully executes a command.

Then, we initialize the boolean variable boRetVal to false. This variable is used to store the success or failure status of operations performed in the loop, such as receiving commands or sending data.

Next, we clear the LoRa serial buffer using the flushBuffer() function.

We use rcvCommand() to receive commands sent from the mobile application through the BLE interface of the RYLR999 module. The function waits for a command within the specified timeout period. If a command is successfully received and parsed, the function returns true; otherwise, it returns false.

If no command is received within the allowed time, the LCD displays the message “No BT Command Received”. This indicates that the system did not receive any input from the mobile application.

If a command is received, the program checks whether it matches one of the valid predefined commands:

  • L1Bulb ON 
  • L0Bulb OFF 
  • F1 → Fan ON 
  • F0 → Fan OFF

Only these commands are accepted. 

If the command is valid, the Arduino sends it to the target through the LoRa communication interface using the boSendData() function. 

If the command is successfully transmitted, the LCD displays “Cmd Rcvd & Sent” along with the command that was received.

void loop() {
  String expected_reply = "DONE";
  bool boRetVal = false;

  flushBuffer();  // clear rx data

  // get commands using BT functionality using Mobile Application
  boRetVal = rcvCommand(START_CHAR_TIME_OUT_BT_COMM);

  if (boRetVal == false) {
    // Displaying Failed Msg
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("No BT Command");
    lcd.setCursor(0, 1);
    lcd.print("Received");
  } else {
    // check if valid command is received
    if ((receivedCommand == sCMDLampON) || (receivedCommand == sCMDLampOFF) || (receivedCommand == sCMDFanON) || (receivedCommand == sCMDFanOFF)) {
      // transmits receivedCommand
      boRetVal = boSendData(receivedCommand);
      if (boRetVal == true) {
        // Displaying Sent Msg
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Cmd Rcvd & Sent");
        lcd.setCursor(0, 1);
        lcd.print(receivedCommand);
        delay(1000);

After the command is successfully transmitted through LoRa, the Arduino waits for a confirmation message from the target. The function chkReply() listens for incoming data from the LoRa module and checks whether the expected reply string “DONE” is received within the specified timeout period. If the correct reply is detected, the function returns true; otherwise, it returns false.

If the expected confirmation message is received from the target unit, the LCD displays “Reply Received:” on the first line and shows the reply message (in this case, “DONE”) on the second line. 

If no response is received, the LCD displays “No Reply Received”

  boRetVal = chkReply(expected_reply, REPLY_END_CHAR, WAIT_FOR_TARGET_REPLY);

        if (boRetVal == true) {
          // Displaying received Msg
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("Reply Received:");
          lcd.setCursor(0, 1);
          lcd.print(expected_reply);
        } else {
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("No Reply");
          lcd.setCursor(0, 1);
          lcd.print("Received");
        }

If the command could not be transmitted through the LoRa module, the LCD displays “Command Sending Failed”

If the received command from the mobile application does not match any of the predefined valid commands, it is considered invalid. In this case, the LCD displays “Invalid:” followed by the received command on the first line and “Command Received” on the second line. This helps the user identify incorrect commands sent from the mobile application.

 } else {
        // Displaying Failed Msg
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Command");
        lcd.setCursor(0, 1);
        lcd.print("Sending Failed");
      }
    } else {
      // Displaying Invalid Msg
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Invalid:");
      lcd.print(receivedCommand);
      lcd.setCursor(0, 1);
      lcd.print("Command Received");
    }
  }
}
Functions Related to LoRa Communication

The following functions are used to manage communication between the Arduino Nano and the RYLR999 LoRa module.

sendCrLf() Function sends a Carriage Return (CR) and Line Feed (LF) character to the LoRa module through the serial interface. These characters are required at the end of every AT command sent to the RYLR999 module.

flushBuffer() Function clears the serial receive buffer used for LoRa communication.

chkReply() Function checks whether a specific reply is received from the LoRa module within a defined time period. First, the function waits until at least one character becomes available in the serial buffer. The waiting process is controlled using a loop that decreases the timeout value every millisecond.

If data becomes available before the timeout expires, the function reads the incoming message using readStringUntil().

The received message is then checked to see whether it contains the expected string stored in chkString. If the expected text is found, the function returns true, indicating that the correct reply was received. Otherwise, it returns false.

void sendCrLf(void) {
  Serial.write(0x0D);  // Carriage Return
  Serial.write(0x0A);  // Line Feed
}

// clear receive buffer of LORA
void flushBuffer(void) {
  while (Serial.available() > 0) {
    Serial.read();
  }
}

// check data on rx pin of LORA functionality
bool chkReply(String chkString, char receiveUntil, unsigned int timeout) {
  String receivedString;       // save received data in this string object
  bool boReturnValue = false;  // function's return value

  // wait for reply
  do {
    timeout--;
    delay(1);  // delay of 1 ms
  } while ((Serial.available() < MIN_CHAR_TO_RCV) && (timeout > 0));

  if (timeout) {
    // if timeout is left then a reply is received check for the string in the reply
    receivedString = Serial.readStringUntil(receiveUntil);
    if (receivedString.indexOf(chkString) != -1) {
      boReturnValue = true;
    } else {
      boReturnValue = false;
    }
  } else {
    boReturnValue = false;
  }

  // return result
  return boReturnValue;
}

Next, we define the function to reset the RYLR999 module to its factory default configuration using AT command AT+FACTORY. The function declares a few variables for this process.

boReturnValue stores the result of the function (success or failure).

downCounter acts as a small counter while waiting for replies from the module.

receivedString is used to store any incoming response from the LoRa module.

When the module receives the reset command, it typically responds with two messages:

  • The first message, “+FACTORY” – indicates that the reset command has been accepted. 
  • The second message, “+READY” – indicates that the module has completed the reset and is ready to operate.

Next, the command AT+FACTORY is sent to the module through the serial interface. The sendCrLf() function then sends the required carriage return and line feed characters, which signal the end of the AT command.

The program then waits for the first response “+FACTORY” using the chkReply() function to confirm that the reset command was received.

If the response is received, the program then waits for the second message “+READY”, which confirms that the reset process has completed.

Finally, the function returns the value of boReturnValue. If both responses are received successfully, the function returns true, indicating that the LoRa module was reset properly. Otherwise, it returns false.

// Reset settings to factory defaults for LORA functionality
bool boRestoreFactoryDefaults(void) {
  const char factoryDefaultCmd[] = "AT+FACTORY";  // command to be sent
  bool boReturnValue = false;                     // function's return value
  char downCounter = 100;                         // Down counter to wait for reply
  String receivedString;                          // save received data in this string object

  String chkRcvString1 = "+FACTORY";
  String chkRcvString2 = "+READY";

  // send command
  Serial.print(factoryDefaultCmd);
  sendCrLf();

  // check first string in reply
  boReturnValue = chkReply(chkRcvString1, REPLY_END_CHAR, REPLY_TIMEOUT_IN_MS);
  if (boReturnValue == true) {
    // check second string in reply
    boReturnValue = chkReply(chkRcvString2, REPLY_END_CHAR, REPLY_TIMEOUT_IN_MS);
  }

  // return result
  return boReturnValue;
}

We use the function boSetAddress() to assign a unique address to the RYLR999 module. 

The command AT+ADDRESS= is used to configure the module address. The Arduino sends this command through the serial interface along with the value defined by SELF_ADDRESS. After sending the command, the function sendCrLf() adds the required carriage return and line feed characters.

After sending the command, the program waits for the module’s response. If the module responds with “+OK”, it means the address has been configured successfully. The function returns true if the correct response was received, otherwise it returns false.

// setting the address of LORA
bool boSetAddress(void) {
  const char setAddressCmd[] = "AT+ADDRESS=";  // command to be sent
  bool boReturnValue = false;                  // function's return value
  String chkRcvString = "+OK";

  // send command
  Serial.print(setAddressCmd);
  Serial.print(SELF_ADDRESS);
  sendCrLf();

  // check reply
  boReturnValue = chkReply(chkRcvString, REPLY_END_CHAR, REPLY_TIMEOUT_IN_MS);

  // return result
  return boReturnValue;
}
Sending Data Through LoRa

We use boSendData to send a message to another LoRa node.

The LoRa module requires the AT+SEND command to transmit data. This command includes three important parameters: the target’s address, the length of the data, and the actual message to be transmitted. 

The Arduino constructs this command by printing AT+SEND= followed by the target’s address (TARGET_ADDRESS), the length of the message (data.length()), and the message content itself. These values are separated by commas. 

After forming the complete command, the Arduino sends it to the module through the serial interface and calls the sendCrLf() function to terminate the command properly.

The program then waits for the module’s response using the chkReply() function. If the module returns “+OK”, it indicates that the data was transmitted successfully. In that case, the function returns true; otherwise, it returns false, indicating that the transmission failed.

// Send data to LORA in command form
bool boSendData(String data) {
  const char sendDataCmd[] = "AT+SEND=";  // command to be sent
  bool boReturnValue = false;             // function's return value
  String chkRcvString = "+OK";

  // send command
  Serial.print(sendDataCmd);
  Serial.print(TARGET_ADDRESS);
  Serial.print(',');
  Serial.print(data.length());
  Serial.print(',');
  Serial.print(data);
  sendCrLf();

  // check reply
  boReturnValue = chkReply(chkRcvString, REPLY_END_CHAR, REPLY_TIMEOUT_IN_MS);

  // return result
  return boReturnValue;
}
Receiving Commands from Bluetooth

rcvCommand is used to receive commands sent from the mobile application through the BLE interface of the RYLR999 module. It checks whether a valid command has been received within the specified timeout period.

The function returns true if a complete command is received successfully, otherwise it returns false.

Next, we declare a few variables:

  • boReturnValue stores the result of the function.
  • startTime is used to measure the time taken to receive the command, which helps detect timeout conditions.

The function first waits for the start character of a command. The loop continuously checks the Bluetooth serial buffer until the start character (*) is detected, or the timeout period expires.

If the start character is detected before the timeout expires, the program begins reading the command. The function readStringUntil() reads incoming data from the Bluetooth serial interface until the end character (#) is received.

The received data is stored in the variable receivedCommand.

If the end character (#) is received within the specified timeout, the command is considered valid and boReturnValue is set to true.

If the end character is not received within the specified time, the command is considered incomplete, and the function returns false.

Finally, the function returns the value of boReturnValue. It returns true if a valid command was received and false if no command was received.

// Receive Command from Rx pin of Bluetooth functionality
bool rcvCommand(unsigned int timeout) {
  bool boReturnValue = false;  // function's return value
  unsigned long startTime;

  // wait for reply
  do {
    timeout--;
    delay(1);  // delay of 1 ms
    if (((btSerial.available() >= MIN_CHAR_TO_RCV) && (btSerial.read() == START_CHAR_BT_COMM)) || (timeout == 0)) {
      break;
    }
  } while (1);

  // if start character is received within time then timeout will be greater than or equal to 1
  if (timeout) {
    startTime = millis();
    receivedCommand = btSerial.readStringUntil(END_CHAR_BT_COMM);
    if ((millis() - startTime) <= END_CHAR_TIME_OUT_BT_COMM) {
      boReturnValue = true;  // it means in readStringUntil() function '#' is received
    } else {
      boReturnValue = false;  // it means in readStringUntil() function timeout has occurred
    }
  } else {
    boReturnValue = false;  // it means get character timeout has occured
  }

  // return result
  return boReturnValue;
}

Arduino Code for Target 

In this code, the target Arduino receives the commands over LoRa and controls the electrical appliances as per commands received using relay module.

/*
Code to receive commands to control the appliances from another Arduino using RLYR999 Module and send "DONE" in reply after implementing the required task and display the communication and tasks over I2C LCD.
by playwithcircuit.com
*/
#include <LiquidCrystal_I2C.h>

// below MACROS are for LORA communication
#define REPLY_TIMEOUT_IN_MS 300
#define REPLY_END_CHAR '\n'
#define CMD_END_CHAR '\n'
#define MODULE_ADDRESS 1
#define CONTROLLER_ADDRESS 0
#define MIN_CHAR_TO_RCV 1
#define WAIT_FOR_REQUEST 3000

// these four commands can be received from controller module
String sCMDLampON = "L1";
String sCMDLampOFF = "L0";
String sCMDFanON = "F1";
String sCMDFanOFF = "F0";

// save received command in this global string object
String receivedCommand;

// Init LCD at 0x27, 16x2
LiquidCrystal_I2C lcd(0x27, 16, 2);

// Appliance Control MACROS
#define LAMP_CTRL_PIN 11
#define FAN_CTRL_PIN 12

#define LAMP_ON() digitalWrite(LAMP_CTRL_PIN, 0)
#define LAMP_OFF() digitalWrite(LAMP_CTRL_PIN, 1)
#define FAN_ON() digitalWrite(FAN_CTRL_PIN, 0)
#define FAN_OFF() digitalWrite(FAN_CTRL_PIN, 1)

void setup() {
  boolean boRetVal = false;
  // begin serial communication at baud 115200,n,8,1
  // to communicate with the RF module
  Serial.begin(115200);

  // Initialize the LCD
  lcd.init();
  // Turn ON the Backlight
  lcd.backlight();
  // Clear the display buffer
  lcd.clear();

  // initialize appliances pins and turn off
  pinMode(LAMP_CTRL_PIN, OUTPUT);
  pinMode(FAN_CTRL_PIN, OUTPUT);
  LAMP_OFF();
  FAN_OFF();

  receivedCommand.reserve(50);  // prevents fragmentation, as multiple times data shall be received in this string

  delay(1000);

  flushBuffer();  // clear rx data

  // Reset settings to factory defaults
  boRetVal = boRestoreFactoryDefaults();

  // setting the address if reset successfully
  if (boRetVal == true) {
    flushBuffer();  // clear rx data
    boRetVal = boSetAddress();
  }

  if (boRetVal == true) {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Module Init");
    lcd.setCursor(0, 1);
    lcd.print("Successful");
    delay(1000);
  } else {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Module Init");
    lcd.setCursor(0, 1);
    lcd.print("Failed");
    while (1)
      ;
  }
}

void loop() {
  String expected_reply = "DONE";
  bool boRetVal = false;

  // check string sent by controller
  boRetVal = rcvCommand(CMD_END_CHAR, WAIT_FOR_REQUEST);

  if (boRetVal == false) {
    // Displaying Failed Msg
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("No Command");
    lcd.setCursor(0, 1);
    lcd.print("Received");
  } else {
    // check if valid command is received
    if ((receivedCommand == sCMDLampON) || (receivedCommand == sCMDLampOFF) || (receivedCommand == sCMDFanON) || (receivedCommand == sCMDFanOFF)) {
      // Implement receivedCommand
      vImplementTask(receivedCommand);
      // Displaying Sent Msg
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Command Received");
      lcd.setCursor(0, 1);
      lcd.print("Task Done");
      delay(1000);
      // transmits receivedCommand
      boRetVal = boSendData(expected_reply);
      if (boRetVal == true) {
        // Displaying Sent Msg
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Reply Sent");
      } else {
        // Displaying Failed Msg
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Reply");
        lcd.setCursor(0, 1);
        lcd.print("Sending Failed");
      }
      delay(1000);
    } else {
      // Displaying Invalid Msg
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Invalid:");
      lcd.print(receivedCommand);
      lcd.setCursor(0, 1);
      lcd.print("Command Received");
    }
  }
}

void sendCrLf(void) {
  Serial.write(0x0D);  // Carriage Return
  Serial.write(0x0A);  // Line Feed
}

void flushBuffer(void) {
  while (Serial.available() > 0) {
    Serial.read();
  }
}

bool rcvCommand(char receiveUntil, unsigned int timeout) {
  bool boReturnValue = false;  // function's return value
  unsigned long startTime;
  int firstComma;
  int secondComma;
  int thirdComma;
  String data;
  // wait for reply
  do {
    timeout--;
    delay(1);  // delay of 1 ms
  } while ((Serial.available() < MIN_CHAR_TO_RCV) && (timeout > 0));

  if (timeout) {
    startTime = millis();
    // if timeout is left then a reply is received check for the string in the reply
    data = Serial.readStringUntil(receiveUntil);
    if ((millis() - startTime) <= timeout) {
      boReturnValue = true;  // it means in readStringUntil() function receiveUntil character is received
      // as the data will be in form of "+RCV=0,2,F1,-88,11" we need to extract command between second and third comma
      firstComma = data.indexOf(',');
      secondComma = data.indexOf(',', firstComma + 1);
      thirdComma = data.indexOf(',', secondComma + 1);
      // extract value between 2nd and 3rd comma
      receivedCommand = data.substring(secondComma + 1, thirdComma);
    } else {
      boReturnValue = false;  // it means in readStringUntil() function timeout has occurred
    }
  } else {
    boReturnValue = false;  // it means get character timeout has occured
  }

  // return result
  return boReturnValue;
}

bool chkReply(String chkString, char receiveUntil, unsigned int timeout) {
  String receivedString;       // save received data in this string object
  bool boReturnValue = false;  // function's return value

  // wait for reply
  do {
    timeout--;
    delay(1);  // delay of 1 ms
  } while ((Serial.available() < MIN_CHAR_TO_RCV) && (timeout > 0));

  if (timeout) {
    // if timeout is left then a reply is received check for the string in the reply
    receivedString = Serial.readStringUntil(receiveUntil);
    if (receivedString.indexOf(chkString) != -1) {
      boReturnValue = true;
    } else {
      boReturnValue = false;
    }
  } else {
    boReturnValue = false;
  }

  // return result
  return boReturnValue;
}

bool boRestoreFactoryDefaults(void) {
  const char factoryDefaultCmd[] = "AT+FACTORY";  // command to be sent
  bool boReturnValue = false;                     // function's return value
  char downCounter = 100;                         // Down counter to wait for reply
  String receivedString;                          // save received data in this string object

  String chkRcvString1 = "+FACTORY";
  String chkRcvString2 = "+READY";

  // send command
  Serial.print(factoryDefaultCmd);
  sendCrLf();

  // check first string in reply
  boReturnValue = chkReply(chkRcvString1, REPLY_END_CHAR, REPLY_TIMEOUT_IN_MS);
  if (boReturnValue == true) {
    // check second string in reply
    boReturnValue = chkReply(chkRcvString2, REPLY_END_CHAR, REPLY_TIMEOUT_IN_MS);
  }

  // return result
  return boReturnValue;
}

bool boSetAddress(void) {
  const char setAddressCmd[] = "AT+ADDRESS=";  // command to be sent
  bool boReturnValue = false;                  // function's return value
  String chkRcvString = "+OK";

  // send command
  Serial.print(setAddressCmd);
  Serial.print(MODULE_ADDRESS);
  sendCrLf();

  // check reply
  boReturnValue = chkReply(chkRcvString, REPLY_END_CHAR, REPLY_TIMEOUT_IN_MS);

  // return result
  return boReturnValue;
}

bool boSendData(String data) {
  const char sendDataCmd[] = "AT+SEND=";  // command to be sent
  bool boReturnValue = false;             // function's return value
  String chkRcvString = "+OK";

  // send command
  Serial.print(sendDataCmd);
  Serial.print(CONTROLLER_ADDRESS);
  Serial.print(',');
  Serial.print(data.length());
  Serial.print(',');
  Serial.print(data);
  sendCrLf();

  // check reply
  boReturnValue = chkReply(chkRcvString, REPLY_END_CHAR, REPLY_TIMEOUT_IN_MS);

  // return result
  return boReturnValue;
}

// this function is used to control the relay which controls the appliances using digital pins
void vImplementTask(String data) {
  if (receivedCommand == sCMDLampON) {
    LAMP_ON();
  } else if (receivedCommand == sCMDLampOFF) {
    LAMP_OFF();
  } else if (receivedCommand == sCMDFanON) {
    FAN_ON();
  } else if (receivedCommand == sCMDFanOFF) {
    FAN_OFF();
  }
}

Code Explanation

In this code most of the LoRa configuration and communication functions used —such as module initialization, setting the address, sending data, checking replies, and clearing the serial buffer—are similar to those explained in the controller code. Therefore, in this code we focus only on the parts that are specific to the target functionality, particularly appliance control and command parsing.

Appliance Control Configuration

First, we define macros for Arduino pins used to control the relay module.

Pin 11 controls the relay connected to the bulb (lamp), while pin 12 controls the relay connected to the fan.

Now, to simplify appliance control operations, we will define macros that act as short commands for switching appliances ON or OFF. Since the relay module used in this project is active-low, writing 0 activates the relay and turns the appliance ON, while writing 1 deactivates the relay and turns it OFF.

#define LAMP_CTRL_PIN 11
#define FAN_CTRL_PIN 12

#define LAMP_ON() digitalWrite(LAMP_CTRL_PIN, 0)
#define LAMP_OFF() digitalWrite(LAMP_CTRL_PIN, 1)
#define FAN_ON() digitalWrite(FAN_CTRL_PIN, 0)
#define FAN_OFF() digitalWrite(FAN_CTRL_PIN, 1)
Setup Function

In the setup() function hardware initialization is similar to the controller side. The only additional functionality in the target side is the initialization of the appliance control pins.

Here, both relay control pins are configured as output pins. Immediately after initialization, the program turns both appliances OFF to ensure that no load is activated unintentionally when the system starts.

The remaining steps in the setup function—such as clearing buffers, resetting the LoRa module, and setting the module address—operate exactly the same way as explained in the controller code.

  // initialize appliances pins and turn off
  pinMode(LAMP_CTRL_PIN, OUTPUT);
  pinMode(FAN_CTRL_PIN, OUTPUT);
  LAMP_OFF();
  FAN_OFF();
Loop Function

The loop() function performs three main tasks:

  • Receive commands from the controller node
  • Execute the requested appliance control task
  • Send a confirmation reply back to the controller

The target continuously listens for incoming LoRa data using the rcvCommand() function. This function reads data from the LoRa module and extracts the command embedded inside the received packet.

If no command is received within the specified timeout period, the LCD displays a message indicating that no command was received.

// check string sent by controller
  boRetVal = rcvCommand(CMD_END_CHAR, WAIT_FOR_REQUEST);

If the received command matches one of the predefined valid commands (L1, L0, F1, or F0), the function vImplementTask() is called. This function activates or deactivates the appropriate relay to control the connected appliance.

 // Implement receivedCommand
      vImplementTask(receivedCommand);

After completing the requested task, the target sends a response back to the controller.

The target sends the message “DONE” through the LoRa module to indicate that the command has been successfully implemented. The LCD also displays the transmission status of this confirmation message.

The LoRa module sends received data in the following format:

+RCV=<address>,<length>,<data>,<RSSI>,<SNR>

For example:

+RCV=0,2,F1,-88,11

To extract the actual command (F1 in this case), the program identifies the positions of the commas in the received string.

The command is located between the second and third comma, so the program extracts that command portion using the below code which is present in function rcvCommand(). This extracted command is then used to determine which appliance should be controlled.

// as the data will be in the form of "+RCV=0,2,F1,-88,11" we need to extract command between second and third comma.
This extracted command is then used to determine which appliance should be controlled.
      firstComma = data.indexOf(',');
      secondComma = data.indexOf(',', firstComma + 1);
      thirdComma = data.indexOf(',', secondComma + 1);
      // extract value between 2nd and 3rd comma
      receivedCommand = data.substring(secondComma + 1, thirdComma);
    } 
Appliance Control Function

This function performs the actual relay switching operation based on the received command.

If the command corresponds to turning the bulb or fan ON or OFF, the function calls the corresponding macros (LAMP_ON(), LAMP_OFF(), FAN_ON(), or FAN_OFF()). These macros control the digital output pins connected to the relay module.

As a result, the relay switches the connected appliance ON or OFF according to the received command.

// this function is used to control the relay which controls the appliances using digital pins
void vImplementTask(String data) {
  if (receivedCommand == sCMDLampON) {
    LAMP_ON();
  } else if (receivedCommand == sCMDLampOFF) {
    LAMP_OFF();
  } else if (receivedCommand == sCMDFanON) {
    FAN_ON();
  } else if (receivedCommand == sCMDFanOFF) {
    FAN_OFF();
  }
}

Installing the LightBlue BLE application

To send commands from the smartphone to the controller node, we use the LightBlue BLE mobile application. This application allows us to connect to BLE-enabled devices and manually send data.

Install the LightBlue BLE app on your Android device from the Google Play Store. Follow the steps shown in the video below to connect the mobile app to the RYLR999 module and send commands to the system.

Testing the System

Now, since the hardware setup is complete and the code has been uploaded to both Arduino boards, the entire system can be tested.

Power both the controller and target circuits. Open the LightBlue BLE application on your smartphone and connect to the RYLR999 module.

During testing, observe the LCD displays on both sides and the relay module connected to the target unit. When a command is sent from the smartphone, the controller side LCD should indicate that the command has been received and transmitted. The target side LCD should display the received command and confirm that the task has been executed.

At the same time, observe the relay module and the connected appliances to verify that the correct device is being switched ON or OFF. After executing the command, the target sends a confirmation message back to the controller node.

The video below shows the complete testing process:

Note

Make sure that the supply wire (Vcc) of the LoRa module is disconnected from 5V before writing the code into Arduino because the pins Tx and Rx which are used to flash the code into Arduino are the same pins which are used to communicate with the module, hence it creates problem while writing code to Arduino. After flashing the code reconnect the Vcc of Module to 5V and reset the Arduino.

Troubleshooting Guide

If the system does not work as expected, check the following points:

Issue 1:  LoRa Modules Not Communicating

Ensure that the controller and target modules have different addresses and that the correct address is used when sending data. Also verify that both modules are powered properly and connected to the correct RX and TX pins.

Issue 2: No Command Received on Target

If the target LCD shows “No Command Received”, check whether the LoRa modules are correctly initialized. Also confirm that both modules are configured with the correct baud rate and communication settings.

Issue 3: BLE Device Not Visible in LightBlue App

Make sure the controller unit is powered and the BLE functionality of the RYLR999 module is active. If the device does not appear in the list, try restarting the module or refreshing the scan in the LightBlue application.

Issue 4: Relay Not Switching the Appliance

Verify that the relay module receives power from the Arduino and connected to the correct Arduino pins. Also check the external wiring of the relay connected to the appliance.

Issue 5: LCD Not Displaying Any Text

Check the I2C connections (SDA and SCL) between the LCD and Arduino. Also confirm that the LCD address defined in the code matches the actual I2C address of the display.

Issue 6: The mobile application writes value but it is not received in Controller setup.

Make sure the mobile application is connected to the correct BLE as when both the controller and target are powered up, the mobile shows the BLE modules of both the controller and target when both are in range. Hence first turn ON the contoller circuit search for REYAX module and connect with it (also write the MAC id of module for future reference) then turn ON the target setup. Later from the MAC addresses we can find we are connected with the right module or not.

FAQ’S

Why is LoRa used instead of Wi-Fi or Bluetooth?

LoRa is designed for long-range and low-power communication, making it suitable for applications where devices are located far apart or where internet connectivity is not available.

Can more appliances be added to this system?

Yes. Additional appliances can be controlled by connecting more relay channels and defining additional commands in the code.

Can the smartphone control the target directly?

No. The smartphone communicates with the controller node through BLE, and the controller forwards the commands to the receiver using LoRa communication.

What frequency does the RYLR999 LoRa module operate on?

The module typically operates in the frequency range of 820-960 MHz.