เริ่มใช้งาน MQTT กับ COWTH Broker ด้วย ESP32: ส่งค่า Sensor และรับข้อมูลแบบ Real-time
เริ่มใช้งาน MQTT กับ COWTH Broker ด้วย ESP32: ส่งค่า Sensor และรับข้อมูลแบบ Real-time
COWTH คือ MQTT platform made in Thailand สำหรับคนไทยที่อยากเริ่มทำ IoT ได้ง่ายขึ้น ในบทความนี้เราจะลองใช้งาน
broker.cowth.devกับบอร์ด ESP32 จำนวน 2 ตัว โดยตัวแรกทำหน้าที่ส่งข้อมูล และอีกตัวทำหน้าที่รับข้อมูล

เป้าหมายของบทความนี้
บทความนี้ผมจะพาทดลองใช้งาน MQTT ของ COWTH แบบง่ายที่สุด แต่เป็น flow ที่ใช้จริงในงาน IoT ได้เลย
เราจะมีอุปกรณ์ 2 ฝั่ง
-
ESP32 Publisher
อ่านค่า Analog จาก LDR และ Potentiometer แล้วส่งขึ้น MQTT Broker -
ESP32-S3 Subscriber
Subscribe topic เดียวกัน แล้วรับข้อมูลที่ Publisher ส่งมาแสดงบน Serial Monitor
ภาพรวมการทำงานจะเป็นแบบนี้
flowchart TD
sensor["LDR + Potentiometer<br/>Analog Sensor Input"]
pub["ESP32 Publisher<br/>Read sensor and publish MQTT"]
broker["broker.cowth.dev<br/>COWTH MQTT Broker"]
sub["ESP32-S3 Subscriber<br/>Subscribe MQTT topic"]
serial["Serial Monitor<br/>Display received payload"]
sensor --> pub
pub -->|MQTT Publish| broker
broker -->|MQTT Subscribe| sub
sub --> serial
หรือพูดง่าย ๆ คือ ESP32 ตัวหนึ่งส่งค่า sensor เข้า COWTH Broker แล้ว ESP32-S3 อีกตัวรอฟังข้อมูลแบบ real-time
รูปอุปกรณ์ที่ใช้ทดสอบ
รูปบอร์ด ESP32 ฝั่ง Publisher

รูปบอร์ด ESP32-S3 ฝั่ง Subscriber

รูป Terminal ทดสอบจริง

MQTT คืออะไรแบบสั้น ๆ
MQTT เป็น protocol สำหรับส่งข้อมูลระหว่างอุปกรณ์ IoT ผ่านแนวคิดแบบ Publish / Subscribe
แทนที่อุปกรณ์จะต้องคุยกันโดยตรง อุปกรณ์ทุกตัวจะคุยผ่านตัวกลางที่เรียกว่า MQTT Broker
ตัวอย่างเช่น
ESP32 A publish ข้อมูลไปที่ topic:
sandbox/your_user/esp_sensor/
ESP32 B subscribe topic เดียวกัน:
sandbox/your_user/esp_sensor/
เมื่อ ESP32 A ส่งข้อมูล ESP32 B ก็จะได้รับทันที
ข้อดีคืออุปกรณ์ไม่จำเป็นต้องรู้ IP ของกันและกัน รู้แค่ broker, username, password และ topic ที่ใช้ร่วมกันก็พอ
COWTH Broker Endpoint ที่ใช้ในตัวอย่างนี้
ในการทดลองนี้ใช้ MQTT Broker ของ COWTH ที่ endpoint:
Host: broker.cowth.dev
Port: 1883
Protocol: MQTT over TCP
หมายเหตุ: ตัวอย่างนี้เป็นการใช้งาน MQTT TCP port
1883สำหรับการทดลองพื้นฐาน ถ้าใช้งานจริงใน production ควรพิจารณาเรื่อง TLS, authentication policy, topic policy และ credential rotation เพิ่มเติม
Library ที่ต้องติดตั้งใน Arduino IDE หรือ PlatformIO
ตัวอย่างนี้ใช้ Arduino framework และ library หลัก ๆ คือ
PubSubClient
Adafruit GFX Library
Adafruit SSD1306
WiFi
Wire
ถ้าใช้ PlatformIO สามารถใส่ใน platformio.ini ประมาณนี้
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
knolleary/PubSubClient
adafruit/Adafruit GFX Library
adafruit/Adafruit SSD1306
สำหรับฝั่ง Subscriber ถ้าไม่ได้ใช้จอ OLED จะใช้แค่ PubSubClient กับ WiFi ก็พอ
โครงสร้าง Topic ที่ใช้
ในตัวอย่างนี้ใช้ topic รูปแบบ sandbox:
💡 หมายเหตุ: นำ username และ password ที่ได้รับในเมลที่ทำการกรอกผ่านหน้าเว็ป broker.cowth.dev
sandbox/<username>/esp_sensor/
ตัวอย่างเช่น
sandbox/sbx_xxxxxxxx/esp_sensor/
แนวคิดคือให้แต่ละ user มี prefix ของตัวเอง เพื่อลดโอกาส topic ชนกันระหว่างผู้ใช้งานหลายคน
สำหรับบทความนี้ผมจะใช้ placeholder แบบนี้แทน credential จริง
const char *MQTT_USERNAME = "YOUR_COWTH_USERNAME";
const char *MQTT_PASSWORD = "YOUR_COWTH_PASSWORD";
const char *MQTT_TOPIC = "sandbox/YOUR_COWTH_USERNAME/esp_sensor/";
ฝั่งที่ 1: ESP32 Publisher
ฝั่ง Publisher ทำหน้าที่อ่านค่า Analog จาก
- LDR ที่ GPIO35
- Potentiometer ที่ GPIO36
- แสดงค่าบนจอ OLED SSD1306
- ส่งข้อมูลขึ้น MQTT ทุก 1 วินาที
Payload ที่ส่งจะเป็น string ง่าย ๆ แบบนี้
LDR_VALUE,POT_VALUE
ตัวอย่าง payload:
2380,1542
Wiring ตัวอย่าง
ESP32 GPIO35 <--- LDR analog output
ESP32 GPIO36 <--- Potentiometer analog output
ESP32 GPIO21 <--- OLED SDA
ESP32 GPIO22 <--- OLED SCL
3.3V <--- OLED VCC / Sensor VCC
GND <--- Common GND
GPIO35 และ GPIO36 เป็น input-only pin เหมาะกับการอ่านค่า analog บน ESP32
Code: ESP32 Publisher
ก่อนนำไปใช้จริง ให้เปลี่ยน
YOUR_WIFI_SSID,YOUR_WIFI_PASSWORD,YOUR_COWTH_USERNAMEและYOUR_COWTH_PASSWORDเป็นค่าของคุณเอง
#include <Arduino.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <PubSubClient.h>
#include <WiFi.h>
#include <Wire.h>
const char *WIFI_SSID = "YOUR_WIFI_SSID";
const char *WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char *MQTT_HOST = "broker.cowth.dev";
const uint16_t MQTT_PORT = 1883;
const char *MQTT_USERNAME = "YOUR_COWTH_USERNAME";
const char *MQTT_PASSWORD = "YOUR_COWTH_PASSWORD";
const char *MQTT_TOPIC = "sandbox/YOUR_COWTH_USERNAME/esp_sensor/";
const unsigned long PUBLISH_INTERVAL_MS = 1000;
const uint8_t LDR_PIN = 35;
const uint8_t POT_PIN = 36;
const uint8_t OLED_ADDRESS = 0x3C;
const uint8_t OLED_WIDTH = 128;
const uint8_t OLED_HEIGHT = 64;
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, -1);
unsigned long lastPublishAt = 0;
int lastLdrValue = 0;
int lastPotValue = 0;
bool displayReady = false;
void updateDisplay() {
if (!displayReady) {
return;
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("IP: ");
display.println(WiFi.localIP());
display.setCursor(0, 22);
display.print("LDR: ");
display.println(lastLdrValue);
display.setCursor(0, 42);
display.print("POTENTIOMETER: ");
display.println(lastPotValue);
display.display();
}
void connectWiFi() {
if (WiFi.status() == WL_CONNECTED) {
return;
}
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("WiFi connected, IP: ");
Serial.println(WiFi.localIP());
}
void connectMqtt() {
while (!mqttClient.connected()) {
String clientId = "esp32-sensor-pub-";
clientId += String((uint32_t)ESP.getEfuseMac(), HEX);
Serial.print("Connecting MQTT...");
if (mqttClient.connect(clientId.c_str(), MQTT_USERNAME, MQTT_PASSWORD)) {
Serial.println("connected");
return;
}
Serial.print("failed, rc=");
Serial.print(mqttClient.state());
Serial.println(" retrying in 5 seconds");
delay(5000);
}
}
void publishSensorData() {
lastLdrValue = analogRead(LDR_PIN);
lastPotValue = analogRead(POT_PIN);
char payload[32];
snprintf(payload, sizeof(payload), "%d,%d", lastLdrValue, lastPotValue);
if (mqttClient.publish(MQTT_TOPIC, payload)) {
Serial.print("Published: ");
Serial.println(payload);
} else {
Serial.println("Publish failed");
}
updateDisplay();
}
void setup() {
Serial.begin(115200);
delay(1000);
Wire.begin();
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
Serial.println("OLED not found. Check I2C wiring/address.");
} else {
displayReady = true;
updateDisplay();
}
analogReadResolution(12);
analogSetPinAttenuation(LDR_PIN, ADC_11db);
analogSetPinAttenuation(POT_PIN, ADC_11db);
connectWiFi();
mqttClient.setServer(MQTT_HOST, MQTT_PORT);
connectMqtt();
}
void loop() {
connectWiFi();
connectMqtt();
mqttClient.loop();
unsigned long now = millis();
if (now - lastPublishAt >= PUBLISH_INTERVAL_MS) {
lastPublishAt = now;
publishSensorData();
}
}
จุดที่ผมอยากเสริมให้นะครับ Publisher
1. Client ID ไม่ควรซ้ำกัน
ในโค้ดนี้ใช้ MAC จาก ESP32 มาช่วยสร้าง client id
String clientId = "esp32-sensor-pub-";
clientId += String((uint32_t)ESP.getEfuseMac(), HEX);
เพราะ MQTT client แต่ละตัวควรมี client id ที่ไม่ซ้ำกัน ถ้า client id ซ้ำกัน broker อาจมองว่าเป็น client เดียวกัน และเกิดการเตะ connection เก่าออกได้
2. ใช้ mqttClient.loop() เสมอ
ใน loop หลักต้องเรียก
mqttClient.loop();
เพื่อให้ PubSubClient จัดการ MQTT packet ภายใน เช่น keep alive และ packet ที่รอรับจาก broker
3. ส่งข้อมูลทุก 1 วินาทีโดยไม่ใช้ delay() ยาว ๆ
โค้ดใช้ millis() เพื่อจับเวลา publish
if (now - lastPublishAt >= PUBLISH_INTERVAL_MS) {
lastPublishAt = now;
publishSensorData();
}
วิธีนี้ดีกว่าการใช้ delay(1000) ตรง ๆ เพราะยังเปิดโอกาสให้ mqttClient.loop() ทำงานต่อเนื่องได้
ฝั่งที่ 2: ESP32-S3 Subscriber
ฝั่ง Subscriber ทำหน้าที่เชื่อมต่อ Wi-Fi, เชื่อมต่อ MQTT Broker แล้ว subscribe topic เดียวกับ Publisher
เมื่อมีข้อมูลเข้ามา function callback จะถูกเรียก
void onMqttMessage(char *topic, byte *payload, unsigned int length)
จากนั้นข้อมูลจะถูกพิมพ์ออก Serial Monitor
Code: ESP32-S3 Subscriber
#include <Arduino.h>
#include <PubSubClient.h>
#include <WiFi.h>
const char *WIFI_SSID = "YOUR_WIFI_SSID";
const char *WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char *MQTT_HOST = "broker.cowth.dev";
const uint16_t MQTT_PORT = 1883;
const char *MQTT_USERNAME = "YOUR_COWTH_USERNAME";
const char *MQTT_PASSWORD = "YOUR_COWTH_PASSWORD";
const char *MQTT_TOPIC = "sandbox/YOUR_COWTH_USERNAME/esp_sensor/";
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
void connectWiFi() {
if (WiFi.status() == WL_CONNECTED) {
return;
}
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("WiFi connected, IP: ");
Serial.println(WiFi.localIP());
}
void onMqttMessage(char *topic, byte *payload, unsigned int length) {
Serial.print("Received [");
Serial.print(topic);
Serial.print("]: ");
for (unsigned int index = 0; index < length; index++) {
Serial.write(payload[index]);
}
Serial.println();
}
void connectMqtt() {
while (!mqttClient.connected()) {
String clientId = "esp32-sensor-sub-";
clientId += String((uint32_t)ESP.getEfuseMac(), HEX);
Serial.print("Connecting MQTT...");
if (mqttClient.connect(clientId.c_str(), MQTT_USERNAME, MQTT_PASSWORD)) {
Serial.println("connected");
mqttClient.subscribe(MQTT_TOPIC);
Serial.print("Subscribed: ");
Serial.println(MQTT_TOPIC);
return;
}
Serial.print("failed, rc=");
Serial.print(mqttClient.state());
Serial.println(" retrying in 5 seconds");
delay(5000);
}
}
void setup() {
Serial.begin(115200);
delay(1000);
connectWiFi();
mqttClient.setServer(MQTT_HOST, MQTT_PORT);
mqttClient.setCallback(onMqttMessage);
connectMqtt();
}
void loop() {
connectWiFi();
connectMqtt();
mqttClient.loop();
}
ผลลัพธ์ที่คาดหวัง
เมื่อเปิด Serial Monitor ของฝั่ง Publisher จะเห็นประมาณนี้
Connecting WiFi...
WiFi connected, IP: 192.168.1.42
Connecting MQTT...connected
Published: 2350,1488
Published: 2361,1510
Published: 2372,1502
ฝั่ง Subscriber จะเห็นประมาณนี้
Connecting WiFi...
WiFi connected, IP: 192.168.1.56
Connecting MQTT...connected
Subscribed: sandbox/YOUR_COWTH_USERNAME/esp_sensor/
Received [sandbox/YOUR_COWTH_USERNAME/esp_sensor/]: 2350,1488
Received [sandbox/YOUR_COWTH_USERNAME/esp_sensor/]: 2361,1510
Received [sandbox/YOUR_COWTH_USERNAME/esp_sensor/]: 2372,1502
ถ้าเห็นข้อมูลวิ่งแบบนี้ แปลว่า ESP32 ทั้งสองตัวเชื่อมต่อผ่าน COWTH Broker ได้สำเร็จแล้ว
ทดสอบด้วย mosquitto CLI เพิ่มเติม
ถ้ามี mosquitto-clients สามารถทดสอบจากคอมพิวเตอร์ได้ด้วย
Subscribe
mosquitto_sub -h broker.cowth.dev -p 1883 \
-u "YOUR_COWTH_USERNAME" \
-P "YOUR_COWTH_PASSWORD" \
-t "sandbox/YOUR_COWTH_USERNAME/esp_sensor/" \
-v
Publish
mosquitto_pub -h broker.cowth.dev -p 1883 \
-u "YOUR_COWTH_USERNAME" \
-P "YOUR_COWTH_PASSWORD" \
-t "sandbox/YOUR_COWTH_USERNAME/esp_sensor/" \
-m "1234,2048"
ถ้าใช้คำสั่ง publish จากคอมพิวเตอร์ ฝั่ง ESP32-S3 Subscriber ก็ควรได้รับข้อความเช่นกัน
การแก้ปัญหา
1. ต่อ Wi-Fi ไม่ได้
ให้เช็ก SSID และ password ก่อน โดยเฉพาะถ้าใช้ ESP32 รุ่นที่รองรับเฉพาะ Wi-Fi 2.4GHz ต้องแน่ใจว่า router เปิด 2.4GHz อยู่
2. MQTT connect ไม่ผ่าน
ให้ดูค่า mqttClient.state() จาก Serial Monitor
ค่าที่เจอบ่อย เช่น
failed, rc=-2
มักเกี่ยวกับ network connection หรือ broker endpoint ติดต่อไม่ได้
failed, rc=4
มักเกี่ยวกับ username/password ไม่ถูกต้อง
3. Subscriber ไม่ได้รับข้อมูล
ให้เช็ก 3 จุดนี้ก่อน
1. Topic ตรงกันไหม
2. Username/password ตรงกันไหม
3. Publisher publish สำเร็จไหม
โดย topic ต้องเหมือนกันทุกตัวอักษร เช่น
sandbox/YOUR_COWTH_USERNAME/esp_sensor/
ถ้าอีกฝั่งเขียนเป็น
sandbox/YOUR_COWTH_USERNAME/esp_sensor
โดยไม่มี / ท้ายสุด จะถือว่าเป็นคนละ topic
4. OLED ไม่ขึ้น
ให้เช็ก I2C address ว่าเป็น 0x3C หรือ 0x3D และเช็กสาย SDA/SCL ให้ถูกต้อง
ค่า default ในตัวอย่างนี้คือ
const uint8_t OLED_ADDRESS = 0x3C;
ข้อควรระวัง
ตัวอย่างนี้ตั้งใจให้เข้าใจ flow MQTT แบบเร็วที่สุด จึงยังมีหลายเรื่องที่ควรปรับก่อนใช้งานจริง เช่น
- ไม่ควร hardcode Wi-Fi password และ MQTT password ใน firmware ที่แจกให้คนอื่น
- ควรแยกไฟล์ config หรือใช้ provisioning mode สำหรับตั้งค่า Wi-Fi
- ควรออกแบบ topic ให้เป็นระบบ เช่น แยก device id, sensor type และ command topic
- ควรพิจารณา MQTT over TLS ถ้าส่งข้อมูลสำคัญ
- ควรมีระบบ rotate credential ถ้า token หลุด
- ควรเพิ่ม reconnect backoff แทนการ retry แบบ fixed delay
ตัวอย่าง topic ที่เป็นระบบมากขึ้น เช่น
sandbox/YOUR_COWTH_USERNAME/devices/esp32-001/telemetry
sandbox/YOUR_COWTH_USERNAME/devices/esp32-001/command
sandbox/YOUR_COWTH_USERNAME/devices/esp32-001/status
แบบนี้จะต่อยอดไปเป็น dashboard, device management หรือ OTA command ในอนาคตได้ง่ายกว่า
สรุป
บทความนี้เราได้ทดลองใช้งาน COWTH MQTT Broker กับ ESP32 แบบครบ flow ตั้งแต่
ESP32 Publisher -> COWTH Broker -> ESP32-S3 Subscriber
ฝั่ง Publisher อ่านค่า LDR และ Potentiometer แล้วส่งเป็น payload ผ่าน MQTT ส่วนฝั่ง Subscriber รับข้อมูลจาก topic เดียวกันและแสดงผลบน Serial Monitor
นี่คือพื้นฐานสำคัญของระบบ IoT หลายแบบ เช่น
- ระบบ monitor sensor
- ระบบควบคุมอุปกรณ์ระยะไกล
- ระบบ dashboard แบบ real-time
- ระบบแจ้งเตือนจากอุปกรณ์ภาคสนาม
- ระบบ prototype สำหรับทดสอบ device ก่อนขึ้น production
สำหรับผม COWTH ไม่ได้อยากเป็นแค่ MQTT broker อีกตัวหนึ่ง แต่อยากเป็น platform ที่ทำให้คนไทยเริ่มทำ IoT ได้ง่ายขึ้น ทดสอบได้เร็วขึ้น และต่อยอดจาก prototype ไปสู่ product จริงได้ง่ายขึ้น
COWTH MQTT BROKER
จุดเริ่มต้นเล็ก ๆ ของ IoT platform ที่ทำขึ้นจากมุมมองของ embedded developer จริง ๆ
ขอบคุณทุกท่านที่อ่านมาถึงตรงนี้ ผมหวังเป็นอย่างยิ่งว่าบทความนี้จะช่วยให้ทุกท่านได้ความรู้ในสิ่งที่กำลังศึกษาอยู่