BENTO Game Console — Developer II

คาบ 11 — WiFi + MQTT: high-score board

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

ช่วง network / IoT · 3 คน/บอร์ด

เป้าหมายวันนี้ (today's win)

วันนี้เราจะทำให้คะแนนจากเกมเดินทางได้ 2 ทาง:

  1. ส่งออก (publish) — เล่น Snake/Flappy จบ ได้คะแนนเท่าไร ส่งขึ้น broker ด้วย mqtt.publish
  2. รับเข้า (subscribe + get_message) — เห็นคะแนนของเพื่อนอีกบอร์ดวิ่งเข้ามาที่หน้าจอเรา

ปลายทางที่อยากเห็นตอนจบคาบ: สองบอร์ดต่อ WiFi เดียวกัน ใช้ topic เดียวกัน แล้วคะแนนของกันและกันโผล่ขึ้นมาจริง ๆ

คาถาประจำคาบ: "WiFi คือถนน, MQTT คือไปรษณีย์ — เราแค่ฝากจดหมายชื่อว่า 'คะแนน' ขึ้นรถไปส่ง"

ภาพรวม: คะแนนเดินทางจากบอร์ดสู่กระดานอย่างไร

บอร์ด A Snake: 1250 MQTT Broker TOPIC: tesaiot/class/leaderboard บอร์ด B Flappy: 17 publish get_message()

ทุกบอร์ดที่ subscribe topic เดียวกัน จะได้รับทุกข้อความที่ publish เข้า topic นั้น นี่คือหัวใจของ pub/sub: ผู้ส่งกับผู้รับไม่ต้องรู้จักกันตรง ๆ broker เป็นคนกระจายให้

ทำไม pub/sub ถึงทรงพลัง: ผู้ส่งไม่ต้องรู้จักผู้รับ

จุดที่ต่างจากการต่อสายตรงสองเครื่อง: บอร์ดเรา ไม่ได้ส่งถึงบอร์ด B/C/D เราแค่ส่ง "เข้า topic" แล้ว broker เป็นคนกระจายให้ทุกคนที่สมัครไว้ จะมีผู้รับ 1 บอร์ดหรือ 30 บอร์ด โค้ดฝั่งส่งก็เหมือนเดิม

นี่เรียกว่า decoupling (แยกผู้ส่งออกจากผู้รับ) เป็นแนวคิดที่ใช้ซ้ำในงาน IoT จริงทั้งหมด ตั้งแต่เซนเซอร์ในโรงงานไปจนถึงรถยนต์ที่ส่งสถานะขึ้นคลาวด์

Pub Sub Model | MQTT Essentials Part 3 — HiveMQ — อธิบายโมเดล publish/subscribe + บทบาทของ broker สั้น ๆ ตรงกับที่เราทำคาบนี้

ภาพ MQTT จากของจริง: หนึ่ง broker หลายผู้ฟัง

ตัวอย่างจริงนอกห้องเรียน: ปลั๊กไฟ เซนเซอร์อุณหภูมิ มือถือ คอมพิวเตอร์ — ทุกอย่าง publish/subscribe ผ่าน broker ตัวเดียว แต่ละ "เรื่อง" คือ topic แยกกัน (เช่น /plug1/voltage, /thermo/temperature)


ที่มา: "MQTT single broker multiple listener" — Ademant, CC BY-SA 4.0, Wikimedia Commons

leaderboard ของเราคือเวอร์ชันจิ๋วของภาพนี้: หลายบอร์ดคุยกันผ่าน broker ตัวเดียว ด้วย topic เดียว ส่วนเครื่องหมาย # / + ในภาพคือ wildcard ของ topic — เก็บไว้ลองตอนน้องอยากรับหลาย topic พร้อมกัน

4 ขั้นของคาบนี้ (ตรงกับ s12_net.py)

1) WiFi wifi.connect wifi.ip() 2) MQTT mqtt.connect mqtt.subscribe 3) ส่งคะแนน mqtt.publish "ชื่อ:คะแนน" 4) รับคะแนน mqtt.get_message .decode()

ในไฟล์ฝึก practise_codes/s12_net.py มีช่องเว้นไว้ให้น้อง ๆ เติม 4 จุด ตรงกับ 4 ขั้นนี้พอดี อาจารย์เขียนโครงรอบ ๆ ให้แล้ว เราเติมเฉพาะบรรทัดสำคัญ

ขั้นที่ 1 — ต่อ WiFi ก่อนเสมอ

ก่อนจะคุย MQTT ได้ บอร์ดต้องมีถนนวิ่งก่อน นั่นคือ WiFi wifi.connect คืน True เมื่อต่อสำเร็จ:

    import wifi

    SSID     = "YOUR_WIFI_SSID"          # ชื่อ WiFi (2.4GHz) ของห้องเรา
    PASSWORD = "YOUR_WIFI_PASSWORD"      # รหัส WiFi

    print("กำลังต่อ WiFi ...")
    if not wifi.connect(SSID, PASSWORD):     # คืน True เมื่อต่อสำเร็จ
        print("ต่อ WiFi ไม่ได้ ลองเช็ค SSID/รหัสอีกที")
        raise SystemExit                     # ต่อไม่ได้ก็ไม่ต้องไปต่อ
    print("ต่อ WiFi แล้ว IP =", wifi.ip())   # โชว์ IP ที่ได้มา

SSID/PASSWORD ตรงนี้เป็นแค่ตัวอย่าง (placeholder) ก่อนรันจริงในห้อง น้อง ๆ ต้องแก้ให้ตรงกับ WiFi ที่อาจารย์เตรียมไว้ และต้องเป็นคลื่น 2.4GHz เท่านั้น

ตรรกะของบล็อกนี้มีแค่ 2 ปลายทาง: ต่อผ่าน → ไปต่อ, ต่อไม่ผ่าน → raise SystemExit หยุดทันที

DISCONNECTED เพิ่งบูต ยังไม่มีเน็ต CONNECTED มี IP แล้ว ไปต่อ MQTT SystemExit หยุดโปรแกรม connect()=True connect()=False → raise SystemExit

ต่อสำเร็จ serial console จะพิมพ์ IP ที่ได้มา — นี่คือสถานะ "เน็ตพร้อม" ที่เราอยากเห็นก่อนไปขั้นต่อไป:

ถ้าต่อ WiFi ไม่ผ่าน ก็ raise SystemExit หยุดไปเลย ดีกว่าปล่อยให้โค้ดวิ่งต่อแล้วพังตรง MQTT แบบงง ๆ

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

เข้าใจฮาร์ดแวร์ · WiFi + MQTT (ทบทวน + ลึกขึ้น)

ทบทวน: ชิป PSoC Edge ไม่มีวิทยุในตัว → ใช้ชิป AIROC CYW55513 แยก · MQTT broker = ตัวกลาง publish / subscribe


ขวา: ตัวอย่างโมดูลวิทยุ WiFi + Bluetooth จริง (กล่องโลหะ = ตัว shield คลื่น, ลายทองแดงข้าง ๆ = เสาอากาศบน PCB) — CYW55513 บนบอร์ดเราเป็นชิปประเภทเดียวกัน · ที่มา: "Espressif ESP-WROOM-32 Wi-Fi & Bluetooth Module" — Brian Krent, CC BY-SA 4.0, Wikimedia Commons

  • client_id ต้องไม่ซ้ำ — broker แยกแต่ละบอร์ดด้วย id ถ้าซ้ำจะเตะกันหลุด
  • payload ที่ได้จาก MQTT เป็น bytes → ต้อง .decode() เป็น str หรือแปลงเป็นตัวเลขก่อนใช้
  • หลายบอร์ด subscribe topic เดียวกัน = ได้ข้อความพร้อมกันหมด

Developer I ต่อ WiFi เป็นแล้ว — Developer II เพิ่มความระวัง: id ไม่ซ้ำ + decode payload ก่อนใช้

ขั้นที่ 2 — ต่อ MQTT broker แล้ว subscribe

พอมีถนนแล้ว ก็ไปที่ทำการไปรษณีย์ (broker) พอร์ต MQTT มาตรฐานคือ 1883:

    import mqtt

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

    print("กำลังต่อ MQTT broker ...")
    if not mqtt.connect(BROKER, 1883, client_id=PLAYER):   # คืน True เมื่อสำเร็จ
        print("ต่อ broker ไม่ได้")
        raise SystemExit
    print("ต่อ broker แล้ว")

    mqtt.subscribe(TOPIC)                    # สมัครรับทุกข้อความใน topic นี้

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

ขั้นที่ 3 — ส่งคะแนนเกมขึ้นกระดาน (publish)

หัวใจของคาบ: เล่น Snake/Flappy จบ ได้ my_score มา แล้วห่อเป็นข้อความสั้น ๆ ส่งขึ้นไป

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

รูปแบบข้อความ (payload) ที่เราตกลงกันทั้งห้อง:

payload=PLAYERชื่อบอร์ด   :   my_scoreคะแนน"board-01:1250"\text{payload} = \underbrace{\text{PLAYER}}_{\text{ชื่อบอร์ด}}\;\text{ : }\;\underbrace{\text{my\_score}}_{\text{คะแนน}} \quad\Rightarrow\quad \texttt{"board-01:1250"}

ทำไมต้องตกลงรูปแบบให้ตรงกัน เพราะฝั่งที่รับต้อง split(":") แยกชื่อกับคะแนนออกมา ถ้ารูปแบบไม่ตรง อ่านไม่ออก นี่คือบทเรียนแรกของ "โปรโตคอลข้อความ" ในงาน IoT

ขั้นที่ 4 — วนรับคะแนนของเพื่อน (get_message)

mqtt.get_message() คืน (topic, payload) เป็น bytes เมื่อมีข้อความใหม่ หรือคืน None เมื่อยังไม่มี:

    import time

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

    mqtt.disconnect()                            # จบคาบ ปิดการเชื่อมต่อให้เรียบร้อย

จุดที่นิสิตพลาดบ่อย: ลืม .decode() แล้วจะเห็น b'board-02:980' มี b' นำหน้า เพราะ MQTT ส่งมาเป็น bytes ไม่ใช่ str เราต้องแปลงเองเสมอ

เห็นภาพการเดินทางของข้อความทีละจังหวะ

โค้ด publish หนึ่งบรรทัดที่บอร์ดเรา กลายเป็นข้อความที่โผล่บนหลายบอร์ดได้อย่างไร ลองดูจังหวะของมัน:

  1. บอร์ด A publish ข้อความ ครั้งเดียว เข้า topic
  2. broker รับไว้ แล้วทำสำเนากระจายให้ ทุกบอร์ดที่ subscribe พร้อมกัน
  3. บอร์ด B/C/D เรียก get_message() แล้วได้คะแนนเดียวกันมาแสดง

สังเกตว่าบอร์ด A ทำงานแค่ "ส่งเข้า topic" ที่เหลือ broker จัดการเอง น้องไม่ต้องเขียนโค้ดวนส่งไปทีละบอร์ดเลย นี่คือเหตุผลที่ pub/sub สเกลขึ้นง่าย

ทั้งโปรแกรม s12_net.py ในภาพเดียว — connect → publish → รับ

4 ขั้นที่แยกกันบนสไลด์ก่อน ประกอบเป็นเส้นทางเดียวของข้อความ: แถวบน = ต่อให้พร้อม (WiFi → broker → publish ครั้งเดียว) · แถวล่าง = วนรับ 15 วิ แล้วปิดการเชื่อมต่อ

START:22 wifi.connect() ×≤6:25-30 WiFi ok? SystemExit:31-33 mqtt.connect(1883):38 broker ok? SystemExit:39-40 subscribe + publish คะแนน:44 · :48-50 ใช่ ไม่ ใช่ ไม่ เข้าลูปรับ 15 วิdeadline :53 time <deadline? get_message():55 มีข้อความ? print data.decode():57-58 sleep(0.2):59 disconnect() · END :61-62 ใช่ ใช่ ไม่ วนกลับ (พัก 0.2 วิ) ไม่ (หมดเวลา) → ปิดการเชื่อมต่อ

สัญกรณ์: ▭ฟ้า = ประมวลผล · ▭เขียว = ส่ง/รับข้อความ · ◇ม่วง = ตัดสินใจ · ▭เทามน = เริ่ม/จบ · เส้นประ = วนกลับ

ยึดตามไฟล์เฉลย: ใน solution_codes/s12_net.py การต่อ WiFi ห่อด้วยลูป ลองซ้ำสูงสุด 6 ครั้ง (:25-30, พัก 2 วิ/ครั้ง) — สไลด์ขั้นที่ 1 ย่อเป็นครั้งเดียวเพื่อความกระชับ ผังนี้จึงเขียน wifi.connect() ×≤6 ตามของจริง · โปรโตคอลข้อความ "PLAYER:score":48; ฝั่งรับ data.decode() แล้วค่อย split(":"):58

เอาไปต่อกับเกมจริง: เรียกตอน GAME OVER

ในเกมของเรา (Snake/Flappy) จุดที่ควรส่งคะแนนคือ "ตอนจบรอบ" ไม่ใช่ทุกเฟรม วางสาย publish ไว้ตรงที่เกมจบพอดี:

    # --- ในเกม Snake/Flappy: เมื่อจบรอบ (GAME OVER) ---
    if game_over:
        payload = "%s:%d" % (PLAYER, score)      # score = คะแนนรอบนี้
        mqtt.publish(TOPIC, payload)             # ส่งครั้งเดียวตอนจบ
        print("ส่งคะแนนขึ้นกระดานแล้ว:", payload)

เหตุผลที่ส่งครั้งเดียวตอนจบ: คะแนนเปลี่ยนทุกเฟรมก็จริง แต่กระดานสนใจแค่ "คะแนนสุดท้าย" การส่งทุกเฟรมจะถล่ม broker โดยเปล่าประโยชน์ — ส่งเท่าที่จำเป็นคือมารยาทของงานเครือข่าย

ส่งทุกเฟรม vs ส่งตอนจบ — ต่างกันแค่ไหน

ลองคิดเป็นตัวเลข เกมหนึ่งรอบเล่นนาน 10 วินาทีที่ 50 fps ถ้าเรา publish ทุกเฟรม จำนวนข้อความคือ:

Nทุกเฟรม=fps×วินาที=50×10=500ข้อความ/รอบN_{\text{ทุกเฟรม}} = \text{fps} \times \text{วินาที} = 50 \times 10 = 500 \quad\text{ข้อความ/รอบ}

Nตอนจบ=1ข้อความ/รอบNทุกเฟรมNตอนจบ=500×N_{\text{ตอนจบ}} = 1 \quad\text{ข้อความ/รอบ} \qquad\Rightarrow\qquad \frac{N_{\text{ทุกเฟรม}}}{N_{\text{ตอนจบ}}} = 500\times

ข้อมูลที่มีค่าจริง ๆ คือ "คะแนนสุดท้าย" แค่ตัวเดียว อีก 499 ข้อความเป็นภาระเปล่า ๆ ที่ broker กับ WiFi ต้องแบกไปฟรี ๆ การเลือก "ส่งเท่าที่จำเป็น" คือหัวใจของการออกแบบระบบเครือข่ายที่ดี

วิธีรัน: เปิดใน BENTO IDE แล้วกด Program to Device

ไฟล์เน็ตตัวนี้รันบนบอร์ดเหมือนทุกคาบที่ผ่านมา ขั้นตอนเดิม:

  1. เปิด s12_net.py ใน BENTO IDE
  2. แก้ SSID / PASSWORD / PLAYER ให้ตรงของจริง
  3. กด Program to Device แล้วดูผลใน serial console

เราไม่ใช้ exec(open(...)) ในการรันนะ การรันเกม/โปรแกรมบนบอร์ดของเราคือกด Program to Device เสมอ วิธีนี้เฟิร์มแวร์จะจัดการโหลดไฟล์ลงบอร์ดให้ถูกต้องเอง

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

อาการ สาเหตุ วิธีแก้
ต่อ WiFi ไม่ผ่าน SSID/รหัสผิด หรือใช้ 5GHz ใช้ 2.4GHz, พิมพ์ SSID/รหัสให้เป๊ะ
ต่อ broker ไม่ได้ WiFi ยังไม่พร้อม / พอร์ตผิด ต่อ WiFi ให้ผ่านก่อน, พอร์ต 1883
get_message() คืน None ตลอด topic ไม่ตรงกันสองบอร์ด TOPIC ต้องสะกดเหมือนกันเป๊ะ
ขึ้น b'...' หน้าข้อความ ลืม .decode() payload เป็น bytes ต้อง .decode()
เกมกระตุก ส่งช้า publish ทุกเฟรม ส่งครั้งเดียวตอน GAME OVER
คะแนนเพื่อนทับชื่อกัน PLAYER ซ้ำกัน แต่ละบอร์ดตั้งชื่อไม่ซ้ำ

ตัวที่เจอบ่อยสุดคือลืม .decode() — สังเกตว่า output ฝั่งซ้ายมี b' นำหน้า (ยังเป็น bytes อยู่) พอใส่ .decode() ก็กลายเป็น str สะอาด ๆ ที่ split(":") ต่อได้:

เชื่อมโยงรากฐาน — leaderboard นี้คือวิศวกรรม IoT จริง

โค้ดส่งคะแนนที่น้องเพิ่งเขียน ไม่ใช่ของเล่นแยกจากงานจริง มันคือรากฐานเดียวกับที่ใช้ทำผลิตภัณฑ์ IoT:

Embedded · วิทยุจริง
PSoC Edge ไม่มีวิทยุในตัว ต้องพึ่งชิป CYW55513 แยก คุยผ่านบัส SDHC — การ "ต่อ WiFi" คือการสั่งฮาร์ดแวร์วิทยุจริง ๆ
Python · bytes vs str
payload ที่ได้จาก MQTT เป็น bytes ต้อง .decode() เป็น str ก่อน นี่คือเรื่อง type จริงที่เจอทุกครั้งที่อ่านข้อมูลจากเครือข่าย/ไฟล์/เซนเซอร์
Algorithms · โปรโตคอล
การตกลงรูปแบบ "ชื่อ:คะแนน" แล้ว split(":") ฝั่งรับ คือ message protocol แบบย่อ — หลักเดียวกับ JSON/Protobuf ในระบบจริง
และโมเดล publish/subscribe ที่ broker แยกผู้ส่งออกจากผู้รับ (decoupling) คือสถาปัตยกรรมเดียวกับที่โรงงาน รถยนต์ และ smart home ใช้ส่งข้อมูลขึ้นคลาวด์ — ต่างกันแค่ payload กับ topic เท่านั้น
Embedded · วิทยุจริง ต่อ WiFi = สั่งชิป CYW Python · bytes→str .decode() ก่อนใช้ Algorithms · protocol "ชื่อ:คะแนน" + split pub/sub broker decoupling ผลิตภัณฑ์ IoT จริง โรงงาน·รถยนต์·smart home→cloud เปลี่ยนแค่ payload/topic


ลำโพงอัจฉริยะ/smart-home hub ก็ "ต่อ WiFi แล้ว publish/subscribe ขึ้นคลาวด์" ด้วยสถาปัตยกรรมเดียวกับ leaderboard ของเรา · ที่มา: "Google Home Hub on table" — Y2kcrazyjoker4, CC BY-SA 4.0, Wikimedia Commons

เป้าหมายของคอร์สนี้คือ พอน้องส่งคะแนนเกมขึ้นกระดานเป็น น้องก็ส่งค่าเซนเซอร์ของผลิตภัณฑ์จริงขึ้นคลาวด์เป็น — ด้วยโครงสร้างก้อนเดียวกัน เล่นเป็น แล้วต่อยอดไปสร้างของจริงได้

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

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

  1. เติมครบ 4 ช่อง ใน practise_codes/s12_net.py — เดี๋ยวเฉลยพร้อมกันในห้อง
  2. ต่อ WiFi ผ่าน เห็น IP, ต่อ broker ผ่าน, publish คะแนนของตัวเองขึ้น topic ได้
  3. สองบอร์ดในกลุ่ม เห็นคะแนนของกันและกัน วิ่งเข้ามาใน serial (decode ถูก)
  4. oral review — อธิบายได้ว่า publish/subscribe ต่างกันยังไง และทำไมต้อง .decode()

ทำเอง 30% (เลือกอย่างน้อย 1):

  • เก็บคะแนนสูงสุดที่รับมาไว้ใน best = {} (dict ชื่อ→คะแนน) แล้ว print กระดานเรียงจากมากไปน้อย
  • เอา mqtt.publish ไปวางจริงตอน GAME OVER ใน Snake หรือ Flappy ของกลุ่ม (full_games/)
  • ห่อ publish/get_message ด้วย try/except ให้เน็ตล่มแล้วเกมไม่ crash

ส่งงาน: branch feat/leaderboard → commit ไฟล์ทีม → PR (Action py_compile เขียว)

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

เฉลยตัวนี้มีไว้ให้ เทียบ ไม่ใช่ให้ลอกวางส่ง คะแนนคาบนี้อยู่ที่ใบงานกับการที่น้องอธิบาย publish/subscribe ด้วยคำของตัวเองได้ ไม่ใช่ที่โค้ดตรงเฉลยเป๊ะ วิธีที่ได้ผลจริงคือ อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือ ตอนพิมพ์เองนั่นแหละที่สมองจำ 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 เป็นแค่ placeholder และต้องเป็นคลื่น 2.4GHz เท่านั้น ตรงกับตารางกับดักที่เราดูไปแล้ว (ต่อ 5GHz จะไม่ติด)
  • BROKER กับ TOPIC (:17-18) คือ "ที่ทำการไปรษณีย์" กับ "ชื่อกล่องจดหมาย" จากสไลด์ภาพรวม ทั้งห้องต้องใช้ TOPIC สะกดเหมือนกันเป๊ะ ไม่งั้น get_message() จะคืน None ตลอด
  • PLAYER (:20) ทำหน้าที่สองอย่างพร้อมกัน: เป็นชื่อในกระดาน และเดี๋ยวถูกส่งเป็น client_id ให้ broker แยกบอร์ด ตรงกับกฎ "id ต้องไม่ซ้ำ" ในสไลด์ฮาร์ดแวร์ ซ้ำเมื่อไรบอร์ดเตะกันหลุด

ค่าตั้งที่ต้องแก้บ่อย ให้มันอยู่ที่เดียวบนหัวไฟล์เสมอ วันหลังน้องจะขอบคุณตัวเองที่ไม่ต้องไล่หามันกลางโค้ด

เฉลย · ก้อนที่สอง — ต่อ WiFi แบบ "ลองซ้ำแล้วค่อยยอมแพ้"

บนสไลด์ขั้นที่ 1 อาจารย์ย่อการต่อ 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 ที่ได้มา
  • ตัวแปร ok เริ่มที่ False (:24) แล้วพลิกเป็น True กับ break เฉพาะตอนต่อสำเร็จ (:26-28) นี่คือ pattern "flag + ลูป" มาตรฐาน ต่อได้เมื่อไรออกจากลูปทันที ไม่ลองต่อทิ้งเปล่า
  • ทำไม range(6) ไม่ใช่ลองครั้งเดียว? เพราะการปลุกชิปวิทยุ (WCM) ใช้เวลาตั้งตัว time.sleep(2) (:30) คั่นให้มันหายใจ ลองรวดเดียวมักพลาดทั้งที่ SSID ถูก นี่คือความจริงของฮาร์ดแวร์ที่โค้ดบนกระดาษไม่เห็น
  • ถ้าครบ 6 ครั้งยังไม่ติด if not ok: จะ raise SystemExit (:31-33) หยุดตรงนี้เลย ดีกว่าปล่อยให้ไหลไปพังตรง MQTT แบบงง ตรงกับคาถา "ต่อ WiFi ไม่ผ่านก็ไม่ต้องไปต่อ"
  • นี่คือ state machine ตัวเดียวกับผังในสไลด์ "ทั้งโปรแกรมในภาพเดียว" ที่เขียนว่า wifi.connect() ×≤6 — โค้ดก้อนนี้คือกล่องนั้นแบบเต็ม

เขียนโค้ดคุยกับฮาร์ดแวร์ ให้เผื่อ "ครั้งแรกมันยังไม่พร้อม" ไว้เสมอ ลองซ้ำอย่างมีขีดจำกัด แล้วถ้าหมดโควตาค่อยยอมแพ้อย่างมีศักดิ์ศรี ด้วย raise SystemExit

เฉลย · ก้อนที่สาม — เข้าไปรษณีย์ แล้วห่อคะแนนเป็นข้อความ

พอมีถนน (WiFi) แล้ว ก็ต่อ 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(BROKER, 1883, client_id=PLAYER) (:38) ใช้ guard แบบเดียวกับ WiFi เป๊ะ ต่อไม่ได้ก็ raise SystemExit พอร์ต 1883 คือพอร์ต MQTT มาตรฐาน และ client_id=PLAYER คือเหตุผลที่ชื่อบอร์ดต้องไม่ซ้ำ
  • mqtt.subscribe(TOPIC) (:44) วางไว้ ก่อน publish จงใจ สมัครรับก่อนแล้วค่อยส่ง จะได้ไม่พลาดคะแนนของเพื่อนที่เข้ามาระหว่างนั้น นี่คือ pub/sub decoupling จากสไลด์ "ผู้ส่งไม่ต้องรู้จักผู้รับ": เราคุยกับ topic ไม่ได้คุยกับบอร์ดใครโดยตรง
  • payload = "%s:%d" % (PLAYER, my_score) (:48) คือหัวใจ เราตกลง โปรโตคอลข้อความ ว่า "ชื่อ:คะแนน" ทั้งห้อง เพราะฝั่งรับต้อง split(":") แยกคืน ถ้ารูปแบบไม่ตรงกัน อ่านไม่ออกทันที
  • my_score = 1250 เป็นแค่ค่าจำลอง (:47) ในเกมจริงตรงนี้คือคะแนนตอน GAME OVER ตามสไลด์ "เรียกตอน GAME OVER" ส่งครั้งเดียวพอ

ก่อนสองเครื่องจะคุยกันรู้เรื่อง ต้องตกลง "รูปแบบข้อความ" ให้ตรงกันก่อนเสมอ "ชื่อ:คะแนน" วันนี้คือ JSON/Protobuf เวอร์ชันจิ๋วในระบบจริงพรุ่งนี้

เฉลย · ก้อนที่สี่ + ภาพประกอบทั้งไฟล์

ปิดท้ายด้วยลูปฟังคะแนนเพื่อน แล้วเก็บกวาดปิดการเชื่อมต่อให้เรียบร้อย:

# --- 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("จบคาบ ปิดการเชื่อมต่อเรียบร้อย")
  • deadline = time.time() + 15 (:53) แล้ววน while time.time() < deadline คือการ "จับเวลาแทนการนับรอบ" ฟังไป 15 วิแล้วออกเอง ไม่ค้างตลอดกาล
  • if msg is not None: (:56) สำคัญมาก เพราะ get_message() คืน None เมื่อยังไม่มีข้อความ เช็ก None ก่อนค่อยแกะ ไม่งั้น unpack topic, data = msg จะพัง
  • data.decode() (:58) คือกับดักที่นิสิตพลาดบ่อยสุด payload มาเป็น bytes เห็น b'...' นำหน้า ต้อง .decode() เป็น str ก่อน split(":") ตรงกับสไลด์ before/after
  • mqtt.disconnect() (:61) ปิดการเชื่อมต่อตอนจบ คืนบอร์ดสู่สถานะที่รู้แน่ (known state) เป็นนิสัยเก็บกวาดจาก Developer I: ออกจากงานยังไง ทิ้งเครื่องไว้เรียบร้อยแบบนั้น

สี่ก้อนนี้ต่อกันเป็นเส้นเดียว แต่ละก้อนพึ่งก้อนก่อนหน้า:

ก้อน บรรทัด แนวคิดใหม่ที่เพิ่ม ประกอบต่อจากก้อนก่อน
1 WiFi :22-34 ต่อวิทยุจริง + ลองซ้ำ ×6 + guard for/if/flag ที่เคยเขียนมาแล้ว
2 MQTT :36-44 client_id + พอร์ต 1883 + subscribe guard แบบเดียวกับ WiFi ก้อน 1
3 publish :46-50 โปรโตคอลข้อความ "%s:%d" ต้องมี broker จากก้อน 2 ก่อนถึงส่งได้
4 receive :52-62 ลูปจับเวลา + None-check + .decode() + cleanup ฟังจาก TOPIC เดียวกับที่ subscribe ก้อน 2

อ่านตารางนี้จากบนลงล่างจะเห็นว่า งานเน็ตไม่ใช่เวทมนตร์ก้อนเดียว มันคือสี่ก้อนเล็กที่ต่อกัน แต่ละก้อนเปิดทางให้ก้อนถัดไป ถ้าก้อน 1 ไม่ผ่าน ก้อน 2-4 ก็ไม่มีความหมาย จับลำดับนี้ให้ได้ก่อนพิมพ์เอง

เชื่อมจุด — publish คะแนนวันนี้ มันคือเมล็ดของอะไร

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

ที่มา — บรรทัดนี้ยืนอยู่บนอะไร ย้อนไปสไลด์ "ทำไม pub/sub ถึงทรงพลัง: ผู้ส่งไม่ต้องรู้จักผู้รับ" กับสไลด์ภาพรวม ที่คะแนนเดินทางผ่าน broker แทนที่จะลากสายตรงหากันทีละบอร์ด ปัญหาที่ pub/sub เกิดมาแก้แต่แรกคือ "จะให้ผู้ส่งหนึ่งคน คุยกับผู้รับกี่คนก็ได้ โดยไม่ต้องรู้จักกัน" ได้ยังไง คำตอบคือส่ง "เข้า topic" ไม่ใช่ส่ง "ถึงใคร" และ Developer I ที่น้องต่อ WiFi เป็นแล้ว คือถนนที่ปูรอไว้ให้ก้อนนี้พอดี

ที่ไป — บรรทัดนี้จะโตเป็นอะไร ทวนสไลด์ "leaderboard นี้คือวิศวกรรม IoT จริง" ที่อาจารย์บอกว่า พอส่งคะแนนขึ้นกระดานเป็น น้องก็ส่งค่าเซนเซอร์ขึ้นคลาวด์เป็น ด้วยโครงก้อนเดียวกัน เปลี่ยนแค่ payload กับ TOPIC และคาบหน้าเป็น Capstone ออกแบบเกมเอง จุดที่ leaderboard จะกลับมาคือ "ฝังตอน GAME OVER ในเกมของกลุ่ม"

ที่มา pub/sub · ต่อ WiFi เป็น (Developer I + สไลด์ภาพรวม) วันนี้ publish "ชื่อ:คะแนน" เข้า topic (leaderboard) ที่ไป เซนเซอร์→คลาวด์ · เกม Capstone (ผลิตภัณฑ์ IoT จริง)

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

  • จำสไลด์ "ผู้ส่งไม่ต้องรู้จักผู้รับ" ได้ไหม ทำไมบอร์ดเราถึง publish เข้า TOPIC แทนที่จะส่งตรงไปหาบอร์ด B/C/D ทีละตัว? (ใบ้: ถ้าห้องมี 30 บอร์ด โค้ดฝั่งส่งของเราต้องเปลี่ยนไหม)
  • ถ้าวันนี้เราส่ง "board-01:1250" ขึ้น topic ได้ พรุ่งนี้เราส่ง "livingroom:27.5" (อุณหภูมิห้อง) ขึ้น topic เดียวกันได้ไหม โครงโค้ดต้องเปลี่ยนตรงไหนบ้าง? (ใบ้: แค่ payload กับ TOPIC)
  • สังเกตไหมว่า data.decode() ตอนรับ MQTT มันคือเรื่องเดียวกับตอนอ่านค่าดิบจากเซนเซอร์แล้วต้องแปลง type ก่อนใช้? bytes → str วันนี้ กับ raw → ค่าที่อ่านได้ คือหลักเดียวกัน

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

ใช้จริงที่ไหน — pub/sub · โปรโตคอลข้อความ · publish-ตอนจำเป็น

เทคนิควันนี้ไม่ใช่แค่แบบฝึกในห้อง มันคือสิ่งที่ระบบจริงข้างนอกใช้กันทุกวัน:

เซนเซอร์โรงงาน/ฟาร์ม → คลาวด์ เซนเซอร์ broker/cloud publish เป็นช่วง ๆ — ส่งเท่าที่มีค่าจริง บ้านอัจฉริยะ · topic เป็นชั้น home/ home/livingroom/light home/kitchen/temp แต่ละอุปกรณ์มี topic ของตัวเอง เกมหลายผู้เล่น · broker กระจายให้ทุกเครื่อง publish 1 ครั้ง broker ผู้เล่น A ผู้เล่น B ผู้เล่น C กองรถ · client_id ต้องไม่ซ้ำ รถ · car-01 รถ · car-02 รถ · car-03 ศูนย์กลาง GPS
  • เซนเซอร์โรงงาน/ฟาร์มส่งค่าขึ้นคลาวด์ — วัดอุณหภูมิ/ความชื้น/การสั่น publish ขึ้น broker เป็นช่วง ๆ ตรงกับหลัก "publish ตอน GAME OVER ไม่ใช่ทุกเฟรม" — ส่งเท่าที่มีค่าจริง ไม่ถล่ม broker
  • บ้านอัจฉริยะ (Home Assistant / MQTT) — ปลั๊ก หลอดไฟ เซนเซอร์ประตู แต่ละอย่างมี topic ของตัวเอง เช่น home/livingroom/light ตรงกับหลัก "ทั้งห้องตกลง TOPIC ให้ตรงกัน" — ต่างแค่ตั้งชื่อ topic เป็นชั้น ๆ
  • เกมหลายผู้เล่น / สถานะเรียลไทม์ — ตำแหน่งผู้เล่น คะแนน สถานะห้อง sync ผ่าน broker กลาง ทุกเครื่อง subscribe เห็นพร้อมกัน คือ pub/sub decoupling ตัวเดียวกับ leaderboard ขยายขนาด
  • กองรถ / โลจิสติกส์ติดตามพิกัด — รถแต่ละคัน publish GPS ด้วย client_id ไม่ซ้ำ ศูนย์กลาง subscribe ดูทั้งกอง ตรงกับกฎ "id ต้องไม่ซ้ำ" — ซ้ำเมื่อไรเตะกันหลุด งานจริงคือเรื่องคอขาดบาดตาย

สี่อย่างข้างบนไม่มีอันไหนเป็นของสมมติเลย ทุกอันวางอยู่บนสามท่าเดิมของเราวันนี้ pub/sub · โปรโตคอลข้อความ · ส่งเท่าที่จำเป็น เปลี่ยนแค่ payload กับ topic เท่านั้นเอง

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

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

แบบแบน — split(":") อ่านตามตำแหน่ง board-01 : 1250 +ฟิลด์ board-01:1250:42:3 เพิ่ม time/level → ตำแหน่งเลื่อน ฝั่งรับนับผิดง่าย เปลี่ยนวิธีคิด: อ่านตามชื่อ ไม่ใช่ตำแหน่ง แบบมีคีย์ — JSON อ่านตามชื่อฟิลด์ {"name":"board-01","score":1250,"time":42,"level":3} เพิ่มฟิลด์ใหม่ได้ ฝั่งเดิมไม่พัง — อ่านด้วยคีย์ ไม่ใช่ตำแหน่ง
  • ถ้าต้องส่งมากกว่าคะแนน — อยากส่งเวลา ด่านที่ถึง และชื่อจริงในข้อความเดียว รูปแบบ "ชื่อ:คะแนน" จะพอไหม ออกแบบ payload ใหม่ยังไงให้ฝั่งรับแยกคืนง่าย (ลองมองไปทาง JSON)
  • ถ้าเน็ตหลุดกลางเกม — WiFi หลุดตอนกำลังจะ publish จะกันเกมไม่ให้ crash ยังไง เก็บคะแนนที่ค้างไว้ส่งซ้ำตอนเน็ตกลับได้ไหม (ใบ้: try/except + คิวเก็บคะแนน ตรงกับงาน 30%)
  • ถ้ามี 30 บอร์ดยิงคะแนนพร้อมกัน — กระดานจะรกมาก จะกรอง/จัดอันดับให้เหลือ Top 5 ยังไง เก็บใน best = {} (ชื่อ→คะแนนสูงสุด) แล้วเรียงยังไงไม่ต้องคำนวณใหม่ทุกข้อความ
  • สะพานสู่คาบหน้า (Capstone ออกแบบเกมเอง) — หลายกลุ่มทำคนละเกม จะออกแบบ TOPIC ยังไงให้คะแนนแต่ละกลุ่ม ไม่ปนกัน (ใบ้: topic เป็นชั้น เช่น tesaiot/class/<ชื่อเกม>/leaderboard) — นี่คือสิ่งที่เกม Capstone ต้องตัดสินใจ

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

fit-css

Marp deck — Fundamental of Embedded Systems Developer II (MicroPython-only, 13 คาบ) คาบ 11 · WiFi + MQTT: high-score board Render: npx @marp-team/marp-cli session-11.md -o session-11.html GUI-first. ภาพจริงบนบอร์ดฝังผ่าน img/. โค้ดอ้างอิง: practise_codes/s12_net.py (ฉบับฝึก)

← Roadmap (TOC)