Extra — รีโมตคุม LED/เสียงข้ามบอร์ด

กดปุ่มที่บอร์ดหนึ่ง → ไฟติด + มีเสียง ที่อีกบอร์ด

โปรเจกต์เสริม (WiFi + MQTT) — ต่อยอดจากคาบ 13
ใช้ 2 บอร์ด · 1 รีโมต + 1 ปลายทาง · topic เดียว: cmd/led


ที่มา: "Mqtt-hor.svg" — OASIS, Public domain, Wikimedia Commons

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

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

เราจะทำ รีโมตไร้สาย ง่าย ๆ ด้วยของที่มีอยู่แล้ว: ปุ่ม + LED + ลำโพง + WiFi

บอร์ด A = รีโมต (sender)
มีปุ่มเดียว = SW1 (ใช้เป็น toggle)
กด SW1 สลับ "on"/"off"
ตัวมันเองไม่เปิดไฟ แค่ "สั่ง"
บอร์ด B = ปลายทาง (actuator)
ฟัง topic ตลอด
ได้ "on" → ไฟติด + เสียง
ได้ "off" → ไฟดับ + เสียง
A — คนสั่ง (sender) B — คนทำ (actuator) cmd/led: "on"/"off" สั่งทางเดียว — A ไม่ต้องรู้ว่า B เป็นใคร

ของจริงรอบตัว: สวิตช์ไฟอัจฉริยะในบ้าน, ปุ่มฉุกเฉินสั่งหยุดสายพานในโรงงาน (Industry 4.0), รีโมตเปิด-ปิดอุปกรณ์ — ทั้งหมดคือ "กดที่นี่ ทำงานที่โน่น" แบบเดียวกับที่เราจะทำเลย


ที่มา: "TP-Link Kasa Smart Home WLAN-Steckdose HS110" — www.digitalpush.net, CC BY 4.0, Wikimedia Commons · ปลั๊ก WiFi สั่งเปิด-ปิดจากมือถือ = "กดที่นี่ ทำงานที่โน่น" ของจริง

เกร็ด: ทำไม embedded สำคัญ — Genesis: แบ่งงานให้ชิปที่ถนัด

เครื่องเกมเก่งขึ้นด้วยการ partitioning — แบ่งงานให้ชิปที่ถนัด (Genesis: 68000 ทำเกม, Z80 ทำเสียง)

เชื่อมกับวันนี้: ระบบกระจายของน้อง (บอร์ดหนึ่ง ส่ง อีกบอร์ด เล่นเสียง/คุม LED) คือ partitioning ข้ามเครื่อง — แนวคิดเดียวกันในสเกลใหญ่ขึ้น


ที่มา: "Sega-Genesis-Mk2-Motherboard-01.jpg" — Evan-Amos, Public domain, Wikimedia Commons · เปิดฝาเครื่องดูจริง: ชิปคนละตัวรับงานคนละอย่าง (CPU เกม + ชิปเสียงแยกกัน) คือ partitioning ที่จับต้องได้

ที่มา: en.wikipedia.org/wiki/Sega_Genesis

ไดอะแกรม pub/sub: topic cmd/led

รีโมตไม่ได้ต่อสายตรงไปหาไฟ มันแค่หย่อนคำสั่งเข้า topic กลาง แล้วใครฟัง topic นั้นก็ได้รับ

บอร์ด A — รีโมต กดปุ่ม SW1 (toggle) publish "on"/"off" Broker test.mosquitto.org cmd/led บอร์ด B — ปลายทาง subscribe cmd/led LED + เสียง publish deliver payload คือ string สั้น ๆ: "on" หรือ "off" เท่านั้น — ทั้งสองฝั่งตกลงกันไว้

หัวใจคือ topic + payload: ตกลงชื่อ topic เดียวกัน (cmd/led) และรูปแบบข้อความเดียวกัน (on/off) แค่นี้สองบอร์ดก็คุยรู้เรื่อง

ดูคำสั่งวิ่งข้ามอากาศ

กด SW1 ที่บอร์ด A → คำสั่งเป็น "ก้อนข้อความสั้น ๆ" วิ่งเข้า broker แล้วถูกส่งต่อให้บอร์ด B ไฟจึงติด/ดับตามคำสั่ง

สังเกตว่าบอร์ด A กับ B ไม่ได้รู้จักกันเลย — ต่างฝ่ายต่างคุยกับ broker เท่านั้น นี่คือหัวใจของ pub/sub: ผู้ส่งกับผู้รับ แยกขาดจากกัน (decoupled) เพิ่ม-ลดบอร์ดปลายทางได้โดยฝั่งรีโมตไม่ต้องแก้อะไรเลย

04 Subscribe To MQTT Topic To Control Led Using ESP32 — Age of Robotics — บอร์ดปลายทาง subscribe topic แล้วสั่งไฟติด/ดับตามข้อความ ตรงกับรีโมตคุม LED ที่เราทำวันนี้

เดินโค้ดฝั่งรีโมต (sender)

ต่อ WiFi → ต่อ broker → แล้ววนอ่าน ปุ่มเดียว (SW1) ใช้เป็น toggle ส่งคำสั่งเฉพาะ จังหวะเพิ่งกด (กันส่งรัวตอนกดค้าง)

    sw    = gpio.button(0)             # บอร์ดนี้มีปุ่มเดียว = SW1
    prev  = sw.is_pressed()
    state = False                      # สถานะไฟปลายทางที่เราจำไว้ (เริ่ม "ปิด")

    while True:
        now = sw.is_pressed()
        # จับขอบขาลง (ปล่อย -> กด) แล้วสลับสถานะ on<->off
        if now and not prev:
            state = not state
            cmd = "on" if state else "off"
            mqtt.publish(TOPIC, cmd)
            print("ส่งคำสั่ง ->", cmd)
        prev = now
        time.sleep_ms(20)

prev คือเทคนิค edge detection เดียวกับคาบปุ่ม FSM — จับ "เพิ่งกด" ไม่ใช่ "กำลังกดค้าง" · ปุ่มเดียวก็คุมได้ครบเพราะแต่ละครั้งที่กด เราสลับ state แล้วส่งค่าใหม่

แต่ละช่อง = 1 รอบลูป (20 ms) — เส้นบนคือ now, ลูกศรชี้รอบที่ now and not prev เป็นจริง HIGH LOW publish! publish! กดค้าง — ไม่ส่งซ้ำ

ภาพนี้คือสัญญาณปุ่มจริง ๆ ทีละรอบลูป: ตอนกดค้าง now เป็น HIGH หลายรอบติด แต่ now and not prev เป็นจริง แค่รอบแรก (วงแดง) เท่านั้น — จึง publish ครั้งเดียวต่อการกดหนึ่งครั้ง ไม่ท่วม broker

มองเป็น state machine: ปุ่มเดียวสลับสองสถานะ

ตัวแปร state มีแค่สองค่า (False/True) ทุกครั้งที่จับ ขอบขาลง (now and not prev) เราพลิกสถานะแล้วส่งคำสั่งที่ตรงกับสถานะใหม่

ทำไมต้องจับ "ขอบ" ไม่ใช่ "ระดับ"? ปุ่มหนึ่งครั้งที่นิ้วเรากดค้างไว้ จริง ๆ แล้วลูปวนผ่านหลายรอบ (รอบละ 20 ms) ถ้าเช็กแค่ "กดอยู่ไหม" มันจะส่ง on รัว ๆ ทุกรอบจนท่วม broker · เก็บค่ารอบก่อน (prev) ไว้เทียบ จึงรู้ว่า "นี่คือรอบแรกที่เพิ่งกด" แล้วส่งแค่ครั้งเดียว

เดินโค้ดฝั่งปลายทาง (actuator)

subscribe หนึ่งครั้ง แล้ววน get_message() ซึ่ง ไม่บล็อก — ได้คำสั่งมาก็ลงมือทำ

    mqtt.subscribe(TOPIC)              # ขอฟังคำสั่งทั้งหมดที่เข้า topic นี้

    while True:
        msg = mqtt.get_message()       # (topic, payload) bytes หรือ None
        if msg is not None:
            topic, data = msg
            command = data.decode()    # payload เป็น bytes ต้อง .decode() ก่อน
            if command == "on":
                led.on()
                game.sfx("select")     # เสียงตอบรับ "เปิดแล้ว"
            elif command == "off":
                led.off()
                game.sfx("back")       # เสียงตอบรับ "ปิดแล้ว"
        time.sleep_ms(50)

อย่าลืม .decode() — payload ที่ได้มาเป็น bytes (เช่น b"on") เทียบกับ string ตรง ๆ ไม่ติด

get_message() ไม่บล็อก None? มีข้อความ? data.decode() bytes → str "on" → led.on() "off" → led.off() + game.sfx() ใช่ (topic,payload) None → วนต่อ (sleep 50 ms) ไม่ค้างรอ

โครงนี้คือหัวใจของ "ไม่บล็อก": ถ้า get_message() คืน None ก็แค่วนกลับไปทำงานอื่นต่อ ไม่ยืนค้างรอข้อความ — ได้ข้อความเมื่อไรค่อย .decode() แล้วสั่งงาน LED/เสียง

เสียงในคาบนี้ — ปลายทางใช้เสียง UI สั้น ๆ ตอบรับคำสั่ง: ได้ "on" เล่น select, ได้ "off" เล่น back ให้ผู้ใช้รู้ว่าคำสั่งถึงแล้ว (กดเล่นฟังได้จริง):

ui_select (เปิด)

ui_back (ปิด)

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

ทำไม "กดแล้วติดทันที"? — งบเวลา (latency)

เวลาตั้งแต่นิ้วกดปุ่มฝั่ง A จนไฟติดฝั่ง B ไม่ใช่ศูนย์ มันคือผลรวมของหลายช่วงต่อกัน:

tรวม=tscan+tup+tbroker+tdown+tactt_{\text{รวม}} = t_{\text{scan}} + t_{\text{up}} + t_{\text{broker}} + t_{\text{down}} + t_{\text{act}}

แต่ละพจน์มาจากโค้ดที่เราเพิ่งเขียนตรง ๆ: t_scan คือคาบลูปปุ่มฝั่ง sender (20ms\le 20\,\text{ms} จาก sleep_ms(20)), t_up/t_down คือเวลาวิ่งบน WiFi ขาขึ้น/ขาลง, t_broker คือเวลา broker ส่งต่อ, ส่วน t_act คือคาบ poll ของ get_message() ฝั่งปลายทาง (50ms\le 50\,\text{ms} จาก sleep_ms(50))

รวมแล้วราว ๆ 128ms<15s128\,\text{ms} < \tfrac{1}{5}\,\text{s} — สายตาคนเรารับรู้ความหน่วงราว 100150ms100\text{–}150\,\text{ms} ขึ้นไป จึง "รู้สึก" เหมือนกดแล้วติดทันที · อยากให้ไวขึ้นอีก? ลด sleep_ms(50) ฝั่งปลายทาง พจน์ tactt_{\text{act}} จะเล็กลง แต่แลกกับ CPU ที่วน poll ถี่ขึ้น

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

ใช้ 2 บอร์ด (คู่กันสนุกสุด): บอร์ดหนึ่งเป็นรีโมต อีกบอร์ดเป็นปลายทาง

  1. ต่อ WiFi 2.4GHz ผ่าน เมนู Wi-Fi บนจอ ทั้งสองบอร์ดก่อน (เซฟรหัสไว้ครั้งเดียว)
  2. บอร์ด B: เปิด ex_c1_actuator.py กด Program to Device → รอขึ้น "พร้อมรับคำสั่ง"
  3. บอร์ด A: เปิด ex_c1_sender.py กด Program to Device
  4. กด SW1 ที่บอร์ด A → ไฟบอร์ด B ติด + มีเสียง · กด SW1 อีกครั้ง → ดับ (ปุ่มเดียวสลับไปมา)


ปุ่ม Program to Device มุมซ้ายบนของ BENTO IDE (ขั้นตอน 2–3) — กดแล้วเซฟเป็น main.py ลงบอร์ดและรันทันที · ที่มา: BENTO IDE (course asset)

มีบอร์ดเดียวก็เทสต์ได้: รัน actuator บอร์ดเดียว แล้วใช้แอป MQTT บนมือถือ publish on/off เข้า topic cmd/led ดูไฟตอบสนอง

ทั้งสองไฟล์ตั้ง TOPIC = "cmd/led" ตรงกัน และ client_id ไม่ซ้ำกัน (remote-sender กับ remote-actuator)

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

โครงนี้เล็กมาก แต่เป็น แกนเดียวกับระบบ IoT จริง — เปลี่ยนแค่ payload กับปลายทาง

  • บ้านอัจฉริยะ: เปลี่ยน led.on() เป็นสั่งรีเลย์ → เปิด-ปิดปลั๊ก/หลอดไฟจริงจากมือถือ
  • Industry 4.0: ปุ่มฉุกเฉิน publish "stop" → เครื่องจักรทุกตัวที่ subscribe หยุดพร้อมกัน
  • คำสั่งหลายแบบ: ขยาย payload เป็น "on"/"off"/"blink"/"red" แล้วแตก if/elif เพิ่ม
  • มัลติเพลเยอร์: สองบอร์ดส่ง "on"/"off" หากันได้ทั้งคู่ (ต่างคนต่างเป็นทั้ง sender + actuator) → กลายเป็นปุ่มสั่งงานข้ามบอร์ดแบบสองทาง


ที่มา: "Emergency stop button" — Cjp24, CC BY-SA 3.0, Wikimedia Commons · ปุ่มฉุกเฉินในโรงงานคือ sender ตัวจริง — กดทีเดียว publish "stop" ให้เครื่องจักรทุกตัวหยุดพร้อมกัน

น้อง ๆ ลองคิดดูว่าอุปกรณ์รอบตัวที่ "กดที่นี่ ทำงานที่โน่น" มีอะไรบ้าง แล้วลองทำให้บอร์ดเราเป็นแบบนั้น

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

โปรเจกต์เล็ก ๆ นี้รวมรากฐานสี่ด้านเข้าด้วยกัน ทุกอย่างที่ใช้คือเครื่องมือจริงของวิศวกรฝังตัว ไม่ใช่ลูกเล่นของเล่น

  • Embedded — GPIO เข้า/ออก: ฝั่งรีโมตอ่านปุ่มเป็น digital input ฝั่งปลายทางสั่ง LED เป็น digital output สองหน้าที่พื้นฐานที่สุดของ MCU คนละบอร์ดแต่ทำงานประสานกันผ่านเครือข่าย
  • Python — callback + edge detection: เก็บค่ารอบก่อน (prev) ไว้เทียบกับรอบนี้ คือเทคนิคเดียวกับ FSM ปุ่ม ใช้จับ "เพิ่งกด" บน input แทบทุกชนิด (สวิตช์ rotary encoder ฯลฯ)
  • อัลกอริทึม — pub/sub decoupling: ผู้ส่งกับผู้รับไม่รู้จักกัน คุยผ่าน topic กลาง เป็นรูปแบบสถาปัตยกรรมที่ระบบกระจายจริง (IoT, message queue) ใช้เพื่อให้ขยายสเกลได้โดยไม่ต้องแก้ของเดิม
  • กราฟิก/UX — feedback ทันใจ: ไฟติด + เสียงตอบรับภายใน ~128 ms ทำให้ผู้ใช้ "รู้สึก" ว่าคำสั่งถึงปลายทางแล้ว หลักการ latency budget เดียวกับที่ออกแบบ UI ทุกระบบที่ต้องตอบสนองเร็ว
ผลิตภัณฑ์ IoT หนึ่งเดียว Embedded GPIO เข้า/ออก Python edge detection Algorithm pub/sub decoupling กราฟิก/UX latency / feedback

จากเกม "กดที่นี่ ไฟติดที่โน่น" น้องได้ครบทั้ง GPIO, การจัดการ input ใน Python, สถาปัตยกรรมระบบกระจาย และการออกแบบ feedback ที่ตอบสนองทันใจ — สี่เสาที่อยู่ในผลิตภัณฑ์ IoT จริงทุกตัว

fit-css

← Extra Index