ชุดใบงานปฏิบัติการ: ระบบ Modbus RTU & TCP ด้วย ESP32

เรียนรู้การเชื่อมต่อระบบสื่อสารอุตสาหกรรม Modbus จากพื้นฐานสู่ระดับประยุกต์ใช้งานจริง 10 บทเรียน

ระดับ 1: พื้นฐาน

ใบงานที่ 1: การสื่อสาร RS485 เบื้องต้นและการทดสอบ Loopback

วัตถุประสงค์: เรียนรู้โครงสร้างสายสัญญาณ RS485, โมดูล MAX485/SP3485 และการควบคุมพิน DE/RE บน ESP32

💡 คำอธิบายเสริมให้เข้าใจง่าย:

Modbus คืออะไร? คือภาษา (Protocol) ที่อุปกรณ์อุตสาหกรรมใช้คุยกันผ่านสายสัญญาณ 2 เส้น (A และ B)

RS485 คืออะไร? คือชนิดของสายสัญญาณ physical (การใช้ความต่างศักย์ไฟฟ้าส่งข้อมูล) สาย A กับ B ทำงานแบบ Differential Signal ทนสัญญาณรบกวนได้สูงมาก ยาวได้ถึง 1.2 กิโลเมตร

ทำไมต้องมีขา DE / RE? สื่อสารแบบ Half-Duplex (ผลัดกันพูด) ขา DE (Driver Enable) ใช้เปิดตอนส่งข้อมูล และ RE (Receiver Enable) ใช้เปิดตอนรับข้อมูล ปกติต่อรวมกันเข้าพิน GPIO เดียวกัน

การต่อวงจร (Hardware Wiring)

ESP32 MAX485 Module GPIO16 (RX2) -----------> RO (Receiver Output) GPIO17 (TX2) -----------> DI (Driver Input) GPIO4 -----------> DE + RE (ต่อขนานกัน) 3.3V / 5V -----------> VCC GND -----------> GND

โค้ดตัวอย่าง ESP32 (Arduino IDE)

ติดตั้งไลบรารี: ModbusMaster โดย Doc Walker ผ่าน Library Manager

#include <HardwareSerial.h>

#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17

void setup() {
  Serial.begin(115200);
  pinMode(MAX485_DE_RE, OUTPUT);
  digitalWrite(MAX485_DE_RE, LOW); // เริ่มต้นตั้งเป็นโหมดรับ (Receive Mode)

  // เริ่มต้น HardwareSerial2 สำหรับ RS485
  Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);
  Serial.println("RS485 Hardware Test Ready");
}

void loop() {
  // สลับเป็นโหมดส่ง (Transmit Mode)
  digitalWrite(MAX485_DE_RE, HIGH);
  delay(1);
  Serial2.println("Hello Modbus World!");
  Serial2.flush(); // รอส่งข้อมูลเสร็จสิ้น
  
  // สลับกลับเป็นโหมดรับ (Receive Mode)
  digitalWrite(MAX485_DE_RE, LOW);

  delay(2000);
}

🎯 โจทย์ท้าทาย:

ลองใช้บอร์ด ESP32 สองบอร์ด ต่อสาย A-A และ B-B เข้าหากัน แล้วทดสอบส่งข้อความสวนกันเพื่อเช็คว่าโมดูลสื่อสารได้ถูกต้องหรือไม่

ระดับ 2: เริ่มต้น

ใบงานที่ 2: การอ่านค่าเซนเซอร์อุณหภูมิและความชื้น XY-MD02 (Modbus RTU Master)

วัตถุประสงค์: สื่อสารอ่านค่า Register จาก Modbus Slave จริง ผ่านฟังก์ชัน Read Input Registers (0x04)

💡 คำอธิบายเสริมให้เข้าใจง่าย:

Modbus Register คืออะไร? เปรียบเหมือน "ตู้ไปรษณีย์" ที่มีหมายเลขกำกับ (Address)

  • 01 Read Coils (0x01): อ่านสถานะ Digital Output (ON/OFF)
  • 02 Read Discrete Inputs (0x02): อ่านสถานะ Digital Input (ON/OFF)
  • 03 Read Holding Registers (0x03): อ่าน/เขียนค่าตัวเลข (Read/Write 16-bit)
  • 04 Read Input Registers (0x04): อ่านค่าตัวเลขจากเซนเซอร์ได้อย่างเดียว (Read-only 16-bit)

เซนเซอร์ XY-MD02 มี Address สถิตคือ Slave ID = 1, คำนวณอุณหภูมิที่ Register 0x0001 และความชื้นที่ Register 0x0002 (ต้องหารด้วย 10.0)

โค้ดตัวอย่าง ESP32

#include <ModbusMaster.h>

#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17

ModbusMaster node;

void preTransmission() {
  digitalWrite(MAX485_DE_RE, HIGH);
}

void postTransmission() {
  digitalWrite(MAX485_DE_RE, LOW);
}

void setup() {
  Serial.begin(115200);
  pinMode(MAX485_DE_RE, OUTPUT);
  digitalWrite(MAX485_DE_RE, LOW);

  Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);

  // ตั้งค่า Modbus Slave ID = 1
  node.begin(1, Serial2);
  node.preTransmission(preTransmission);
  node.postTransmission(postTransmission);
}

void loop() {
  uint8_t result;
  
  // อ่าน Input Register ตั้งแต่ 0x0001 จำนวน 2 ตัว
  result = node.readInputRegisters(0x0001, 2);

  if (result == node.ku8MBSuccess) {
    float temperature = node.getResponseBuffer(0) / 10.0;
    float humidity = node.getResponseBuffer(1) / 10.0;

    Serial.print("Temp: ");
    Serial.print(temperature);
    Serial.print(" C | Humidity: ");
    Serial.print(humidity);
    Serial.println(" %");
  } else {
    Serial.print("Modbus Error: ");
    Serial.println(result, HEX);
  }

  delay(2000);
}

🎯 โจทย์ท้าทาย:

แปลงค่าอุณหภูมิที่อ่านได้เป็นองศาฟาเรนไฮต์ (F) แล้วแสดงผลผ่าน Serial Monitor

ระดับ 3: ปานกลาง

ใบงานที่ 3: การอ่านค่าแร่ธาตุในดิน NPK 7-in-1 Soil Sensor

วัตถุประสงค์: อ่านค่าข้อมูลหลายค่าต่อเนื่องกัน (Multiple Registers) จากเซนเซอร์วัดคุณภาพดินอุตสาหกรรม

💡 คำอธิบายเสริมให้เข้าใจง่าย:

เซนเซอร์ NPK ในเกษตรแม่นยำใช้ Modbus RTU เช่นกัน โดยส่งค่า 7 ค่ารวดเดียว: Moisture, Temp, EC, pH, Nitrogen (N), Phosphorus (P), Potassium (K)

การส่งครั้งเดียวคำสั่งเดียวอ่านได้ 7 Register รวด ช่วยลดความซับซ้อนและประหยัดเวลาส่งข้อมูลในบัส

โค้ดตัวอย่าง ESP32

#include <ModbusMaster.h>

#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17

ModbusMaster node;

void preTransmission() { digitalWrite(MAX485_DE_RE, HIGH); }
void postTransmission() { digitalWrite(MAX485_DE_RE, LOW); }

void setup() {
  Serial.begin(115200);
  pinMode(MAX485_DE_RE, OUTPUT);
  digitalWrite(MAX485_DE_RE, LOW);
  Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);

  node.begin(1, Serial2); // Slave ID 1
  node.preTransmission(preTransmission);
  node.postTransmission(postTransmission);
}

void loop() {
  uint8_t result;
  // อ่าน Read Holding Registers (0x03) ตั้งแต่ Address 0x0000 จำนวน 7 ตัว
  result = node.readHoldingRegisters(0x0000, 7);

  if (result == node.ku8MBSuccess) {
    float moisture = node.getResponseBuffer(0) / 10.0;
    float temp     = node.getResponseBuffer(1) / 10.0;
    uint16_t ec    = node.getResponseBuffer(2);
    float ph       = node.getResponseBuffer(3) / 10.0;
    uint16_t n     = node.getResponseBuffer(4);
    uint16_t p     = node.getResponseBuffer(5);
    uint16_t k     = node.getResponseBuffer(6);

    Serial.printf("Moist: %.1f%% | Temp: %.1fC | EC: %d us/cm | pH: %.1f
", moisture, temp, ec, ph);
    Serial.printf("N: %d mg/kg | P: %d mg/kg | K: %d mg/kg
", n, p, k);
    Serial.println("--------------------------------------------------");
  } else {
    Serial.println("Read Failed!");
  }
  delay(3000);
}
ระดับ 4: ปานกลาง

ใบงานที่ 4: การสั่งงานเปิด-ปิด RS485 Relay Board (Modbus Write Single Coil)

วัตถุประสงค์: เรียนรู้การส่งคำสั่งเขียนข้อมูล (Write Single Coil 0x05 / Write Single Register 0x06) เพื่อควบคุมเอาต์พุต

💡 คำอธิบายเสริมให้เข้าใจง่าย:

โมดูล Relay RS485 ใช้คำสั่ง Write Coil (0x05) ในการสั่ง ON (0xFF00) หรือ OFF (0x0000) ที่ Coil Address ต่างๆ เช่น Relay 1 = 0x0000, Relay 2 = 0x0001

โค้ดตัวอย่าง ESP32

#include <ModbusMaster.h>

#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17

ModbusMaster node;

void preTransmission() { digitalWrite(MAX485_DE_RE, HIGH); }
void postTransmission() { digitalWrite(MAX485_DE_RE, LOW); }

void setup() {
  Serial.begin(115200);
  pinMode(MAX485_DE_RE, OUTPUT);
  digitalWrite(MAX485_DE_RE, LOW);
  Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);

  node.begin(1, Serial2); // Relay Board Slave ID 1
  node.preTransmission(preTransmission);
  node.postTransmission(postTransmission);
}

void loop() {
  Serial.println("Turning Relay 1 ON...");
  // writeSingleCoil(Coil_Address, State)
  node.writeSingleCoil(0x0000, true); 
  delay(3000);

  Serial.println("Turning Relay 1 OFF...");
  node.writeSingleCoil(0x0000, false);
  delay(3000);
}
ระดับ 5: ปานกลางค่อนข้างสูง

ใบงานที่ 5: การอ่านมิเตอร์ไฟฟ้า PZEM-017 AC/DC Energy Meter (Float/32-bit Data)

วัตถุประสงค์: จัดการข้อมูลขนาดใหญ่แบบ 32-bit (Combine 2 Registers) และค่าความแม่นยำสูง

💡 คำอธิบายเสริมให้เข้าใจง่าย:

Modbus Register 1 ตัวมีขนาด 16 บิต (0-65535) แต่ข้อมูลอย่าง กำลังไฟฟ้า (Watt) หรือ พลังงานสะสม (kWh) มีค่าสูงเกิน 65,535 จึงต้องใช้ 2 Registers รวมกันเป็น 32 บิต

การแปลง 32 บิต: (Register High << 16) | Register Low

โค้ดตัวอย่าง ESP32

#include <ModbusMaster.h>

#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17

ModbusMaster node;

void preTransmission() { digitalWrite(MAX485_DE_RE, HIGH); }
void postTransmission() { digitalWrite(MAX485_DE_RE, LOW); }

void setup() {
  Serial.begin(115200);
  pinMode(MAX485_DE_RE, OUTPUT);
  digitalWrite(MAX485_DE_RE, LOW);
  Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);

  node.begin(1, Serial2);
  node.preTransmission(preTransmission);
  node.postTransmission(postTransmission);
}

void loop() {
  uint8_t result = node.readInputRegisters(0x0000, 6);

  if (result == node.ku8MBSuccess) {
    uint16_t voltage_raw = node.getResponseBuffer(0);
    uint16_t current_raw = node.getResponseBuffer(1);
    
    // Power ใช้ 32-bit (Reg 2 + Reg 3)
    uint32_t power_raw = ((uint32_t)node.getResponseBuffer(3) << 16) | node.getResponseBuffer(2);

    float voltage = voltage_raw * 0.1;
    float current = current_raw * 0.01;
    float power   = power_raw * 0.1;

    Serial.printf("Voltage: %.1f V | Current: %.2f A | Power: %.1f W
", voltage, current, power);
  } else {
    Serial.println("PZEM Read Error");
  }
  delay(2000);
}
ระดับ 6: สูง

ใบงานที่ 6: การสร้าง ESP32 จำลองตัวเองเป็น Modbus Slave (RTU Slave)

วัตถุประสงค์: ทำให้ ESP32 ทำหน้าที่เป็น Slave Device ตอบสนองต่อคำสั่งอ่าน/เขียนจาก Master (เช่น จอ HMI หรือ PLC)

💡 คำอธิบายเสริมให้เข้าใจง่าย:

คราวนี้ ESP32 จะไม่ถามใครแล้ว แต่จะนั่งรอ Master (เช่น จอ HMI หรือ PLC) มาขออ่านค่าเซนเซอร์ที่ต่ออยู่กับ ESP32 หรือส่งคำสั่งมาเปิดไฟที่ ESP32

โค้ดตัวอย่าง ESP32

ติดตั้งไลบรารี: ModbusRTUSlave โดย Carbonell

#include <ModbusRTUSlave.h>

#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17

ModbusRTUSlave modbus(Serial2, MAX485_DE_RE);

// สร้างตารางข้อมูล Register ของ Slave
bool coils[8];
uint16_t holdingRegisters[8];

void setup() {
  Serial.begin(115200);
  Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);

  // ตั้งค่าป้ายชื่อบอก Modbus Slave
  modbus.configureCoils(coils, 8);
  modbus.configureHoldingRegisters(holdingRegisters, 8);

  // เริ่มต้น Slave ID = 2, Baudrate = 9600
  modbus.begin(2, 9600);
}

void loop() {
  // อัปเดตข้อมูลจำลองใส่ Holding Register 0
  holdingRegisters[0] = analogRead(34); // อ่านค่า ADC
  holdingRegisters[1] = millis() / 1000; // ค่าเวลา

  modbus.poll(); // คอยฟังสัญญาณคำสั่งจาก Master
}
ระดับ 7: สูง

ใบงานที่ 7: การรับ-ส่งข้อมูลระหว่าง ESP32 กับ PLC (Mitsubishi FX3U / Siemens S7)

วัตถุประสงค์: สื่อสารระหว่าง ESP32 (Master) และ PLC (Slave) ผ่าน Modbus RTU เพื่ออ่านค่า Register D และสั่งงาน M Relay

💡 คำอธิบายเสริมให้เข้าใจง่าย:

ในโรงงาน PLC เช่น Mitsubishi FX3U (ติดบอร์ด FX3U-485ADP-MB) มีความเสถียรสูง คุมจักรกลหนัก แต่ขาดฟังก์ชันส่งข้อมูลขึ้น Cloud

เราสามารถใช้ ESP32 ต่อ Modbus RTU ไปดึงข้อมูล Register D (เช่น D0, D100) มาประมวลผลหรือส่งขึ้นระบบ IoT ได้ทันที

ตารางการแมป Address (Mitsubishi FX3U Modbus Map)

โค้ดตัวอย่าง ESP32 อ่าน D0 จาก PLC

#include <ModbusMaster.h>

#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17

ModbusMaster plcNode;

void preTransmission() { digitalWrite(MAX485_DE_RE, HIGH); }
void postTransmission() { digitalWrite(MAX485_DE_RE, LOW); }

void setup() {
  Serial.begin(115200);
  pinMode(MAX485_DE_RE, OUTPUT);
  Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);

  plcNode.begin(1, Serial2); // PLC Station Address = 1
  plcNode.preTransmission(preTransmission);
  plcNode.postTransmission(postTransmission);
}

void loop() {
  // อ่านค่า Data Register D0 จาก PLC (Address 0x0000)
  uint8_t result = plcNode.readHoldingRegisters(0x0000, 1);

  if (result == plcNode.ku8MBSuccess) {
    uint16_t plc_d0_value = plcNode.getResponseBuffer(0);
    Serial.printf("PLC Register D0 Value: %d
", plc_d0_value);
  } else {
    Serial.println("PLC Communication Failed!");
  }
  delay(1000);
}
ระดับ 8: ขั้นสูง

ใบงานที่ 8: การสื่อสาร Modbus TCP Master ผ่านเครือข่าย Wi-Fi

วัตถุประสงค์: เปลี่ยนจากการใช้สาย RS485 มาใช้การส่งข้อมูล Modbus ผ่านโครงข่าย Ethernet / Wi-Fi (IP Address & Port 502)

💡 คำอธิบายเสริมให้เข้าใจง่าย:

Modbus TCP ต่างจาก Modbus RTU อย่างไร?

  • Modbus RTU: วิ่งบนสาย RS485 ต้องคำนวณ Checksum (CRC) ต่อท้ายแพ็กเกจ
  • Modbus TCP: วิ่งบน Wi-Fi/LAN (IP Address) ไม่ต้องใช้ CRC (เพราะ TCP/IP จัดการให้อยู่แล้ว) และทำงานเร็วขึ้นสูงมาก
  • พอร์ตมาตรฐานของ Modbus TCP คือ Port 502

โค้ดตัวอย่าง ESP32 (Modbus TCP Client/Master)

ติดตั้งไลบรารี: ModbusIP_ESP8266 โดย emelianov

#include <WiFi.h>
#include <ModbusIP_ESP8266.h>

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

IPAddress remoteServer(192, 168, 1, 100); // IP ของ Modbus TCP Slave Server
ModbusIP mb;

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("
WiFi Connected!");

  mb.client(); // กำหนดโหมดเป็น Master (Client)
}

void loop() {
  if (mb.isConnected(remoteServer)) {
    uint16_t res[2];
    // อ่าน Holding Register Address 0 จำนวน 1 ค่า จาก Server
    mb.readHreg(remoteServer, 0, res, 1);
    mb.task(); // ประมวลผลแบ็กกราวด์ Modbus TCP
    
    Serial.printf("Data from TCP Server: %d
", res[0]);
  } else {
    mb.connect(remoteServer); // พยายามเชื่อมต่อ Server Port 502
    Serial.println("Connecting to Modbus TCP Server...");
  }
  delay(2000);
}
ระดับ 9: ขั้นสูง

ใบงานที่ 9: การสร้าง ESP32 Modbus TCP Server (Slave) สำหรับ SCADA / Node-RED

วัตถุประสงค์: เปิด ESP32 เป็น Server ให้ซอฟต์แวร์ SCADA, Node-RED หรือ HMI มาอ่าน/เขียนข้อมูลผ่าน Wi-Fi

💡 คำอธิบายเสริมให้เข้าใจง่าย:

จำลอง ESP32 เป็น PLC หรือ Smart Device บนเครือข่าย Wi-Fi โดยเปิดพอร์ต 502 รอรับการเชื่อมต่อจากซอฟต์แวร์ เช่น Node-RED, Ignition SCADA, หรือโปรแกรม Modbus Poll

โค้ดตัวอย่าง ESP32 Modbus TCP Server

#include <WiFi.h>
#include <ModbusIP_ESP8266.h>

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

// กำหนด Register Offset
const int SENSOR_HREG = 100;

ModbusIP mb;

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  mb.server(); // กำหนดเป็น Server (Slave) Listen Port 502
  
  // เพิ่ม Holding Register ที่ Address 100
  mb.addHreg(SENSOR_HREG);
}

void loop() {
  mb.task(); // คอยรับ-ส่งข้อมูล TCP

  // อัปเดตค่าจำลองเข้าไปที่ Register 100
  uint16_t adcVal = analogRead(34);
  mb.Hreg(SENSOR_HREG, adcVal);

  delay(10);
}

🎯 โจทย์ท้าทาย:

ใช้โปรแกรม Modbus Poll หรือ Node-RED ในคอมพิวเตอร์ ยิง Modbus TCP Read มายัง IP Address ของ ESP32 เพื่อดูค่า ADC 34 Real-time

ระดับ 10: ผู้เชี่ยวชาญ

ใบงานที่ 10: โปรเจกต์ RS485 to Modbus TCP Gateway (Bridge)

วัตถุประสงค์: สร้างอุปกรณ์แปลงโปรโตคอล (Gateway) รับข้อมูลจาก Modbus RTU (RS485) มาแปลงเป็น Modbus TCP ส่งต่อให้ SCADA ผ่าน Wi-Fi

💡 คำอธิบายเสริมให้เข้าใจง่าย:

อุปกรณ์ในโรงงานส่วนใหญ่รุ่นเก่าใช้ RS485 แต่ระบบควบคุมสมัยใหม่ในห้อง Control Room ใช้ Ethernet/Wi-Fi (TCP/IP)

ESP32 จะทำหน้าที่เป็น "ตัวแปลภาษา" (Gateway) โดยฝั่งหนึ่งใช้ RS485 ไปอ่านค่าจากเซนเซอร์ แล้วนำค่านั้นมาพักไว้ใน TCP Register เพื่อให้ SCADA ดึงผ่าน Wi-Fi

ผังโครงสร้าง Gateway System

[RS485 Sensors] <--- Modbus RTU ---> [ ESP32 Gateway ] <--- Modbus TCP (Wi-Fi) ---> [ SCADA / Dashboard ] (Slave ID 1) (Modbus Server) (Client / PC)

โค้ดตัวอย่าง ESP32 Full Gateway

#include <WiFi.h>
#include <ModbusMaster.h>
#include <ModbusIP_ESP8266.h>

#define MAX485_DE_RE 4
#define RX2_PIN 16
#define TX2_PIN 17

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

ModbusMaster rtuNode;
ModbusIP tcpServer;

void preTransmission() { digitalWrite(MAX485_DE_RE, HIGH); }
void postTransmission() { digitalWrite(MAX485_DE_RE, LOW); }

unsigned long lastUpdate = 0;

void setup() {
  Serial.begin(115200);
  
  // 1. Setup RS485 Hardware
  pinMode(MAX485_DE_RE, OUTPUT);
  Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);
  rtuNode.begin(1, Serial2); // อ่าน RTU Slave ID 1
  rtuNode.preTransmission(preTransmission);
  rtuNode.postTransmission(postTransmission);

  // 2. Setup Wi-Fi & Modbus TCP Server
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  Serial.print("Gateway IP: ");
  Serial.println(WiFi.localIP());

  tcpServer.server();
  tcpServer.addHreg(0); // Map ค่า Temp จาก RTU
  tcpServer.addHreg(1); // Map ค่า Hum จาก RTU
}

void loop() {
  tcpServer.task(); // รักษาสายเชื่อมต่อ Modbus TCP

  // อ่านค่าจาก RS485 ทุกๆ 2 วินาที แล้วแมปลง TCP Register
  if (millis() - lastUpdate > 2000) {
    lastUpdate = millis();

    uint8_t result = rtuNode.readInputRegisters(0x0001, 2);
    if (result == rtuNode.ku8MBSuccess) {
      uint16_t tempRaw = rtuNode.getResponseBuffer(0);
      uint16_t humRaw  = rtuNode.getResponseBuffer(1);

      // อัปเดตข้อมูลเข้า Modbus TCP Server Register
      tcpServer.Hreg(0, tempRaw);
      tcpServer.Hreg(1, humRaw);

      Serial.printf("[Gateway Sync] Temp Raw: %d | Hum Raw: %d
", tempRaw, humRaw);
    }
  }
}