IOT with netpie 2020 esp8266
IOT with netpie 2020 esp32
OT with broker.mqtt-dashboard esp8266 + nano
IOT with mqtt esp32
IOT with mqtt in4 out4 esp32
การสือสาร CANBUS arduino nano sent String
หุ่นยนต์ หมา สี่ขา servo 8 ตัว ควบคุมผ่าน internet mqttgo.io
การส่งข้อมูลจาก esp32 หรือ esp8266 ไปยังเก็บที่ database mysql
การสือสาร CANBUS arduino nano2 sent hex
การสือสาร esp_now สำหรับ esp32 ใช้ควบคุมฝูงโดรน
การสือสาร esp_now สำหรับ esp32 ใช้ควบนาฬิกาหลอดไฟ 7 segment 220 vote
การสือสาร esp_now สำหรับ esp32 ใช้ควบคุมไฟวิ่งตามเสียงเพลง
ตัวอย่างใบงาน espnow โดยใช้ esp32
ตัวอย่างใบงาน iot โดยใช้ esp32
การสร้าง หุ่นยนต์ ESP32 บังคับ ผ่าน Bluetooth
การสร้าง หุ่นยนต์ ESP32 บังคับ ผ่าน WIFI
โปรเจค ESP32 เปิด-ปิดไฟ ผ่านแอปบน Mac ด้วย Blynk
การอ่านข้อมูลจาก LIDAR
การทดสอบ แสดง gauge
คู่มือใบงาน ESP32 IoT MQTT (HiveMQ)
ยินดีต้อนรับสู่ชุดใบงานสำหรับเรียนรู้ ESP32 ระดับมืออาชีพ จำนวน 10 ใบงาน เรียนรู้ตั้งแต่พื้นฐานจนถึงการสร้าง Web Dashboard ร่วมกับ MQTT
🔌 การต่อสายฮาร์ดแวร์ (Pinout)
Inputs (สวิตช์แบบ Active Low - ใช้งานแบบ Pull-up):
- SW1 = GPIO 4
- SW2 = GPIO 5
- SW3 = GPIO 18
- SW4 = GPIO 19
- SW Reset WiFi = GPIO 15
Outputs (โหลดหรือ LED แบบ Active High):
- O1 = GPIO 13
- O2 = GPIO 12
- O3 = GPIO 14
- O4 = GPIO 27
กรุณาเลือกใบงานจากแถบเมนูด้านซ้ายเพื่อเริ่มการเรียนรู้
Lab 01: Basic I/O & Hardware Test
ทดสอบการอ่านค่าอินพุต (สวิตช์) และเขียนค่าออกเอาต์พุต (LED/Relay) เพื่อให้มั่นใจว่าฮาร์ดแวร์ทำงานได้ถูกต้อง โดยใช้คำสั่ง INPUT_PULLUP เพื่อไม่ต้องต่อตัวต้านทานภายนอก
// Lab 01: Basic I/O & Hardware Test
#define SW1 4
#define SW2 5
#define SW3 18
#define SW4 19
#define O1 13
#define O2 12
#define O3 14
#define O4 27
void setup() {
Serial.begin(115200);
pinMode(SW1, INPUT_PULLUP);
pinMode(SW2, INPUT_PULLUP);
pinMode(SW3, INPUT_PULLUP);
pinMode(SW4, INPUT_PULLUP);
pinMode(O1, OUTPUT);
pinMode(O2, OUTPUT);
pinMode(O3, OUTPUT);
pinMode(O4, OUTPUT);
}
void loop() {
digitalWrite(O1, !digitalRead(SW1));
digitalWrite(O2, !digitalRead(SW2));
digitalWrite(O3, !digitalRead(SW3));
digitalWrite(O4, !digitalRead(SW4));
delay(50);
}
Lab 02: Smart WiFi Connection (WiFiManager)
เชื่อมต่อ WiFi โดยไม่ฮาร์ดโค้ดรหัสผ่านลงในโค้ด ใช้ไลบรารี WiFiManager โดยหากหาเน็ตไม่เจอ ESP32 จะปล่อย AP ออกมา และสามารถกดปุ่มที่ขา 15 เพื่อล้างค่าได้
// Lab 02: Smart WiFi Connection
#include <WiFi.h>
#include <WiFiManager.h>
#define WIFI_RESET_PIN 15
void setup() {
Serial.begin(115200);
pinMode(WIFI_RESET_PIN, INPUT_PULLUP);
WiFiManager wm;
if (digitalRead(WIFI_RESET_PIN) == LOW) {
wm.resetSettings();
ESP.restart();
}
wm.autoConnect("ESP32_Setup", "password");
Serial.println("Connected to WiFi!");
}
void loop() {
if (digitalRead(WIFI_RESET_PIN) == LOW) {
delay(100);
if (digitalRead(WIFI_RESET_PIN) == LOW) {
WiFiManager wm;
wm.resetSettings();
ESP.restart();
}
}
}
Lab 03: Hello HiveMQ (MQTT Connection)
การตั้งค่าและเชื่อมต่อ ESP32 เข้ากับระบบ Cloud ผ่าน HiveMQ Public Broker โดยใช้ไลบรารี PubSubClient
// Lab 03: Hello HiveMQ
#include <WiFi.h>
#include <WiFiManager.h>
#include <PubSubClient.h>
#define WIFI_RESET_PIN 15
const char* mqtt_server = "broker.hivemq.com";
String clientId = "ESP32_Client_" + String(random(0xffff), HEX);
WiFiClient espClient;
PubSubClient client(espClient);
void connectMQTT() {
while (!client.connected()) {
if (client.connect(clientId.c_str())) {
Serial.println("MQTT Connected!");
} else {
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(WIFI_RESET_PIN, INPUT_PULLUP);
WiFiManager wm;
wm.autoConnect("ESP32_Setup", "password");
client.setServer(mqtt_server, 1883);
}
void loop() {
if (!client.connected()) connectMQTT();
client.loop();
}
Lab 04: Single Input Publish
อ่านค่าการกดปุ่ม SW1 (Debounce ป้องกันสวิตช์เบิ้ล) และส่งข้อมูล "ON" หรือ "OFF" ขึ้นไปยัง Topic บน MQTT
// Lab 04: Single Input Publish
// อ่านค่าสวิตช์ SW1 (Pin 4) แล้วส่งไปที่ HiveMQ (Publish)
#include <WiFi.h>
#include <WiFiManager.h>
#include <PubSubClient.h>
#define WIFI_RESET_PIN 15
#define SW1 4
const char* mqtt_server = "broker.hivemq.com";
const int mqtt_port = 1883;
String clientId = "ESP32_Client_IOT_" + String(random(0xffff), HEX);
const char* topic_pub_sw1 = "myiot/student1/sw1"; // เปลี่ยน student1 เป็นชื่อคุณ
WiFiClient espClient;
PubSubClient client(espClient);
int lastSw1State = HIGH;
void connectMQTT() {
while (!client.connected()) {
Serial.print("กำลังเชื่อมต่อ MQTT...");
if (client.connect(clientId.c_str())) {
Serial.println("เชื่อมต่อสำเร็จ!");
} else {
Serial.print("ล้มเหลว ลองใหม่ใน 5 วินาที");
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(WIFI_RESET_PIN, INPUT_PULLUP);
pinMode(SW1, INPUT_PULLUP);
WiFiManager wm;
if (digitalRead(WIFI_RESET_PIN) == LOW) { wm.resetSettings(); ESP.restart(); }
wm.autoConnect("ESP32_Setup", "password");
client.setServer(mqtt_server, mqtt_port);
}
void loop() {
if (!client.connected()) connectMQTT();
client.loop();
// อ่านค่า SW1 (LOW = กด, HIGH = ปล่อย)
int currentSw1State = digitalRead(SW1);
// ตรวจสอบว่ามีการเปลี่ยนแปลงสถานะหรือไม่ (Edge Detection)
if (currentSw1State != lastSw1State) {
delay(50); // Debounce
currentSw1State = digitalRead(SW1);
if (currentSw1State != lastSw1State) {
lastSw1State = currentSw1State;
// ส่งข้อมูล Publish ไปที่ MQTT
if (currentSw1State == LOW) {
client.publish(topic_pub_sw1, "ON");
Serial.println("SW1 Pressed -> Sent: ON");
} else {
client.publish(topic_pub_sw1, "OFF");
Serial.println("SW1 Released -> Sent: OFF");
}
}
}
if (digitalRead(WIFI_RESET_PIN) == LOW) {
delay(100);
if(digitalRead(WIFI_RESET_PIN) == LOW) {
WiFiManager wm;
wm.resetSettings();
ESP.restart();
}
}
}
Lab 05: Single Output Subscribe
การรับคำสั่งจาก MQTT มาเปิด/ปิด โหลด O1 โดยใช้ Callback Function
// Lab 05: Single Output Subscribe
// รับคำสั่งจาก HiveMQ (Subscribe) มาควบคุม LED/Output 1 (Pin 13)
#include <WiFi.h>
#include <WiFiManager.h>
#include <PubSubClient.h>
#define WIFI_RESET_PIN 15
#define O1 13
const char* mqtt_server = "broker.hivemq.com";
const int mqtt_port = 1883;
String clientId = "ESP32_Client_IOT_" + String(random(0xffff), HEX);
const char* topic_sub_o1 = "myiot/student1/o1";
WiFiClient espClient;
PubSubClient client(espClient);
// ฟังก์ชันทำงานเมื่อได้รับข้อมูลจาก MQTT
void callback(char* topic, byte* payload, unsigned int length) {
String message = "";
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.print("ได้รับข้อความ Topic: ");
Serial.print(topic);
Serial.print(" -> ข้อมูล: ");
Serial.println(message);
// ควบคุม Output O1
if (String(topic) == topic_sub_o1) {
if (message == "ON") {
digitalWrite(O1, HIGH);
} else if (message == "OFF") {
digitalWrite(O1, LOW);
}
}
}
void connectMQTT() {
while (!client.connected()) {
Serial.print("กำลังเชื่อมต่อ MQTT...");
if (client.connect(clientId.c_str())) {
Serial.println("เชื่อมต่อสำเร็จ!");
// หลังจากเชื่อมต่อสำเร็จ ต้อง Subscribe Topic ทันที
client.subscribe(topic_sub_o1);
} else {
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(WIFI_RESET_PIN, INPUT_PULLUP);
pinMode(O1, OUTPUT);
digitalWrite(O1, LOW);
WiFiManager wm;
if (digitalRead(WIFI_RESET_PIN) == LOW) { wm.resetSettings(); ESP.restart(); }
wm.autoConnect("ESP32_Setup", "password");
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback); // ผูกฟังก์ชัน callback รับข้อมูล
}
void loop() {
if (!client.connected()) connectMQTT();
client.loop();
}
Lab 06: Full I/O MQTT Control
รวมระบบทั้ง 4 อินพุต และ 4 เอาต์พุต โดยใช้ Array ทำให้โค้ดสั้นลง และใช้ Wildcard (#) ในการ Subscribe
// Lab 06: Full I/O MQTT Control
// ควบคุม Input 4 ช่อง ส่ง Publish และ Output 4 ช่องรับ Subscribe
#include <WiFi.h>
#include <WiFiManager.h>
#include <PubSubClient.h>
#define WIFI_RESET_PIN 15
// พิน I/O
const int swPins[4] = {4, 5, 18, 19};
const int outPins[4] = {13, 12, 14, 27};
int lastSwState[4] = {HIGH, HIGH, HIGH, HIGH};
const char* mqtt_server = "broker.hivemq.com";
const int mqtt_port = 1883;
String clientId = "ESP32_Client_IOT_" + String(random(0xffff), HEX);
// Topics
String baseTopic = "myiot/student1/";
String subTopic = baseTopic + "out/#"; // Subscribe ทุก Topic ที่ขึ้นต้นด้วย out/
WiFiClient espClient;
PubSubClient client(espClient);
void callback(char* topic, byte* payload, unsigned int length) {
String message = "";
for (int i = 0; i < length; i++) message += (char)payload[i];
String topicStr = String(topic);
// เช็คว่า Topic ตรงกับ Output ไหน
for(int i=0; i<4; i++) {
String checkTopic = baseTopic + "out/" + String(i+1);
if(topicStr == checkTopic) {
if(message == "ON") digitalWrite(outPins[i], HIGH);
else if(message == "OFF") digitalWrite(outPins[i], LOW);
}
}
}
void connectMQTT() {
while (!client.connected()) {
if (client.connect(clientId.c_str())) {
client.subscribe(subTopic.c_str());
} else {
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(WIFI_RESET_PIN, INPUT_PULLUP);
for(int i=0; i<4; i++) {
pinMode(swPins[i], INPUT_PULLUP);
pinMode(outPins[i], OUTPUT);
digitalWrite(outPins[i], LOW);
}
WiFiManager wm;
if (digitalRead(WIFI_RESET_PIN) == LOW) { wm.resetSettings(); ESP.restart(); }
wm.autoConnect("ESP32_Setup", "password");
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback);
}
void loop() {
if (!client.connected()) connectMQTT();
client.loop();
// ตรวจสอบสวิตช์ทั้ง 4 ปุ่ม
for(int i=0; i<4; i++) {
int currentState = digitalRead(swPins[i]);
if(currentState != lastSwState[i]) {
delay(50);
if(digitalRead(swPins[i]) == currentState) {
lastSwState[i] = currentState;
String pubTopic = baseTopic + "in/" + String(i+1);
String msg = (currentState == LOW) ? "ON" : "OFF";
client.publish(pubTopic.c_str(), msg.c_str());
}
}
}
}
Lab 07: Professional Payload Structuring (JSON)
การส่งข้อมูลเป็น JSON String เช่น {"sw1":1, "sw2":0} ซึ่งเป็นมาตรฐานอุตสาหกรรม ต้องใช้ไลบรารี ArduinoJson
// Lab 07: Professional Payload Structuring (JSON)
// การใช้ JSON ในการแพ็คข้อมูลหลายๆ ค่าส่งไปใน Topic เดียว
#include <WiFi.h>
#include <WiFiManager.h>
#include <PubSubClient.h>
#include <ArduinoJson.h> // ติดตั้ง Library: ArduinoJson by Benoit Blanchon (version 6 or 7)
#define WIFI_RESET_PIN 15
const int swPins[4] = {4, 5, 18, 19};
const int outPins[4] = {13, 12, 14, 27};
int lastSwState[4] = {HIGH, HIGH, HIGH, HIGH};
const char* mqtt_server = "broker.hivemq.com";
const int mqtt_port = 1883;
String clientId = "ESP32_Client_IOT_" + String(random(0xffff), HEX);
String pubTopic = "myiot/student1/status";
String subTopic = "myiot/student1/command";
WiFiClient espClient;
PubSubClient client(espClient);
void callback(char* topic, byte* payload, unsigned int length) {
// รับข้อมูล JSON และแยกวิเคราะห์ (Parse)
StaticJsonDocument<256> doc;
DeserializationError error = deserializeJson(doc, payload, length);
if (error) {
Serial.print("JSON Parse failed: ");
Serial.println(error.c_str());
return;
}
// คาดหวัง JSON: {"o1":1, "o2":0, "o3":1, "o4":0}
if (doc.containsKey("o1")) digitalWrite(outPins[0], doc["o1"] ? HIGH : LOW);
if (doc.containsKey("o2")) digitalWrite(outPins[1], doc["o2"] ? HIGH : LOW);
if (doc.containsKey("o3")) digitalWrite(outPins[2], doc["o3"] ? HIGH : LOW);
if (doc.containsKey("o4")) digitalWrite(outPins[3], doc["o4"] ? HIGH : LOW);
}
void connectMQTT() {
while (!client.connected()) {
if (client.connect(clientId.c_str())) {
client.subscribe(subTopic.c_str());
} else delay(5000);
}
}
void setup() {
Serial.begin(115200);
pinMode(WIFI_RESET_PIN, INPUT_PULLUP);
for(int i=0; i<4; i++) { pinMode(swPins[i], INPUT_PULLUP); pinMode(outPins[i], OUTPUT); }
WiFiManager wm;
if (digitalRead(WIFI_RESET_PIN) == LOW) { wm.resetSettings(); ESP.restart(); }
wm.autoConnect("ESP32_Setup", "password");
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback);
}
void loop() {
if (!client.connected()) connectMQTT();
client.loop();
bool changed = false;
for(int i=0; i<4; i++) {
int currentState = digitalRead(swPins[i]);
if(currentState != lastSwState[i]) {
delay(50);
if(digitalRead(swPins[i]) == currentState) {
lastSwState[i] = currentState;
changed = true;
}
}
}
// ถอดสถานะมีการเปลี่ยนแปลง ให้ส่ง JSON ออกไป
if (changed) {
StaticJsonDocument<256> doc;
doc["sw1"] = (lastSwState[0] == LOW) ? 1 : 0;
doc["sw2"] = (lastSwState[1] == LOW) ? 1 : 0;
doc["sw3"] = (lastSwState[2] == LOW) ? 1 : 0;
doc["sw4"] = (lastSwState[3] == LOW) ? 1 : 0;
char buffer[256];
serializeJson(doc, buffer);
client.publish(pubTopic.c_str(), buffer);
Serial.println(buffer);
}
}
Lab 08: Device Reliability (LWT & Retain)
ใช้ Last Will and Testament (LWT) เพื่อให้ Broker แจ้งเตือนเวลาอุปกรณ์เน็ตหลุดโดยอัตโนมัติ
// Lab 08: Device Reliability (LWT & Retain)
// การใช้ Last Will and Testament (LWT) แจ้งเตือนเมื่ออุปกรณ์ออฟไลน์ และ Retain ข้อความ
#include <WiFi.h>
#include <WiFiManager.h>
#include <PubSubClient.h>
#define WIFI_RESET_PIN 15
const int swPins[4] = {4, 5, 18, 19};
const char* mqtt_server = "broker.hivemq.com";
const int mqtt_port = 1883;
String clientId = "ESP32_Client_IOT_" + String(random(0xffff), HEX);
const char* topic_status = "myiot/student1/connection";
WiFiClient espClient;
PubSubClient client(espClient);
void connectMQTT() {
while (!client.connected()) {
Serial.print("Connecting to MQTT...");
// กำหนด LWT (Topic, QoS, Retain, Message)
// หาก ESP32 ขาดการเชื่อมต่อ Broker จะส่งข้อความ "offline" ออกไปแทน
boolean retain = true;
if (client.connect(clientId.c_str(), "", "", topic_status, 0, retain, "offline")) {
Serial.println("Connected!");
// เมื่อเชื่อมต่อสำเร็จ ให้ส่งสถานะ "online" พร้อมตั้ง Retain = true
client.publish(topic_status, "online", retain);
} else {
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(WIFI_RESET_PIN, INPUT_PULLUP);
WiFiManager wm;
if (digitalRead(WIFI_RESET_PIN) == LOW) { wm.resetSettings(); ESP.restart(); }
wm.autoConnect("ESP32_Setup", "password");
client.setServer(mqtt_server, mqtt_port);
}
void loop() {
if (!client.connected()) connectMQTT();
client.loop();
}
Lab 09: Local Web Server (SPIFFS/LittleFS)
เสิร์ฟไฟล์ HTML หน้าเว็บ (UI) โดยตรงจากหน่วยความจำ (SPIFFS) ของ ESP32 ไปยังเบราว์เซอร์
// Lab 09: Local Web Server (SPIFFS) & index.html
// การใช้งาน Web Server ภายใน ESP32 เพื่อเสิร์ฟไฟล์ index.html (หน้า Dashboard)
#include <WiFi.h>
#include <WiFiManager.h>
#include <WebServer.h>
#include <SPIFFS.h> // ใช้ระบบไฟล์ SPIFFS สำหรับเก็บหน้าเว็บ
#define WIFI_RESET_PIN 15
WebServer server(80); // สร้าง Web Server บนพอร์ต 80
void setup() {
Serial.begin(115200);
pinMode(WIFI_RESET_PIN, INPUT_PULLUP);
// เริ่มต้นใช้งาน SPIFFS (หากไม่เคย format จะ format ให้)
if (!SPIFFS.begin(true)) {
Serial.println("เกิดข้อผิดพลาดในการเมานท์ SPIFFS (ตรวจสอบการตั้งค่า Partition Scheme)");
return;
}
WiFiManager wm;
if (digitalRead(WIFI_RESET_PIN) == LOW) { wm.resetSettings(); ESP.restart(); }
wm.autoConnect("ESP32_Setup", "password");
// ตั้งค่า Route หน้าแรก (Root) ให้ส่งไฟล์ /index.html ไปยัง Client
server.on("/", HTTP_GET, [](){
if (SPIFFS.exists("/index.html")) {
File file = SPIFFS.open("/index.html", "r");
server.streamFile(file, "text/html");
file.close();
} else {
server.send(404, "text/plain", "File Not Found. กรุณาอัปโหลดไฟล์ผ่าน Sketch Data Upload");
}
});
server.begin();
Serial.print("Web Server Started! เปิดเบราว์เซอร์แล้วเข้าไปที่ http://");
Serial.println(WiFi.localIP());
}
void loop() {
server.handleClient(); // รอรับการเชื่อมต่อจากเบราว์เซอร์
}
Lab 10: Ultimate Pro IoT Node (Integration)
ใบงานขั้นสุดยอด: นำโค้ด ESP32 ทุกส่วนมารวมกัน และในไฟล์ index.html (Web Dashboard) จะมีการเขียน JavaScript เพื่อใช้ Paho MQTT เชื่อมต่อโดยตรงกับ WebSockets ของ HiveMQ
ตัวอย่างหน้าตา UI สำหรับควบคุม:
<!-- ตัวอย่างส่วนหนึ่งของ index.html ใน Lab 10 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js"></script>
<script>
const client = new Paho.MQTT.Client("broker.hivemq.com", 8000, "WebClient_" + Date.now());
client.onMessageArrived = function(message) {
if(message.destinationName === "myiot/pro/lwt") {
document.getElementById("deviceStatus").textContent = message.payloadString;
}
else if (message.destinationName === "myiot/pro/status") {
const data = JSON.parse(message.payloadString);
if (data.sw1 !== undefined) updateUI('sw1', data.sw1);
}
};
client.connect({ onSuccess: () => {
client.subscribe("myiot/pro/status");
client.subscribe("myiot/pro/lwt");
}});
function sendCmd(outputId, state) {
const payload = JSON.stringify({ [outputId]: state ? 1 : 0 });
const msg = new Paho.MQTT.Message(payload);
msg.destinationName = "myiot/pro/command";
client.send(msg);
}
</script>
เพื่อดูหน้าเว็บจริงแบบเต็มรูปแบบ คุณสามารถเปิดไฟล์ D:\antigravity_iot\Lab10_Ultimate_Pro_Node\data\index.html ได้เลยครับ
Lab 11: 2-Way Toggle Control & Web Dashboard
ควบคุม Output 4 ช่อง ทั้งจากการกดสวิตช์กายภาพแบบสลับสถานะ (Toggle) และจาก Web Dashboard เมื่อใดที่มีการกดสวิตช์ หรือคลิกปุ่มบนเว็บ สถานะจะถูกซิงค์กันแบบ Real-time โดยปุ่มบนเว็บจะแสดงสีดำเมื่อปิด (OFF) และสีแดงเมื่อเปิด (ON)
ตัวอย่างหน้าตา UI สำหรับปุ่มกด (สีดำ/แดง):
/* สถานะ OFF (สีดำ) */
.control-btn { background: #1f2937; color: #9ca3af; }
/* สถานะ ON (สีแดง) */
.control-btn.on { background: #ef4444; color: white; }
ตัวอย่าง Logic สำหรับการทำงานแบบ Toggle ที่ปุ่มกด:
// ตรวจสอบการกดปุ่ม (สวิตช์กายภาพ) Edge Detection (ขอบขาลง)
if(current == LOW && lastSwState[i] == HIGH) {
delay(50); // Debounce ป้องกันสวิตช์เบิ้ล
if(digitalRead(swPins[i]) == LOW) {
// ทำการ Toggle สลับสถานะ (ถ้า ON ให้ OFF, ถ้า OFF ให้ ON)
outState[i] = !outState[i];
changed = true; // ตั้งค่า Flag เพื่อรอการ Publish ขึ้น MQTT ต่อไป
}
}
สำหรับโค้ดฉบับสมบูรณ์พร้อมรันระดับมืออาชีพ ทั้งฝั่ง ESP32 (C++) และ Web Dashboard (HTML/JS) สามารถเปิดดูได้ในโปรเจกต์ D:\antigravity_iot\Lab11_Toggle_Dashboard ครับ!