Sentinel: Industry 4.0 Smart Monitoring Node
A distributed industrial gateway leveraging dual-node processing and alpha-filtering for high-speed mechanical health monitoring. Bridging the gap between raw sensor data and actionable intelligence through a responsive HMI and remote WiFi dashboard.
Devices & Components
1
Arduino USB 2.0 CABLE TYPE A/B 1M
1
BMP280 Module
1
USB to micro USB cable
1
Espressif ESP32 S3 BOX 3
1
DHT11 Temp Sensor
1
Raspberry Pi Pico
2
Medium breadboard
1
MPU6050 Module
1
Jumper Wires
Software & Tools
Arduino IDE
Project description
Code
ESP32-S3-BOX3 Node
cpp
Code for ESP32-S3-BOX3 Node (Check GitHub Repo)
1/* 2* Project Name: Sentinel 3* Designed For: ESP32 S3 BOX 3 4* 5* 6* License: GPL3+ 7* This project is licensed under the GNU General Public License v3.0 or later. 8* You are free to use, modify, and distribute this software under the terms 9* of the GPL, as long as you preserve the original license and credit the original 10* author. For more details, see <https://www.gnu.org/licenses/gpl-3.0.en.html>. 11* 12* Copyright (C) 2026 Ameya Angadi 13* 14* Code Created And Maintained By: Ameya Angadi 15* Last Modified On: March 30, 2026 16* Version: 1.0.0 17* 18*/ 19 20#include <Arduino.h> 21#include <WiFi.h> 22#include <WebServer.h> 23#include <time.h> 24#include <Preferences.h> 25#include <esp_display_panel.hpp> 26#include <lvgl.h> 27#include <ui.h> 28#include "lvgl_v8_port.h" 29 30using namespace esp_panel::drivers; 31using namespace esp_panel::board; 32 33// --- CONFIGURATION --- 34const char* ssid = ""; 35const char* password = ""; 36#define RXD1 40 37#define TXD1 41 38 39WebServer server(80); 40Preferences prefs; 41Board *board = nullptr; 42 43// Global Data 44float g_temp = 0, g_hum = 0, g_pres = 0; 45float g_x = 0, g_y = 0, g_z = 0, g_vibe = 0; 46String g_status = "WAITING"; 47String g_last_maint = "01/01/2026"; 48String g_next_maint = "01/07/2026"; 49int currentBrightness = 100; 50unsigned long lastWifiCheck = 0; 51 52// Web Server Background Color Logic (Vibrant Shades) 53void handleRoot() { 54String bg = "#1a1a1a"; // Dark Gray (default) 55if (g_status == "DANGER") bg = "#ff4b2b"; // Vibrant Safety Red 56else if (g_status == "CAUTION") bg = "#ff9800"; // Vibrant Safety Orange 57 58String html = "<html><head><style>"; 59html += "body { font-family: 'Segoe UI', sans-serif; background: " + bg + "; color: white; padding: 25px; margin: 0; transition: background 0.4s; }"; 60html += ".card { background: rgba(0,0,0,0.75); padding: 30px; border-radius: 20px; border: 1px solid rgba(255,255,255,0.2); max-width: 450px; margin: auto; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }"; 61html += "h2 { margin: 0; text-transform: uppercase; letter-spacing: 2px; text-shadow: 2px 2px 4px rgba(0,0,0,0.5); }"; 62html += ".metric { margin: 18px 0; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 10px; }"; 63html += ".val { font-weight: bold; font-size: 1.6em; color: white; }"; 64html += ".label { color: #bbb; display: block; font-size: 0.85em; text-transform: uppercase; margin-bottom: 5px; }"; 65html += "</style></head><body><div class='card'>"; 66html += "<h2>SYSTEM " + g_status + "</h2><br>"; 67 68html += "<div class='metric'><span class='label'>Vibration Intensity</span><span class='val'>" + String(g_vibe) + " G</span></div>"; 69html += "<div class='metric'><span class='label'>Temperature</span><span class='val'>" + String(g_temp) + " C</span></div>"; 70html += "<div>" + String(g_hum) + "% Humidity | " + String(g_pres) + " hPa</div><br>"; 71html += "<div style='font-size: 0.8em; color: #aaa;'>Service: " + g_last_maint + " | Due: " + g_next_maint + "</div>"; 72html += "</div></body></html>"; 73server.send(200, "text/html", html); 74} 75 76// --- EVENT HANDLERS --- 77 78void onBrightnessChange(lv_event_t * e) { 79lv_obj_t * slider = lv_event_get_target(e); 80int val = lv_slider_get_value(slider); 81if (val < 10) val = 10; 82currentBrightness = val; 83board->getBacklight()->setBrightness(currentBrightness); 84} 85 86void updateSettingsScreenUI() { 87if (millis() - lastWifiCheck > 2000) { 88lastWifiCheck = millis(); 89String ssidStr = WiFi.SSID(); 90if(ssidStr.isEmpty()) ssidStr = "Searching..."; 91lv_label_set_text(ui_LabelWifiSSID, ("SSID: " + ssidStr).c_str()); 92 93if (WiFi.status() == WL_CONNECTED) { 94lv_label_set_text(ui_LabelWifiStatus, "Status: Connected"); 95lv_obj_set_style_text_color(ui_LabelWifiStatus, lv_color_hex(0x00FF00), 0); 96} else { 97lv_label_set_text(ui_LabelWifiStatus, "Status: Disconnected"); 98lv_obj_set_style_text_color(ui_LabelWifiStatus, lv_color_hex(0xFF0000), 0); 99} 100} 101} 102 103void onMaintBtnClicked(lv_event_t * e) { 104struct tm info; 105if (getLocalTime(&info)) { 106char buf[15]; 107strftime(buf, sizeof(buf), "%d/%m/%Y", &info); 108g_last_maint = String(buf); 109info.tm_mon += 6; 110mktime(&info); 111strftime(buf, sizeof(buf), "%d/%m/%Y", &info); 112g_next_maint = String(buf); 113 114prefs.begin("maint", false); 115prefs.putString("last", g_last_maint); 116prefs.putString("next", g_next_maint); 117prefs.end(); 118 119lv_label_set_text(ui_LabelLastMaintainaceDate, g_last_maint.c_str()); 120lv_label_set_text(ui_LabelNextMaintDate, g_next_maint.c_str()); 121} 122} 123 124void setup() { 125Serial.begin(115200); 126Serial1.begin(115200, SERIAL_8N1, RXD1, TXD1); 127Serial1.setTimeout(50); 128 129board = new Board(); 130board->init(); 131assert(board->begin()); 132lvgl_port_init(board->getLCD(), board->getTouch()); 133 134lvgl_port_lock(-1); 135ui_init(); 136 137if(ui_ButtonUpdateMaint) lv_obj_add_event_cb(ui_ButtonUpdateMaint, onMaintBtnClicked, LV_EVENT_CLICKED, NULL); 138 139if (ui_SliderBrightness) { 140lv_slider_set_range(ui_SliderBrightness, 10, 100); 141lv_slider_set_value(ui_SliderBrightness, 100, LV_ANIM_OFF); 142lv_obj_add_event_cb(ui_SliderBrightness, onBrightnessChange, LV_EVENT_VALUE_CHANGED, NULL); 143} 144 145prefs.begin("maint", true); 146g_last_maint = prefs.getString("last", "01/01/2026"); 147g_next_maint = prefs.getString("next", "01/07/2026"); 148prefs.end(); 149 150lv_label_set_text(ui_LabelLastMaintainaceDate, g_last_maint.c_str()); 151lv_label_set_text(ui_LabelNextMaintDate, g_next_maint.c_str()); 152lvgl_port_unlock(); 153 154WiFi.begin(ssid, password); 155server.on("/", handleRoot); 156server.begin(); 157configTime(19800, 0, "pool.ntp.org"); 158} 159 160void loop() { 161server.handleClient(); 162static unsigned long lastClock = 0; 163struct tm timeinfo; 164 165if (lv_scr_act() == ui_SettingScreen) { 166lvgl_port_lock(-1); 167updateSettingsScreenUI(); 168lvgl_port_unlock(); 169} 170 171if (Serial1.available() > 0) { 172String raw = Serial1.readStringUntil('\n'); 173if (raw.startsWith("$[")) { 174String data = raw.substring(raw.indexOf("$[") + 2, raw.indexOf("]")); 175float v[7]; int count = 0; 176char* ptr = strtok((char*)data.c_str(), ","); 177while (ptr != NULL && count < 7) { v[count++] = atof(ptr); ptr = strtok(NULL, ","); } 178 179if (count == 7) { 180g_temp = v[0]; g_hum = v[1]; g_pres = v[2]; 181g_x = v[3]; g_y = v[4]; g_z = v[5]; g_vibe = v[6]; 182 183lvgl_port_lock(-1); 184uint32_t s_color = 0x00FF00; 185if (g_temp > 55.0 || g_vibe > 0.70) { g_status = "DANGER"; s_color = 0xFF0000; } 186else if (g_temp > 42.0 || g_vibe > 0.15) { g_status = "CAUTION"; s_color = 0xFFA500; } 187else { g_status = "HEALTHY"; s_color = 0x00FF00; } 188 189lv_label_set_text(ui_LabelHealthStatus, g_status.c_str()); 190lv_obj_set_style_text_color(ui_LabelHealthStatus, lv_color_hex(s_color), 0); 191 192char b[16]; 193snprintf(b, sizeof(b), "%.1f C", g_temp); 194lv_label_set_text(ui_LabelTemp, b); lv_label_set_text(ui_LabelTempES, b); 195 196snprintf(b, sizeof(b), "%.0f %%", g_hum); 197lv_label_set_text(ui_LabelHum, b); lv_label_set_text(ui_LabelHumES, b); 198 199snprintf(b, sizeof(b), "%.1f hPa", g_pres); lv_label_set_text(ui_LabelBMPressure, b); 200snprintf(b, sizeof(b), "X: %.2f G", g_x); lv_label_set_text(ui_LabelXaxis, b); 201snprintf(b, sizeof(b), "Y: %.2f G", g_y); lv_label_set_text(ui_LabelYaxis, b); 202snprintf(b, sizeof(b), "Z: %.2f G", g_z); lv_label_set_text(ui_LabelZaxis, b); 203snprintf(b, sizeof(b), "%.2f G", g_vibe); lv_label_set_text(ui_LabelCombinedMPU, b); 204lvgl_port_unlock(); 205} 206} 207} 208 209if (millis() - lastClock > 1000) { 210lastClock = millis(); 211if (getLocalTime(&timeinfo)) { 212char t_b[10], p_b[5], d_b[15]; 213strftime(t_b, sizeof(t_b), "%I:%M", &timeinfo); 214strftime(p_b, sizeof(p_b), "%p", &timeinfo); 215strftime(d_b, sizeof(d_b), "%d/%m/%Y", &timeinfo); 216lvgl_port_lock(-1); 217lv_label_set_text(ui_LabelTime12Hr, t_b); 218lv_label_set_text(ui_LabelTimeAMPM, p_b); 219lv_label_set_text(ui_LabelDate, d_b); 220lvgl_port_unlock(); 221} 222} 223delay(2); 224}
Pico DAQ Node
cpp
Code for the Pico DAQ Node (Check GitHub Repo)
1/* 2* Project Name: Sentinel 3* Designed For: Raspberry Pi Pico/Pico W 4* 5* 6* License: GPL3+ 7* This project is licensed under the GNU General Public License v3.0 or later. 8* You are free to use, modify, and distribute this software under the terms 9* of the GPL, as long as you preserve the original license and credit the original 10* author. For more details, see <https://www.gnu.org/licenses/gpl-3.0.en.html>. 11* 12* Copyright (C) 2026 Ameya Angadi 13* 14* Code Created And Maintained By: Ameya Angadi 15* Last Modified On: March 30, 2026 16* Version: 1.0.0 17* 18*/ 19 20#include <Wire.h> 21#include <DHT.h> 22#include <Adafruit_BMP280.h> 23#include <Adafruit_MPU6050.h> 24#include <Adafruit_Sensor.h> 25 26#define DHTPIN 15 27DHT dht(DHTPIN, DHT11); 28Adafruit_BMP280 bmp(&Wire); 29Adafruit_MPU6050 mpu; 30 31// Calibration Globals 32float baseMag = 0; 33float offX = 0, offY = 0, offZ = 0; 34float filtX = 0, filtY = 0, filtZ = 0; 35const float alpha = 0.8; // Filter strength (0.8 to 0.9 is best) 36 37void initSensors() { 38Wire.end(); Wire1.end(); 39Wire.setSDA(4); Wire.setSCL(5); Wire.begin(); 40bmp.begin(0x76); 41Wire1.setSDA(2); Wire1.setSCL(3); Wire1.begin(); 42mpu.begin(0x68, &Wire1); 43dht.begin(); 44} 45 46void calibrateMPU() { 47Serial.println("Calibrating... Keep Still!"); 48float sumX = 0, sumY = 0, sumZ = 0, sumMag = 0; 49int samples = 100; 50 51for(int i = 0; i < samples; i++) { 52sensors_event_t a, g, temp; 53mpu.getEvent(&a, &g, &temp); 54sumX += a.acceleration.x; 55sumY += a.acceleration.y; 56sumZ += a.acceleration.z; 57sumMag += sqrt(pow(a.acceleration.x, 2) + pow(a.acceleration.y, 2) + pow(a.acceleration.z, 2)); 58delay(10); 59} 60 61offX = sumX / samples; 62offY = sumY / samples; 63offZ = sumZ / samples; 64baseMag = sumMag / samples; 65Serial.printf("Baseline Set: %.2f m/s^2\n", baseMag); 66} 67 68void setup() { 69Serial.begin(115200); 70Serial1.begin(115200); // TX: GP0 71initSensors(); 72calibrateMPU(); // Run Tare Calibration 73} 74 75void loop() { 76sensors_event_t a, g, temp_mpu; 77if (!mpu.getEvent(&a, &g, &temp_mpu)) { initSensors(); return; } 78 79// 1. DYNAMIC HIGH-PASS FILTER (The "Gravity Canceller") 80// We estimate gravity (Low Pass) 81filtX = (alpha * filtX) + ((1.0 - alpha) * a.acceleration.x); 82filtY = (alpha * filtY) + ((1.0 - alpha) * a.acceleration.y); 83filtZ = (alpha * filtZ) + ((1.0 - alpha) * a.acceleration.z); 84 85// We subtract gravity to get only Linear Acceleration (High Pass) 86float linX = a.acceleration.x - filtX; 87float linY = a.acceleration.y - filtY; 88float linZ = a.acceleration.z - filtZ; 89 90// 2. CALCULATE VIBRATION INTENSITY (G-Force) 91// This is the "Magnitude" of just the vibration components 92float vibeMag = sqrt(pow(linX, 2) + pow(linY, 2) + pow(linZ, 2)) / 9.81; 93 94// 3. GET ENVIRONMENTAL DATA 95float t = dht.readTemperature(); 96float h = dht.readHumidity(); 97float p = bmp.readPressure() / 100.0F; 98 99// --- PACKET SENDING --- 100// $[T, H, P, RelX, RelY, RelZ, RelVibe]* 101Serial1.print("$["); 102Serial1.print(t, 1); Serial1.print(","); 103Serial1.print(h, 0); Serial1.print(","); 104Serial1.print(p, 1); Serial1.print(","); 105Serial1.print(linX, 2); Serial1.print(","); // Cleaned X 106Serial1.print(linY, 2); Serial1.print(","); // Cleaned Y 107Serial1.print(linZ, 2); Serial1.print(","); // Cleaned Z 108Serial1.print(vibeMag, 2); // Cleaned Combined G 109Serial1.println("]*"); 110 111delay(100); 112}
Local HTML File
markup
Code for the Local HTML File (Check GitHub Repo)
1<!DOCTYPE html> 2<html> 3<head> 4<title>Sentinel Remote Dashboard</title> 5<style> 6body { background: #000; color: white; text-align: center; font-family: sans-serif; } 7iframe { width: 400px; height: 415px; border: 2px solid #333; border-radius: 10px; margin-top: 50px; } 8.info { margin-top: 10px; color: #888; } 9</style> 10</head> 11<body> 12<h1>Predictive Maintenance Monitor</h1> 13<iframe id="sentinelFrame" src="http://10.247.207.62"></iframe> 14 15<script> 16setInterval(function() { 17document.getElementById('sentinelFrame').src = document.getElementById('sentinelFrame').src; 18}, 500); 19</script> 20</body> 21</html>
Downloadable files
Circuit_Diagram
Use this circuit diagram to assemble the circuit.
Circuit_Diagram.png

Documentation
Sentinel Github Repo
Check this repo for all code and design files.
https://github.com/ameya-angadi/Sentinel
Comments
Only logged in users can leave comments