ภาพพื้นหลังปกบทเรียน

บทเรียน 4.5 — MQTT กับแพลตฟอร์มที่ติดตั้งเอง: telemetry และ command

MQTT (1883) · Telemetry ขาออก และ Command ขากลับ บน broker ของเราเอง

โมดูล 4 — เชื่อมต่อแพลตฟอร์ม IoT

ต่อจากบทเรียน 4.4 — MQTT: pub/sub topic QoS และงบข้อมูล

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

เรื่องที่เราให้ 70% ผู้เรียนเขียน 30%

70% — เฟิร์มแวร์ + แพลตฟอร์มทำให้แล้ว TCP/IP · แพ็กเก็ต MQTT · broker · ฐานข้อมูล · กราฟ 30% — งานของเรา ส่งอะไร ชื่ออะไร ถี่แค่ไหน โค้ดสั้นลง แต่การตัดสินใจหนักขึ้น — นี่คือหน้าตาของงานวิศวกรรมระบบ ไลบรารีตอบแทนไม่ได้ เพราะสี่คำถามนั้นเป็นคำถามเรื่องระบบ ไม่ใช่เรื่องโค้ด

สิ่งที่ทำให้แล้ว (70%)
สแตก TCP/IP และการต่อ WiFi · การเข้ารหัสแพ็กเก็ต MQTT ตามมาตรฐาน · การส่ง keepalive ให้เองเป็นระยะ · ฝั่งแพลตฟอร์ม: broker EMQX, บริดจ์ที่ subscribe device/+/telemetry รออยู่แล้ว, ฐานข้อมูลอนุกรมเวลา และกราฟที่สร้างจากชื่อคีย์ JSON อัตโนมัติ

สิ่งที่เป็นงานของเรา (30%)
ตัดสินใจว่า ส่งอะไร ตั้งชื่อว่าอะไร ถี่แค่ไหน และ ทำอะไรกับคำสั่งที่รับกลับมา — สี่คำถามนี้ไม่มีไลบรารีไหนตอบแทนได้ เพราะมันคือคำถามเรื่องระบบ ไม่ใช่เรื่องโค้ด

สังเกตว่าโค้ดของชุดบทเรียนนี้สั้นกว่าบทเรียน 3.7–3.9 มาก แต่การตัดสินใจหนักกว่า — นี่คือหน้าตาของงานวิศวกรรมระบบจริง

โค้ดที่สั้นลงไม่ได้แปลว่างานง่ายลง มันแปลว่าน้ำหนักย้ายไปอยู่ที่การออกแบบ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

โมดูล mqtt มีหกชื่อ เท่านี้จริง ๆ — และเพดานของแต่ละตัว

เรียกอย่างไร คืนอะไร เพดานและกับดัก
mqtt.connect(broker, port=1883, client_id="psoc-edge", username=None, password=None, keepalive=60) True / False client_id ใช้ได้ 31 ตัวอักษร ที่เกินถูกตัดทิ้งเงียบ ๆ แล้ว broker ค่อยปฏิเสธทีหลัง · broker ยาวได้ 63 · username กับ password อย่างละ 31 · เขียน keep_alive ไม่ได้ ต้อง keepalive
mqtt.disconnect() None ตัดการเชื่อมต่อและ คืน client_id ให้ว่าง ต่อใหม่ด้วยชื่อเดิมได้ทันที · ไม่คืน True จึงห้ามใส่ใน if
mqtt.publish(topic, payload, qos=0) True / False ยังไม่ได้ต่อแล้วเรียก ได้ OSError ไม่ใช่ False · ต่ออยู่แต่ส่งไม่ผ่าน ได้ False · จึงมีสามทางออก ไม่ใช่สอง
mqtt.subscribe(topic, qos=0) True / False broker ปฏิเสธ (เช่นติด ACL) ก็คืน False เหมือนกัน ไม่โยน error · ยังไม่ได้ต่อจึงจะได้ OSError
mqtt.is_connected() True / False เปลี่ยนเป็น False เองเมื่อ broker ตัดเรา ไม่ต้องรอให้ publish พัง
mqtt.get_message() None หรือ tuple (topic, payload) ไม่เคยบล็อก · topic เป็น str สูงสุด 127 ไบต์ · payload เป็น bytes สูงสุด 255 ไบต์ ต้อง .decode() ก่อนใช้ · เกินเพดานถูกตัดกลางคันเงียบ ๆ

retain ไม่ใช่พารามิเตอร์ เอกสารในเครื่องมือบางที่เขียนว่า mqtt.publish(topic, payload, qos=0, retain=False) แต่ตัวจริงรับได้แค่สามอาร์กิวเมนต์ ใส่ retain=True ไปจะได้ TypeError ทันที และในซอร์สค่านั้นถูกตั้งเป็น false ตายตัวอยู่แล้ว

port ตั้งได้ก็จริง แต่ตัวจริงส่งข้อมูลรับรอง TLS เป็นค่าว่างเสมอ จึงต่อได้เฉพาะพอร์ตที่เปิดรับแบบข้อความเปล่า ชี้ไปพอร์ต TLS จริงแล้วจะล้มตอนจับมือ — ชุดบทเรียนถัดไปเราจะใช้คนละโมดูลกันไปเลยเพราะเหตุนี้

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ครึ่งหลังของชุดบทเรียน — แพลตฟอร์มที่เราติดตั้งเอง

หน้าจอ Dashboard ของ TESAIoT Community Edition

ภาพหน้าจอ: TESAIoT Community Edition v1.1.8 — เอกสารของ repo (Apache-2.0)

เราจะไม่ยืมแดชบอร์ดของใคร — แต่ละทีมติดตั้ง TESAIoT Community Edition บนเครื่องของตัวเองด้วย Docker

  • make install (ธง PREBUILT=1 ดึงอิมเมจสำเร็จรูป) ใช้เวลาราว 15–30 นาที
  • ลำดับสำคัญ: make up ต้องมาก่อน make init-pki ถ้าสลับกันจะค้างตั้งแต่บูตแรก
  • RAM อย่างน้อย 8 GB ตามเอกสารไทยของ repo — น้อยกว่านี้ฐานข้อมูลถูกระบบฆ่าทิ้งกลางทาง
  • มีเอกสารภาษาไทยครบ — README.th.md และ docs/th/ อีก 15 ไฟล์

นี่คือครั้งแรกที่ทีมได้เป็นเจ้าของ ทั้งอุปกรณ์และแพลตฟอร์ม ไม่ใช่แค่ผู้ใช้บริการของใคร

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

กับดักพอร์ต 1883 — เอกสารบอกอย่าง ไฟล์ตั้งค่าทำอีกอย่าง

บอร์ดใน LAN 192.168.1.77 ยิงไปที่พอร์ต 1883 เครื่องที่รัน TESAIoT CE 127.0.0.1:11883 เห็นได้เฉพาะในเครื่อง 0.0.0.0:1883 เห็นได้จากทั้ง LAN broker EMQX ข้างในฟังที่ 0.0.0.0:1883 อยู่แล้ว ปัญหาอยู่ที่ชั้นเผยแพร่พอร์ตของ compose ต่อไม่ติดเลย ไม่ใช่รหัสผิด ไม่ใช่ ACL แก้บรรทัดเดียวใน docker-compose.yml — และย้อนกลับเมื่อสอนเสร็จ

เอกสารของ CE ทุกฉบับเขียนว่าพอร์ต 1883 แต่ docker-compose.yml เผยแพร่จริงเป็น 127.0.0.1:11883:1883 คือทั้งเลขพอร์ตต่างจากที่เขียนไว้ และผูกกับ loopback ทำให้ บอร์ดที่อยู่ใน LAN เดียวกันเข้าไม่ถึงเลย (คอมเมนต์ในไฟล์บอกว่าทำไว้เลี่ยงชนกับซอฟต์แวร์ตัวอื่นบนเครื่องผู้พัฒนา)

-      - "127.0.0.1:11883:1883"
+      - "0.0.0.0:1883:1883"     # classroom only - plaintext MQTT บน LAN

ใช้ docker compose up -d emqx เท่านั้น — restart จะไม่อ่านการตั้งค่าพอร์ตใหม่

เอกสารกับไฟล์ตั้งค่าขัดกันได้เสมอ — ไฟล์ตั้งค่าคือความจริง เพราะมันคือสิ่งที่เครื่องอ่าน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

1883 ส่งรหัสผ่านเป็นข้อความเปล่า — พูดให้ตรง

แผนภาพการโจมตีแบบคนกลาง ผู้โจมตีแทรกตัวระหว่างสองฝ่ายที่คุยกัน

ภาพ: Miraceti / Wikimedia Commons — CC BY-SA 3.0

พอร์ต 1883 ไม่มีการเข้ารหัส ทั้ง username, password และ payload ทุกไบต์ เดินทางบน WiFi ในรูปข้อความอ่านออกได้ ใครที่ดักจับสัญญาณวงเดียวกันได้ ก็อ่านรหัสของอุปกรณ์เราได้ และ ปลอมเป็นอุปกรณ์ของเราส่งข้อมูลปลอมเข้าแพลตฟอร์มได้ทันที

  • CE เองระบุไว้ในไฟล์ตั้งค่าว่าพอร์ต 1883 มีไว้สำหรับ local/dev เท่านั้น
  • API ของแพลตฟอร์มจะเขียน log เตือนทุกครั้งที่อุปกรณ์โหมด server_tls เข้ามาทางพอร์ต 1883
  • ดังนั้นสิ่งที่เราทำวันนี้คือ การฝึกในสนามซ้อมที่ปิดล้อม ไม่ใช่วิธีที่ใช้กับของจริง

ชุดบทเรียนถัดไป (บทเรียน 4.7–4.9) เราจะเปลี่ยนไปพอร์ต 8884 พร้อม TLS แล้วเทียบให้เห็นด้วยตาว่าสิ่งที่ดักได้ต่างกันอย่างไร — วันนี้จึงเป็นครึ่งแรกของบทเรียนเรื่องความปลอดภัย ไม่ใช่บทเรียนที่จบในตัว

จำประโยคนี้ให้ได้: "ต่อได้" กับ "ต่อได้อย่างปลอดภัย" เป็นคนละคำถาม และเราเพิ่งตอบข้อแรก

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ขึ้นทะเบียนอุปกรณ์ก่อน แล้วค่อยต่อ

หน้าจอรายการอุปกรณ์ (Devices) ของ TESAIoT Community Edition

ภาพหน้าจอ: TESAIoT Community Edition v1.1.8 — เอกสารของ repo (Apache-2.0)

แพลตฟอร์มนี้ ไม่มีการลงทะเบียนอัตโนมัติ — อุปกรณ์ที่ไม่รู้จักถูกปฏิเสธตั้งแต่ตอน CONNECT

  1. Devices → Add device แล้ว ตั้ง device_id เองให้สั้น เช่น team03 (ระบบรับ 3–64 ตัว) — ถ้าปล่อยให้สุ่ม จะได้ UUID ยาว 36 ตัว ยาวเกิน 31 ตัวที่โมดูล mqtt รับได้ ถูกตัดเงียบ แล้วต่อไม่ติดโดยไม่บอกสาเหตุ
  2. ขอรหัส: POST /api/v1/devices/<id>/reset-mqtt-password คืน mqtt_username และรหัสผ่านมาตรง ๆ (อย่าใช้ /reset-password ซึ่งคืนคนละอย่าง)
  3. ตรวจว่าสถานะอุปกรณ์เป็น active และโหมดเป็น server_tls (ค่าเริ่มต้นอยู่แล้ว)
  4. กรอกลงโค้ดโดยให้ client_id == username == device_id ตรงกันเป๊ะ

ที่นี่ไม่ยอมรับ "เกือบตรง" — ผิดตัวเดียวใน device_id คือถูกปฏิเสธ และข้อความปฏิเสธไม่ได้บอกว่าผิดตรงไหน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

แกะโค้ดจริง — ท่าที่ 1 ต่อ WiFi แล้วต่อ broker

# --- ท่าที่ 1: ต่อเน็ตให้ได้ก่อน แล้วค่อยแนะนำตัวกับ broker ---
wifi.connect(WIFI_SSID, WIFI_PASSWORD)
lcd.print("WiFi:", wifi.ip())

ok = mqtt.connect(BROKER, port=1883, client_id=DEVICE_ID,
                  username=DEVICE_ID, password=MQTT_PASS, keepalive=60)
lcd.print("<span class=ok>MQTT ต่อแล้ว</span>" if ok else "MQTT ต่อไม่ได้")

ชื่อคีย์เวิร์ดคือ keepalive ไม่ใช่ keep_alive — เอกสารช่วยเหลือใน IDE เขียนผิดจุดนี้ พิมพ์ตามแล้วได้ TypeError ทันที

keepalive=60 แปลว่าถ้าเราเงียบเกิน 60 วินาที broker จะตัดทิ้ง — ส่งทุก 5 วินาทีจึงปลอดภัย แต่ถ้าเปลี่ยนไปส่งทุก 5 นาที ต้องขยับค่านี้ตาม ไม่งั้นจะโดนตัดเป็นรอบ ๆ

mqtt.connect() คืน True/False ไม่โยน exception — ถ้าไม่ตรวจค่าที่คืน โปรแกรมจะวิ่งต่อทั้งที่ไม่มีการเชื่อมต่อ แล้ว publish ทุกใบหายเงียบ

แผนภาพลำดับข้อความ MQTT ตั้งแต่ CONNECT CONNACK ไปจนถึง PUBLISH

ภาพ: Simon A. Eugster / Wikimedia Commons — CC BY-SA 4.0 · ภาพนี้มีธง retain ซึ่งโมดูล mqtt ของเราไม่มี

บรรทัดแรกที่ต้องเขียนหลัง connect() คือบรรทัดที่ บอกให้รู้ว่าต่อติดหรือไม่ติด ไม่ใช่บรรทัดถัดไปของงาน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

แกะโค้ดจริง — ท่าที่ 2 ส่งค่าจริงทุก 5 วินาที

    # --- ท่าที่ 2: อ่านเซนเซอร์ → ประกอบ JSON → publish (ใน publish_telemetry) ---
    try:
        ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
        data = {"ax": round(ax, 2), "ay": round(ay, 2), "az": round(az, 2),
                "pot": round(sensors.pot.percent(), 1)}
    except OSError:
        return None                            # อ่านพลาดหนึ่งรอบ ข้ามไปรอบหน้า
    ...
    mqtt.publish(TOPIC_PUB, json.dumps(data))  # แบน ไม่ห่อ แพลตฟอร์มห่อให้เอง
ค่าจากเซนเซอร์ ax = 0.1234567 pot = 48.23456 ทศนิยมยาวไม่มีประโยชน์ round() ก่อน 0.12 48.2 payload สั้นลงเกือบครึ่ง json.dumps() {"ax":0.12, "ay":-9.75, "az":0.31, "pot":48.2} สตริงเดียว ~80 ไบต์ publish ขึ้น broker

sensors.bmi270.motion() คืนหกค่าในการอ่านครั้งเดียว — อ่านทีเดียวดีกว่าเรียกหลายฟังก์ชัน เพราะค่าทั้งหกมาจากช่วงเวลาเดียวกันจริง · บรรทัดที่อ่านค่าอยู่ใน try เพราะอ่านพลาดหนึ่งรอบต้องไม่ทำให้ทั้งโปรแกรมตาย

round() ไม่ใช่เรื่องความสวยงาม: 0.1234567890 กิน 12 ไบต์ ส่วน 0.12 กิน 4 ไบต์ — คูณด้วยจำนวนฟิลด์และรอบต่อวันแล้วคือค่าเน็ตที่ประหยัดได้ฟรี · json.dumps() แปลง dict เป็นสตริงที่ทุกภาษาอ่านออก คือจุดที่ข้อมูลเลิกเป็นของ Python

ส่งแบน ไม่ต้องห่อ บริดจ์เป็นคนห่อให้เอง แล้วเติม device_id (จาก topic) กับ timestamp ให้ด้วย
ห่อเองซ้ำจะได้ชื่อวัดขึ้นต้น data_ แล้วตารางหน่วยฝั่งเซิร์ฟเวอร์หาไม่เจอ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

แกะโค้ดจริง — ท่าที่ 3 รับคำสั่งกลับมาสั่ง LED

mqtt.subscribe(TOPIC_CMD)               # ท่าที่ 3: subscribe ครั้งเดียว แล้ว poll ถี่ ๆ ในลูปเดียวกับ publish
...
    msg = mqtt.get_message()            # ไม่มีข้อความ = None
    if msg is not None:
        try:
            cmd = json.loads(msg[1].decode())   # msg[1] คือ payload เป็น bytes
        except ValueError:
            cmd = {}                    # คนส่งมั่วได้เสมอ โปรแกรมต้องไม่ตาย
        if cmd.get("cmd") == "toggle":
            led_on = not led_on         # จำสถานะเอง
            lamp.value(1 if led_on else 0)      # lamp = led_named("RGB_GREEN", "LED2")
            led_remote.value(1 if led_on else 0)
    ...
    time.sleep_ms(100)
ลูปเดียว ทำสองหน้าที่ — จับเวลาเองว่าถึงรอบ publish หรือยัง poll poll poll publish (ครบ 5 วิ) poll poll poll poll poll ทุก ๆ 100 มิลลิวินาทีเราถามหนึ่งครั้ง — คนกดปุ่มบนคอมไม่มีทางกดเร็วกว่านี้จนข้อความทับกัน ถ้าเขียน time.sleep(5) แล้วค่อย poll ครั้งเดียว — คำสั่งที่ส่งมาระหว่างนั้นจะเหลือแค่ใบสุดท้าย

get_message() คืน tuple (topic, payload) หรือ None ต้องตรวจ is not None ก่อนเสมอ · payload เป็น bytes จึงต้อง .decode() ก่อน json.loads() · ต้อง จำสถานะ LED ในตัวแปร Python เอง เพราะ gpio.led().value() ตอบแค่ระดับของขา ณ วินาทีที่ถาม · lamp ถูกเลือกตามชื่อ — led_named("RGB_GREEN", "LED2") หาดวงสีเขียวจาก gpio.board_info()["led_names"]: Dev Kit ได้ RGB_GREEN · Eva ได้ LED2 เพราะเลขดัชนีต่างกันตามบอร์ด และ led(0)/led(1) บน Dev Kit อยู่บน SoM ซึ่งยังไม่มีใครยืนยันว่ามองเห็นบนบอร์ดประกอบ

คำสั่งจากภายนอกคือ ข้อมูลที่เราไม่ได้เขียนเอง — ตรวจก่อนใช้เสมอ ทั้งว่ามีจริงไหม แปลงได้ไหม และอยู่ในชุดค่าที่เรารู้จักไหม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ท่าที่ 4 ที่ไม่มีในโครง — mqtt.disconnect() และเหตุผลที่ควรเรียก

# --- จบงานแล้วบอก broker ให้รู้ ไม่ใช่หายไปเฉย ๆ ---
mqtt.disconnect()                 # คืน None ไม่ใช่ True
print(mqtt.is_connected())        # False ทันที ไม่ต้องรอ keepalive หมดอายุ
ไม่เรียก แล้วกดรันใหม่ทันที บอร์ดหายไปเฉย ๆ broker ยังไม่รู้ client_id เดิมยังถูกจองอยู่ราวหนึ่งนาที รันใหม่ด้วยชื่อเดิม broker เตะตัวเก่าออก เห็นเป็นอาการ "ต่อติดแล้วหลุดสลับกัน" เสียเวลาไล่หาสาเหตุที่ไม่ได้อยู่ในโค้ด เรียกก่อนจบเสมอ broker รู้ทันทีว่าเราไปแล้ว client_id ว่างทันที ต่อใหม่ชื่อเดิมได้เลย is_connected() ตอบ False ตรงกับความจริง รันซ้ำได้ต่อเนื่องโดยไม่ต้องรอ ตอนฝึกที่รันวันละสิบรอบ ต่างกันมาก

โค้ดหลักของชุดบทเรียนนี้วนลูปไม่รู้จบ จึงไม่เคยเดินมาถึงบรรทัด disconnect() แต่พอทีมกด Ctrl-C หรือกดรันใหม่ บอร์ดหายไปโดยที่ broker ยังนับว่าเรายังอยู่ เพราะฝั่งนั้นรอจนครบ keepalive ก่อนถึงจะยอมรับว่าเราไปแล้ว ระหว่างนั้นชื่อ client_id เดิมยังถูกจองอยู่

disconnect() จึงมีค่าที่สุดตอน เลิกใช้งานตามตั้งใจ ไม่ใช่ตอนพัง เขียนไว้ท้ายไฟล์ หรือใน except KeyboardInterrupt: แล้วอาการ "ต่อติดแล้วหลุดสลับกัน" ที่อยู่ในตารางกับดักจะหายไปเองครึ่งหนึ่ง

ลองไฟล์ 07_disconnect_frees_id.py — มันต่อ ส่ง ตัด แล้วต่อใหม่ด้วยชื่อเดิมทันที ให้เห็นว่าทำได้จริงเมื่อบอกลาอย่างถูกวิธี

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ข้อมูลไหลไปทางไหน — ตั้งแต่เซนเซอร์ถึงกราฟ

เซนเซอร์ BMI270 · pot โค้ดของเรา json.dumps EMQX broker 1883 บริดจ์ subscribe ไว้แล้ว ฐานข้อมูล อนุกรมเวลา กราฟ อัปเดตสด ขาขึ้น — ผู้เรียนเขียนแค่สองกล่องแรก ที่เหลือแพลตฟอร์มต่อไว้ให้แล้ว ส่งถูก topic แล้วเห็นกราฟทันที โดยไม่ต้องสร้างแดชบอร์ดเอง ขาลง — คำสั่งจาก MQTT Explorer เดินย้อนเส้นเดียวกันกลับมาที่ LED ถ้าไม่เห็นข้อมูลบนกราฟ ให้ไล่จากซ้ายไปขวาทีละกล่อง อย่าเดาจากปลายทาง

จุดตรวจที่ใช้ได้จริงเวลาข้อมูลไม่ขึ้น: MQTT Explorer เห็นข้อความไหม ถ้าเห็น แปลว่าสองกล่องแรกและ broker ทำงานครบ ปัญหาอยู่ที่ topic ผิดรูปหรือ JSON ไม่ถูกต้อง (บริดจ์ทิ้ง JSON เสียเงียบ ๆ พร้อมเขียน log) ถ้าไม่เห็น ปัญหายังอยู่ฝั่งบอร์ด

ระบบยาวหกกล่อง แต่การดีบักไม่เคยยาวกว่า "หาให้เจอว่ากล่องสุดท้ายที่ยังเห็นข้อมูลคือกล่องไหน"

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

วิธีรันบนบอร์ด

1 · เตรียมฝั่งเครื่อง CE ขึ้นแล้ว · แก้พอร์ต 1883 ขึ้นทะเบียน device_id สั้น จด IP ของเครื่องไว้ 2 · แก้ค่าบนหัวไฟล์ WIFI_SSID / WIFI_PASSWORD BROKER · DEVICE_ID · MQTT_PASS TOPIC_PUB · TOPIC_CMD 3 · รันแล้วดูสองจอ จอบอร์ด: สถานะการเชื่อมต่อ จอคอม: MQTT Explorer แล้วทดสอบขากลับ
  1. บนเครื่องที่รัน CE — ตรวจว่า docker compose ps เห็น emqx สถานะ up และพอร์ตขึ้นเป็น 0.0.0.0:1883 แล้วจริง
  2. หา IP ของเครื่องนั้นใน LAN (ไม่ใช่ localhost — บอร์ดอยู่คนละเครื่อง) แล้วทดสอบจากบอร์ดด้วย wifi.ping("192.168.1.50") ก่อน ถ้า ping ไม่ผ่าน อย่าเพิ่งเสียเวลากับ MQTT
  3. บนคอม เปิด MQTT Explorer ต่อไปที่ IP เดียวกัน พอร์ต 1883 ด้วยชื่อผู้ใช้/รหัสของอุปกรณ์ที่ได้ตอนเพิ่มอุปกรณ์ใน CE แล้ว subscribe device/# ไว้ล่วงหน้า
  4. บนบอร์ด เปิดหน้า BENTO Playground ค้างไว้ แล้วเปิด s10_mqtt_telemetry.py แก้ค่าบนหัวไฟล์ให้เป็นของคุณ
  5. เติมช่องว่าง pass ให้ครบ แล้วกด Program to Device — จากนั้นมองสองจอสลับกัน
  6. ทดสอบขากลับ: ใน MQTT Explorer พิมพ์ publish ไปที่ device/<device_id>/commands ด้วย payload {"cmd":"toggle"} แล้วดู LED

ตัวอย่างของบทเรียน 4.4–4.6 — สามไฟล์แรกคือชุดที่พา JSON ใบแรกขึ้น broker แล้วสั่งกลับมาได้

ต้องทำในบทเรียน · เปิดตามลำดับนี้ ทั้งชุดราว 35 นาที

ลำดับ · เรื่อง · เวลา ไฟล์ ลงมือทำอะไร แล้วจะเข้าใจอะไร
1 · ตั้งชื่อ topic ก่อนต่อ broker · 8 นาที 01_topic_design.py ตั้ง topic ที่ไม่ชนกับผู้เรียนคนอื่นบน broker เดียวกัน และรู้ว่า wildcard ใส่ใน publish ไม่ได้ · จะเข้าใจว่าชื่อ topic เป็นของที่ออกแบบก่อนต่อ broker ไม่ใช่ค่อยคิดตอนโค้ดพร้อมส่งแล้ว
2 · บันไดสามขั้น WiFi → TCP → MQTT · 15 นาที 03_connect_and_publish.py ส่ง JSON ใบแรกขึ้น broker ได้ และรู้ว่าพังขั้นไหนเมื่อมันพัง · จะเห็นว่าการต่อคือบันไดสามขั้น WiFi → TCP → MQTT ที่ล้มได้คนละแบบ
3 · สั่งกลับจากคอมพิวเตอร์ · 12 นาที 04_subscribe_command.py ซ้อมครึ่งหลังของ MVP — รับ {"cmd":"beep"} กับ {"cmd":"count"} แล้วบอร์ดตอบทันที (คำสั่ง toggle ที่สลับ LED จริงอยู่ในไฟล์ฝึก s10_mqtt_telemetry.py ของบทเรียน 4.6) · จะเข้าใจว่าการสั่งกลับมาที่บอร์ดคือฝั่ง subscribe ไม่ใช่ฝั่ง publish

ติดตรงไหน เปิดอันนี้

อาการที่เจอ ไฟล์ที่ตอบอาการนั้น
ส่งได้ทุก 5 วินาทีแล้ว แต่กดสั่งจากคอมทีไรบอร์ดไม่ตอบ 05_send_every_5s_still_listen.py — นับเวลาถึงกำหนดด้วยนาฬิกา แทนการหลับรอ ไฟล์นี้ไม่ต้องแก้อะไรก่อนกด Run
ไม่แน่ใจว่าจะใส่ฟิลด์อะไรลง payload 02_payload_shape.py — เทียบสามรูปแบบที่ยาวไม่เท่ากันทั้งที่บอกเรื่องเดียวกัน
โค้ดบอกว่าส่งแล้ว แต่ MQTT Explorer ไม่เห็นอะไรเลย 06_sent_is_not_delivered.py — นับที่ปลายทาง ไม่ใช่นับที่ต้นทาง

อ่านเสริมนอกเวลา — เรื่องนี้เป็นเนื้อหาของบทเรียน 4.7–4.9 ไม่ใช่เกณฑ์ผ่านของวันนี้: 06_secure_publish_loop.py คือลูปเดียวกันนี้ แต่วิ่งบนช่องที่เข้ารหัสไปยังแพลตฟอร์ม วันนี้ยังรันไม่ได้จนกว่าทีมจะได้ device_id ของตัวเอง เปิดอ่านเทียบโครงได้ แต่อย่าเพิ่งพยายามรัน

หนึ่งบอร์ดหนึ่ง device_id — ห้ามสองบอร์ดใช้ client_id เดียวกัน เพราะ broker จะเตะเครื่องเก่าออกทุกครั้งที่เครื่องใหม่ต่อเข้ามา แล้วทั้งสองเครื่องจะหลุดสลับกันเป็นลูป · ไฟล์ 04 06 07 ต่อ broker ฝึกสาธารณะ broker.hivemq.com ที่คนอื่นก็ใช้ ก่อนรันให้แก้ team03 ทั้งใน DEVICE_ID และใน topic เป็นรหัสของคุณ เช่น nok4821

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0