Wireless Appliance Control From Mobile Using Arduino and LoRa

by Rachana Jain in Circuits > Arduino

897 Views, 5 Favorites, 0 Comments

Wireless Appliance Control From Mobile Using Arduino and LoRa

Long Range Arduino-based Wireless Appliance Control using Mobile.jpg

Want to control a light or home appliance located hundreds of meters or even kilometers away using only your smartphone. No Wi-Fi routers, no internet connection, and no complicated networking.

That’s exactly what LoRa communication makes possible.

In this project we build a long-range wireless appliance control system using two Arduino Nano boards and the RYLR999 LoRa + BLE module. A smartphone sends commands via Bluetooth, which are then transmitted over LoRa to control electrical loads such as a lamp or fan remotely.

The best part is that LoRa allows reliable communication over kilometers while consuming very little power.

Let’s build it.

Supplies

Hardware

  1. Arduino Nano (2x)
  2. RYLR999 LoRa + BLE module (2x)
  3. 3.3V–5V bidirectional voltage level shifter
  4. 16×2 I2C LCD display
  5. 2-channel relay module
  6. 12V DC fan
  7. 240V AC bulb
  8. Jumper wires
  9. 12V power supply

Software

  1. Arduino IDE
  2. LiquidCrystal_I2C library
  3. LightBlue BLE mobile application

How the System Works

This project is divided into two main sections.

Controller Unit (User Side)

The controller unit acts as the interface between the smartphone and the LoRa network.

A smartphone connects to the RYLR999 module using Bluetooth Low Energy (BLE). When a command is sent from the mobile app, the Arduino reads the command and forwards it to the LoRa transmitter.

The LoRa module then sends the command wirelessly over long distances.

Target Unit (Appliance Side)

The target unit receives the LoRa transmission.

Another RYLR999 module connected to a second Arduino Nano receives the command and passes it to the microcontroller. The Arduino then interprets the instruction and activates a relay module to control electrical appliances.

In this example, the system controls:

  1. A 240V AC bulb
  2. A 12V DC fan

After executing the command, the target sends a confirmation message back to the controller, which is displayed on the LCD.

Why a Relay Is Required

Arduino boards operate at low voltage (5V) and can supply only a small amount of current through their digital pins.

However, most electrical appliances operate at much higher voltages and currents, such as 220V AC or 12V DC motors.

Directly connecting such loads to a microcontroller would damage the board and could be dangerous.

A relay module solves this problem by acting as an electrically controlled switch. The Arduino sends a small control signal to the relay, and the relay safely switches the higher-voltage load.

This allows a microcontroller to control real-world appliances while keeping the control electronics isolated from high-power circuits.

Controller Wiring

Wiring Connections for the Controller Setup.png
20260303_152550.jpg

The controller unit contains:

  1. Arduino Nano
  2. RYLR999 LoRa + BLE module
  3. Voltage level shifter
  4. I2C LCD

The RYLR999 communicates with Arduino using UART serial communication. Since the LoRa module operates at 3.3V logic while Arduino works at 5V, a bidirectional level shifter is required between them.

Power is supplied from the Arduino’s 5V rail to the module.

The LoRa transmit and receive pins connect to the Arduino serial interface through the voltage shifter, allowing commands to be transmitted over long-range radio.

For Bluetooth communication, an additional serial interface is created in the Arduino code using SoftwareSerial, allowing BLE commands from the smartphone to be received independently.

The LCD is connected through the I2C interface, using Arduino pins A4 and A5 for SDA and SCL.

During operation the LCD displays:

  1. Module initialization
  2. Received commands
  3. Transmission status
  4. Confirmation messages from the target unit

This feedback makes debugging and system monitoring much easier.

Target Wiring

Wiring Target Arduino Nano with RYLR999 Module.png
20260303_152744.jpg

The target unit contains:

  1. Arduino Nano
  2. RYLR999 LoRa module
  3. Voltage level shifter
  4. Relay module
  5. LCD display

The wiring between the Arduino and the LoRa module is similar to the controller setup, except the BLE interface is not used here.

Instead, the module continuously listens for incoming LoRa messages.

When a message is received, the Arduino parses the command and triggers the relay outputs.

Two relay channels are used in this example:

  1. Relay channel 1 (Pin D11) – Controls the AC bulb
  2. Relay channel 2 (Pin D12) – Controls the DC fan

When a relay is activated, its internal contacts close and complete the electrical circuit powering the appliance.

This setup allows the Arduino to safely control both AC and DC loads.

⚠️ Safety note:
Always disconnect the AC mains supply while wiring the relay circuit.


Command Format Used in the System

The smartphone sends simple commands to the controller through the BLE app.

Example commands:

*L1# Turn bulb ON
*L0# Turn bulb OFF
*F1# Turn fan ON
*F0# Turn fan OFF

The controller extracts the payload and forwards it through LoRa.

The receiver interprets the commands and activates the corresponding relay.

After executing the command, the receiver sends a confirmation message back.

Example response:

DONE


Testing the System

Long Range Arduino based Wireless Appliance Control System using Mobile

To test the project:

  1. Power both Arduino boards.
  2. Open the LightBlue BLE app on your smartphone.
  3. Connect to the RYLR999 module.
  4. Send the command strings manually.
  5. Observe the LCD displays on both units.

You should see:

  1. Command received on controller
  2. Command transmitted over LoRa
  3. Command received on target
  4. Appliance activated
  5. Confirmation response displayed

With proper antenna placement, LoRa communication can easily reach several kilometers depending on the environment.

Arduino Code for Controller

/*
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;
}

Arduino Code for Target

/*
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();
}
}

Full Code and Detailed Explanation

The complete Arduino source code, command structure explanation, and troubleshooting guide are available in the full tutorial:

👉 Full detailed tutorial:

https://playwithcircuit.com/long-range-appliance-control-from-smartphone-using-arduino/