
In this tutorial we will discuss Node-to-Node LoRa network communication.
Introduction
A node in our context consists of the following:
- A LoRa module
- A microcontroller development board
- A power source
- Any extras such as additional sensors, displays, modules, actuators, etc.
The LoRa module in our application is the PTSolns LoRa SX1276 915MHz Shield. The shield is stacked onto a compatible Uno development board, in this case the PTSolns Uno R3+. The power source can be a wall adapter, a 5V battery pack, or a similar supply. No additional sensors or peripherals are required since the onboard AHT20 sensor will be used to measure temperature and include it in the transmitted payload.
This tutorial is configured for 915MHz operation for North America.
When LoRa devices communicate directly with each other without a gateway, as demonstrated in this tutorial, the setup is referred to as a Node-to-Node LoRa Network, as shown below.

There are multiple ways to implement simple data transmission between two nodes using one way communication. The simplest approach has the transmitter continuously sending payloads without checking whether the receiver successfully received them. This approach requires simpler code but is less robust, as some payloads may be lost.
The second approach, which is used in this tutorial and the accompanying code, implements an acknowledgement mechanism. After transmitting a payload, the transmitter waits for an acknowledgement from the receiver. If no acknowledgement is received within a defined timeout, the same payload is retransmitted. This retry process occurs a specified number of times before the transmitter gives up and moves on to sending a new payload with updated sensor data.
This acknowledgement based approach is more robust, at the cost of slightly more complex code, although the increase in complexity is minimal. Both approaches are valid depending on the application, but this tutorial focuses on the acknowledgement method.
In this example, the transmitter sends a temperature reading and the receiver displays the received value in the Serial Monitor. No display hardware is required. The receiver also outputs the signal to noise ratio (SNR) and the received signal strength indicator (RSSI).
SNR compares the received signal level to the background noise and indicates how reliably the LoRa data can be decoded, particularly at long range or low signal levels. RSSI represents the absolute received signal strength at the receiver and provides a general indication of link strength independent of noise.
Once the network is set up and running, readers are encouraged to experiment. Observe how SNR and RSSI change as the nodes are moved farther apart, when an antenna is removed, or when a higher gain antenna is used.
Hardware
The following hardware is used in this tutorial:
- 2 pcs PTSolns LoRa SX1276 915MHz Shield
- 2 pcs PTSolns Uno R3+
Of course we will need two USB cables capable to transfer data.
The LoRa shields are simply stacked onto the Uno boards. No additional wiring is required and the hardware is ready to be programmed.
Software
The Uno boards are programmed using PTSolns IDE, although other compatible IDEs may also be used. To begin, open the application and select the PTSolns Uno R3+ from the Board Manager. Then select the correct port corresponding to the connected board.
The same sketch is used for both the transmitter and the receiver. A single line near the top of the sketch around lines 25 to 26 is commented or uncommented depending on which role is being programmed.
Two additional libraries are required and must be installed prior to uploading the sketch. These can be installed directly from the Library Manager within PTSolns IDE:
- LoRa.h: Used to initialize the LoRa module and handle payload transmission and reception.
- PTSolns_AHx.h: Used to initialize the onboard AHT20 sensor and retrieve temperature data. Although this tutorial only uses temperature, humidity data can easily be added to the payload if desired.
// Example: LoRa-to-LoRa Network
// Last Update: Jan 3, 2026
// Author: PTSolns
//
// DESCRIPTION
// This example shows a LoRa-to-LoRa network application.
// The transmitter node sends temperature data taken from the onboard AHT20 sensor
// The transmitter sends the message, and waits for an ACK from the receiver. It attempts 5 times before sending a new payload.
// The Receiver sends an ACK when it has received a payload, and prints this to Serial Monitor.
// Also included are SNR and RSSI info as well as a counter.
//
// HARDWARE
// Stack the PTSolns LoRa SX1276 915MHz Shield onto a PTSolns Uno R3+ (or similar dev board)
// Make sure to use USB cables that are data transfer capable.
//
// INSTRUCTIONS
// Comment/uncomment lines 25 and 26 depending if you're programming TX or RX
#include <SPI.h>
#include <Wire.h>
#include <LoRa.h>
#include <PTSolns_AHTx.h>
// Select TX or RX Mode
//#define COMPILE_TX
#define COMPILE_RX
// LoRa configuration parameters
const long LORA_FREQUENCY = 915E6;
const int LORA_TX_POWER = 20;
const int LORA_SPREADING_FACTOR = 11;
const long LORA_BANDWIDTH = 125E3;
const int LORA_CODING_RATE = 8;
const int LORA_SYNC_WORD = 0x12;
unsigned long previousMillis = 0;
const unsigned long interval = 60000;
const unsigned long ack_time = 5000;
PTSolns_AHTx aht;
#ifdef COMPILE_TX
int retryCount = 0;
const int maxRetries = 5;
bool ackReceived = false;
int counterTx = 0;
#endif
void setup() {
Serial.begin(9600);
while (!Serial) {}
Serial.println("LoRa-to-LoRa Network Example");
LoRa.setPins(10, 9, 2);
if (!LoRa.begin(LORA_FREQUENCY)) {
Serial.println("LoRa init failed!");
while (1) {}
}
if ((LORA_SPREADING_FACTOR == 12) || (LORA_TX_POWER == 20)) {
LoRa.setTxPower(LORA_TX_POWER, 1);
} else {
LoRa.setTxPower(LORA_TX_POWER);
}
LoRa.setSpreadingFactor(LORA_SPREADING_FACTOR);
LoRa.setSignalBandwidth(LORA_BANDWIDTH);
LoRa.setCodingRate4(LORA_CODING_RATE);
LoRa.setSyncWord(LORA_SYNC_WORD);
LoRa.enableCrc();
LoRa.receive();
Serial.println("LoRa init OK");
#ifdef COMPILE_TX
if (!aht.begin()) {
Serial.println("AHT20 not found!");
while (1) {}
}
#endif
}
#ifdef COMPILE_TX
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis < interval) {
return;
}
previousMillis = currentMillis;
counterTx++;
float temperature = 0.0f;
float humidity = 0.0f;
AHTxStatus st = aht.readTemperatureHumidity(temperature, humidity, 120);
(void)st;
String payload = String(counterTx) + "," + String(temperature, 1);
Serial.print("Sending: ");
Serial.println(payload);
retryCount = 0;
ackReceived = false;
while (!ackReceived && retryCount < maxRetries) {
LoRa.idle();
LoRa.beginPacket();
LoRa.print(payload);
LoRa.endPacket();
LoRa.receive();
Serial.println("Packet sent, waiting for ACK...");
unsigned long startWait = millis();
while (millis() - startWait < ack_time) {
int packetSize = LoRa.parsePacket();
if (packetSize > 0) {
String incoming = "";
while (LoRa.available()) {
incoming += (char)LoRa.read();
}
if (incoming == "ACK") {
Serial.println("ACK received");
ackReceived = true;
break;
}
}
}
if (!ackReceived) {
retryCount++;
Serial.print("Retry ");
Serial.println(retryCount);
}
}
if (!ackReceived) {
Serial.println("Failed to receive ACK after 5 attempts.");
}
}
#endif
#ifdef COMPILE_RX
void loop() {
int packetSize = LoRa.parsePacket();
if (packetSize <= 0) {
return;
}
float snr = LoRa.packetSnr();
int rssi = LoRa.packetRssi();
String incoming = "";
while (LoRa.available()) {
incoming += (char)LoRa.read();
}
int sep = incoming.indexOf(',');
if (sep < 0) {
Serial.print("Received malformed payload: ");
Serial.println(incoming);
LoRa.receive();
return;
}
int counter = incoming.substring(0, sep).toInt();
float temperature = incoming.substring(sep + 1).toFloat();
String newLine = "Counter = " + String(counter) + ", " + String(temperature, 1) + " C";
Serial.print("Received packet | SNR: ");
Serial.print(snr, 1);
Serial.print(" dB | RSSI: ");
Serial.print(rssi);
Serial.println(" dBm");
Serial.print("Received: ");
Serial.println(newLine);
LoRa.idle();
LoRa.beginPacket();
LoRa.print("ACK");
LoRa.endPacket();
Serial.println("ACK sent");
LoRa.receive();
}
#endif
Conclusion
With the LoRa-to-LoRa Network functioning, the Serial Monitor output on the receiver side should look similar to the following:

This example can be expanded in several ways, including adding the humidity reading to the payload, and also upgrading from one-way communication to two-way communication so that both LoRa nodes are sending data back and worth.