Extra — ป้ายข้อความข้ามบอร์ด

WiFi + MQTT: บอร์ดหนึ่งพิมพ์ อีกบอร์ดขึ้นจอ

มินิโปรเจกต์เสริม — ต่อยอดจากคาบ WiFi + MQTT
ใช้ 2 บอร์ด · sender + display · topic เดียวกัน

วันนี้เราจะทำ "ป้ายไฟข้ามห้อง" บอร์ดเราเลือกข้อความแล้วกดส่ง อีกบอร์ดขึ้นจอตัวใหญ่ทันที เหมือนป้ายประกาศที่คุมจากระยะไกล

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

เราจะต่อยอด MQTT จาก "ส่งคะแนน" มาเป็น "ส่งข้อความให้บอร์ดอื่นขึ้นจอ" ด้วยจอยเลือก แล้วกด a ส่ง

ฝั่ง sender
จอยซ้าย/ขวา = เลื่อนเลือกข้อความจาก list
กด a = publish ขึ้น msg/board02
ฝั่ง display
subscribe("msg/board02") + get_message()
ได้ข้อความใหม่ → โชว์ตัวใหญ่ด้วย game.Text

ของจริงในโลกใช้แบบนี้เยอะ: ป้ายไฟ LED ในโรงงาน (Industry 4.0), ป้ายบอกคิวในร้าน, จอแจ้งเตือนในห้องควบคุม — ทั้งหมดคือ "ตัวสั่งหนึ่งตัว → จอหลายตัว" ผ่าน MQTT

โปรโตคอลที่เราใช้วันนี้ชื่อ MQTT — ออกแบบมาเพื่องาน IoT โดยเฉพาะ เบา ส่งไว เหมาะกับอุปกรณ์เล็ก ๆ อย่างบอร์ดเรา
ที่มา: "Logo of the MQTT protocol" — OASIS, Public domain, Wikimedia Commons

ดูภาพรับ-ส่งข้อความผ่าน broker ก่อนลงมือ (ส่งจากที่หนึ่ง โผล่อีกที่หนึ่ง) — ตรงกับกระดานข้อความที่เราจะทำ:

MQTT Beginner Tutorial | Send and Receive MQTT Messages in 5 minutes — IoT Frontier

ไดอะแกรม: pub/sub ด้วย topic เดียว

หัวใจคือ topic ถ้าสองบอร์ดใช้ msg/board02 เหมือนกัน ก็เหมือนอยู่ช่องสัญญาณเดียวกัน คุยกันได้ทันที

บอร์ด sender เลือก + กด a Broker test.mosquitto.org :1883 บอร์ด display ขึ้นจอตัวใหญ่ publish deliver topic: msg/board02 · payload: "สวัสดีครับ" (str ไทยส่งตรงได้)

get_message() เป็นแบบ poll (ไม่บล็อก): ยังไม่มีของก็คืน None เกมจึงไม่ค้าง วนเช็กทุกเฟรมได้สบาย

publish ครั้งเดียว ทุกจอขึ้นพร้อมกัน

จุดที่ทำให้ MQTT ทรงพลัง: sender ไม่รู้จัก ว่าปลายทางมีกี่จอ มันแค่หย่อนข้อความเข้า topic — broker เป็นคนคัดลอกส่งต่อให้ทุกตัวที่ subscribe ไว้

ดูจังหวะข้อความวิ่ง: ออกจาก sender → ค้างที่ broker → แตกเข้าทั้งสองจอพร้อมกัน นี่คือ fan-out ของ pub/sub

คณิตของ fan-out: งานฝั่งส่งคงที่ ปลายทางขยายได้

ต่อให้มีกี่จอ ฝั่ง sender ก็ publish แค่ ครั้งเดียว ภาระการคัดลอกเป็นของ broker:

publish ฝั่ง sender=1deliver=N    (N=จำนวนจอที่ subscribe)\text{publish ฝั่ง sender} = 1 \qquad\Longrightarrow\qquad \text{deliver} = N \;\;(N=\text{จำนวนจอที่ subscribe})

แปลเป็นภาษาคน: โค้ดฝั่งเราเท่าเดิมเป๊ะ จะมี 1 จอหรือ 50 จอก็เขียนเหมือนกัน — ต่างจากถ้าต้องต่อสายตรงหากันเอง ที่ยิ่งเพิ่มปลายทางยิ่งต้องแก้โค้ดทุกครั้ง นี่คือเหตุผลที่ระบบ IoT จริงเลือก pub/sub

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

ทุก step คืน True/False ให้เช็กก่อนไปต่อ ต่อ WiFi → ต่อ broker → แล้วค่อยส่ง

    BROKER = "test.mosquitto.org"
    PORT   = 1883
    TOPIC  = "msg/board02"          # ป้ายปลายทาง ฝั่ง display ต้องใช้ชื่อนี้เป๊ะ
    MESSAGES = ["สวัสดีครับ", "ทีมแดงนำอยู่", "เริ่มรอบใหม่ได้เลย",
                "พักก่อนนะ", "สู้ ๆ นะทุกคน"]   # ภาษาไทยล้วน ใส่ลง list ตรง ๆ

    def update():
        global index
        k = game.keys()                       # อ่านจอยรอบนี้
        if k.left:                            # เลื่อนข้อความก่อนหน้า (วนรอบ)
            index = (index - 1) % len(MESSAGES)
            preview.set(MESSAGES[index]); game.sfx("move"); time.sleep_ms(180)
        elif k.right:                         # เลื่อนข้อความถัดไป
            index = (index + 1) % len(MESSAGES)
            preview.set(MESSAGES[index]); game.sfx("move"); time.sleep_ms(180)
        elif k.a:                             # กด a = ส่งข้อความที่เลือกอยู่
            mqtt.publish(TOPIC, MESSAGES[index])   # payload เป็น str ส่งตรงได้
            status.set("ส่งแล้ว -> " + MESSAGES[index]); game.sfx("select")

time.sleep_ms(180) หลังเลื่อน = กันการกดค้างแล้วเลื่อนรัวเกินจนคุมไม่ได้

อ่านจอยหนึ่งรอบ แล้วแตกเป็น 3 ทาง: left/right เลื่อน index, a ส่ง — วนซ้ำทุกเฟรม

เสียงในคาบนี้ — ป้ายข้อความข้ามบอร์ดใช้เสียง UI ของ game.sfx: เลื่อนจอยเลือกข้อความได้ move กด a publish ส่งออกได้ select (กดเล่นฟังได้จริง):

sfx_ui_move

sfx_ui_select

ฟังครบ 21 ตัว + รูปคลื่น/ซูม/spectrogram → Sound Explorer

เดินโค้ด display — รับแล้วขึ้นจอตัวใหญ่

ฝั่งนี้ต้อง subscribe topic เดียวกันก่อน แล้ว get_message() ทุกเฟรมเพื่อรับข้อความล่าสุด

    mqtt.subscribe(TOPIC)                  # ขอฟัง "msg/board02" = พร้อมรับป้าย

    board = game.Text("(ว่าง)", 30, 150, color=game.YELLOW)   # ป้ายใหญ่กลางจอ

    def update():
        msg = mqtt.get_message()           # คืน (topic, payload) bytes หรือ None
        if msg is not None:
            topic, data = msg
            text = data.decode()           # payload เป็น bytes ต้อง decode ก่อน
            board.set(text)                # อัปเดตป้ายเป็นข้อความล่าสุด
            game.sfx("point")
        time.sleep_ms(50)                  # พักนิด ไม่ poll รัวเกิน

จุดพลาดบ่อย: ลืม .decode() แล้วจอขึ้นเป็น b'...' — payload มาเป็น bytes เสมอ ต้อง decode ก่อนโชว์

สามจังหวะของฝั่งรับ: poll → ถ้าไม่ None ก็ decode → วาดป้าย; ถ้า None ก็ข้ามไปไม่ค้างเกม

sleep_ms(50) นั้นมาจากไหน — ดีเลย์ของป้าย

ที่ใส่ time.sleep_ms(50) ไม่ได้สุ่มมา มันคุม ความเร็วที่ป้ายอัปเดต โดยตรง เพราะป้ายจะเห็นข้อความก็ต่อเมื่อ get_message() ถูกเรียกรอบถัดไป ดีเลย์แย่สุดจึงเท่ากับช่วง poll บวกเวลาเดินทางผ่านเน็ต:

tlatency, max    Tpoll  +  Tnetworkt_{\text{latency, max}} \;\approx\; T_{\text{poll}} \;+\; T_{\text{network}}

sleep_ms(50) → ดีเลย์ราว 80 ms คนแทบไม่รู้สึก แต่ยังเหลือเวลาให้ CPU หายใจ ไม่ poll รัวจนเปลือง ถ้าตั้งสั้นกว่านี้ป้ายไวขึ้นนิดเดียวแต่กินทรัพยากรเพิ่มชัด — ของแบบนี้คือการ "จูน" ที่วิศวกร embedded ทำกันจริง

ภาษาไทยขึ้นจอได้เลย — ไม่ต้องทำอะไรเพิ่ม

นี่คือของดีที่ทำให้ป้ายนี้สนุก: ส่งข้อความเป็นภาษาไทยได้ตรง ๆ ฝั่ง sender ใส่ str ไทยลง MESSAGES ฝั่ง display .decode() แล้วโยนเข้า game.Text จอก็วาดให้ครบ


ซ้าย: หน้าตาป้ายที่โค้ดเราวาด · ขวา (ภาพประกอบของจริง): บอร์ด MCU จริงขับจอ OLED ขึ้นตัวอักษร — หลักการเดียวกับที่บอร์ดเราวาดไทยลงจอ — "TTGO Lora32 Node" — -stk, CC BY-SA 4.0, Wikimedia Commons

ตั้งแต่ firmware v1.2.0 ตัวเครื่องมีฟอนต์ไทย + จัด "สระซ้อน/วรรณยุกต์" (cluster shaping) ให้เอง คำที่วางสระบน-ล่างซ้อนกันก็ขึ้นถูกตำแหน่ง ลองส่งดู: ปั๊ม · เกี๊ยว · ผู้รู้ · เปรี้ยว — ไม้โท ไม้ตรี สระอู สระเอีย วางครบทุกตัว

เราเลยแก้ MESSAGES เป็นไทยล้วนได้สบาย เช่น "สวัสดีครับ", "ทีมแดงนำอยู่", "เริ่มรอบใหม่ได้เลย" — payload ก็ยังเป็น str ธรรมดา MQTT ส่งเป็น UTF-8 bytes ให้อัตโนมัติ ฝั่งรับ .decode() กลับมาเป็นไทยเหมือนเดิม

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

โปรเจกต์นี้ต้องใช้ 2 บอร์ด (หรือจับคู่กับเพื่อน): บอร์ดหนึ่งเป็น sender อีกบอร์ดเป็น display

  1. ต่อ WiFi ผ่านเมนู Wi-Fi บนจอก่อน ทั้งสองบอร์ด (เซฟรหัสไว้ครั้งเดียว auto-connect ทุกครั้ง)
  2. บอร์ด A: เปิด ex_c2_display.py แล้วกด Program to Device → จอขึ้น "รอข้อความเข้า ..."
  3. บอร์ด B: เปิด ex_c2_sender.py แล้วกด Program to Device → เลื่อนจอยเลือก แล้วกด a
  4. ดูบอร์ด A: ข้อความเด้งขึ้นจอตัวใหญ่ทันที


ซ้าย: ผังต่อ 2 บอร์ด (sender + display) · ขวา (ภาพประกอบของจริง): บอร์ดวิทยุ MCU ที่ส่งข้อความหากันข้ามอากาศจริง — สเกลเล็กแบบเดียวกับ 2 บอร์ดของเรา — "Meshtastic T-Beam" — Chiffre01, CC0, Wikimedia Commons

เช็กผ่าน: เลือกข้อความไทยต่างกัน 3 อัน กด a ทีละอัน แล้วบอร์ด display ขึ้นเป็นไทยครบทั้ง 3 (สระ/วรรณยุกต์ต้องวางถูกตำแหน่ง) — ทั้งคู่ต้องใช้ TOPIC = msg/board02 ตรงกันเป๊ะ

ป้ายที่เราทำ คือฉบับเล็กของป้ายในโรงงานจริง


ที่มา: "Andon-Board in einem Produktionswerk" — MicroSYST Systemelectronic GmbH, CC BY-SA 4.0, Wikimedia Commons

นี่คือ ป้าย Andon ในโรงงานจริง แขวนเหนือสายการผลิต โชว์ค่าเป้าหมาย/ค่าจริง (Soll/Ist) กับสถานะไฟเขียว-เหลือง-แดง ของแต่ละไลน์ — ศูนย์ควบคุมตัวเดียวส่งข้อมูลขึ้นทุกป้ายในโรงงาน หลักการเดียวกับที่เราเพิ่งเขียน: publish เข้า topic เดียว ป้ายที่ subscribe ขึ้นพร้อมกัน ต่างกันแค่สเกล

เปลี่ยนข้อความทักทายเป็นตัวเลขยอดผลิต เปลี่ยน game.Text สีเดียวเป็นไฟสถานะ 3 สี โครงโค้ดที่น้องมีอยู่ในมือก็กลายเป็นป้าย Andon ได้เลย

เชื่อมโยงรากฐาน: เกมนี้ฝึกวิศวกรรมอะไรบ้าง

โปรเจกต์เล็ก ๆ นี้แตะรากฐาน 4 ด้านที่ใช้สร้างของจริงครบ:

  • Embedded/MCU — บอร์ด PSoC Edge E84 ต่อ WiFi ผ่าน network stack แล้วคุยกันด้วย TCP/IP พอร์ต 1883 (MQTT บน socket); ฝั่ง display อ่านปุ่มผ่าน GPIO เหมือนคาบ interrupt/polling
  • PythonMESSAGES คือ list เลื่อน index แบบวนด้วย % len(...); mqtt.get_message() คืน tuple (topic, bytes) แล้ว unpack; .decode() แปลง bytesstr (เรื่อง type ที่เจอตั้งแต่คาบแรก)
  • Algorithms — การ poll แบบ non-blocking + จูน T_poll ให้ดีเลย์/โหลดสมดุล คือ pattern เดียวกับลูปอ่านเซนเซอร์; topic routing คือการ "จับคู่ที่อยู่" แบบง่าย ๆ
  • Graphicsgame.Text วาดสตริงไทยลงเฟรมบัฟเฟอร์ พร้อม cluster shaping (จัดสระ/วรรณยุกต์ตามพิกัด) ก่อนส่งขึ้นจอ

เห็นไหมว่า "ป้ายไฟข้ามห้อง" สนุก ๆ จริง ๆ แล้วคือ network + โครงสร้างข้อมูล + การวาดภาพ ทำงานด้วยกัน — พอจับหลักได้ น้องต่อยอดไปทำผลิตภัณฑ์ IoT จริงได้เลย

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

ของวันนี้คือโครงเดียวกับ ป้ายไฟอุตสาหกรรม และ เกม multiplayer — เปลี่ยนแค่ payload กับ topic เท่านั้นเอง

ต่อยอดที่ลองได้ (เลือก 1):

  • เพิ่มข้อความไทยของห้องเราเองลง MESSAGES ลองคำที่มีสระซ้อนเยอะ ๆ ดูว่าจอวางครบไหม
  • ใส่ชื่อผู้ส่งไปกับข้อความ เช่น "ทีมแดง:สู้ ๆ" แล้วฝั่ง display split(":") โชว์ว่าใครส่ง
  • ทำหลายป้าย: msg/board02, msg/board03 แล้วเลือกปลายทางด้วยปุ่ม b

ทั้ง 3 ข้อใช้ โครงเดิม ทั้งหมด แตะแค่ payload กับ topic เท่านั้น — ไม่ต้องเขียนระบบเชื่อมต่อใหม่:

โครงเดิม (เหมือนกันหมด) connect → subscribe publish(TOPIC, payload) get_message().decode() 1) ข้อความห้องเราเอง เปลี่ยน payload → ไทยสระซ้อน · topic เดิม 2) ใส่ชื่อผู้ส่ง payload = "ทีมแดง:สู้ ๆ" → ฝั่งรับ split(":") 3) หลายป้าย เปลี่ยน topicmsg/board03 · เลือกด้วยปุ่ม b

โยงโลกจริง:

  • Industry 4.0 — ป้าย Andon บอกสถานะสายการผลิต สั่งจากศูนย์ควบคุมตัวเดียว
  • IoT — จอแจ้งเตือน/ป้ายคิวในร้าน ทั้งหมดคือ pub/sub บน MQTT แบบที่เราเพิ่งทำ
  • multiplayer — เปลี่ยน payload เป็นตำแหน่งผู้เล่น ก็กลายเป็นเกมข้ามบอร์ดได้เลย

น้อง ๆ เพิ่งทำสิ่งที่โรงงานจริงใช้กันทุกวัน ด้วยโค้ดไม่ถึง 70 บรรทัด — นี่คือพลังของ MQTT

fit-css

← Extra Index