คาบ 13 — WiFi + MQTT

leaderboard / เล่นข้ามบอร์ด

ช่วงเชื่อมต่อเครือข่าย — คาบที่ 13 ของ 14
3 คน / 1 บอร์ด · ต่อยอดสู่คาบ 14 (โปรเจกต์ปิดคอร์ส)

วันนี้บอร์ดของเราจะไม่ได้อยู่คนเดียวอีกต่อไป มันจะส่งคะแนนคุยกับบอร์ดเพื่อนผ่านอากาศได้แล้ว

เป้าหมายวันนี้

วันนี้เราจะพาบอร์ดจาก "เล่นคนเดียวบนจอตัวเอง" ไปสู่ "ส่งคะแนนขึ้นกระดานกลาง + รับคะแนนของเพื่อนมาดูได้"

ก้าวที่ 1 — ต่อเน็ต
wifi.connect()wifi.is_connected()wifi.ip()
ต่อ WiFi ห้องเรียนให้ติด แล้วเห็น IP ของตัวเอง
ก้าวที่ 2 — คุยผ่าน MQTT
mqtt.connect / subscribe / publish / get_message
ส่งคะแนนขึ้น leaderboard + รับคะแนนเพื่อนมาแสดง
เล่นคนเดียว คะแนนอยู่บนจอบอร์ดตัวเอง WiFi + MQTT ส่งขึ้นกระดานกลาง ทุกบอร์ดเห็นคะแนนกันและกัน

สิ่งที่อยากให้เห็นตอนจบคาบ: กดรันแล้ว IP ขึ้น, คะแนนของเราถูกส่งขึ้น topic เดียวกัน และเห็นคะแนนของบอร์ดอื่นวิ่งเข้ามาในคอนโซล

ภาพรวม: บอร์ดเรา = node หนึ่งใน "เครือข่าย"

แนวคิด network/IoT ไม่ได้ลึกลับเลย บอร์ดคือ node, broker คือ ที่ทำการไปรษณีย์กลาง, ส่วนคะแนนที่เราส่งก็เหมือน โปสการ์ดใบหนึ่ง

บอร์ด A board-01 Broker test.mosquitto.org :1883 บอร์ด B board-02 publish publish ทุกบอร์ดที่ subscribe topic เดียวกัน จะได้รับโปสการ์ดของทุกคน

หัวใจของ MQTT คือ topic: ถ้าใช้ชื่อ topic เดียวกัน ก็เหมือนอยู่ห้องเดียวกัน ได้ยินกันหมด

publish / subscribe — ส่งครั้งเดียว ถึงทุกบอร์ด

จุดที่ทำให้ leaderboard ของทั้งห้องทำงานได้ คือ บอร์ดเราส่ง 1 ครั้ง แต่ broker กระจายให้ทุกคนที่ subscribe topic เดียวกัน เราไม่ต้องรู้เลยว่ามีใครฟังอยู่บ้าง

  • publisher (คนส่ง) กับ subscriber (คนฟัง) ไม่ต้องรู้จักกันตรง ๆ — broker เป็นตัวกลาง เรียกว่า decouple
  • เพิ่มบอร์ดเข้าห้องอีกกี่เครื่องก็ได้ โค้ดฝั่งคนส่ง ไม่ต้องแก้ แค่บอร์ดใหม่ subscribe topic เดิม

นี่คือเหตุผลที่ MQTT เหมาะกับ IoT จริง ๆ ลองนึกถึงเซ็นเซอร์อุณหภูมิ 100 ตัวในโรงงาน ส่งขึ้น topic เดียว แดชบอร์ดตัวเดียว subscribe ก็เห็นครบ — โครงเดียวกับ leaderboard ของเราเป๊ะ

เห็นภาพเคลื่อนไหว — publish หนึ่งครั้ง วิ่งถึงทุกคน

บอร์ด A publish คะแนนขึ้น broker ครั้งเดียว จากนั้น broker ก็ส่งต่อให้บอร์ด B และ C ที่ subscribe topic นั้นไว้ พร้อมกัน — รวมถึงส่งกลับให้บอร์ด A เองด้วย (เพราะ A ก็ subscribe อยู่)

สังเกตว่า A ไม่ได้ส่งตรงไปหา B หรือ C เลย มันส่งให้ "ห้อง" (topic) เฉย ๆ ใครอยู่ในห้องก็ได้ยินเอง — แยกหน้าที่ "ส่ง" ออกจาก "ใครรับ" ชัดเจน

เชื่อมโยงรากฐาน — embedded + Python ที่อยู่ใต้เกมนี้: (1) ฮาร์ดแวร์ — บอร์ดสื่อสารผ่านชิปวิทยุแยก (AIROC) ผ่านบัส SDIO/UART ทุกอุปกรณ์ IoT จริงก็คือ MCU + radio แบบนี้ (2) Pythonget_message() คืน None เมื่อยังไม่มีของ นี่คือ non-blocking polling เกมจึงวนเช็กได้โดยไม่ค้าง และ payload ที่รับมาเป็น bytes ต้อง .decode() เป็น str ก่อน (เรื่อง type เดิมจากคาบต้น ๆ) — เมื่อจับสองอย่างนี้ได้ คุณก็ต่อเซ็นเซอร์จริงขึ้นคลาวด์ได้ด้วยหลักการเดียวกัน

ก้าวที่ 1 — ต่อ WiFi แล้วเช็กว่าติดจริง

WiFi มี 3 ฟังก์ชันหลักที่เราใช้วันนี้ จำง่าย ๆ คือ ต่อ / เช็ก / ถาม IP

import wifi
import time

# ต่อ WiFi (ห้องเราเป็น 2.4GHz) — ลองสูงสุด 6 ครั้ง (ครั้งแรกมักยังไม่ติด)
ok = False
for i in range(6):
    if wifi.connect("YOUR_WIFI_SSID", "YOUR_WIFI_PASSWORD"):   # คืน True เมื่อสำเร็จ
        ok = True
        break
    print("  ยังไม่ติด ลองใหม่ %d/6 ..." % (i + 1))
    time.sleep(2)                       # พัก 2 วิ ให้ WCM ตั้งตัว
if not ok:                              # ลองครบ 6 ครั้งแล้วยังไม่ติด → อย่าไปต่อ
    print("ต่อ WiFi ไม่ได้ ลองเช็ค SSID/รหัสอีกที")
    raise SystemExit

print("IP ของบอร์ดเรา:", wifi.ip())               # เช่น 192.168.1.42

ทำไมต้องเป็น loop ลองใหม่ ไม่ใช่ยิงครั้งเดียว? เพราะการจับ AP + ขอ IP ใช้เวลาไม่แน่นอน ครั้งแรกจึงมัก "ยังไม่ติด" — วนลองพร้อมหน่วง time.sleep(2) คือแพตเทิร์น retry มาตรฐานของงานเครือข่ายจริง (ตรงกับ s13_net.py:25–33)

ข้อควรระวัง:

  • SSID / PASSWORD วันนี้เป็น ค่า placeholder ต้องแก้ให้ตรงของห้องเราก่อน
  • WiFi บอร์ดต่อได้เฉพาะคลื่น 2.4GHz (อย่าใช้ชื่อ network ที่เป็น 5GHz)

ทางลัดที่แนะนำ: ต่อ WiFi ผ่านเมนู Wi-Fi บนจอ ครั้งเดียว (เซฟรหัสไว้ → บอร์ด auto-connect ทุกครั้งที่เปิด) แล้วโค้ดนี้จะโฟกัสที่ MQTT ล้วน ๆ — บรรทัด wifi.connect() จะผ่านทันทีเพราะต่ออยู่แล้ว

เข้าใจฮาร์ดแวร์ · WiFi ทำงานยังไง

ชิป PSoC Edge ไม่มีวิทยุในตัว — มันจับคู่กับชิปวิทยุแยกชื่อ AIROC CYW55513 (Wi-Fi 6/6E + Bluetooth) คุยกันผ่าน SDIO + UART

"ชิปวิทยุแยก" ในรูปขวาคือการ์ดวิทยุแบบ M.2 (Wi-Fi 6) ตัวจริง — สังเกตขั้วต่อเสาอากาศ MAIN/AUX และโลหะคลุมชิป RF · ที่มา: "Intel Wi-Fi 6 AX200NGW card" — Siarhei Besarab, CC BY-SA 4.0, Wikimedia Commons

  • wifi.connect() = ส่ง SSID/รหัสให้ชิปวิทยุไปต่อ AP (router/แอคเซสพอยต์ของห้อง) แล้วได้ IP address กลับมา
  • MQTT = ที่ทำการไปรษณีย์ (broker): บอร์ดหนึ่ง publish ข้อความไปที่ topic · บอร์ดที่ subscribe topic นั้นก็ได้รับ
  • หลายบอร์ดคุยผ่าน broker ตัวเดียว = หัวใจของ IoT

mqtt.publish("score", "42") = หย่อนจดหมายติดป้าย "score" ที่ไปรษณีย์ — ทุกบอร์ดที่สมัครรับป้ายนี้ได้อ่านหมด (payload ต้องเป็น str หรือ bytes — ถ้าเป็นตัวเลขให้ครอบด้วย str(...) ก่อน)

What is MQTT | MQTT Essentials Part 1 — HiveMQ — publish/subscribe, topic, broker อธิบายภาพรวมตรงกับที่เราต่อในคาบนี้

เกร็ด: AP ตัวจริงที่บอร์ดเราต่อหา

wifi.connect() ปลายทางไม่ใช่ของลึกลับ มันคือกล่อง access point / router แบบในรูปนี้ ที่กระจายคลื่น 2.4GHz อยู่ในห้องเรา

  • บอร์ดส่งชื่อ (SSID) + รหัสไปขอเข้าร่วมเครือข่าย เมื่อผ่าน AP จะแจก IP address ให้ — เหมือนได้เลขห้องในตึก
  • AP ส่วนมากกระจายทั้ง 2.4GHz และ 5GHz แต่ชิปวิทยุของบอร์ดต่อได้เฉพาะ 2.4GHz จึงต้องเลือกชื่อ network ให้ถูกคลื่น
  • พอได้ IP แล้ว บอร์ดถึงจะวิ่งออกอินเทอร์เน็ตไปหา MQTT broker ที่อยู่ไกลออกไปได้

ในผลิตภัณฑ์จริง อุปกรณ์ IoT ทุกตัวก็เริ่มจากก้าวนี้เป๊ะ — จับ AP ให้ได้ IP ก่อน แล้วค่อยคุย protocol ที่อยู่ชั้นบน (MQTT/HTTP) ทีหลัง

ที่มา: "ELECOM WRC-300FEBK WPS WiFi router" — Syced, CC0, Wikimedia Commons

ก้าวที่ 2 — ต่อ MQTT broker + สมัครรับ topic

ต่อ WiFi ติดแล้ว ค่อยต่อ broker ลำดับนี้ห้ามสลับ เพราะ MQTT วิ่งบนเน็ตอีกที

บอร์ดเรา broker mosquitto:1883 1 · CONNECT 2 · CONNACK (ok) — ต่อสำเร็จแล้วค่อย subscribe — 3 · SUBSCRIBE (topic) 4 · SUBACK (ok) พร้อม publish / รับข้อความ
import mqtt

BROKER = "test.mosquitto.org"            # broker สาธารณะ ทดสอบได้เลย (placeholder)
TOPIC  = "tesaiot/class/leaderboard"    # ทุกบอร์ดในห้องใช้ topic นี้ร่วมกัน
PLAYER = "board-01"                      # ชื่อบอร์ดเรา แต่ละคนตั้งไม่ให้ซ้ำ

# พอร์ต MQTT ปกติคือ 1883 — คืน True เมื่อต่อสำเร็จ
if not mqtt.connect(BROKER, 1883, client_id=PLAYER):
    print("ต่อ broker ไม่ได้")
    raise SystemExit

mqtt.subscribe(TOPIC)                    # สมัครรับ = "ขอฟังห้องนี้ด้วย"
print("ต่อ broker แล้ว")

subscribe คือการบอก broker ว่า "เวลามีใครส่งเข้า topic นี้ ส่งมาให้ผมด้วย"

ก้าวที่ 2 (ต่อ) — publish คะแนน + รับคะแนนเพื่อน

ส่งคะแนนด้วย publish, รับคะแนนคนอื่นด้วย get_message ซึ่ง ไม่บล็อก (ไม่มีของก็คืน None)

import time

my_score = 1250                              # สมมุติเพิ่งเล่นจบได้เท่านี้
payload = "%s:%d" % (PLAYER, my_score)       # รูปแบบง่าย ๆ "ชื่อ:คะแนน"
mqtt.publish(TOPIC, payload)                 # ส่งคะแนนขึ้นกระดาน
print("ส่งคะแนนแล้ว ->", payload)

deadline = time.time() + 15                  # วนรับคะแนนเพื่อน 15 วินาที
while time.time() < deadline:
    msg = mqtt.get_message()                 # คืน (topic, payload) bytes หรือ None
    if msg is not None:
        topic, data = msg
        print("ได้คะแนนใหม่:", data.decode())  # payload เป็น bytes ต้อง .decode()
    time.sleep(0.2)                          # พักนิดให้ระบบหายใจ

mqtt.disconnect()                            # ปิดการเชื่อมต่อเมื่อจบ

get_message() คืน None เมื่อยังไม่มีของ — เกมจึงไม่ค้าง วนเช็กไปเรื่อย ๆ ได้

สะพานทฤษฎี → โค้ด (3 จุดสำคัญ):

  • รูปแบบข้อความ (application protocol) — เราออกแบบเองเป็น "ชื่อ:คะแนน" ด้วย "%s:%d" % (PLAYER, my_score) (s13_net.py:48) ฝั่งรับ .decode() แล้ว split(":") แยกกลับ (:57–58) → นี่คือ "protocol เล็ก ๆ" ที่ทั้งห้องตกลงกัน ถ้าอยากส่งหลายฟิลด์ให้เป็นระเบียบกว่านี้ ใช้ json.dumps({"name":PLAYER,"score":my_score}) แล้วฝั่งรับ json.loads(...) ได้เลย
  • QoS ของ mqtt.publish (s13_net.py:49) — MQTT มี 3 ระดับความมั่นใจว่าข้อความถึงปลายทาง: QoS 0 ("ส่งแล้วแล้วกัน" เร็วสุด — ค่าปริยายของ leaderboard นี้), QoS 1 ("อย่างน้อย 1 ครั้ง" อาจซ้ำ), QoS 2 ("ครั้งเดียวเป๊ะ" ช้าสุด) สำหรับคะแนนเกม QoS 0 พอ เพราะเดี๋ยวก็ส่งค่าใหม่ทับอยู่ดี
  • เช็กผลก่อนไปต่อ — ทุก step คืน True/False ถ้า not ok ก็ raise SystemExit ทันที (WiFi :31–33, broker :39–40) ไม่ปล่อยให้พังเงียบ ๆ ตอนหลัง

วงจรชีวิตการเชื่อมต่อ (ลำดับสำคัญมาก)

wifi.connect ต่อเน็ตก่อน mqtt.connect ต่อ broker subscribe ขอฟัง topic publish / get_message disconnect ต่อ WiFi ไม่ติด → อย่าต่อ MQTT · ทุก step คืน True/False ให้เราเช็กก่อนไปต่อ

ลำดับนี้คือ "สัญญา" ของเน็ต: ถ้า step ก่อนหน้าไม่สำเร็จ step ถัดไปจะพังเงียบ ๆ จึงต้องเช็กผลทุกครั้ง

โปรแกรมทั้งตัว s13_net.py — เฟส 1: ต่อเน็ต + broker

ผังนี้คือครึ่งแรกของไฟล์ทั้งไฟล์: จาก START ผ่าน retry loop ต่อ WiFi → เช็ก → ต่อ broker → subscribe เส้นทางล้มเหลวทุกทางลง raise SystemExit

START wifi ติด?:26 wifi.ip():34 broker ติด?:38 subscribe:44 เฟส 2: ส่งคะแนนผังถัดไป ↓ sleep2 · ลองซ้ำ ≤6:29–30 raise SystemExit:32–33 raise SystemExit:39–40 Yes Yes No ลองใหม่ ครบ6 No

เฟส 2: ส่งคะแนน + วนรับ 15 วินาที

payload "%s:%d":48 mqtt.publish:49 time < deadline?:54 disconnect → จบ:61 get_message:55 msg?:56 decode() + print:58 sleep 0.2:59 หมดเวลา Yes Yes No วนรับต่อ

leaderboard ทำงานอย่างไร (รูปแบบข้อความ)

เราตกลงกัน รูปแบบเดียวทั้งห้อง เพื่อให้ทุกบอร์ดอ่านของกันออก: "ชื่อ:คะแนน"

"board-01:1250" payload ที่เราส่ง (str) board-01 · 1250 board-02 · 980 board-03 · 1410 กระดานรวม ของทั้งห้อง

ตอนรับมาเป็น bytes เราจึงต้อง .decode() ก่อนเสมอ แล้วค่อย split(":") ถ้าจะแยกชื่อกับคะแนน

โค้ดของวันนี้: practise vs solution

practise_codes/s13_net.py
ฉบับ "เล่นตาม-เว้นช่อง" มี 4 จุดให้เติม:
(1) wifi.connect · (2) wifi.ip()
(3) mqtt.connect · (4) mqtt.publish
น้อง ๆ เติมเองในคาบ
ฉบับเฉลย (เปิดในห้อง)
ต่อ WiFi + MQTT + leaderboard ครบ
ใช้เทียบหลังลองเองแล้ว
อย่าเพิ่งเปิดก่อนลอง

วิธีรัน: เปิดไฟล์ใน BENTO IDE แล้วกดปุ่ม Program to Device (ปุ่มสีเขียวมุมซ้ายบน — ดูรูป) บอร์ดจะรันให้ทันที (ดูผลที่คอนโซลด้านล่าง)

ปุ่ม Program to Device อยู่บนแถบเครื่องมือ BENTO IDE (มุมซ้ายบน) · กดแล้ว IDE จะเซฟเป็น main.py แล้วสั่งบอร์ดรัน — ผลลัพธ์ขึ้นที่ Terminal ด้านล่าง

เกมเต็มที่ดึงคะแนนจริงมาต่อ leaderboard อยู่ใน full_games/ ไว้ดูเป็นปลายทางได้

ส่งคะแนนถี่แค่ไหนพอดี?

leaderboard ไม่ใช่วิดีโอสด เราอยากเห็นคะแนน ล่าสุด ไม่ใช่คะแนนทุกเสี้ยววินาที — ดังนั้นอย่า publish ในทุกเฟรมของเกม

ถ้าเกมรันที่ ff เฟรมต่อวินาที แล้ว publish ทุกเฟรม ภาระต่อ 1 นาทีคือ

Nmsg/min=f×60=60×60=3,600 ข้อความ/นาทีN_{\text{msg/min}} = f \times 60 = 60 \times 60 = 3{,}600 \ \text{ข้อความ/นาที}

เทียบกับส่ง เมื่อคะแนนเปลี่ยน ซึ่งในเกมจริงเกิดไม่กี่ครั้งต่อนาที ภาระต่างกันเป็นหลักร้อยเท่า:

NทุกเฟรมNเมื่อเปลี่ยน3,6005700×\frac{N_{\text{ทุกเฟรม}}}{N_{\text{เมื่อเปลี่ยน}}} \approx \frac{3{,}600}{5} \approx 700\times

เก็บคะแนนล่าสุดที่ส่งไปไว้ในตัวแปร แล้ว publish ก็ต่อเมื่อ score != last_sent เท่านั้น — ลดภาระ broker, ลดเน็ตหน่วง, และคะแนนบนกระดานก็ยังสด นี่คือเทคนิคเดียวกับที่ใช้คุมอัตราส่งของเซ็นเซอร์จริงในงาน IoT

หมายเหตุ: ไฟล์เดโม s13_net.py ส่งคะแนนค่าคงที่ครั้งเดียว (my_score = 1250 แล้ว publish ที่ :47–50) เพื่อให้ทดสอบง่าย — การ์ด score != last_sent ด้านบนคือแพตเทิร์นที่ควรใส่เมื่อนำไปต่อในลูปเกมจริง ที่คะแนนเปลี่ยนได้

กับดักที่เจอบ่อย

อาการ สาเหตุ วิธีแก้
wifi.connect คืน False SSID/รหัสผิด หรือเป็น 5GHz ใช้ network 2.4GHz + เช็กรหัสอีกครั้ง
wifi.ip() ได้ค่าแปลก ๆ เรียกก่อนต่อสำเร็จ เช็ก is_connected() ให้ True ก่อน
mqtt.connect ติดบ้างไม่ติดบ้าง client_id ซ้ำกับเพื่อน ตั้ง PLAYER ให้ไม่ซ้ำทั้งห้อง
get_message() คืน None ตลอด topic ไม่ตรงกัน TOPIC ต้องเป๊ะ ตัวเดียวกันทุกบอร์ด
data แสดงเป็น b'...' ลืม .decode() payload เป็น bytes ต้อง decode ก่อน
ส่งคะแนนทุกเฟรม เน็ตหน่วง publish ถี่เกิน ส่งเมื่อคะแนน "เปลี่ยน" พอ ไม่ใช่ทุกเฟรม

เช็กผ่าน + ทำเอง 30%

เกณฑ์ผ่านคาบ 13:

  1. ต่อเน็ตได้จริง — รันแล้ว wifi.is_connected() เป็น True และเห็น IP ของบอร์ดเรา
  2. ส่งคะแนนขึ้นกระดานได้mqtt.publish คะแนนขึ้น topic ของห้อง
  3. รับคะแนนเพื่อนมาแสดงได้ — เห็นคะแนนของบอร์ดอื่นวิ่งเข้าคอนโซล (decode ถูก)
  4. อธิบายได้ — ทุกคนเล่าได้ว่า topic คืออะไร และทำไมต้องต่อ WiFi ก่อน MQTT

ส่วนทำเอง 30% (ต่อยอดเลือก 1 อย่าง):

  • เก็บคะแนนที่รับมาใส่ dict แล้ว print เป็นกระดานเรียงจากมากไปน้อย
  • ส่ง state มากกว่าคะแนน เช่น "ชื่อ:คะแนน:ด่าน" แล้วแยกด้วย split(":")
  • ลองสองบอร์ดส่งคะแนนหากันจริง โดยตั้ง PLAYER คนละชื่อแต่ TOPIC เดียวกัน

คาบหน้า (คาบ 14) เราจะรวมทุกอย่างที่ฝึกมาทั้งคอร์สเป็นโปรเจกต์ปิดท้ายของตัวเอง เน็ตที่ทำวันนี้จะเป็นของพิเศษชิ้นสำคัญที่ทำให้งานเราโดดเด่น

เฉลย s13_net.py — อ่านให้เข้าใจ แล้วปิดไฟล์พิมพ์เอง

เฉลยนี้ไม่ได้มีไว้ลอกวางส่ง คะแนนคาบนี้อยู่ที่ใบงานกับการอธิบายด้วยคำพูดของน้องเอง วิธีใช้ให้ได้ผลจริงคือ อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือ ตอนพิมพ์เองนี่แหละสมองจะจำ pattern การต่อเน็ตได้ ต่อไปเราจะแกะเฉลยทีละส่วน ไม่ใช่ดูรวดเดียว เพราะทุกบรรทัดมีเหตุผลของมัน

ส่วนแรก — ตั้งค่าให้ครบก่อนลงมือ:

import wifi
import mqtt
import time

# --- ตั้งค่า: แก้ 4 บรรทัดนี้ให้ตรงกับของจริงก่อนรัน ---
SSID    = "YOUR_WIFI_SSID"          # ชื่อ WiFi (2.4GHz) ของห้องเรา
PASSWORD = "YOUR_WIFI_PASSWORD"     # รหัส WiFi
BROKER  = "test.mosquitto.org"       # MQTT broker สาธารณะ (ทดสอบได้เลย)
TOPIC   = "tesaiot/class/leaderboard"   # ห้องเดียวกันทั้งคาบ ทุกคนใช้ topic นี้

PLAYER  = "board-01"                # ชื่อผู้เล่น/บอร์ดของเรา (แต่ละคนตั้งไม่ซ้ำ)
  • ทำไมกองค่าที่ต้องแก้ไว้ บนสุดที่เดียว (:15-20) แทนที่จะโปรยตามโค้ด? เพราะเปิดไฟล์มาแก้ SSID/PASSWORD/TOPIC/PLAYER ให้จบในหน้าจอเดียว ไม่ต้องไล่หาทีละจุด — ตรงกับสไลด์ practise ที่บอกว่ามี 4 จุดต้องเติม ถ้าฝังค่าลึกในโค้ด เพื่อนที่หยิบไฟล์ไปรันต่อจะแก้ผิดจุดทันที
  • TOPIC (:18) คือ "ห้อง" จากสไลด์ node/broker — ทั้งห้องต้องพิมพ์เป๊ะตัวเดียวกัน พิมพ์ต่างแม้แต่ตัวเดียว จะเข้าคนละห้อง ตรงกับกับดัก "topic ไม่ตรง get_message คืน None ตลอด"
  • PLAYER (:20) ต้องไม่ซ้ำทั้งห้อง เพราะมันไปเป็น client_id ของ broker — จำกับดัก "client_id ซ้ำ ต่อบ้างไม่ติดบ้าง" ได้ไหม สองบอร์ดใช้ชื่อเดียว broker จะเตะตัวเก่าออก
  • import wifi / mqtt / time (:10-12) สามโมดูลพอดีกับงานวันนี้: wifi ต่อเน็ต, mqtt คุย broker, time คุมจังหวะทั้ง retry และ poll

ค่าที่ต้องแก้ก่อนรัน ให้อยู่ที่เดียว มองเห็นหมดในตาเดียว นี่เป็นนิสัยเล็ก ๆ ที่ช่วยทั้งตัวเราและคนที่หยิบโค้ดไปใช้ต่อ

เฉลย · ส่วนที่สอง — ต่อ WiFi แบบวนลองใหม่

# --- 1) ต่อ WiFi ---
print("กำลังต่อ WiFi ...")
ok = False
for i in range(6):                       # ลองสูงสุด 6 ครั้ง (ครั้งแรกมักยังไม่ติด)
    if wifi.connect(SSID, PASSWORD):     # คืน True เมื่อต่อสำเร็จ
        ok = True
        break
    print("  ยังไม่ติด ลองใหม่ %d/6 ..." % (i + 1))
    time.sleep(2)                        # พัก 2 วิ ให้ WCM ตั้งตัว
if not ok:
    print("ต่อ WiFi ไม่ได้ ลองเช็ค SSID/รหัสอีกที")
    raise SystemExit
print("ต่อ WiFi แล้ว IP =", wifi.ip())   # โชว์ IP ที่ได้มา
  • ทำไมต้อง วนลอง 6 ครั้ง ไม่ยิง wifi.connect ครั้งเดียว? เพราะการจับ AP แล้วขอ IP ใช้เวลาไม่แน่นอน ครั้งแรกมักยังไม่ติด นี่คือ retry loop มาตรฐานที่เราคุยกันในสไลด์ ก้าวที่ 1 — ยิงครั้งเดียวแล้วเชื่อว่าติดเลยคือสาเหตุที่โค้ดเน็ตล้มบ่อยที่สุด (:25-30)
  • ok = False แล้วตั้งเป็น True ตอนสำเร็จ (:24,27) คือธง (flag) ที่บอกว่า "ออกจากลูปเพราะติดจริง หรือเพราะลองครบ 6 ครั้งแล้ว" ถ้าไม่มีธงนี้ เราแยกสองกรณีนี้ไม่ออก
  • if not ok: raise SystemExit (:31-33) คือด่านกั้น — ต่อเน็ตไม่ติดก็อย่าไปต่อ MQTT ให้เสียเวลา ตรงกับหลัก "เช็กผลก่อนไปต่อ" จากสไลด์วงจรชีวิตการเชื่อมต่อ ("ถ้า step ก่อนไม่สำเร็จ step ถัดไปจะพังเงียบ ๆ")
  • wifi.ip() เรียก หลัง ต่อสำเร็จเท่านั้น (:34) — จำกับดัก "เรียก ip() ก่อนต่อสำเร็จ ได้ค่าแปลก ๆ" ได้ไหม ที่นี่เราปลอดภัยเพราะผ่านด่าน not ok มาแล้ว

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

เฉลย · ส่วนที่สาม — ต่อ broker แล้วส่งคะแนน

# --- 2) ต่อ MQTT broker ---
print("กำลังต่อ MQTT broker ...")
if not mqtt.connect(BROKER, 1883, client_id=PLAYER):   # พอร์ต MQTT ปกติคือ 1883
    print("ต่อ broker ไม่ได้")
    raise SystemExit
print("ต่อ broker แล้ว")

# สมัครรับข้อความใน topic เดียวกัน เพื่อเห็นคะแนนของทุกบอร์ด
mqtt.subscribe(TOPIC)

# --- 3) ส่งคะแนนของเราขึ้นกระดาน ---
my_score = 1250                              # สมมุติว่าเพิ่งเล่นจบได้เท่านี้
payload = "%s:%d" % (PLAYER, my_score)       # รูปแบบง่าย ๆ "ชื่อ:คะแนน"
mqtt.publish(TOPIC, payload)
print("ส่งคะแนนแล้ว ->", payload)
  • mqtt.connect มา หลัง WiFi เสมอ (:38) — ลำดับนี้ห้ามสลับ MQTT วิ่งบนเน็ตอีกชั้น เหมือนที่เราวาดในสไลด์วงจรชีวิต (wifi.connect → mqtt.connect → subscribe) ต่อ broker ก่อนได้ IP คือต่อไม่ติดแน่นอน
  • client_id=PLAYER เป็น keyword argument (:38) ที่ผูกชื่อบอร์ดเข้ากับ session ของ broker นี่คือเหตุผลที่ PLAYER ต้องไม่ซ้ำ — โยงกลับไปกับดัก client_id ซ้ำโดยตรง
  • ทำไม subscribe มา ก่อน publish (:44 ก่อน :49)? เพราะเราอยากได้ยินของทุกคน รวมทั้งของตัวเอง — จำสไลด์ "publish หนึ่งครั้ง วิ่งถึงทุกคน" ที่บอกว่า broker ส่งกลับให้ A เองด้วยไหม ถ้า subscribe ทีหลัง อาจพลาดข้อความที่ส่งมาช่วงต้น
  • payload = "%s:%d" % (PLAYER, my_score) (:48) คือ application protocol เล็ก ๆ ที่ทั้งห้องตกลงกัน รูปแบบ "ชื่อ:คะแนน" จากสไลด์ leaderboard — ฝั่งส่งประกอบ string ฝั่งรับ split(":") แยกกลับ ตกลงรูปแบบให้ตรงกันคือหัวใจของการคุยข้ามเครื่อง

subscribe ก่อน publish ไม่ใช่เรื่องบังเอิญ เราตั้งใจ "เข้าห้องไปนั่งฟัง" ให้เรียบร้อยก่อนค่อยพูด จะได้ไม่พลาดเสียงใคร รวมถึงเสียงของเราเอง

เฉลย · ส่วนที่สี่ — วนรับ แล้วเก็บกวาดให้เรียบร้อย

# --- 4) วนรับคะแนนของเพื่อน 15 วินาที ---
deadline = time.time() + 15
while time.time() < deadline:
    msg = mqtt.get_message()                 # คืน (topic, payload) เป็น bytes หรือ None
    if msg is not None:
        topic, data = msg
        print("ได้คะแนนใหม่:", data.decode())  # payload เป็น bytes ต้อง .decode()
    time.sleep(0.2)                          # พักนิดให้ระบบหายใจ

mqtt.disconnect()
print("จบคาบ ปิดการเชื่อมต่อเรียบร้อย")
  • get_message() เป็นแบบ ไม่บล็อก — ไม่มีของก็คืน None (:55-56) เราจึงวน while เช็กเรื่อย ๆ ได้โดยโปรแกรมไม่ค้าง นี่คือ non-blocking polling ที่เราย้ำในสไลด์ ก้าวที่ 2 และเป็นเหตุผลที่เกมจริงเอาโครงนี้ไปแทรกในลูปได้โดยเกมไม่กระตุก
  • topic, data = msg แกะ tuple ออกเป็นสองตัว แล้ว data.decode() (:57-58) เพราะ payload ที่วิ่งมาบนเน็ตเป็น bytes เสมอ ต้องแปลงเป็น str ก่อนอ่าน — จำกับดัก "แสดงเป็น b'...' เพราะลืม decode" ได้ไหม บรรทัดนี้แหละที่กัน
  • deadline = time.time() + 15 แล้ว while time.time() < deadline (:53-54) คือการ จำกัดเวลาแทนวนไม่รู้จบ — เดโมนี้ตั้งใจให้จบเองใน 15 วิ จะได้ทดสอบง่าย ในเกมจริงเราจะเปลี่ยน deadline เป็นเงื่อนไข "ยังเล่นอยู่" แทน
  • mqtt.disconnect() (:61) คือการเก็บกวาด คืนการเชื่อมต่อให้ broker อย่างเรียบร้อย ไม่ทิ้ง session ค้าง — นิสัยเดียวกับที่งาน embedded ต้องปิดของที่เปิดไว้เสมอ ออกจากงานให้เครื่องอยู่ในสถานะที่รู้แน่

ห้าส่วนนี้ไต่ระดับกันยังไง:

ส่วน บรรทัด แนวคิดใหม่ที่เพิ่มเข้ามา ของเดิมที่เอากลับมาใช้
ตั้งค่า :15-20 รวมค่าที่ต้องแก้ไว้ที่เดียว (config block) ตัวแปร + constant
ต่อ WiFi :25-34 retry loop + ธง ok + ด่าน raise SystemExit for ลูป + เช็ก True/False
ต่อ broker :38-44 ลำดับ wifi→mqtt, client_id, subscribe ก่อน publish ด่านกั้นเดิมของ WiFi
ส่งคะแนน :48-49 application protocol "%s:%d" (ชื่อ:คะแนน) string format
วนรับ :53-61 non-blocking poll + .decode() + จำกัดเวลา + disconnect while ลูป + guard เดิม

ทั้งไฟล์คือด่านกั้นเดิมซ้ำ ๆ แค่เปลี่ยนของที่กั้น: ต่อเน็ตติดไหม, ต่อ broker ติดไหม, มีข้อความไหม เห็นจังหวะไต่ระดับนี้แล้ว น้องจะเขียนโค้ดเน็ตของตัวเองได้โดยไม่ต้องท่อง

เชื่อมจุด — เน็ตวันนี้มาจากไหน แล้วจะพาเราไปถึงไหน

หยุดคิดสักครู่ก่อนปิดคาบ ไม่ใช่เรื่องไวยากรณ์ แต่เรื่องมองให้ทะลุว่า "ส่งคะแนนขึ้นกระดาน" ที่เราเพิ่งทำ จริง ๆ มันคือชิ้นส่วนของอะไร

ที่มา — เน็ตวันนี้ยืนอยู่บนอะไร ย้อนไปสไลด์ "publish / subscribe — ส่งครั้งเดียว ถึงทุกบอร์ด" เราคุยเรื่อง decouple: คนส่งกับคนรับไม่ต้องรู้จักกัน broker เป็นตัวกลาง และ get_message() ที่คืน None เมื่อไม่มีของ ก็คือแนวคิดเดียวกับ non-blocking ที่เราใช้วนเช็กปุ่มในเกมมาตั้งแต่คาบต้น ๆ — "วนถามเรื่อย ๆ โดยไม่ค้าง" ส่วนโครง for i in range(6) ที่วนต่อ WiFi ก็คือ for ลูปตัวเดิมที่เราใช้มาตั้งแต่คาบแรกสุด แค่เอามากั้นการต่อเน็ต

ที่ไป — เน็ตวันนี้จะโตเป็นอะไร ในสไลด์เช็กผ่านท้ายคาบ อาจารย์บอกว่าคาบ 14 คือโปรเจกต์ปิดคอร์ส สังเกตไหมว่า pattern "อ่านค่า → ประกอบ payload → publish → ฝั่งโน้น subscribe → decode → ใช้งาน" ไม่ได้ผูกกับคำว่าคะแนนเลย เปลี่ยน my_score เป็นค่าเซ็นเซอร์ เปลี่ยนปลายทางเป็นแดชบอร์ดคลาวด์ ก็กลายเป็นระบบ IoT จริงทันที นี่คือโครงเดียวที่อุปกรณ์ IoT ทุกตัวในโลกใช้

ลองตอบคำถามพวกนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:

  • จำ get_message() ที่คืน None เมื่อยังไม่มีของได้ไหม มันคือเรื่องเดียวกับตอนเราวนเช็กปุ่ม joystick ที่ "ไม่มีก็ผ่านไป" หรือเปล่า
  • ถ้าวันนี้เราส่ง "board-01:1250" ขึ้น topic ได้ พรุ่งนี้เราส่ง "sensor-3:27.5" (อุณหภูมิ) ขึ้นคลาวด์ ได้ไหม เปลี่ยนแค่ค่ากับปลายทาง โครงเดิมทั้งดุ้น
  • สังเกตไหมว่า "subscribe topic เดียวกันแล้วได้ยินกันหมด" คือเรื่องเดียวกับการเข้าห้องแชตห้องเดียวกัน — เปลี่ยนคำว่าคะแนนเป็นข้อความ ก็ได้แอปแชตย่อ ๆ
ที่มา for ลูป + non-blocking (วนเช็กปุ่ม/decouple) วันนี้ publish คะแนน + subscribe รับ (leaderboard ข้ามบอร์ด) ที่ไป เซ็นเซอร์ขึ้นคลาวด์ (โปรเจกต์คาบ 14 / IoT จริง)

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

ใช้จริงที่ไหน — WiFi + MQTT pub/sub

เทคนิควันนี้ไม่ใช่แค่เกม leaderboard มันคือโครงที่ระบบจริงทั่วโลกใช้อยู่ทุกวินาที:

บ้านอัจฉริยะ · smart home สวิตช์ · เซ็นเซอร์ · หลอดไฟ publish แอปกลาง subscribe เห็นครบ เซ็นเซอร์โรงงาน · IIoT 100+ ตัว broker topic เดียว · แดชบอร์ดเดียวเห็นครบ ติดตามยานพาหนะ · fleet GPS publish เมื่อขยับพอ — ไม่ส่งทุกวินาที แชต · presence ข้อความ 1 ส่งครั้งเดียว → ทุกคนที่ subscribe เห็น
  • บ้านอัจฉริยะ (Home Assistant / smart home) — สวิตช์ เซ็นเซอร์ หลอดไฟ ต่างก็ publish สถานะขึ้น topic ตัวเอง แล้วแอปกลาง subscribe เห็นครบ เพิ่มอุปกรณ์ใหม่ไม่ต้องแก้แอป — คือ decouple จากสไลด์ publish/subscribe เป๊ะ
  • เซ็นเซอร์ในโรงงาน (IIoT telemetry) — อุณหภูมิ/ความสั่นนับร้อยจุด publish ขึ้น broker ตัวเดียว แดชบอร์ดตัวเดียว subscribe ก็มอนิเตอร์ได้หมด — คือ "เซ็นเซอร์ 100 ตัว topic เดียว" ที่พูดถึงในสไลด์ publish โดยตรง
  • ติดตามยานพาหนะ/ขนส่ง (fleet tracking) — รถแต่ละคัน publish พิกัด GPS เป็นระยะ ส่ง เมื่อขยับพอควร ไม่ใช่ทุกเสี้ยววินาที — ตรงกับสไลด์ "ส่งคะแนนถี่แค่ไหนพอดี" ที่ให้ส่งเมื่อค่าเปลี่ยน
  • แอปแชต/สถานะออนไลน์ (presence) — คนในห้องแชตเดียวกันคือ subscriber ของ topic เดียวกัน ส่งครั้งเดียวถึงทุกคน — โครงเดียวกับ topic = "ห้อง" ที่เป็นหัวใจของคาบนี้

ไม่มีข้อไหนเป็นของสมมติเลย ทุกข้อคือ pub/sub บน MQTT ที่เดินอยู่จริงในผลิตภัณฑ์ระดับโลก น้องเพิ่งเขียนโครงเดียวกับมันไปเมื่อกี้นี้เอง

ต่อยอด — คิดต่อเอง

ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาโปรเจกต์จริงได้:

แบบเดิม — payload แบน "ชื่อ:คะแนน" "board-01:1250" ชื่อซ้ำเพิ่มบรรทัด · ปลอมง่าย · มีแค่คะแนน พอมีหลายบอร์ด/คนป่วน → กระดานเชื่อไม่ได้ เปลี่ยนวิธีคิด ต่อยอด — เก็บใน dict + ตรวจสอบ + ส่ง state dict คีย์=ชื่อ → กระดานสด ตรวจ payload → กันปลอม ส่ง state ทั้งเกม → ต่อคาบ 14 ออกแบบให้เชื่อถือได้ + ต่อยอดได้ ก่อนขยายจริง
  • กระดานเรียงอันดับสด — ถ้าเก็บคะแนนที่รับมาใส่ dict (คีย์ = ชื่อบอร์ด) แล้วอยากโชว์ Top 3 เรียงมากไปน้อย อัปเดตทุกครั้งที่มีคะแนนใหม่ จะออกแบบยังไงให้ชื่อซ้ำทับค่าเดิม ไม่ใช่เพิ่มบรรทัดใหม่เรื่อย ๆ
  • กันคะแนนปลอม — ถ้ามีคนแอบ publish "board-99:999999" มาป่วนกระดาน เราจะออกแบบ payload ให้ตรวจสอบได้ยังไง (ใบ้: เพิ่มฟิลด์ลายเซ็นง่าย ๆ หรือช่วงคะแนนที่เป็นไปได้) — โจทย์เดียวกับที่วิศวกรจริงต้องคิดเรื่องความน่าเชื่อถือของข้อมูล
  • เน็ตหลุดกลางเกมแล้วต่อกลับเอง — ตอนนี้ raise SystemExit เมื่อต่อไม่ติด แต่ถ้าเน็ตหลุดตอนเล่นอยู่ล่ะ จะเอา retry loop ของ WiFi (:25-30) มาห่อ mqtt.get_message ให้ต่อกลับเองโดยไม่รีสตาร์ตเกมยังไง
  • จากคะแนนเป็นสถานะทั้งเกม (สะพานสู่คาบ 14) — ถ้าโปรเจกต์ปิดคอร์สอยากให้สองบอร์ดเห็นกันเรียลไทม์ (เช่นตำแหน่งตัวละคร ไม่ใช่แค่คะแนนตอนจบ) จะออกแบบ payload กับอัตรา publish ยังไงให้ลื่นแต่ไม่ถล่ม broker — คิดไว้ เพราะคาบหน้าจะได้หยิบมาใช้จริง

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

fit-css

ภาพปลายทาง: คะแนนจากหลายบอร์ดขึ้นกระดานเดียวกันผ่าน MQTT

← Roadmap (TOC)