Sentinel V2: Industry 4.0 Smart Monitoring Node

by Ameya Angadi in Circuits > Raspberry Pi

26 Views, 0 Favorites, 0 Comments

Sentinel V2: Industry 4.0 Smart Monitoring Node

20261009_233741.jpg

In modern industrial environments, the "health" of a machine is often invisible until a catastrophic failure occurs. While many IoT solutions attempt to provide predictive maintenance, they frequently suffer from two critical flaws: Hardware Overload and Data Noise. Standard single-chip monitoring systems often crash or experience significant latency when trying to manage high-resolution displays, WiFi stacks, and precise sensor acquisition simultaneously. This leads to "blind spots" where transient mechanical faults are missed.


Furthermore, raw accelerometer data is inherently biased by Earth's gravity, making it difficult to isolate true mechanical vibration from the static orientation of the sensor without complex, manual calibration. Finally, fragile physical screens mounted directly on vibrating, dirty machinery introduce a massive point of failure.


Read more about Machine Health Monitoring Systems here: https://reyax.com/application/1

Supplies

20261009_230500.jpg

To build the complete Sentinel V2 ecosystem, you will need:

  1. 1x Raspberry Pi Pico (Edge Node)
  2. 1x Raspberry Pi Pico W (Central Gateway)
  3. 2x REYAX RYLR999 Transceiver Modules (LoRa + BLE Modules)
  4. Amazon: REYAX RYLR999 EVK
  5. Digikey: RYLR999-30dBm-LoRa-20dBm
  6. eBay: RYLR999 Module
  7. India (Intercode Technologies): Intercode
  8. Robu.in: RYLR999 Long Range Transceiver
  9. India (Astute): Astute Group
  10. India (Excel Eltech): Excel Eltech
  11. 1x MPU6050 (Tri-Axis Accelerometer)
  12. 1x BMP280 (Barometric Pressure Sensor)
  13. 1x DHT11 (Temperature & Humidity Sensor)
  14. Jumper Wires & Breadboards
  15. Micro USB Cables & 5V Power Supply

Hardware Setup & System Architecture

This project utilizes a Distributed Computing Architecture, requiring the assembly of two distinct nodes: the Central Gateway (Pico W) and the Sensor DAQ Node (Pi Pico).

Assembly of the Pi Pico DAQ Node (Edge)

Sentinel Node V2.png

The Raspberry Pi Pico acts as the high-speed Data Acquisition (DAQ) node. It is responsible for sampling the sensor array and performing the heavy DSP filtering.

  1. Vibration Sensor (MPU6050): Use the dedicated I2C Bus 1 for maximum isolation. Connect SDA to GPIO 2 and SCL to GPIO 3.
  2. Environment Sensors (BMP280 & DHT11): Use the I2C Bus 0. For the BMP280, connect SDA to GPIO 4 and SCL to GPIO 5. Connect the DHT11 Data Pin to GPIO 15.
  3. Power: Connect the power of each sensor to the 3V3 Out and GND pins of the Pico.
  4. Mechanical Mounting: The MPU6050 must be securely mounted to the machinery being monitored. The High-Pass Alpha Filter logic assumes the sensor is rigidly attached to the vibration source to ensure Signal Integrity.

Wiring the REYAX RYLR999 Transceivers

Sentinel Gateway.png

The RYLR999 module is the backbone of this project, but it requires careful power management. While its data lines operate at 3.3V logic (perfect for the Pico), the RF amplifier draws a massive burst of current to blast a 30 dBm signal and strictly requires 5V to operate.

  1. Power Distribution: Connect the VDD pin of the RYLR999 directly to the VBUS pin on your Raspberry Pi Pico/Pico W. Connect GND to the Pico's ground. This taps the raw 5V from your USB supply and bypasses the Pico's internal 3.3V regulator.
  2. Dual UART Connections (Edge Node): The RYLR999 features two physical UARTs—one for LoRa and one for BLE.
  3. Connect the Pico's GP0 (TX) to the module's RXD (LoRa).
  4. Connect the Pico's GP1 (RX) to the module's TXD (LoRa).
  5. Connect the Pico's GP8 (TX) to the module's RXD_BLE (BLE).
  6. Connect the Pico's GP9 (RX) to the module's TXD_BLE (BLE).
  7. UART Connections (Gateway Node): The central Pico W only needs the LoRa connection to receive data. Connect its GP0 to RXD and GP1 to TXD.

AT Command Testing & Configuration

Before writing any complex sensor logic, you must verify that the REYAX modules are functional and configured to speak the same RF language. The RYLR999 operates via simple ASCII AT commands.

Flash a basic Serial Passthrough sketch to your Pico to send commands from your Arduino IDE Serial Monitor (set to 115200 baud, "Both NL & CR"). Execute the following sequence on both modules:

  1. Test Connection: Send AT. The module should reply with +OK.
  2. Set Operating Band: Send AT+BAND=866000000 to set the frequency to the license-exempt 866 MHz industrial band (For India). (Set frequency as per your country rules and regulations).
  3. Set Network ID: Send AT+NETWORKID=18 to isolate your factory's data from adjacent RF noise.
  4. Set Spreading Factor & Bandwidth: Send AT+PARAMETER=9,7,1,12. This dictates the chirp duration and bandwidth. Both the gateway and the node must share these exact parameters to decode each other's packets.
  5. Assign Node Addresses: On your Gateway module, send AT+ADDRESS=0. On your Node module, send AT+ADDRESS=101.
  6. Set LoRa Module Power: If you are testing the modules indoors or close to each other then reduce the power to 10 dBm by sending AT+CRFOP=10. This reduces the output power and prevents any sort of damage to modules. Increase power (Max 30 dBm) only if you need maximum range.
  7. Set BLE Name: Use AT+NAME=Sentinel_Node_101 command to set BLE name. Change the LoRa pins with respective RX and TX pins with BLE pins.


Upload this code for setting AT parameters:

// --- LoRa UART pins (UART0 = Serial1) ---
const int lora_tx = 0; // GP16
const int lora_rx = 1; // GP17

void setup() {
// Start USB connection to Laptop
Serial.begin(115200);

// Start Hardware UART connection to LoRa Module
Serial1.setTX(lora_tx);
Serial1.setRX(lora_rx);
Serial1.begin(115200);
}

void loop() {
// If the laptop sends an AT command, pass it to the LoRa module
if (Serial.available()) {
Serial1.write(Serial.read());
}

// If the LoRa module replies, pass it back to the laptop
if (Serial1.available()) {
Serial.write(Serial1.read());
}
}

The "Virtual HMI" Via BLE Diagnostics

Screenshot_20261010_000048_LightBlue.jpg
Screenshot_20261010_000100_LightBlue.jpg
Screenshot_20261010_000108_LightBlue.jpg
Screenshot_20261010_000118_LightBlue.jpg
Screenshot_20261010_000130_LightBlue.jpg

Instead of mounting a fragile LCD screen on vibrating machinery, we leverage the RYLR999's Transparent GATT Bluetooth functionality.

When a field technician approaches the machine, they simply open a standard BLE testing app (like LightBlue) on their smartphone. By connecting to the module's default UUID (4880c12c-fdcb-4077-8920-a450d7f9b907) and enabling UTF-8 string notifications, the smartphone immediately becomes a live diagnostic dashboard. The edge node code pushes formatted telemetry (e.g., V:0.5G, T:28C) directly to the phone completely independent of the main LoRa network.

Software Development (Edge & Gateway)

The software for both nodes was developed using the Arduino IDE.

Part 1: The Pico DAQ Edge Node By offloading the sensor tasks to the Pi Pico, we ensure continuous data acquisition without network-induced latency. The edge node actively cancels gravity to extract the vibeMag variable, formats the data into a concise ASCII payload, and transmits it via AT+SEND=0,length,payload to the central gateway every two seconds.


/*
* Project Name: Sentinel V2 - Edge Sensing Node
* Designed For: Raspberry Pi Pico & REYAX RYLR999
*
* License: GPL3+
* This project is licensed under the GNU General Public License v3.0 or later.
* You are free to use, modify, and distribute this software under the terms
* of the GPL, as long as you preserve the original license and credit the original
* author. For more details, see <https://www.gnu.org/licenses/gpl-3.0.en.html>.
*
* Copyright (C) 2026 Ameya Angadi
*
* Code Created And Maintained By: Ameya Angadi
* Last Modified On: October 26, 2026
* Version: 1.0.0
*
*/

#include <Wire.h>
#include <DHT.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>

// --- NODE CONFIGURATION ---
#define NODE_ID 101 // Change to 102, 103 for subsequent nodes
#define GATEWAY_ID 0 // Central Hub Address
#define NETWORK_ID 18 // Shared LoRa Network ID
#define LORA_BAND 866000000

// --- PIN DEFINITIONS ---
#define DHTPIN 15
#define LORA_TX 0 // Pico GP0 (UART0 TX) -> RYLR999 RXD
#define LORA_RX 1 // Pico GP1 (UART0 RX) -> RYLR999 TXD
#define BLE_TX 8 // Pico GP8 (UART1 TX) -> RYLR999 RXD_BLE
#define BLE_RX 9 // Pico GP9 (UART1 RX) -> RYLR999 TXD_BLE

DHT dht(DHTPIN, DHT11);
Adafruit_BMP280 bmp(&Wire);
Adafruit_MPU6050 mpu;

// Calibration & Filter Globals
float filtX = 0, filtY = 0, filtZ = 0;
const float alpha = 0.85; // Gravity cancellation filter factor
unsigned long lastBroadcast = 0;
const unsigned long broadcastInterval = 2000; // 2 seconds

void sendATCommand(HardwareSerial &port, String cmd) {
port.print(cmd + "\r\n");
delay(100);
}

void initSensors() {
Wire.end();
Wire1.end();
// Wire (I2C0) on GP4/GP5 for BMP280
Wire.setSDA(4);
Wire.setSCL(5);
Wire.begin();
bmp.begin(0x76);
// Wire1 (I2C1) on GP2/GP3 for MPU6050
Wire1.setSDA(2);
Wire1.setSCL(3);
Wire1.begin();
mpu.begin(0x68, &Wire1);
dht.begin();
}

void calibrateMPU() {
Serial.println("[SETUP] Calibrating MPU6050...");
float sumX = 0, sumY = 0, sumZ = 0;
for(int i = 0; i < 100; i++) {
sensors_event_t a, g, t;
mpu.getEvent(&a, &g, &t);
sumX += a.acceleration.x;
sumY += a.acceleration.y;
sumZ += a.acceleration.z;
delay(10);
}
filtX = sumX / 100.0;
filtY = sumY / 100.0;
filtZ = sumZ / 100.0;
Serial.println("[SETUP] Calibration Complete.");
}

void setup() {
Serial.begin(115200);

// Initialize LoRa UART (Serial1 on GP0/GP1)
Serial1.setTX(LORA_TX);
Serial1.setRX(LORA_RX);
Serial1.begin(115200);

// Initialize BLE UART (Serial2 on GP8/GP9)
Serial2.setTX(BLE_TX);
Serial2.setRX(BLE_RX);
Serial2.begin(115200);

delay(1000);
initSensors();
calibrateMPU();

// Configure Reyax LoRa Radio Parameters
Serial.println("[SETUP] Configuring RYLR999...");
sendATCommand(Serial1, "AT+BAND=" + String(LORA_BAND));
sendATCommand(Serial1, "AT+NETWORKID=" + String(NETWORK_ID));
sendATCommand(Serial1, "AT+ADDRESS=" + String(NODE_ID));
sendATCommand(Serial1, "AT+CRFOP=15"); // Transmit power (~30 dBm)

Serial.println("[READY] Sentinel V2 Edge Node Online.");
}

void loop() {
sensors_event_t a, g, temp_mpu;
if (!mpu.getEvent(&a, &g, &temp_mpu)) {
initSensors();
return;
}

// 1. Dynamic Gravity High-Pass Filter (Extract pure vibration)
filtX = (alpha * filtX) + ((1.0 - alpha) * a.acceleration.x);
filtY = (alpha * filtY) + ((1.0 - alpha) * a.acceleration.y);
filtZ = (alpha * filtZ) + ((1.0 - alpha) * a.acceleration.z);

float linX = a.acceleration.x - filtX;
float linY = a.acceleration.y - filtY;
float linZ = a.acceleration.z - filtZ;
float vibeMag = sqrt(pow(linX, 2) + pow(linY, 2) + pow(linZ, 2)) / 9.81;

// 2. Sample Environmental Data & Broadcast
// Add these two static variables right above your broadcast IF statement
static bool bleScheduled = false;
static String queuedBleMsg = "";

// 2. Sample Environmental Data & Broadcast
if (millis() - lastBroadcast >= broadcastInterval) {
lastBroadcast = millis();

float t = dht.readTemperature();
float h = dht.readHumidity();
float p = bmp.readPressure() / 100.0F;

if (isnan(t)) t = 0.0;
if (isnan(h)) h = 0.0;
if (isnan(p)) p = 0.0;

// --- 1. LORA TRANSMISSION (To Central Hub) ---
// (LoRa can handle the long string easily)
String payload = "$[" + String(t, 1) + "," + String(h, 0) + "," +
String(p, 1) + "," + String(linX, 2) + "," +
String(linY, 2) + "," + String(linZ, 2) + "," +
String(vibeMag, 2) + "]*";
String loraCmd = "AT+SEND=" + String(GATEWAY_ID) + "," + String(payload.length()) + "," + payload;
Serial1.print(loraCmd + "\r\n");

// Queue the BLE message to be sent later.
// Format: V:0.5,T:28,P:1015\r\n
// This is exactly 19 bytes! It fits perfectly inside a single, instant BLE packet.
queuedBleMsg = "V:" + String(vibeMag, 1) + ",T:" + String(t, 0) + ",P:" + String(p, 0) + "\r\n";
bleScheduled = true; // Set flag to trigger BLE later
}

// --- 2. STAGGERED BLE TRANSMISSION ---
// Wait 400ms AFTER the LoRa burst finishes before hitting the BLE UART
if (bleScheduled && (millis() - lastBroadcast >= 400)) {
bleScheduled = false;
Serial2.print(queuedBleMsg);
}
}


Part 2: The Central Pico W Gateway

The gateway listens for +RCV= packets coming over the UART from its local RYLR999 module. It dynamically parses the sender ID, unpacks the vibration and environmental data, and stores it in an internal array. It then hosts an asynchronous Web Server on your local Wi-Fi network.


/*
* Project Name: Sentinel V2 - Central IoT Gateway
* Designed For: Raspberry Pi Pico W & REYAX RYLR999
*
* License: GPL3+
* This project is licensed under the GNU General Public License v3.0 or later.
* You are free to use, modify, and distribute this software under the terms
* of the GPL, as long as you preserve the original license and credit the original
* author. For more details, see <https://www.gnu.org/licenses/gpl-3.0.en.html>.
*
* Copyright (C) 2026 Ameya Angadi
*
* Code Created And Maintained By: Ameya Angadi
* Last Modified On: October 26, 2026
* Version: 1.0.0
*
*/

#include <WiFi.h>
#include <WebServer.h>

// --- NETWORK CREDENTIALS ---
const char* ssid = "";
const char* password = "";

// --- RADIO CONFIGURATION ---
#define GATEWAY_ID 0
#define NETWORK_ID 18
#define LORA_BAND 866000000
#define LORA_TX 0 // Pico W GP0 -> RYLR999 RXD
#define LORA_RX 1 // Pico W GP1 -> RYLR999 TXD

WebServer server(80);

// --- MULTI-NODE STORAGE ---
struct NodeRecord {
int id;
float temp;
float hum;
float pres;
float x;
float y;
float z;
float vibe;
int rssi;
int snr;
String status;
unsigned long lastSeen;
bool active;
};

#define MAX_NODES 16
NodeRecord nodes[MAX_NODES];
int registeredNodeCount = 0;

void sendATCommand(String cmd) {
Serial1.print(cmd + "\r\n");
delay(100);
}

// Ingest and parse: +RCV=<Address>,<Length>,<Data>,<RSSI>,<SNR>
void parseLoRaPacket(String msg) {
if (!msg.startsWith("+RCV=")) return;

msg.remove(0, 5); // Strip "+RCV="
int c1 = msg.indexOf(',');
int c2 = msg.indexOf(',', c1 + 1);
int c3 = msg.lastIndexOf(',');
int c4 = msg.lastIndexOf(',', c3 - 1);

if (c1 == -1 || c2 == -1 || c3 == -1 || c4 == -1) return;

int senderId = msg.substring(0, c1).toInt();
String dataPayload = msg.substring(c2 + 1, c4);
int rssi = msg.substring(c4 + 1, c3).toInt();
int snr = msg.substring(c3 + 1).toInt();

// Validate $[ ... ]* framing
if (!dataPayload.startsWith("$[") || !dataPayload.endsWith("]*")) return;
dataPayload = dataPayload.substring(2, dataPayload.length() - 2);

float v[7];
int count = 0;
char* ptr = strtok((char*)dataPayload.c_str(), ",");
while (ptr != NULL && count < 7) {
v[count++] = atof(ptr);
ptr = strtok(NULL, ",");
}

if (count == 7) {
// Determine machine health status
String status = "HEALTHY";
if (v[0] > 55.0 || v[6] > 0.70) {
status = "DANGER";
} else if (v[0] > 42.0 || v[6] > 0.15) {
status = "CAUTION";
}

// Update existing node or dynamically register a new one
int nodeIndex = -1;
for (int i = 0; i < registeredNodeCount; i++) {
if (nodes[i].id == senderId) {
nodeIndex = i;
break;
}
}

if (nodeIndex == -1 && registeredNodeCount < MAX_NODES) {
nodeIndex = registeredNodeCount++;
nodes[nodeIndex].id = senderId;
}

if (nodeIndex != -1) {
nodes[nodeIndex].temp = v[0];
nodes[nodeIndex].hum = v[1];
nodes[nodeIndex].pres = v[2];
nodes[nodeIndex].x = v[3];
nodes[nodeIndex].y = v[4];
nodes[nodeIndex].z = v[5];
nodes[nodeIndex].vibe = v[6];
nodes[nodeIndex].rssi = rssi;
nodes[nodeIndex].snr = snr;
nodes[nodeIndex].status = status;
nodes[nodeIndex].lastSeen = millis();
nodes[nodeIndex].active = true;
}
}
}

// REST Endpoint: Returns telemetry JSON array for all active nodes
void handleApiData() {
String json = "[";
for (int i = 0; i < registeredNodeCount; i++) {
if (i > 0) json += ",";
json += "{";
json += "\"id\":" + String(nodes[i].id) + ",";
json += "\"temp\":" + String(nodes[i].temp, 1) + ",";
json += "\"hum\":" + String(nodes[i].hum, 0) + ",";
json += "\"pres\":" + String(nodes[i].pres, 1) + ",";
json += "\"x\":" + String(nodes[i].x, 2) + ",";
json += "\"y\":" + String(nodes[i].y, 2) + ",";
json += "\"z\":" + String(nodes[i].z, 2) + ",";
json += "\"vibe\":" + String(nodes[i].vibe, 2) + ",";
json += "\"rssi\":" + String(nodes[i].rssi) + ",";
json += "\"snr\":" + String(nodes[i].snr) + ",";
json += "\"status\":\"" + nodes[i].status + "\",";
json += "\"age\":" + String((millis() - nodes[i].lastSeen) / 1000);
json += "}";
}
json += "]";
server.send(200, "application/json", json);
}

// Web Server Root: Dynamic Dashboard with 1-second auto-refresh
void handleRoot() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sentinel V2 | SCADA Telemetry</title>
<style>
* { box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0c0d0e; color: #fff; margin: 0; padding: 20px; }
header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #222; padding-bottom: 15px; margin-bottom: 25px; }
h1 { margin: 0; font-size: 1.4rem; letter-spacing: 1px; }
#grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px; }
.card { background: #16181a; border-radius: 14px; padding: 20px; border: 1px solid #2a2d30; transition: border 0.3s; }
.card.HEALTHY { border-left: 6px solid #00e676; }
.card.CAUTION { border-left: 6px solid #ff9800; background: rgba(255, 152, 0, 0.05); }
.card.DANGER { border-left: 6px solid #ff4b2b; background: rgba(255, 75, 43, 0.08); }
.card.OFFLINE { border-left: 6px solid #555; opacity: 0.6; }
.card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.node-title { font-size: 1.1rem; font-weight: bold; margin: 0; }
.badge { font-size: 0.75rem; font-weight: bold; padding: 4px 8px; border-radius: 6px; text-transform: uppercase; }
.badge.HEALTHY { background: rgba(0,230,118,0.2); color: #00e676; }
.badge.CAUTION { background: rgba(255,152,0,0.2); color: #ff9800; }
.badge.DANGER { background: rgba(255,75,43,0.2); color: #ff4b2b; }
.badge.OFFLINE { background: #333; color: #aaa; }
.metric-group { display: flex; justify-content: space-between; margin: 10px 0; border-bottom: 1px solid #222; padding-bottom: 6px; }
.label { color: #888; font-size: 0.85rem; }
.val { font-weight: bold; font-size: 1.05rem; }
.rf-bar { font-size: 0.75rem; color: #666; margin-top: 10px; display: flex; justify-content: space-between; }
</style>
</head>
<body>
<header>
<h1>SENTINEL V2 // INDUSTRIAL STAR GATEWAY</h1>
<div id="status">Listening...</div>
</header>
<div id="grid"></div>

<script>
async function updateDashboard() {
try {
const res = await fetch('/api/data');
const data = await res.json();
const grid = document.getElementById('grid');
grid.innerHTML = '';

if(data.length === 0) {
grid.innerHTML = '<div style="color:#666;">No active edge nodes registered. Awaiting LoRa telemetry...</div>';
return;
}

data.forEach(node => {
const isOffline = node.age > 10;
const status = isOffline ? 'OFFLINE' : node.status;
const card = document.createElement('div');
card.className = `card ${status}`;
card.innerHTML = `
<div class="card-head">
<span class="node-title">Machine ID #${node.id}</span>
<span class="badge ${status}">${status}</span>
</div>
<div class="metric-group">
<span class="label">Vibration Intensity</span>
<span class="val">${node.vibe.toFixed(2)} G</span>
</div>
<div class="metric-group">
<span class="label">Bearing Temperature</span>
<span class="val">${node.temp.toFixed(1)} °C</span>
</div>
<div class="metric-group">
<span class="label">Duct Pressure</span>
<span class="val">${node.pres.toFixed(1)} hPa</span>
</div>
<div class="metric-group">
<span class="label">Humidity</span>
<span class="val">${node.hum.toFixed(0)} %</span>
</div>
<div class="rf-bar">
<span>Tri-Ax: [${node.x.toFixed(2)}, ${node.y.toFixed(2)}, ${node.z.toFixed(2)}]</span>
<span>RSSI: ${node.rssi} dBm | SNR: ${node.snr} dB | ${node.age}s ago</span>
</div>
`;
grid.appendChild(card);
});
} catch (err) {
console.error("Dashboard update failed:", err);
}
}
// Instant initial load, then refresh every 1000ms
updateDashboard();
setInterval(updateDashboard, 1000);
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}

void setup() {
Serial.begin(115200);

// Initialize LoRa UART (Serial1 on GP0/GP1)
Serial1.setTX(LORA_TX);
Serial1.setRX(LORA_RX);
Serial1.begin(115200);

delay(1000);
Serial.println("[SETUP] Initializing Gateway RYLR999...");
sendATCommand("AT+BAND=" + String(LORA_BAND));
sendATCommand("AT+NETWORKID=" + String(NETWORK_ID));
sendATCommand("AT+ADDRESS=" + String(GATEWAY_ID));

// Connect to Local Wi-Fi
Serial.print("[WIFI] Connecting to " + String(ssid));
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\n[WIFI] Connected! Gateway IP: " + WiFi.localIP().toString());

// Setup Web Server Routes
server.on("/", handleRoot);
server.on("/api/data", handleApiData);
server.begin();
Serial.println("[HTTP] Web server running.");
}

void loop() {
server.handleClient();

// Read incoming LoRa packets from RYLR999
while (Serial1.available() > 0) {
String packet = Serial1.readStringUntil('\n');
packet.trim();
if (packet.length() > 0) {
Serial.println("[RAW RCV] " + packet);
parseLoRaPacket(packet);
}
}
}

Web Server Implementation

Screenshot 2026-10-10 012136.png

The Pico W hosts a built-in HTTP Web Server optimized to serve a Minimalist Dashboard designed for remote oversight.

  1. Asynchronous Refresh: Instead of reloading an HTML iframe, the dashboard utilizes a custom JavaScript fetch() loop to poll a JSON endpoint (/api/data) every second. This ensures a steady, reliable stream of telemetry without overwhelming the network bandwidth.
  2. Context-Aware Visual Alerts: Depending on the health status, the dashboard's CSS automatically transitions between Industrial Gray (Healthy), Safety Orange (Caution), and Vibrant Red (Danger). This creates a highly visible remote alarm system.

Real-World NLOS Range Testing

20260816_155215(1).jpg
Screenshot 2026-10-18 213202.png

LoRa radio waves do not simply punch through dense machinery. However, in an industrial facility, the 866 MHz signal thrives on the Waveguide Effect.

During range testing, we achieved successful data transmission at distances exceeding 7.5 kilometers. Rather than relying purely on Line of Sight, the 30 dBm signal from the RYLR999 bounces off concrete walls, metallic ductwork, and physical corridors. By carefully configuring the Spreading Factor to SF9 and keeping the packet size small, the central gateway is able to extract weak, bouncing signals from beneath the industrial noise floor with zero packet loss.

Conclusion

The Sentinel V2 Predictive Maintenance Hub represents a significant step forward in low-cost, high-reliability industrial monitoring. By moving away from traditional monolithic software designs and embracing a Distributed Computing Architecture, the project successfully solves the common issues of system latency and unexpected machine down-time.

Through the combination of High-Pass Alpha Filtering for gravity-free vibration sensing, a powerful dual-radio LoRa/BLE implementation via the REYAX RYLR999, and a dynamic Wi-Fi Gateway, the Sentinel Hub provides a comprehensive solution for factories. It eliminates the fragile edge display, empowering workers with a Virtual HMI right on their smartphones, while ensuring remote SCADA operators have instant, color-coded visibility over the entire facility floor.


Hope you like the project. If you want to explore more, check out my profile for related projects, and don’t forget to follow me for updates on new tutorials and advanced projects!