Extra — โหวต/quiz สด

WiFi + MQTT · กดเลือก แล้วผลขึ้นกระดานทันที

มินิโปรเจกต์เสริม — ต่อยอดจากคาบ WiFi + MQTT
ใช้ 2 บอร์ด · บอร์ดหนึ่งเป็น "รีโมตโหวต" อีกบอร์ดเป็น "จอประกาศผล"

นึกถึงตอนวิทยากรถามแล้วทุกคนกดมือถือโหวต Slido / Kahoot — วันนี้เราจะทำของแบบนั้นเองด้วยบอร์ดของเรา

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

เราจะสร้าง ระบบโหวตสด ง่าย ๆ ที่ของจริงเขาใช้กันทุกวัน:

voter (รีโมตโหวต)
ดันจอยเลือก A/B/C/D → กด A ยืนยัน
mqtt.publish("poll/q1", "A")
tally (จอประกาศผล)
ฟัง poll/q1 นับใส่ dict
วาดแท่งคะแนนใหม่ทุกครั้งที่มีโหวต
voter tally publish "A" → poll/q1

ใช้ที่ไหนได้บ้าง: โหวตในคลาส, quiz live, แบบสอบถามความเห็น, นับมือในที่ประชุม — แนวคิดเดียวกับ Slido / Mentimeter เป๊ะ ๆ

สิ่งที่อยากเห็นตอนจบ: กดโหวตจากบอร์ดหนึ่ง แล้วแท่งบนอีกบอร์ดขยับขึ้นทันที


หน้าจอ tally จริงบนบอร์ด BENTO — ตัวเลขใน dict กลายเป็นความสูงแท่งของ A/B/C/D · ที่มา: BENTO game console (course asset)

ไดอะแกรม pub/sub — topic เดียว ได้ยินกันหมด

หัวใจคือ topic poll/q1: ใครส่งเข้า topic นี้ ทุกคนที่ subscribe จะได้รับ

voter กด A/B/C/D Broker test.mosquitto.org :1883 tally นับ + วาดแท่ง publish "A" subscribe topic = poll/q1 · payload = ตัวอักษรเดียว "A" / "B" / "C" / "D"

payload เราตั้งใจให้ สั้นที่สุด คือตัวอักษรเดียว ฝั่งรับจึงแค่ decode() แล้วเอาไปนับได้เลย

pub/sub ตัวจริงใช้แบบนี้ทั้งวงการ

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


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

สังเกต subscribe: /plug1/# กับ /+/current — นั่นคือ wildcard ขอรับหลาย topic ทีเดียว ตอนนี้เราใช้ topic เดียว poll/q1 ก่อน พอเพิ่มหลายคำถามค่อยใช้ wildcard รับทุกข้อรวดเดียวได้

ทำไมต้องมี broker — ดูที่ "จำนวนสาย"

ถ้าไม่มีตัวกลาง บอร์ดทุกคู่ต้องต่อหากันเอง จำนวนสายโตไว้มาก เทียบ N บอร์ด:

ต่อตรงทุกคู่=(N2)=N(N1)2    O(N2)ผ่าน broker=N    O(N)\text{ต่อตรงทุกคู่} = \binom{N}{2} = \frac{N(N-1)}{2} \;\sim\; O(N^2) \qquad\text{ผ่าน broker} = N \;\sim\; O(N)

  • 12 บอร์ดแบบต่อตรง ต้องดูแล 12×112=66\frac{12 \times 11}{2} = 66 การเชื่อมต่อ — เพิ่มบอร์ดทีเดียวสายโตเป็นกำลังสอง
  • ผ่าน broker แต่ละบอร์ดต่อ "เส้นเดียว" ไป broker รวมแค่ 12 เส้น เพิ่มบอร์ดก็โตแบบเส้นตรง

นี่คือเหตุผลที่ระบบ IoT เลือก pub/sub: voter จะมีกี่เครื่องก็ได้ ต่างคนต่างยิงเข้า poll/q1 เส้นเดียว ไม่ต้องรู้จักกันเลย — broker จัดการกระจายให้

จังหวะเวลา 1 โหวต — ไล่ทีละสเต็ป

ก่อนเดินโค้ด ลองดูภาพ "เวลา" ว่าโหวต 1 ครั้งวิ่งผ่านระบบยังไง (บนลงล่าง = เวลาเดินไปข้างหน้า)

ฝั่ง tally subscribe ค้างไว้ก่อน พอ voter publish broker ก็ส่งต่อให้ทันที — ทุกบรรทัดในภาพ map ตรงกับโค้ดสองหน้าถัดไปเป๊ะ ๆ

เดินโค้ด voter — เลือกแล้วส่ง

ฝั่ง voter จับขอบ "ดันจอย 1 ครั้ง = เลื่อน 1 ช่อง" และ "กด A 1 ครั้ง = ส่ง 1 โหวต"

    index = 0                       # ตำแหน่งตัวเลือกที่กำลังชี้อยู่
    prev_a = False                  # จำสถานะปุ่ม A รอบก่อน (จับขอบขาลง)
    prev_dir = False                # กันเลื่อนรัวตอนดันจอยค้าง

    while True:
        k = game.keys()
        if k.back:                  # BACK = ออก
            game.clear()
            break

        moving = k.left or k.right
        if moving and not prev_dir:                  # เลื่อนทีละ 1 ต่อการดัน 1 ครั้ง
            index = (index + (1 if k.right else -1)) % len(CHOICES)
            pick.set("ตัวเลือก: " + CHOICES[index])
        prev_dir = moving

        if k.a and not prev_a:                        # กด A = ยืนยันโหวต
            mqtt.publish(TOPIC, CHOICES[index])      # payload เป็น str เช่น "A"
        prev_a = k.a
        time.sleep_ms(40)

เทคนิคเดิมจากคาบปุ่ม: เทียบสถานะรอบก่อน (prev_a) เพื่อนับ "กดครั้งเดียว" ไม่ใช่กดค้างแล้วรัว

ภาพบน: เส้นสัญญาณปุ่มตามเวลา จุดแดง "นับ!" เกิดเฉพาะ ตอนเปลี่ยนจากปล่อย→กด (ขอบขาขึ้น) กดค้างยาวแค่ไหนก็ไม่นับซ้ำ — นี่คือเหตุผลที่ต้องจำ prev_a

เดินโค้ด tally — นับใส่ dict แล้ววาดแท่ง

ฝั่ง tally เก็บคะแนนใน dict หนึ่งตัว แล้ววาดแท่งใหม่ทุกครั้งที่มีโหวตเข้ามา

    votes = {c: 0 for c in CHOICES}    # dict นับคะแนน เริ่มที่ 0 ทุกตัว
    mqtt.subscribe(TOPIC)              # ขอฟังทุกโหวตในห้องนี้

    while True:
        msg = mqtt.get_message()       # คืน (topic, bytes) หรือ None (ไม่บล็อก)
        if msg is not None:
            _topic, data = msg
            choice = data.decode().strip()   # payload เป็น bytes ต้อง decode
            if choice in votes:              # นับเฉพาะตัวเลือกที่รู้จัก
                votes[choice] += 1
                redraw()                     # แท่งสูง = 1 + คะแนน * UNIT
        time.sleep_ms(50)

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

ไล่ตามลูกศร: bytes b"B"decode() ได้ "B"votes["B"] += 1 → แปลงเลขเป็นพิกเซลด้วย h = 1 + votes[c]*UNIT แล้ววาดแท่งใหม่ — ตัวเลขใน dict กลายเป็นความสูงบนจอตรง ๆ

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

ใช้ 2 บอร์ด (ต่อ WiFi เดียวกัน, ใช้ TOPIC เดียวกันให้เป๊ะ):

บอร์ด ไฟล์ที่รัน หน้าที่
เครื่องที่ 1 codes/ex_b4_voter.py รีโมตโหวต — กดเลือกแล้วส่ง
เครื่องที่ 2 codes/ex_b4_tally.py จอประกาศผล — นับ + วาดแท่ง

ขั้นตอน:

  1. ต่อ WiFi ผ่านเมนู Wi-Fi บนจอ ก่อนทั้งสองบอร์ด (เซฟรหัส → auto-connect)
  2. เปิดไฟล์ใน BENTO IDE → กด Program to Device ทีละบอร์ด
  3. โหวตจากบอร์ด voter แล้วดูแท่งบน tally ขยับ


สองบอร์ดต่อ WiFi เดียวกันแล้ว (มุมซ้ายบนขึ้น Wi-Fi ต่อแล้ว): ซ้าย = voter กด A ส่ง · ขวา = tally แท่งขึ้นตามโหวต · ที่มา: BENTO game console (course asset)

หน้าจอ tally ที่ควรเห็น: ทุกครั้งที่โหวตเข้ามา แท่งของตัวเลือกนั้นเด้งสูงขึ้น ทันที ไม่ต้องรีเฟรช

มีบอร์ดเดียวก็ลองได้: รัน tally ไว้ แล้วโหวตจากแอป MQTT บนมือถือ ส่ง "A" ไป poll/q1 ก็เห็นแท่งขึ้นเหมือนกัน

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

"รีโมตโหวต" ที่เราทำ ก็คือสิ่งเดียวกับ audience response clicker ที่ใช้จริงในงานสัมมนา/ห้องเรียน (ในภาพ) — กดเลือกตัวเลือก ผลขึ้นจอรวมทันที ต่างกันแค่ของเราใช้ MQTT แทนคลื่นวิทยุเฉพาะของมัน

ระบบเล็ก ๆ วันนี้คือต้นแบบของของจริงหลายอย่าง:

  • IoT / Industry 4.0 — เปลี่ยน payload จาก "โหวต" เป็น "ค่าเซนเซอร์" (อุณหภูมิ/ความสั่น) แล้ว tally ก็กลายเป็น dashboard เฝ้าระวังสายการผลิต ทันที
  • Quiz/Polling จริง — เพิ่ม topic poll/q2, poll/q3 เปลี่ยนคำถามได้หลายข้อ เหมือน Kahoot
  • Multiplayer — payload เดียวกันนี้ส่ง "ท่าเดิน" ของผู้เล่นได้ ต่อยอดเป็นเกมแข่งข้ามบอร์ด
  • ความน่าเชื่อถือ — ของจริงจะใส่ broker ส่วนตัว + TLS + ยืนยันตัวตน แทน broker สาธารณะ

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

ที่มา: "CLiKAPAD Prioritizer Audience Response Keypad" — CLiKAPAD, CC BY-SA 3.0, Wikimedia Commons

เชื่อมโยงรากฐาน · เกมโหวตซ่อนวิศวกรรมไว้กี่ชั้น

เกมโหวตเล็ก ๆ คาบนี้แตะรากฐานที่วิศวกรฝังตัวใช้จริงพอดี:

  • Python — dict + iteration: votes = {c: 0 for c in CHOICES} คือ hash map นับความถี่ (frequency count) แล้ว votes[choice] += 1 คือการอัปเดตค่าผ่าน key — โครงสร้างข้อมูลพื้นฐานที่ใช้ได้ทุกที่ ไม่ใช่แค่เกม
  • Python — event-driven / non-blocking: get_message() คืน None เมื่อยังไม่มีของ จอจึงไม่ค้าง วน poll สลับงานอื่นได้ คือหัวใจของ superloop + polling บน MCU ที่ไม่มี OS
  • อัลกอริทึม — ความซับซ้อน O(N) vs O(N²): กราฟจำนวนสายเมื่อกี้คือเหตุผลเชิงคณิตศาสตร์ว่าทำไมต้องมีตัวกลาง — การคิดเรื่อง scale เป็นทักษะออกแบบระบบจริง
  • ฝังตัว — networking & decoupling: voter กับ tally ไม่รู้จักกันเลย รู้แค่ชื่อ topic ร่วมกัน นี่คือ loose coupling ที่ทำให้ระบบจริงเพิ่ม/ถอดอุปกรณ์ได้โดยไม่ต้องแก้โค้ดฝั่งอื่น
  • กราฟิก — data visualization: แปลงตัวเลขใน dict เป็นความสูงแท่ง (h = 1 + votes[c] * UNIT) คือการ map ข้อมูล → พิกัดหน้าจอ หลักการเดียวกับ dashboard/กราฟทุกตัว

Pub Sub Model | MQTT Essentials Part 3 — HiveMQ

fit-css

← Extra Index