Extra — Leaderboard ทั้งห้อง

WiFi + MQTT: คะแนนของทุกบอร์ดขึ้นกระดานเดียวกัน

มินิโปรเจกต์ต่อยอด — ทำหลังผ่านคาบ WiFi/MQTT แล้ว
โค้ดเดียวจบ ทุกบอร์ดรันไฟล์อันเดียวกัน

วันนี้เราจะทำให้ทั้งห้อง "เล่นแข่งกันจริง" — จบเกมปุ๊บ คะแนนเราไปโผล่บนจอเพื่อนทุกคน และคะแนนเพื่อนก็มาโผล่บนจอเรา

เป้าหมาย + ใช้จริงตรงไหน

เราจะต่อยอด leaderboard จาก "พิมพ์ในคอนโซล" ให้เป็น กระดานบนจอ ที่อัปเดตสด ๆ จากทุกบอร์ดในห้อง

  • จบเกม → publish คะแนนของเราขึ้น topic กลาง
  • subscribe topic เดียวกัน → รับคะแนนของเพื่อนทุกคน
  • เก็บคะแนน "สูงสุดของแต่ละคน" ใน dict แล้วโชว์ top-5 บนจอด้วย game.Text

use case จริง: นี่คือแพตเทิร์น pub/sub แบบเดียวกับที่โรงงาน Industry 4.0 ใช้ — เครื่องจักรหลายตัว publish ค่าวัด, dashboard กลาง subscribe มารวมแสดง บอร์ดของเราวันนี้ก็คือ sensor node + dashboard ย่อ ๆ ตัวหนึ่ง

ภาพจริงของ "ทั้งห้องเล่นแข่งกัน": งาน LAN party คนนับพันต่อเครื่องเข้าเครือข่ายเดียว เล่นเกมเดียวกันพร้อมกัน — วันนี้เราย่อมันลงมาเหลือทั้งห้องบนบอร์ด Bento
ที่มา: "Winter 2004 DreamHack LAN Party" — Toffelginkgo, CC BY-SA 3.0, Wikimedia Commons

หัวใจคือ topic เดียวกัน = อยู่ห้องเดียวกัน ได้ยินกันหมด เปลี่ยนแค่ชื่อ PLAYER ก็เป็นคนละบอร์ด

ไดอะแกรม pub/sub

ทุกบอร์ดทั้ง publish และ subscribe topic tesaiot/class/score พร้อมกัน — ส่งก็ได้ ฟังก็ได้

บอร์ด A board-01 Broker test.mosquitto.org :1883 บอร์ด B board-02 publish subscribe publish subscribe topic: tesaiot/class/score payload: "ชื่อ:คะแนน" เช่น "board-01:1250" board-01:1250 board-02:980

แพ็กเก็ตวิ่งจากบอร์ดเข้า broker แล้ว broker กระจายต่อให้ทุกคนที่ subscribe — นี่คือ publish/subscribe ที่กำลัง "เคลื่อนไหว" จริง ๆ ไม่ใช่แค่ลูกศรนิ่ง ๆ

payload เป็น str ฝั่งส่ง แต่ตอนรับจะมาเป็น bytes เสมอ ต้อง .decode() ก่อนแยกด้วย split(":")

ลองเปิด mosquitto_sub (หรือ MQTT Explorer) บนเครื่อง subscribe topic เดียวกัน จะเห็นข้อความของทุกบอร์ดวิ่งเข้ามาสด ๆ แบบนี้:

ภาพจำลองคอนโซล mosquitto_sub -v ขณะ subscribe topic tesaiot/class/score — รูปแบบบรรทัด <topic> <payload> ตรงกับที่เครื่องมือจริงพิมพ์ออกมา

payload เดินทางยังไง · str ออกไป bytes กลับมา

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

ฝั่งส่งเราเอาตัวเลขมาทำเป็น str แล้ว publish · ระหว่างทาง MQTT ห่อเป็น UTF-8 bytes ให้เอง · ฝั่งรับจึงได้ bytes กลับมา ต้อง .decode() คืนเป็น str แล้วค่อย split(":") กับ int() แยกชื่อกับคะแนนออกมา

ดูแบบไล่ทีละขั้นจากซ้ายไปขวา ตามลำดับที่ข้อมูลถูกแปลงในแต่ละช่อง:

1250 + ชื่อ ตัวเลข "board-01:1250" str b'board-01..' UTF-8 bytes .decode() → str .split(":") list int(score) คะแนน ฝั่งส่ง: publish ฝั่งรับ: get_message → แยกข้อมูล

ถ้าจอขึ้นเป็น b'board-01:1250' (มี b' นำหน้า) แปลว่าลืม .decode() — นี่คือบั๊กอันดับหนึ่งของคาบนี้

เทียบให้เห็นชัดใน REPL: ลืม .decode() แล้ว split จะพังทันที แต่ใส่แล้วแยกข้อมูลได้สวยงาม

ภาพจำลองหน้าจอ REPL (MicroPython) เทียบ payload ที่ลืม/ใส่ .decode() — ค่าและข้อความ error ตรงกับที่เกิดขึ้นจริงเมื่อรันบนบอร์ด

เข้าใจฮาร์ดแวร์ · ทำไม MQTT เหมาะกับ IoT

หลายบอร์ด publish / subscribe ผ่าน broker ตัวเดียว — บอร์ดไม่ต้องรู้จักกันเอง แค่รู้ topic เดียวกัน

ขวา: บอร์ดตระกูล PSoC จริง (พร้อมตัวโปรแกรม MiniProg) — ก้อนซิลิคอนเดียวกับ PSoC Edge ที่เราถืออยู่ บล็อกไดอะแกรมซ้ายคือ "ข้างใน" ของอุปกรณ์จริงตัวนี้
ที่มา: "PSoC Express Development Board 1" — Dacs, CC BY-SA 4.0, Wikimedia Commons

  • เพิ่มบอร์ดได้ไม่จำกัด (broker เป็นตัวกลาง) = scale ง่าย — นี่คือเหตุผลที่ IoT ใช้ MQTT
  • topic ทำหน้าที่ routing (เช่น game/score, sensor/temp) — สมัครรับเฉพาะป้ายที่เราสนใจ

1 broker + topic เป็นป้าย = ทั้งห้องเรียนคุยกันได้ผ่านตัวกลางตัวเดียว

ทำไม broker ถึง "scale ง่าย" จริง · ดูที่จำนวนสาย

ถ้าให้บอร์ดทุกใบต่อตรงหากันเอง (mesh) จำนวนสายจะโตแบบ กำลังสอง แต่ผ่าน broker ตัวกลางจะโตแบบ เส้นตรง

mesh=N(N1)2  =  O(N2)broker=N  =  O(N)\text{mesh} = \frac{N(N-1)}{2} \;=\; O(N^2) \qquad\qquad \text{broker} = N \;=\; O(N)

ก่อนดูกราฟ ดูที่ "รูปทรง" ของการต่อสายก่อน — ฝั่งซ้าย (mesh) ทุก node ต้องลากสายหากันเองทุกคู่ พอเพิ่ม node สายเลยพุ่ง · ฝั่งขวา (broker) เพิ่ม node ก็แค่ลากเข้าตัวกลาง เส้นเดียว:

mesh: เพิ่ม node = สายพุ่ง broker: เพิ่ม node = +1 สาย สาย = N(N-1)/2 → 15 broker สาย = N → 6

ทีนี้พอเอาจำนวนสายไปพล็อตตาม NN จะเห็นเส้นแดง (mesh) แยกหนีเส้นน้ำเงิน (broker) ชัดเจน:

  • NN คือจำนวนบอร์ดในห้อง · ห้องเรา 12 คน: ต่อกันเอง = 66 สาย แต่ผ่าน broker = 12 สาย
  • เพิ่มอีก 1 บอร์ด: mesh ต้องลากสายใหม่ถึง NN เส้น แต่ broker เพิ่มแค่ เส้นเดียว

นี่คือเหตุผลทางวิศวกรรมที่ IoT เลือก MQTT จริง ๆ — ไม่ใช่แค่ "สะดวก" แต่เพราะมันโตแบบ O(N)O(N) ไม่ใช่ O(N2)O(N^2)

เดินโค้ด (1) — ต่อเน็ต + ส่งคะแนน

ลำดับสำคัญ: WiFi ก่อน แล้วค่อย MQTT ถ้าต่อ WiFi ผ่านเมนูบนจอไว้แล้ว บรรทัด connect จะผ่านทันที

    import wifi
    import mqtt
    import bentogame as game

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

    if not wifi.is_connected():
        wifi.connect(SSID, PASSWORD)   # ต่อเน็ตก่อน
    mqtt.connect(BROKER, 1883, client_id=PLAYER)
    mqtt.subscribe(TOPIC)              # ขอฟัง topic เดียวกัน

    my_score = 1250                    # คะแนนที่เพิ่งเล่นจบได้
    mqtt.publish(TOPIC, "%s:%d" % (PLAYER, my_score))   # "board-01:1250"

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

ลำดับ 4 ขั้นต้องเป๊ะตามนี้ — WiFi ก่อน เสมอ ถ้าสลับไป mqtt.connect ก่อนต่อเน็ตจะ error ทันที:

1 wifi.connect ต่อเน็ตก่อน 2 mqtt.connect ค่อยต่อ broker 3 subscribe ขอฟัง topic 4 publish ส่งคะแนน ผิดลำดับ = ต่อไม่ติด MQTT ก่อน WiFi → error

เปิดไฟล์นี้ใน BENTO IDE แล้วกดปุ่ม Program to Device (มุมซ้ายบน) เพื่อส่งโค้ดลงบอร์ด — เป็นปุ่มเดียวกับที่เราใช้ตลอดคอร์ส:

BENTO IDE — ปุ่ม Program to Device มุมซ้ายบน · คอนโซลด้านล่างคือผลตอนกดรันลงบอร์ดจริง

เดินโค้ด (2) — เก็บใน dict + โชว์ top-5

รับคะแนนเพื่อนด้วย get_message() (ไม่บล็อก: ไม่มีของก็คืน None) แล้วเก็บคะแนนสูงสุดต่อชื่อ

    game.start()
    board = {PLAYER: my_score}         # dict: ชื่อ -> คะแนนสูงสุด
    game.Text("LEADERBOARD ทั้งห้อง", 40, 24, game.CYAN)
    rows = [game.Text("", 60, 80 + i * 48, game.WHITE) for i in range(5)]

    def redraw():
        top = sorted(board.items(), key=lambda kv: kv[1], reverse=True)[:5]
        for i in range(5):
            if i < len(top):
                name, score = top[i]
                rows[i].set("%d. %-12s %6d" % (i + 1, name, score))

    def update():
        msg = mqtt.get_message()       # (topic, bytes) หรือ None
        if msg is not None:
            _t, data = msg
            name, score = data.decode().split(":")   # bytes -> decode -> split
            if int(score) > board.get(name, -1):     # เก็บคะแนนสูงสุดเท่านั้น
                board[name] = int(score); redraw()

    game.run(update, fps=10)           # BACK = ออก, START = เริ่มใหม่

game.run วน update() ให้ทุกเฟรม เราจึง poll ข้อความใหม่ตลอด กระดานอัปเดตสด

สิ่งที่ update() ทำในแต่ละเฟรม — ถ้าไม่มีข้อความก็ข้ามไปเลย (ไม่บล็อกเกม):

get_message() มีของ? / None .decode() bytes→str .split(":") ชื่อ, คะแนน score > max? เก็บคะแนนสูงสุด board[name]=score redraw() วาด top-5 ไม่มีข้อความ → ข้ามไปเฟรมถัดไป (ไม่บล็อก)

ผลบนจอบอร์ดหน้าตาประมาณนี้ (อันดับ 1 ไฮไลต์เหลือง คะแนนชิดขวาด้วย %6d):

ภาพจำลองหน้าจอบอร์ด สร้างตาม game.Text ในโค้ดจริง (หัวข้อ CYAN ที่ (40,24) · แต่ละแถวที่ y=80+i·48)

กระดานเรียงใหม่เองทุกครั้ง · นี่คือ sorted() ทำงาน

ทุกครั้งที่คะแนนใหม่เข้ามา sorted(...) จะเรียงทั้ง dict ใหม่ แล้ว redraw() วาด top-5 ทับของเดิม คนที่คะแนนพุ่งก็ไต่อันดับขึ้นมาให้เห็นสด ๆ

  • คะแนนของ board-03 เด้งจาก 980 เป็น 1950 → sorted ดันมันขึ้นอันดับ 1 ทันที
  • เราไม่ต้องเขียน logic "ใครแซงใคร" เอง — แค่เก็บคะแนนสูงสุดใน dict แล้วให้ sorted จัดอันดับให้ทุกเฟรม

เห็นภาพไหมว่า "โครงสร้างข้อมูล (dict) + อัลกอริทึมเรียง (sort)" สองอย่างนี้ทำงานร่วมกันเป็นกระดานสดได้ยังไง

วิธีรัน + ต้องกี่บอร์ด

รัน: เปิด codes/ex_a1_leaderboard.py ใน BENTO IDE แล้วกด Program to Device ทุกบอร์ดรัน ไฟล์อันเดียวกัน เปลี่ยนแค่บรรทัด PLAYER ให้ไม่ซ้ำ

  • 1 บอร์ด ก็เห็นกระดาน (มีชื่อตัวเองคนเดียว) — ใช้ทดสอบว่าต่อเน็ต/ส่งได้
  • 2 บอร์ดขึ้นไป ถึงจะสนุก: คะแนนเพื่อนวิ่งเข้ามาขึ้นกระดานสด ๆ
  • ทั้งห้องตั้ง TOPIC เป๊ะตัวเดียวกัน → กลายเป็นกระดานรวมห้องเดียว

กดปุ่ม Program to Device (เขียว มุมซ้ายบน) ในทุกบอร์ด

กับดักที่เจอบ่อย: client_id (PLAYER) ซ้ำกัน broker จะเตะกันหลุด — ตั้งชื่อไม่ซ้ำทั้งห้อง และ topic ต้องสะกดตรงกันทุกตัวอักษร

ต่อยอดสู่โลกจริง

แพตเทิร์น "หลาย node → broker → กระดานรวม" ที่เราเพิ่งทำ คือโครงเดียวกับงานจริงเป๊ะ ๆ

  • IoT / Industry 4.0: เครื่องจักรหลายตัว publish อุณหภูมิ/รอบหมุน, dashboard กลาง subscribe มารวม monitor — เปลี่ยน "คะแนน" เป็น "ค่าเซนเซอร์" ก็ได้ทันที (ลอง publish ค่าจาก sensors.bmi270.acceleration())
  • Multiplayer จริง: ส่ง state มากกว่าคะแนน เช่น "ชื่อ:คะแนน:ด่าน" แล้ว split(":") หลายช่อง → ทำ co-op / แข่งเรียลไทม์ได้
  • เก็บประวัติ: ให้บอร์ดหนึ่งทำหน้าที่ logger เขียนคะแนนลงไฟล์ตลอดคาบ
เกมเรา ระบบจริง คะแนนผู้เล่น ค่าเซนเซอร์ บอร์ดแต่ละใบ เครื่องจักร กระดานคะแนน SCADA dashboard โครงเดียวกัน: node → broker → กระดานรวม

ภาพขวาคือห้องควบคุมจริง: วิศวกรหนึ่งคนเฝ้า "กระดานรวม" ที่รับค่าจากอุปกรณ์กระจายอยู่ทั้งระบบ — กระดานคะแนนของเราคือเวอร์ชันจิ๋วของหน้าจอนี้

สิ่งที่น้อง ๆ ทำวันนี้ ไม่ใช่ของเล่น — มันคือสถาปัตยกรรม pub/sub ที่ใช้จริงในระบบ IoT ระดับโรงงาน เราแค่ใส่มันลงในเกมให้สนุกก่อน

ที่มา: "Hideji Kawasaki operates the SCADA system" — U.S. Navy photo by Joe Schmitt, Public domain, Wikimedia Commons

เชื่อมโยงรากฐาน · กระดานคะแนนหนึ่งจอ โยงไปถึงอะไรบ้าง

กระดานเล็ก ๆ ที่คะแนนเพื่อนวิ่งเข้ามานี้ รวมรากฐานสี่ด้านที่เราใช้ตลอดคอร์สไว้ในงานเดียว:

Embedded / MCU WiFi · network node · poll ไม่บล็อก Python dict · str↔bytes · split/int Algorithms sort O(N logN) · O(N) vs O(N²) Graphics พิกัด y=80+i·48 · redraw on change Leaderboard ทั้งห้อง

Embedded / MCU — บอร์ดต่อ WiFi ขึ้น network stack กลายเป็น node หนึ่งบนเครือข่าย; update() ที่ส่งให้ game.run() ถูกเรียกตามจังหวะเฟรม เราเลย poll ข้อความใหม่ได้เรื่อย ๆ โดยไม่บล็อก

Python — เก็บคะแนนใน dict (ชื่อ → คะแนนสูงสุด); สร้างแถวด้วย list comprehension; แปลง str ↔ bytes ด้วย .decode(); แยกข้อมูลด้วย .split(":") + int(); กัน payload เพี้ยนด้วย try/except

Algorithmssorted(..., key=lambda kv: kv[1], reverse=True) คือการ เรียงลำดับ O(NlogN)O(N\log N); การเลือกใช้ broker แทน mesh คือ complexity O(N)O(N) vs O(N2)O(N^2); ตัว pub/sub เองคือ message routing ด้วย topic

Graphics — วาง game.Text แต่ละแถวที่พิกัด y=80+i×48y = 80 + i\times 48 (ระบบพิกัด y มาก = ล่าง); อัปเดตเฉพาะตอนข้อมูลเปลี่ยน (redraw on change) ให้จอไม่กระพริบ

คลิปเสริม: กระดานคะแนนเรียลไทม์ของเกมจริงเก็บและเรียงคะแนนหลายผู้เล่นอย่างไร — ตรงกับกระดานที่เราเพิ่งทำเป๊ะ

Create & Scale Real-Time Leaderboards (for Games & Apps) — DragonflyDB

fit-css

← Extra Index