Review: REYAX RYLR998 LoRa Modules
When building IoT systems, your choice of wireless protocol can make or break the project. WiFi is ubiquitous but power-hungry and range-limited; Bluetooth is great for short distances but fails through walls. After testing the REYAX RYLR998 LoRa modules using ESP32 nodes and ESPHome, I’ve found a middle ground that offers incredible range and reliability without the overhead of a traditional network.

Transparency Note: REYAX provided two RYLR998 modules for evaluation. As always, all opinions are our own. REYAX had no editorial input and did not see this review prior to publication.
Table of Contents
Simplicity Meets Functionality
The RYLR998 immediately stands out due to its straightforward AT command interface. While many LoRa modules require complex libraries or “firmware wizardry,” REYAX uses a clean, predictable UART structure at 115200 baud.
The module handles the heavy lifting – complex modulation, error correction, and transmission timing – internally. This frees up your microcontroller (like an ESP32 or Arduino) to focus on your application logic rather than managing a radio stack.
The test setup
To see how these modules perform in the real world, I configured two independent ESP32 Devkit v1 nodes.
- Node A (Address 100): The transmitter/receiver.
- Node B (Address 200): The remote node.
- Network ID: 18 (both nodes must match to communicate).
The integration relies on ESPHome’s custom UART components and lambda functions to construct AT commands on the fly. When Node 100 sends a message, it formats a string, calculates the length, and pushes it through the UART interface. The receiving node then parses the incoming string to extract the message, RSSI (Received Signal Strength Indicator), and SNR (Signal-to-Noise Ratio).
Real-world performance: Urban testing
In a dense urban environment—filled with houses, trees, and competing signals—I successfully bridged a distance of 300 meters without a single dropped packet. What makes this impressive is that it was achieved with:
- Stock Antennas: No high-gain modifications.
- Default Power Settings: Optimized for energy efficiency.
- Low Power Draw: Minimal impact on battery-operated nodes.
The link margins remained healthy throughout the test, suggesting that in a “line-of-sight” rural environment, these modules could easily reach several kilometers.
Configuration & code
In ESPHome, timing is everything. I implemented a boot sequence to ensure the RYLR998 is fully initialized before the system starts polling for data.
1. Initialization
YAML
on_boot:
priority: 100.0
then:
- delay: 5s
- uart.write: "AT\r\n"
- delay: 0.5s
- uart.write: "AT+ADDRESS=100\r\n"
- delay: 0.5s
- uart.write: "AT+NETWORKID=18\r\n"The 0.5-second delay is critical; it allows the module to stabilize before receiving configuration commands.
2. Sending messages via HA services
I created a custom service that allows Home Assistant to trigger a LoRa transmission easily:
services:
- service: send_lora_message
variables:
dest_address: int
message: string
then:
- lambda: |-
int len = message.length();
char cmd[100];
snprintf(cmd, sizeof(cmd), "AT+SEND=%d,%d,%s\r\n", dest_address, len, message.c_str());
id(lora_uart).write_str(cmd);
ESP_LOGI("LoRa", "Sending to %d: %s", dest_address, message.c_str());
3. Receiving messages
This ESPHome lambda code functions as a custom serial parser for the RYLR998. It runs every 500ms to check for incoming messages, extracts specific data fields, and pushes that data to ESPHome sensors.
interval:
- interval: 500ms
then:
- lambda: |-
static std::string uart_buffer;
while (id(lora_uart).available()) {
uint8_t byte;
if (id(lora_uart).read_byte(&byte)) {
uart_buffer += (char)byte;
if (byte == '\n') {
std::string data = uart_buffer;
uart_buffer = "";
ESP_LOGD("LoRa", "Received line: %s", data.c_str());
if (data.find("+RCV=") != std::string::npos) {
size_t pos = data.find("+RCV=");
data = data.substr(pos + 5);
data.erase(data.find_last_not_of("\r\n") + 1);
ESP_LOGD("LoRa", "Parsing: %s", data.c_str());
int field = 0;
std::string src_addr, data_len, message, rssi, snr;
std::string current = "";
for (size_t i = 0; i < data.length(); i++) {
if (data[i] == ',') {
if (field == 0) src_addr = current;
else if (field == 1) data_len = current;
else if (field == 2) message = current;
else if (field == 3) rssi = current;
current = "";
field++;
} else {
current += data[i];
}
}
if (field == 4) snr = current;
if (!src_addr.empty()) {
id(lora_src_address).publish_state(src_addr);
}
if (!message.empty()) {
id(lora_message).publish_state(message);
}
if (!rssi.empty()) {
char *endptr;
float rssi_val = strtof(rssi.c_str(), &endptr);
if (endptr != rssi.c_str()) {
id(lora_rssi).publish_state(rssi_val);
}
}
if (!snr.empty()) {
char *endptr;
float snr_val = strtof(snr.c_str(), &endptr);
if (endptr != snr.c_str()) {
id(lora_snr).publish_state(snr_val);
}
}
ESP_LOGI("LoRa", "Message from %s: %s (RSSI: %s dBm, SNR: %s dB)",
src_addr.c_str(), message.c_str(), rssi.c_str(), snr.c_str());
}
}
}
}Integration with Home Assistant
Once the ESPHome nodes are flashed, the RYLR998 modules become first-class citizens in Home Assistant.
Incoming data populates text sensors, while signal quality (RSSI/SNR) appears as diagnostic sensors. This allows for powerful automations, such as:
- Triggering a notification if a remote gate (too far for WiFi) is opened.
- Logging soil moisture from a sensor at the edge of a large property.
- Monitoring the health of the wireless link via the SNR values.

Conclusion
The REYAX RYLR998 offers exceptional value for anyone needing long-range communication with minimal complexity. Whether you are building a remote sensor network or a node-to-node messaging system, these modules deliver where WiFi fails.
The Verdict: If you need a “set it and forget it” LoRa solution that integrates cleanly with ESPHome, the RYLR998 belongs on your shortlist.
You wanna get one, or maybe two? 🙂
If you use the link below, we get a small commission – it doesn’t cost you a penny extra!
Full node configuration example:
esphome:
name: esp32-lora-01
friendly_name: esp32-lora-01
platformio_options:
board_build.f_cpu: 240000000L
on_boot:
priority: 100.0
then:
- delay: 5s
- uart.write: "AT\r\n"
- delay: 0.5s
- uart.write: "AT+ADDRESS=100\r\n" # change to 200 for the second module
- delay: 0.5s
- uart.write: "AT+NETWORKID=18\r\n" # Set the Network ID
esp32:
board: esp32dev
framework:
type: arduino
version: recommended
web_server:
port: 80
logger:
level: DEBUG
api:
encryption:
key: !secret api_key
services:
# Creates a Home Assistant service for sending messages
- service: send_lora_message
variables:
dest_address: int
message: string
then:
- lambda: |-
int len = message.length();
char cmd[100];
snprintf(cmd, sizeof(cmd), "AT+SEND=%d,%d,%s\r\n", dest_address, len, message.c_str());
id(lora_uart).write_str(cmd);
ESP_LOGI("LoRa", "Sending to %d: %s", dest_address, message.c_str());
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "LoRA02 Fallback Hotspot"
password: "fallback12345"
uart:
id: lora_uart
tx_pin: GPIO17
rx_pin: GPIO16
baud_rate: 115200
text_sensor:
- platform: template
name: "LoRa Source Address"
id: lora_src_address
icon: mdi:radio-tower
- platform: template
name: "LoRa Message"
id: lora_message
icon: mdi:message
sensor:
- platform: template
name: "LoRa RSSI"
id: lora_rssi
unit_of_measurement: "dBm"
icon: mdi:signal-variant
- platform: template
name: "LoRa SNR"
id: lora_snr
unit_of_measurement: "dB"
icon: mdi:signal-strength-3
interval:
# checks for a new message every 500ms
- interval: 500ms
then:
- lambda: |-
static std::string uart_buffer;
while (id(lora_uart).available()) {
uint8_t byte;
if (id(lora_uart).read_byte(&byte)) {
uart_buffer += (char)byte;
if (byte == '\n') {
std::string data = uart_buffer;
uart_buffer = "";
ESP_LOGD("LoRa", "Received line: %s", data.c_str());
if (data.find("+RCV=") != std::string::npos) {
size_t pos = data.find("+RCV=");
data = data.substr(pos + 5);
data.erase(data.find_last_not_of("\r\n") + 1);
ESP_LOGD("LoRa", "Parsing: %s", data.c_str());
int field = 0;
std::string src_addr, data_len, message, rssi, snr;
std::string current = "";
for (size_t i = 0; i < data.length(); i++) {
if (data[i] == ',') {
if (field == 0) src_addr = current;
else if (field == 1) data_len = current;
else if (field == 2) message = current;
else if (field == 3) rssi = current;
current = "";
field++;
} else {
current += data[i];
}
}
if (field == 4) snr = current;
if (!src_addr.empty()) {
id(lora_src_address).publish_state(src_addr);
}
if (!message.empty()) {
id(lora_message).publish_state(message);
}
if (!rssi.empty()) {
char *endptr;
float rssi_val = strtof(rssi.c_str(), &endptr);
if (endptr != rssi.c_str()) {
id(lora_rssi).publish_state(rssi_val);
}
}
if (!snr.empty()) {
char *endptr;
float snr_val = strtof(snr.c_str(), &endptr);
if (endptr != snr.c_str()) {
id(lora_snr).publish_state(snr_val);
}
}
ESP_LOGI("LoRa", "Message from %s: %s (RSSI: %s dBm, SNR: %s dB)",
src_addr.c_str(), message.c_str(), rssi.c_str(), snr.c_str());
}
}
}
}Please subscribe to our newsletter!
