คาบ 12 — ผล AI ขึ้น MQTT ทันที

ส่งผลออกไป และรับคำสั่งกลับมาอย่างปลอดภัย

คาถาประจำคาบ: ส่งเมื่อมีเรื่องจะบอก — และรับคำสั่งเฉพาะที่เรารู้จัก

ดูของจริงก่อน — เขย่าบอร์ด แล้วผลโผล่บนคอมของเพื่อน

คาบ 11 เราอ่านผล AI ได้แล้ว วันนี้ส่งมันออกไปให้คนอื่นเห็น

  1. เปิด examples/s14/01_ai_to_mqtt.py ตั้ง TEAM ของคุณ แล้วกด Run
  2. อีกเครื่องรัน mosquitto_sub ฟังหัวข้อของทีม
  3. เขย่าบอร์ด ดู JSON ขึ้นที่เครื่องนั้น

ทำไม · คืออะไร · ทำยังไง — แผนที่ของคาบนี้

ทำไม AI ที่บอกอยู่แค่บนจอบอร์ด ไม่มีใครที่ไกลออกไปรู้ ส่งผลออกไปได้ แดชบอร์ดและแอปถึงจะใช้ได้

คืออะไร ท่อสามช่วง: อ่านผล AI → ทำเป็น JSON → mqtt.publish()
แล้วมีท่อย้อนกลับ: รับคำสั่งจาก .../ai/cmd → เปลี่ยนโมเดล

ทำยังไง สามตัวอย่าง: 01 ส่งสถานะ · 02 แยกสถานะกับเหตุการณ์เตือน · 03 รับคำสั่งกลับอย่างปลอดภัย
แล้วต่อด้วย 04 ส่งผลของจอ Smart HMI (คาบ 11) ขึ้น MQTT และดูผลทั้งห้องบน แดชบอร์ดเว็บ

เป้าหมายของคาบนี้

  1. ต่อท่อ อ่านผล AI → JSON → mqtt.publish() ไปที่ bento-aiot/<ทีม>/ai
  2. ออกแบบว่า "ส่งเมื่อไร": เมื่อป้ายเปลี่ยน + heartbeat + ช่วงห่างขั้นต่ำ ไม่ส่งทุกผลดิบ
  3. แยก สถานะ กับ เหตุการณ์เตือน (ใช้ k ใน n จากคาบ 11)
  4. รับคำสั่งกลับอย่างปลอดภัย: ทำตามเฉพาะชื่อที่รู้จักบนบอร์ดจริง
  5. ดูผลบนคอมด้วย mosquitto_sub และลองสั่งด้วย mosquitto_pub
  6. ส่งผลของจอ Smart HMI ขึ้น MQTT แล้วดูบนแดชบอร์ดเว็บ

ต่อจากคาบ 10 — แต่คราวนี้ใช้ broker สาธารณะ

คาบ 10 เราใช้ broker ในวง LAN (พอร์ต 1883) กับหัวข้อ bento/team03/...

คาบนี้กลับไปใช้ broker.hivemq.com กับหัวข้อ bento-aiot/<ทีม>/... แบบคาบ 2 ถึง 4 เพราะเราต้องการให้เครื่องอื่นฟังได้โดยไม่ต้องอยู่วงเดียวกัน

คาบ 10 คาบ 12
broker ในวง LAN broker.hivemq.com
หัวข้อ bento/team03/... bento-aiot/<ทีม>/...
ใครฟังได้ เครื่องในห้อง ใครก็ได้ที่รู้ชื่อหัวข้อ

ข้อสุดท้ายสำคัญมาก เราจะกลับมาที่มันตอนรับคำสั่ง

หน้าตาของข้อความ — JSON สั้น ๆ ที่คนอ่านออก

{"id": "team05", "n": 12, "model": "AnomalousVibration", "label": "anomaly", "conf": 94}
  • id คือ ชื่อทีม ไม่ใช่ id ของโมเดล · n คือตัวนับข้อความ
  • ส่งเฉพาะ ชื่อ โมเดล ไม่ส่ง id ภายในของบอร์ด
  • conf เป็นเปอร์เซ็นต์จำนวนเต็ม: int(conf * 100)
  • หัวข้อสถานะ: bento-aiot/<ทีม>/ai

ส่งเมื่อไร — สามข้อที่ต้องตัดสินใจ

โมเดลตอบได้หลายครั้งต่อวินาที และ broker สาธารณะใช้ร่วมกันทั้งห้อง ถ้าส่งทุกผลดิบ เราทำให้ทุกคนช้า

def should_send(changed, since_ms):
    # ห่างพอ และ (ป้ายต่างจากที่ส่งล่าสุด หรือ ถึง heartbeat)
    if since_ms < GAP_MS:
        return False
    return changed or since_ms >= ALIVE_EVERY_MS
  1. ส่งเมื่อ ป้ายเปลี่ยน
  2. ส่ง heartbeat ทุก ALIVE_EVERY_MS ถึงป้ายไม่เปลี่ยน เพื่อให้ปลายทางรู้ว่าบอร์ดยังมีชีวิต
  3. ห่างกันอย่างน้อย GAP_MS ไม่ว่ากรณีใด

แกะโค้ดจริง — 01 ต่อ broker แล้วส่ง

def go_online(say):
    if not team_ok():
        say("Set TEAM first (team01-team99) - offline", COL_WARN)
        return False
    try:
        if not wifi_up(say):
            return False
        say("Connecting broker ...", COL_WARN)
        if mqtt.connect(BROKER, port=1883, client_id=CLIENT_ID, keepalive=60):
            say("Online: " + TOPIC, COL_OK)
            return True
    except OSError as e:
        print("network:", e)
    say("Broker failed - offline", COL_BAD)
def send(online, obj):
    line = json.dumps(obj)
    print(line)
    if online:
        try:
            return bool(mqtt.publish(TOPIC, line))
        except OSError:
            pass

client_id ซ้ำ = broker เตะตัวเก่าหลุด เราจึงต่อท้ายด้วยเลขจากนาฬิกาบอร์ด และ mqtt.connect() คืน True/False ต้องตรวจเสมอ

ถ้า WiFi หรือ broker ไม่พร้อม ตัวอย่างยังทำงานต่อ

ตั้งแต่ตัวอย่าง 01 เราเขียนให้ ทำงานออฟไลน์ได้

  • ยังไม่แก้ TEAM = ไม่ต่อ broker พิมพ์ JSON ลง Console แทน
  • ต่อ broker ไม่ได้ = ไฟ MQTT บนจอไม่เป็นสีเขียว แต่ AI ยังรันต่อ
  • ส่งไม่สำเร็จ = send() คืน False โปรแกรมไม่ล้ม

เหตุผล: เครือข่ายเสียได้เสมอ ระบบ AI ไม่ควรหยุดเพราะ broker หลุดครั้งหนึ่ง

สถานะ กับ เหตุการณ์เตือน — คนละหัวข้อ

หัวข้อ ส่งเมื่อไร เนื้อหา
bento-aiot/<ทีม>/ai ป้ายเปลี่ยน + heartbeat ผลล่าสุด + "alarm": 0/1
bento-aiot/<ทีม>/event ครั้งเดียวตอนเริ่มเตือน และตอนหายเตือน ai_alarm_start / ai_alarm_end

เหตุการณ์ใช้กฎ k ใน n ของคาบ 11 ไม่ใช่ผลดิบผลเดียว
หัวข้อ event เป็นหัวข้อเดิมของทีม แดชบอร์ดของห้องฟังอยู่แล้ว ชื่อขึ้นต้น ai_ เพื่อไม่ชนกับ event อื่น

แกะโค้ดจริง — 02 ส่งเหตุการณ์เป็นคู่เริ่ม/หาย

def event_msg(start, name, lab, conf):
    return {"id": TEAM, "event": "ai_alarm_start" if start else "ai_alarm_end",
            "model": name, "label": lab, "conf": conf}


def build_screen(name):

ลองเล่น: วางนิ่ง → เขย่าแรงค้าง 3 วินาที → วางนิ่ง แล้วนับว่าการเขย่าหนึ่งครั้งได้ event กี่คู่

ฝั่งคอม: mosquitto_sub -h broker.hivemq.com -t 'bento-aiot/<ทีม>/#' -v

ท่อย้อนกลับ — แอปสั่งเปลี่ยนโมเดล

แอป / mosquitto_pub  ──►  bento-aiot/<ทีม>/ai/cmd  ──►  บอร์ด
{"cmd":"select","model":"HumanActivity"}      {"cmd":"stop"}      {"cmd":"run"}
บอร์ด  ──►  bento-aiot/<ทีม>/ai  (ส่งสถานะใหม่ทันที ช่อง model = คำตอบว่าทำแล้ว)

บอร์ดรับคำสั่งด้วย mqtt.get_message() ที่ถามในลูปหลัก ไม่ใช่ callback

รับคำสั่งแบบไม่ไว้ใจใคร — 03 กรองทุกช่อง

def known_models():
    # {ชื่อ: ลำดับ} เฉพาะชื่อใน ALLOWED ที่อยู่บนบอร์ดตอนนี้
    return {m["name"]: m["index"] for m in edge_ai.models() if m["name"] in ALLOWED}
def parse_cmd(raw, known):
    # คืน ("model", ชื่อ) / ("stop",) / ("run",) หรือ (None, เหตุผล)
    if len(raw) > MAX_CMD_BYTES:
        return None, "too long"
    try:
        c = json.loads(raw.decode())
    except Exception:            # ข้อความจากใครก็ได้: พังแบบไหนก็ต้องไม่ล้มทั้งโปรแกรม
        return None, "not JSON"
    if not isinstance(c, dict):
        return None, "not a JSON object"
    cmd = c.get("cmd")
    if cmd == "select":          # เช็กชนิดก่อน: {"model": [1]} จะทำให้ in known ล้ม
        m = c.get("model")
        return ("model", m) if isinstance(m, str) and m in known else (None, "unknown model")
    if cmd in ("stop", "run"):
        return (cmd,)
    return None, "unknown command"

ทำตามเฉพาะชื่อที่อยู่ใน ALLOWED และมีบนบอร์ดจริง นอกนั้นปฏิเสธพร้อมเหตุผลบนจอ

ทำไมต้องกรองหนักขนาดนี้

broker สาธารณะ ไม่เข้ารหัส ใครรู้ชื่อหัวข้อก็ส่งคำสั่งเข้ามาได้

  • กรองทุกช่องของ JSON ไม่ไว้ใจประเภทและความยาว
  • รับคำสั่งห่างกันอย่างน้อย CMD_GAP_MS กันถูกยิงถี่
  • บอร์ดเก็บข้อความรอไว้ แค่ข้อความเดียว ข้อความใหม่ทับของเก่า
  • ถ้ามีคนส่งแบบ retain ไว้ คำสั่งนั้นจะมาใหม่ทุกครั้งที่รันโปรแกรม
  • select() ช้าได้หลายวินาที จอค้างช่วงนั้น จึงขึ้นป้ายก่อนเรียก

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

ดูและสั่งจากฝั่งคอม

ฟังทุกอย่างของทีม

mosquitto_sub -h broker.hivemq.com -t 'bento-aiot/<ทีม>/#' -v

สั่งเปลี่ยนโมเดล

mosquitto_pub -h broker.hivemq.com -t 'bento-aiot/<ทีม>/ai/cmd' \
  -m '{"cmd":"select","model":"HumanActivity"}'

Emulator: ถ้าใช้ broker.hivemq.com (หรือ test.mosquitto.org, broker.emqx.io) Emulator ต่อ broker จริงผ่าน WebSocket และต่อท้าย client id ด้วย -emu ข้อความจึงออกไปจริง ถ้าต่อไม่ได้ในราว 5 วินาที Console จะบอก แล้วใช้ broker จำลองในเบราว์เซอร์แทน

ผลจริงบนบอร์ด Eva Kit 1.7.8 — แบบออฟไลน์

01 ยังไม่ตั้ง TEAM จึงพิมพ์ JSON ลงจอแทน · ถึง broker 0

02 ส่งสถานะไป 8 ครั้ง ยังไม่มี event

03 รันอยู่ ยังไม่มีคำสั่งเข้ามา

04 จอ Smart HMI ชุด machine พร้อม JSON ล่าสุด
ทุกจอขึ้น "Set TEAM first ... offline" เพราะตอนทดสอบยังไม่ได้แก้ TEAM โปรแกรมจึงทำงานต่อแบบออฟไลน์ตามที่ออกแบบไว้

Smart HMI ขึ้น MQTT

ไฟล์เดียวทั้งห้อง examples/s14/04_hmi_to_mqtt.py

ทุกทีมส่ง JSON หน้าตาเดียวกัน แดชบอร์ดเว็บจึงอ่านได้แบบเดียว

04 — แก้สองบรรทัด แล้วกด Run

TEAM = "teamXX"                  # เลขทีมที่ผู้สอนแจก เช่น team05
KIT = "machine"                  # machine (06) | safety (07) | nursery (08) | voice (09) - ตรงกับไฟล์ของคาบ 11
  • KIT เลือกจอ Smart HMI ชุดเดียวกับที่ทีมลองในคาบ 11
  • บอร์ดที่ต่อ WiFi ที่บันทึกไว้อยู่แล้ว ไม่ต้องแก้ WIFI_SSID WIFI_PASS
  • ยังไม่แก้ TEAM = ทำงานออฟไลน์ พิมพ์ JSON ลง Console แทน
  • กดปุ่มบนจอ = state เปลี่ยน = ส่งทันที แดชบอร์ดเห็นว่ามีคนรับทราบแล้ว
  • ส่งเมื่อ label / state / events เปลี่ยน + heartbeat ทุก 2 วินาที และห่างกันอย่างน้อย 0.2 วินาที

หน้าตาของข้อความจาก Smart HMI

{"id": "team05", "kit": "machine", "model": "AnomalousVibration", "label": "unlabeled",
 "conf": 100, "state": "normal", "events": 0, "n": 1033}
ช่อง ความหมาย
id ชื่อทีม ไม่ใช่ id ของโมเดล
kit machine · safety · nursery · voice
state normal ปกติ · alert ยืนยันแล้ว ยังไม่มีใครรับ · ack มีคนรับทราบแล้ว
events เข้าสู่ alert มาแล้วกี่ครั้ง
n seq ของผล AI ล่าสุด (ไม่ใช่ตัวนับข้อความ)

หัวข้อเดิม: bento-aiot/<ทีม>/ai · ใน 04 ชุด voice นับคำว่า stop เป็นเหตุ (หยุดฉุกเฉินด้วยเสียง) ส่วนจอบ้านของ 09 ในคาบ 11 ใช้ state on/off

แดชบอร์ดเว็บ — เห็นผล AI ของทั้งสี่ชุดในหน้าเดียว

ทดสอบกับ broker.hivemq.com จริง โดยใช้โปรแกรมบนคอมส่งข้อความแทนบอร์ด (ทีม team95)

ใช้แดชบอร์ด ทีละขั้น

  1. เปิดไฟล์ examples/s14/web/ai_dashboard.html ด้วย Chrome หรือ Edge
  2. ช่อง ทีม (TEAM) พิมพ์ชื่อทีมเดียวกับ TEAM ในบอร์ด
  3. ช่อง broker เลือก broker.hivemq.com (หลัก)
  4. กด เชื่อมต่อใหม่ รอจนป้ายเป็น เชื่อมต่อแล้ว สีเขียว
  5. ดูตาราง "4 ชุด Smart HMI ตอนนี้เป็นอย่างไร" แตะแถวเพื่อเลื่อนไปที่การ์ดของชุดนั้น
  6. ส่งคำสั่งกลับ (ใช้กับตัวอย่าง 03): เลือกโมเดล แล้วกด เปลี่ยนโมเดล · หยุด · รัน

หน้าเว็บต่อ broker ผ่าน MQTT over WebSocket (wss://broker.hivemq.com:8884/mqtt) เพราะเบราว์เซอร์ต่อพอร์ต 1883 ตรง ๆ ไม่ได้

ชุดไหนเงียบเกิน 10 วินาที แดชบอร์ดขึ้น ออฟไลน์ (บอร์ดส่ง heartbeat ทุก 2 วินาที)

หน้าเดียวกันบนจอมือถือ

แดชบอร์ดจัดหน้าใหม่เป็นแนวตั้งเองเมื่อจอแคบ

  • ช่องทีม broker และปุ่มเชื่อมต่ออยู่บนสุด
  • การ์ดของแต่ละชุดเรียงลงมา: ผลล่าสุด ความมั่นใจ จำนวนเหตุการณ์ ข้อความลำดับที่เท่าไร

ใช้ตอนเดินดูเครื่องหรือห้องจริง โดยไม่ต้องอยู่หน้าคอม

หน้าเว็บก็ต้องไม่ไว้ใจข้อความ เหมือนบอร์ด

broker สาธารณะไม่เข้ารหัส ใครรู้ชื่อหัวข้อก็ส่งข้อความเข้ามาได้ ทั้งไปที่บอร์ดและไปที่หน้าเว็บ

  • แดชบอร์ดใส่ทุกค่าจาก broker ลงจอเป็น ตัวหนังสือ (textContent) ไม่เคยแปลงเป็น HTML
  • ในภาพทดสอบ แถว generic มีข้อความ <img src=x onerror=alert(1)> ที่ส่งเข้ามาเพื่อลองโจมตี หน้าเว็บแสดงเป็นตัวหนังสือเฉย ๆ ไม่มีอะไรทำงาน
  • ส่งคำสั่งแบบ QoS 0 และ ไม่ retain ไม่งั้นคำสั่งจะกลับมาใหม่ทุกครั้งที่บอร์ดเริ่มโปรแกรม
  • ส่งคำสั่งห่างกันอย่างน้อย 3 วินาที
  • บอร์ดยังทำตามเฉพาะชื่อที่อยู่ใน ALLOWED และมีบนบอร์ดจริง (ตัวอย่าง 03)

งานในคาบ — แผน 3 ช่วง

ช่วงที่ 1 (ทำตาม) ตั้ง TEAM รัน 01 ดูข้อความที่ mosquitto_sub ปรับ GAP_MS แล้วนับจำนวนข้อความต่อนาที

ช่วงที่ 2 (ปรับ) รัน 02 ปรับเกณฑ์เตือน เขย่า 3 ครั้ง นับ event ที่ได้

ช่วงที่ 3 (ท่อของทีม) รัน 03 ให้เพื่อนในทีมสั่งเปลี่ยนโมเดลด้วย mosquitto_pub แล้วลองส่งคำสั่งที่ผิด (ชื่อไม่รู้จัก, JSON เสีย) ดูว่าบอร์ดปฏิเสธอย่างไร

ต่อยอด (ถ้ามีเวลา) รัน 04 กับจอ Smart HMI ชุดของทีม แล้วเปิดแดชบอร์ดเว็บดูผลของทีมเอง

สถานะการทดสอบ — พูดตรง ๆ

  • Eva Kit เฟิร์มแวร์ 1.7.8: ผ่าน (2026-10-15) ตัวอย่าง 01 ถึง 03 แบบ ออฟไลน์ · 04 (2026-10-16) ออฟไลน์ พิมพ์ JSON ถูกรูป
  • Dev Kit เฟิร์มแวร์ 2.4.4: 01 ถึง 03 ผ่าน 3 ใน 3 รอบทางปุ่ม Run ของ BENTO IDE แบบ ออฟไลน์ (บอร์ดไม่ได้ต่อ WiFi) · 04 ยังไม่ได้ทดสอบบน Dev Kit
  • แดชบอร์ดเว็บ: ทดสอบกับ broker.hivemq.com จริง โดยใช้โปรแกรมบนคอมส่งข้อความแทนบอร์ด ปุ่มเปลี่ยนโมเดลส่งคำสั่งถึง broker จริง
  • ยังไม่ได้ทดสอบ: บอร์ดจริงส่งขึ้น broker จริง และบอร์ดรับคำสั่งจาก broker จริง
  • Emulator: ต่อ broker.hivemq.com จริงได้ (ดูสไลด์ "ดูและสั่งจากฝั่งคอม")

ผู้สอนต้องรันสามไฟล์นี้กับ broker จริงหนึ่งรอบก่อนสอนจริง

สรุปคาบ 12

  • ท่อ: อ่านผล AI → JSON → mqtt.publish()
  • ส่งเมื่อ ป้ายเปลี่ยน + heartbeat + ช่วงห่างขั้นต่ำ ไม่ส่งทุกผล
  • สถานะ (/ai) แยกจาก เหตุการณ์เตือน (/event)
  • รับคำสั่งกลับ: กรองทุกช่อง ทำตามเฉพาะชื่อที่รู้จัก และจำไว้ว่า broker สาธารณะไม่เข้ารหัส
  • Smart HMI ส่ง snapshot() ก้อนเดียวกันทุกทีม แดชบอร์ดเว็บจึงอ่านได้แบบเดียว และหน้าเว็บก็ต้องไม่ไว้ใจข้อความเหมือนบอร์ด

ไฟล์ของคาบนี้บน GitHub

กดชื่อไฟล์เพื่อเปิดอ่านหรือคัดลอก

fit-css

VIDEO-SLOT: คลิป 60-90 วินาที สองจอพร้อมกัน: จอบอร์ดกับ terminal ที่รัน mosquitto_sub แล้วเขย่าบอร์ด ข้อความ JSON ขึ้นทันที จากนั้น mosquitto_pub สั่งเปลี่ยนโมเดล

☰ สารบัญ