สร้าง Sentinel Hub: ระบบ Smart Monitoring Node สำหรับ Industry 4.0 แบบ Step-by-Step

Sentinel Hub Preview

วิเคราะห์ปัญหา (Problem Statement Analysis)

ในสภาพแวดล้อมอุตสาหกรรมยุคใหม่ "สุขภาพ" ของเครื่องจักรมักจะเป็นสิ่งที่มองไม่เห็น จนกว่าจะเกิดความเสียหายร้ายแรงขึ้น แม้ว่าระบบ IoT หลายตัวจะพยายามเข้ามาช่วยเรื่องการบำรุงรักษาเชิงคาดการณ์ (Predictive Maintenance) แต่ระบบเหล่านั้นก็มักจะเจอกับข้อบกพร่องสำคัญ 2 อย่าง นั่นคือ Hardware Overload (ฮาร์ดแวร์ทำงานหนักเกินไป) และ Data Noise (ข้อมูลกวนกัน)

ระบบตรวจสอบชิปเดี่ยวแบบมาตรฐานทั่วไปมักจะรวน หรือเกิดความหน่วงสูง (Latency) เวลาที่ต้องจัดการกับหน้าจอความละเอียดสูง, ระบบ WiFi และการดึงข้อมูลเซนเซอร์แบบแม่นยำไปพร้อมๆ กัน ทำให้เกิด "จุดบอด" ที่มองไม่เห็นปัญหาเชิงกลไกชั่วขณะ นอกจากนี้ข้อมูลดิบจากเซนเซอร์วัดความเร่ง (Accelerometer) ก็มักจะถูกแรงโน้มถ่วงของโลกมารบกวน ทำให้แยกแยะการสั่นสะเทือนของเครื่องจักรจริงๆ ออกมาได้ยากหากไม่ได้ปรับเทียบระบบให้ดี ท้ายที่สุด จอภาพราคาประหยัดหลายรุ่นก็มักจะไม่มีหน่วยความจำถาวร ทำให้ข้อมูลประวัติการซ่อมบำรุงหายไปทุกครั้งที่ไฟดับหรือรีสตาร์ทเครื่อง

โปรเจกต์นี้คืออะไร: ทำความรู้จัก The Sentinel Hub

Sentinel Predictive Maintenance Hub คืออีโคซิสเต็มสำหรับการมอนิเตอร์ที่ออกแบบมาเพื่ออุดช่องโหว่ระหว่าง ข้อมูลดิบเชิงกลไก และ การนำข้อมูลไปใช้จริงในระดับอุตสาหกรรม โปรเจกต์นี้ไม่ใช้ชิปเดี่ยวแบบเดิมๆ แต่หันมาใช้สถาปัตยกรรมแบบ Dual-node (สองโหนด) แทน โดยให้ Raspberry Pi Pico รับหน้าที่เป็นตัวจัดการข้อมูล (Data Acquisition) โดยเฉพาะ และใช้ ESP32-S3-BOX-3 เป็นหน้าจอควบคุมอัจฉริยะ (HMI) ควบคู่ไปกับการเป็น Network Gateway

ระบบนี้จะคอยสุ่มตัวอย่างการสั่นสะเทือน 3 แกน, อุณหภูมิ, ความชื้น และความกดอากาศ โดยใช้ทีเด็ดอย่าง Dynamic High-Pass Alpha Filter ในการหักล้างผลกระทบจากแรงโน้มถ่วงแบบดิจิทัล ทำให้ได้ค่า "Linear G-Force" เพียวๆ ที่สะท้อนถึงความเครียดของกลไกได้แม่นยำสุดๆ นอกจากนี้มันยังทำหน้าที่เป็น WiFi Gateway มี Web Dashboard ให้ผู้ดูแลระบบดูสถานะแจ้งเตือนแบบเรียลไทม์ได้จากระยะไกล แถมยังมีระบบ Non-Volatile Storage (NVS) เก็บข้อมูลประวัติการบำรุงรักษาเอาไว้ได้แม้ไฟจะดับก็ตาม

อุปกรณ์ที่ต้องใช้ (Supplies)

สำหรับเพื่อนๆ ที่อยากทำตาม สามารถเตรียมอุปกรณ์ตามนี้ได้เลย (หากใครกำลังมองหาอุปกรณ์ IoT หรือไมโครคอนโทรลเลอร์เจ๋งๆ สามารถเข้าไปดูเพิ่มเติมและสั่งซื้อได้ที่ Globalbyte ครับ)

  • ESP32-S3-BOX-3
  • Raspberry Pi Pico
  • เซนเซอร์ MPU6050 (วัดการสั่นสะเทือน)
  • เซนเซอร์ BMP280 (วัดความกดอากาศ)
  • เซนเซอร์ DHT11 (วัดอุณหภูมิและความชื้น)
  • สาย Jumper Wires
  • สาย USB-C และ Micro USB
  • Breadboard 2 อัน

Step 1: การตั้งค่าฮาร์ดแวร์ (Hardware Setup)

Hardware Circuit Diagram

โปรเจกต์นี้ใช้สถาปัตยกรรมแบบกระจายการประมวลผล ดังนั้นเราต้องประกอบโหนด 2 ส่วนแยกกัน นั่นคือ Intelligent HMI (ESP32-S3) และ Sensor DAQ Node (Pi Pico W) ให้ใช้แผนผังวงจรด้านบนนี้ในการประกอบวงจรได้เลย

Step 2: การประกอบส่วน ESP32-S3-BOX-3

ESP32-S3-BOX-3 จะทำหน้าที่เป็นฮับศูนย์กลาง จัดการเรื่องหน้าจอความละเอียดสูง, ระบบ WiFi และ HTTP Web Server โดยให้เราเชื่อมต่อโมดูลหน้าจอของ ESP32-S3-BOX-3 เข้ากับโมดูล BREAD ของตัวบอร์ดเอง

Step 3: การประกอบส่วน Pi Pico

Pi Pico Assembly

Raspberry Pi Pico W ทำหน้าที่เป็นโหนดดึงข้อมูลความเร็วสูง (DAQ) คอยเก็บและประมวลผลข้อมูลจากเซนเซอร์

  • เซนเซอร์วัดความสั่นสะเทือน (MPU6050): ใช้ I2C Bus 1 (GPIO 2 - SDA, GPIO 3 - SCL) เพื่อแยกการทำงานให้เสถียรที่สุด โดยต่อ SDA เข้ากับ GPIO 2 และ SCL เข้ากับ GPIO 3
  • เซนเซอร์สภาพแวดล้อม (BMP280 & DHT11): ใช้ I2C Bus 0 (GPIO 4 - SDA, GPIO 5 - SCL) สำหรับ BMP280: ต่อ SDA เข้า GPIO 4 และ SCL เข้า GPIO 5 ส่วนขาข้อมูลของ DHT11 ให้ต่อเข้ากับ GPIO 15
  • เรื่องไฟเลี้ยง: ต่อขาพลังงานของแต่ละเซนเซอร์เข้ากับขา 3V3 Out และขา GND ของบอร์ด Pico
  • การติดตั้งเชิงกลไก: ตัว MPU6050 ต้องยึดติดกับเครื่องจักรที่ต้องการตรวจสอบให้แน่นหนาที่สุด เพื่อให้ลอจิก High-Pass Alpha Filter อ่านค่าสัญญาณได้ถูกต้องแม่นยำ

Step 4: การเชื่อมต่อสองบอร์ดเข้าด้วยกัน (ESP32 - Pico)

การเชื่อมต่อ UART:

  • RXD1 (GPIO 40) ของ ESP32 ต่อเข้ากับขา TX (GP0) ของ Pi Pico
  • TXD1 (GPIO 41) ของ ESP32 ต่อเข้ากับขา RX (GP1) ของ Pi Pico

การจัดการพลังงาน: เพื่อให้ WiFi เสถียรและป้องกันไฟตก ให้ต่อขา 3V3 Out ของ ESP32 เข้ากับขา VBUS ของ Pi Pico (หมายความว่า Pico จะรับไฟจาก ESP32) และอย่าลืมต่อสาย Ground (GND) ระหว่างสองบอร์ดให้เป็นจุดเดียวกันด้วย

การติดตั้ง HMI: จัดวางหน้าจอ BOX-3 ให้อยู่ในมุมที่มองเห็นง่าย เพราะมันจะเป็นหน้าจอหลักที่บอกสถานะสุขภาพเครื่องจักรและเวลาที่ซิงค์ผ่าน NTP

Step 5: การออกแบบ UI/UX (Visual Assets)

เอกลักษณ์ทางภาพและหน้าต่างผู้ใช้ของ Sentinel Hub ถูกสร้างขึ้นมาโดยเน้น "ความเร็วในการอ่านข้อมูล" เป็นหลัก ในสภาพแวดล้อมโรงงาน ทุกวินาทีมีค่า การออกแบบจึงเน้นความเปรียบต่างสีที่ชัดเจน และมองเห็นข้อมูลได้ทันทีในพริบตา (At-a-glance)

Step 6: การสร้าง Pixel Art Asset ด้วย GIMP

Pixel Art GIMP

เพื่อรักษาความคลีน และกินหน่วยความจำของ ESP32 ให้น้อยที่สุด โลโก้และองค์ประกอบกราฟิกทั้งหมดจึงถูกสร้างขึ้นใหม่ในสไตล์ Pixel Art โดยใช้โปรแกรม GIMP วาดไอคอนในความละเอียดเฉพาะ (เช่น 50x50) เพื่อให้ภาพยังคงคมชัดบนหน้าจอ BOX-3 โดยไม่เกิดรอยเบลอ และทำการ Export เป็นไฟล์ .png ปกติเพื่อปรับแต่ง UI ให้เหมาะสมที่สุด

Step 7: พัฒนา UI ด้วย SquareLine Studio

SquareLine Studio UI

ส่วนต่อประสานหลักถูกออกแบบผ่าน SquareLine Studio ซึ่งเป็นเครื่องมือสร้าง UI สำหรับ LVGL (Light and Versatile Graphics Library) โดยใช้หลักการ UX ดังนี้:

  • เลย์เอาต์ที่เข้าใจง่าย: มีปุ่มและสไลเดอร์ (เช่น ปรับแสงจอ) ที่โต้ตอบได้ง่าย
  • ป้ายกำกับชัดเจน: มีหน่วยระบุ (เช่น "hPa", "°C") ป้องกันความสับสน
  • ไอคอนมาตรฐาน: ใช้ไอคอนสากลเพื่อให้ทุกคนเข้าใจตรงกันข้ามกำแพงภาษา
  • ตรวจจับข้อผิดพลาดรวดเร็ว: UI ผูกกับข้อมูลเซนเซอร์โดยตรง หากค่าผิดปกติ สีพื้นหลังจะเปลี่ยนทันทีเพื่อให้พนักงานรู้ตัว

หมายเหตุสำคัญ: หากต้องการให้ UI ตรงตามแบบ คุณต้องลบโฟลเดอร์ lvgl, ui และ lv_conf.h ในโฟลเดอร์ Arduino library ตัวเก่าออก แล้วก๊อปปี้ไฟล์จาก Github Repository ลงไปแทนที่ครับ

Step 8: การพัฒนาซอฟต์แวร์และการเขียนโปรแกรม (Software Development)

เราใช้ Arduino IDE ในการเขียนโค้ดทั้งสองโหนดครับ

Part 1: โค้ดสำหรับ ESP32-S3-BOX-3 (Sentinel_ESP32.ino)
รับหน้าที่จัดการกราฟิก LVGL, WiFi และ HTTP Web Server คุณต้องติดตั้งไลบรารี LVGL (v8.3.11) และ ESP_Display_Panel ผ่าน Arduino Library Manager ก่อน และอย่าลืมแก้รหัสผ่าน WiFi ในโค้ดด้วย เลือกรุ่นบอร์ดใน IDE เป็น "ESP32S3 Box"

Part 2: โค้ดสำหรับ Pico DAQ Node (Sentinel_Pico.ino)
รับหน้าที่จัดการเซนเซอร์ด้วยความเร็วสูง โหนดนี้ต้องใช้ Earle Philhower Pico Core (ดูวิธีลงได้ที่ ลิงก์นี้) และติดตั้งไลบรารีของ Adafruit (DHT, BMP280, MPU6050, Unified Sensor) แบบ Manual การโยนงานเซนเซอร์ให้ Pico จะทำให้ ESP32 ทำงานได้อย่างลื่นไหลไม่ค้าง

Step 9: ระบบตรวจสอบระยะไกล (Remote Monitoring)

Sentinel Hub ขยายขีดความสามารถให้มากกว่าแค่อุปกรณ์ตรงหน้า ด้วยการทำตัวเป็น WiFi Gateway ทำให้เราสามารถดูข้อมูลทรัพย์สินในโรงงานได้แบบเรียลไทม์จากคอมพิวเตอร์เครื่องใดก็ได้ในเครือข่ายเดียวกัน

Step 10: การทำ Web Server Implementation

Web Server Implementation

ESP32-S3-BOX-3 มี HTTP Web Server ในตัว หน้าเว็บถูกออกแบบมาให้มินิมอล ทำงานเร็ว ข้อมูลอัปเดตทุกๆ 50ms ให้ตรงกับค่าความสั่นสะเทือนของ Pico พร้อมระบบ Dynamic CSS เปลี่ยนสีพื้นหลังได้ (เทา = ปกติ, ส้ม = ระวัง, แดง = อันตราย) โครงสร้าง HTML แบบเบาบางนี้จะไม่ไปรบกวนการทำงานของ UI หน้าจอหลักแน่นอน

Step 11: การแสดงผลผ่าน Local HTML Dashboard

Local HTML Dashboard

เพื่อความเป็นมืออาชีพ เราได้สร้างไฟล์ Local HTML ขึ้นมาสำหรับมอนิเตอร์บน PC โดยใช้ HTML Iframe ดึงข้อมูลสดๆ จาก IP ของ ESP32 วิธีนี้ทำให้เราจัดหน้าจอรวมหลายๆ เซนเซอร์ไว้ในหน้าต่างเดียวได้ และมีสคริปต์ JavaScript ช่วย Refresh ทุกๆ 500 มิลลิวินาที ไม่ดึงแบนด์วิดท์เครือข่ายมากเกินไป (ไฟล์ชื่อ Sentinel Dashboard.html)

Step 12: การพัฒนาต่อยอดในอนาคต (Future Improvements)

โปรเจกต์นี้พร้อมต่อยอดไปเป็นระบบ Industrial IoT (IIoT) แบบเต็มตัว ก้าวต่อไปคือการเพิ่มโมดูล SD Card เพื่อบันทึกข้อมูล (Local Data Logging) เก็บประวัติเพื่อวิเคราะห์เทรนด์ ซึ่งฐานข้อมูลขนาดใหญ่ที่ได้นี้แหละ จะเป็นรากฐานชั้นดีในการฝึก AI Models ต่อไป เช่น การใช้ TensorFlow Lite ตรวจจับความผิดปกติที่ตามนุษย์มองไม่เห็น

Step 13: บทสรุป (Conclusion)

Conclusion Sentinel

Sentinel Predictive Maintenance Hub คือก้าวสำคัญของการทำระบบมอนิเตอร์อุตสาหกรรมในราคาที่เข้าถึงได้และน่าเชื่อถือ การฉีกกรอบมาใช้ระบบสองโหนดช่วยแก้ปัญหาอาการค้างของระบบได้เด็ดขาด ด้วยลอจิก High-Pass Alpha Filtering, หน่วยความจำ NVS และ WiFi Gateway อุปกรณ์นี้ไม่ได้เป็นแค่เซนเซอร์ แต่มันคือ HMI อัจฉริยะที่ช่วยป้องกันเครื่องจักรพังก่อนเวลาอันควรได้อย่างแท้จริง

Step 14: โค้ดทั้งหมด (Code)

ด้านล่างนี้คือโค้ดทั้งหมดสำหรับโปรเจกต์นี้ คุณสามารถกดปุ่ม Copy มุมขวาบนของแต่ละกล่องโค้ดไปใช้งานได้เลยครับ (สามารถดูเพิ่มเติมได้ที่ GitHub Repo)

Code for the Pico DAQ Node (Sentinel_Pico.ino)

C++ (Pico)
/*
* Project Name: Sentinel
* Designed For: Raspberry Pi Pico/Pico W
*
*
* 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: March 30, 2026
* Version: 1.0.0
*
*/

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

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

// Calibration Globals
float baseMag = 0;
float offX = 0, offY = 0, offZ = 0;
float filtX = 0, filtY = 0, filtZ = 0;
const float alpha = 0.8; // Filter strength (0.8 to 0.9 is best)

void initSensors() {
Wire.end(); Wire1.end();
Wire.setSDA(4); Wire.setSCL(5); Wire.begin();
bmp.begin(0x76);
Wire1.setSDA(2); Wire1.setSCL(3); Wire1.begin();
mpu.begin(0x68, &Wire1);
dht.begin();
}

void calibrateMPU() {
Serial.println("Calibrating... Keep Still!");
float sumX = 0, sumY = 0, sumZ = 0, sumMag = 0;
int samples = 100;

for(int i = 0; i < samples; i++) {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
sumX += a.acceleration.x;
sumY += a.acceleration.y;
sumZ += a.acceleration.z;
sumMag += sqrt(pow(a.acceleration.x, 2) + pow(a.acceleration.y, 2) + pow(a.acceleration.z, 2));
delay(10);
}

offX = sumX / samples;
offY = sumY / samples;
offZ = sumZ / samples;
baseMag = sumMag / samples;
Serial.printf("Baseline Set: %.2f m/s^2\n", baseMag);
}

void setup() {
Serial.begin(115200);
Serial1.begin(115200); // TX: GP0
initSensors();
calibrateMPU(); // Run Tare Calibration
}

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

// 1. DYNAMIC HIGH-PASS FILTER (The "Gravity Canceller")
// We estimate gravity (Low Pass)
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);

// We subtract gravity to get only Linear Acceleration (High Pass)
float linX = a.acceleration.x - filtX;
float linY = a.acceleration.y - filtY;
float linZ = a.acceleration.z - filtZ;

// 2. CALCULATE VIBRATION INTENSITY (G-Force)
// This is the "Magnitude" of just the vibration components
float vibeMag = sqrt(pow(linX, 2) + pow(linY, 2) + pow(linZ, 2)) / 9.81;

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

// --- PACKET SENDING ---
// $[T, H, P, RelX, RelY, RelZ, RelVibe]*
Serial1.print("$[");
Serial1.print(t, 1); Serial1.print(",");
Serial1.print(h, 0); Serial1.print(",");
Serial1.print(p, 1); Serial1.print(",");
Serial1.print(linX, 2); Serial1.print(","); // Cleaned X
Serial1.print(linY, 2); Serial1.print(","); // Cleaned Y
Serial1.print(linZ, 2); Serial1.print(","); // Cleaned Z
Serial1.print(vibeMag, 2); // Cleaned Combined G
Serial1.println("]*");

delay(100);
}

Code for the ESP32-S3-BOX3 Node (Sentinel_ESP32.ino)

C++ (ESP32)
/*
* Project Name: Sentinel
* Designed For: ESP32 S3 BOX 3
*
*
* 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: March 30, 2026
* Version: 1.0.0
*
*/

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <time.h>
#include <Preferences.h>
#include <esp_display_panel.hpp>
#include <lvgl.h>
#include <ui.h>
#include "lvgl_v8_port.h"

using namespace esp_panel::drivers;
using namespace esp_panel::board;

// --- CONFIGURATION ---
const char* ssid = "Galaxy A14 5G 5C4D";
const char* password = "randompassword4321567890";
#define RXD1 40
#define TXD1 41

WebServer server(80);
Preferences prefs;
Board *board = nullptr;

// Global Data
float g_temp = 0, g_hum = 0, g_pres = 0;
float g_x = 0, g_y = 0, g_z = 0, g_vibe = 0;
String g_status = "WAITING";
String g_last_maint = "01/01/2026";
String g_next_maint = "01/07/2026";
int currentBrightness = 100;
unsigned long lastWifiCheck = 0;

// Web Server Background Color Logic (Vibrant Shades)
void handleRoot() {
String bg = "#1a1a1a"; // Dark Gray (default)
if (g_status == "DANGER") bg = "#ff4b2b"; // Vibrant Safety Red
else if (g_status == "CAUTION") bg = "#ff9800"; // Vibrant Safety Orange

String html = "<html><head><style>";
html += "body { font-family: 'Segoe UI', sans-serif; background: " + bg + "; color: white; padding: 25px; margin: 0; transition: background 0.4s; }";
html += ".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); }";
html += "h2 { margin: 0; text-transform: uppercase; letter-spacing: 2px; text-shadow: 2px 2px 4px rgba(0,0,0,0.5); }";
html += ".metric { margin: 18px 0; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 10px; }";
html += ".val { font-weight: bold; font-size: 1.6em; color: white; }";
html += ".label { color: #bbb; display: block; font-size: 0.85em; text-transform: uppercase; margin-bottom: 5px; }";
html += "</style></head><body><div class='card'>";
html += "<h2>SYSTEM " + g_status + "</h2><br>";

html += "<div class='metric'><span class='label'>Vibration Intensity</span><span class='val'>" + String(g_vibe) + " G</span></div>";
html += "<div class='metric'><span class='label'>Temperature</span><span class='val'>" + String(g_temp) + " C</span></div>";
html += "<div>" + String(g_hum) + "% Humidity | " + String(g_pres) + " hPa</div><br>";
html += "<div style='font-size: 0.8em; color: #aaa;'>Service: " + g_last_maint + " | Due: " + g_next_maint + "</div>";
html += "</div></body></html>";
server.send(200, "text/html", html);
}

// --- EVENT HANDLERS ---

void onBrightnessChange(lv_event_t * e) {
lv_obj_t * slider = lv_event_get_target(e);
int val = lv_slider_get_value(slider);
if (val < 10) val = 10;
currentBrightness = val;
board->getBacklight()->setBrightness(currentBrightness);
}

void updateSettingsScreenUI() {
if (millis() - lastWifiCheck > 2000) {
lastWifiCheck = millis();
String ssidStr = WiFi.SSID();
if(ssidStr.isEmpty()) ssidStr = "Searching...";
lv_label_set_text(ui_LabelWifiSSID, ("SSID: " + ssidStr).c_str());

if (WiFi.status() == WL_CONNECTED) {
lv_label_set_text(ui_LabelWifiStatus, "Status: Connected");
lv_obj_set_style_text_color(ui_LabelWifiStatus, lv_color_hex(0x00FF00), 0);
} else {
lv_label_set_text(ui_LabelWifiStatus, "Status: Disconnected");
lv_obj_set_style_text_color(ui_LabelWifiStatus, lv_color_hex(0xFF0000), 0);
}
}
}

void onMaintBtnClicked(lv_event_t * e) {
struct tm info;
if (getLocalTime(&info)) {
char buf[15];
strftime(buf, sizeof(buf), "%d/%m/%Y", &info);
g_last_maint = String(buf);
info.tm_mon += 6;
mktime(&info);
strftime(buf, sizeof(buf), "%d/%m/%Y", &info);
g_next_maint = String(buf);

prefs.begin("maint", false);
prefs.putString("last", g_last_maint);
prefs.putString("next", g_next_maint);
prefs.end();

lv_label_set_text(ui_LabelLastMaintainaceDate, g_last_maint.c_str());
lv_label_set_text(ui_LabelNextMaintDate, g_next_maint.c_str());
}
}

void setup() {
Serial.begin(115200);
Serial1.begin(115200, SERIAL_8N1, RXD1, TXD1);
Serial1.setTimeout(50);

board = new Board();
board->init();
assert(board->begin());
lvgl_port_init(board->getLCD(), board->getTouch());

lvgl_port_lock(-1);
ui_init();

if(ui_ButtonUpdateMaint) lv_obj_add_event_cb(ui_ButtonUpdateMaint, onMaintBtnClicked, LV_EVENT_CLICKED, NULL);

if (ui_SliderBrightness) {
lv_slider_set_range(ui_SliderBrightness, 10, 100);
lv_slider_set_value(ui_SliderBrightness, 100, LV_ANIM_OFF);
lv_obj_add_event_cb(ui_SliderBrightness, onBrightnessChange, LV_EVENT_VALUE_CHANGED, NULL);
}

prefs.begin("maint", true);
g_last_maint = prefs.getString("last", "01/01/2026");
g_next_maint = prefs.getString("next", "01/07/2026");
prefs.end();

lv_label_set_text(ui_LabelLastMaintainaceDate, g_last_maint.c_str());
lv_label_set_text(ui_LabelNextMaintDate, g_next_maint.c_str());
lvgl_port_unlock();

WiFi.begin(ssid, password);
server.on("/", handleRoot);
server.begin();
configTime(19800, 0, "pool.ntp.org");
}

void loop() {
server.handleClient();
static unsigned long lastClock = 0;
struct tm timeinfo;

if (lv_scr_act() == ui_SettingScreen) {
lvgl_port_lock(-1);
updateSettingsScreenUI();
lvgl_port_unlock();
}

if (Serial1.available() > 0) {
String raw = Serial1.readStringUntil('\n');
if (raw.startsWith("$[")) {
String data = raw.substring(raw.indexOf("$[") + 2, raw.indexOf("]"));
float v[7]; int count = 0;
char* ptr = strtok((char*)data.c_str(), ",");
while (ptr != NULL && count < 7) { v[count++] = atof(ptr); ptr = strtok(NULL, ","); }

if (count == 7) {
g_temp = v[0]; g_hum = v[1]; g_pres = v[2];
g_x = v[3]; g_y = v[4]; g_z = v[5]; g_vibe = v[6];

lvgl_port_lock(-1);
uint32_t s_color = 0x00FF00;
if (g_temp > 55.0 || g_vibe > 0.70) { g_status = "DANGER"; s_color = 0xFF0000; }
else if (g_temp > 42.0 || g_vibe > 0.15) { g_status = "CAUTION"; s_color = 0xFFA500; }
else { g_status = "HEALTHY"; s_color = 0x00FF00; }

lv_label_set_text(ui_LabelHealthStatus, g_status.c_str());
lv_obj_set_style_text_color(ui_LabelHealthStatus, lv_color_hex(s_color), 0);

char b[16];
snprintf(b, sizeof(b), "%.1f C", g_temp);
lv_label_set_text(ui_LabelTemp, b); lv_label_set_text(ui_LabelTempES, b);

snprintf(b, sizeof(b), "%.0f %%", g_hum);
lv_label_set_text(ui_LabelHum, b); lv_label_set_text(ui_LabelHumES, b);

snprintf(b, sizeof(b), "%.1f hPa", g_pres); lv_label_set_text(ui_LabelBMPressure, b);
snprintf(b, sizeof(b), "X: %.2f G", g_x); lv_label_set_text(ui_LabelXaxis, b);
snprintf(b, sizeof(b), "Y: %.2f G", g_y); lv_label_set_text(ui_LabelYaxis, b);
snprintf(b, sizeof(b), "Z: %.2f G", g_z); lv_label_set_text(ui_LabelZaxis, b);
snprintf(b, sizeof(b), "%.2f G", g_vibe); lv_label_set_text(ui_LabelCombinedMPU, b);
lvgl_port_unlock();
}
}
}

if (millis() - lastClock > 1000) {
lastClock = millis();
if (getLocalTime(&timeinfo)) {
char t_b[10], p_b[5], d_b[15];
strftime(t_b, sizeof(t_b), "%I:%M", &timeinfo);
strftime(p_b, sizeof(p_b), "%p", &timeinfo);
strftime(d_b, sizeof(d_b), "%d/%m/%Y", &timeinfo);
lvgl_port_lock(-1);
lv_label_set_text(ui_LabelTime12Hr, t_b);
lv_label_set_text(ui_LabelTimeAMPM, p_b);
lv_label_set_text(ui_LabelDate, d_b);
lvgl_port_unlock();
}
}
delay(2);
}

Code for the Local HTML File (Sentinel Dashboard.html)

HTML (Dashboard)
<!DOCTYPE html>
<html>
<head>
    <title>Sentinel Remote Dashboard</title>
    <style>
        body { background: #000; color: white; text-align: center; font-family: sans-serif; }
        iframe { width: 400px; height: 415px; border: 2px solid #333; border-radius: 10px; margin-top: 50px; }
        .info { margin-top: 10px; color: #888; }
    </style>
</head>
<body>
    <h1>Predictive Maintenance Monitor</h1>
    <iframe id="sentinelFrame" src="http://10.247.207.62"></iframe>

    <script>
        setInterval(function() {
            document.getElementById('sentinelFrame').src = document.getElementById('sentinelFrame').src;
        }, 500);
    </script>
</body>
</html>

Step 15: ตรวจสอบข้อมูลทั้งหมดบน Github

หากเพื่อนๆ ต้องการไฟล์การออกแบบ, โค้ดต้นฉบับฉบับเต็ม, รูปภาพ และคู่มือเพิ่มเติม สามารถเข้าไปดูที่ Github Repository ของโปรเจกต์ได้เลยครับ

สนใจเริ่มต้นโปรเจกต์ของตัวเองหรือหาไอเดียใหม่ๆ?

ช้อปอุปกรณ์ทั้งหมด เข้าร่วม Community ของเรา สั่งซื้อผ่าน LINE

อ้างอิงข้อมูลจาก: Globalbyteshop Blog

ต้นฉบับบทความโดย: Ameya Angadi | Original Link | ดาวน์โหลด PDF ต้นฉบับ

*คำเตือน: เนื้อหานี้เป็นการสรุปและเรียบเรียงจากบทความต้นฉบับภาษาอังกฤษ ข้อมูลฉบับภาษาไทยอาจมีความคลาดเคลื่อนบางประการจากการตีความหรือย่อเนื้อหา สามารถตรวจสอบเนื้อหาโดยละเอียดได้ที่ ต้นฉบับภาษาอังกฤษ

 

แท็ก


Blog posts

เข้าสู่ระบบ

ลืมรหัสผ่านใช่ไหม?

ยังไม่มีบัญชีใช่ไหม?
สร้างบัญชี