คาบ 10 — MQTT (1883)

Telemetry ขาออก และ Command ขากลับ บน broker ของเราเอง

คาถาประจำคาบ: ส่งข้อมูลออกไปได้ ยังไม่พอ — ต้องรับคำสั่งกลับมาได้ด้วย ถึงจะเรียกว่าระบบ

ดูของจริงก่อน — บอร์ดคุยกับคอมพิวเตอร์คนละเครื่อง

บอร์ดของเรา อ่านเซนเซอร์ แล้ว publish LED ที่ถูกสั่ง Broker พอร์ต 1883 ตัวกลางเพียงตัวเดียว ไม่เก็บ ไม่ตัดสิน แค่ส่งต่อ MQTT Explorer บนคอมของเรา เห็นทุกข้อความ และพิมพ์คำสั่งกลับได้ PUBLISH — ค่าเซนเซอร์ JSON ทุก 5 วินาที คำสั่ง toggle LED — เดินสวนทางกลับมาที่บอร์ด

คาบ 9 บอร์ดของเราต่อเน็ตได้แล้ว รู้ IP ของตัวเอง และ ping ออกไปข้างนอกได้ แต่ยังไม่มีใครที่ปลายทาง

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

วันนี้บอร์ดเลิกเป็นเครื่องมือวัดที่อยู่ตัวคนเดียว แล้วกลายเป็นสมาชิกของระบบ

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

คำถาม คำตอบของคาบนี้ อยู่ช่วงไหน
Why คาบ 9 บอร์ดมี IP และ ping ออกไปได้แล้ว ทำไมยังไม่พอ เพราะ ping พิสูจน์ได้แค่ว่าสายดี ยังไม่มีใครที่ปลายทางรับข้อมูลของเรา · และของที่ส่งออกได้อย่างเดียวยังไม่เรียกว่าระบบ ระบบต้องรับคำสั่งกลับมาได้ด้วย · ชื่อ topic กับรูปร่าง payload คือ สัญญากับทุกคนที่จะใช้ข้อมูลนี้ต่อ เปลี่ยนทีหลังแปลว่าพังทุกฝั่งพร้อมกัน ครึ่งแรก · pub/sub · topic และ payload
What มีอะไรให้ใช้บ้าง โมดูล mqtt มีหกชื่อ เท่านี้จริง ๆ — และเพดานสี่ข้อที่ต้องจำคู่กันไป: ช่องรับ 1 ข้อความ · payload 255 ไบต์ · topic 127 ไบต์ · client_id 31 ตัวอักษร สไลด์บัญชีหกชื่อ + สไลด์ get_message() ช่องเดียว
How ประกอบยังไงให้ใช้งานได้จริง ต่อ WiFi → ต่อ broker → publish() ค่าจริงทุก 5 วินาที โดยที่ลูปเดียวกันยังฟัง get_message() อยู่ แล้วเอาคำสั่งที่รับมาสั่ง LED บนบอร์ด แปดไฟล์ตัวอย่าง + ใบฝึก + ติดตั้ง CE ด้วย Docker

ปลายทางที่จับต้องได้ — MQTT Explorer บนคอมเห็นค่าจากบอร์ดทุก 5 วินาที และเราพิมพ์คำสั่งจากคอมกลับไปให้ LED บนบอร์ดสลับสถานะได้

คาบ 9 เราพิสูจน์ว่าสายดี · คาบนี้เราหาคนที่ปลายสายเจอ และคุยกันได้สองทาง

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

  1. อธิบายได้ว่า publish/subscribe ต่างจาก client–server ตรงไหน และทำไม IoT ถึงเลือกแบบแรก
  2. ออกแบบ topic ของทีม และ JSON payload ที่ขนาดพอเหมาะ พร้อมเหตุผลของทุกฟิลด์
  3. ใช้ mqtt.connect() / publish() / subscribe() / get_message() ได้ถูกต้องตามข้อจำกัดจริงของเฟิร์มแวร์
  4. ติดตั้ง TESAIoT Community Edition ด้วย Docker แล้วทำให้ข้อมูลของทีมขึ้นกราฟบนแพลตฟอร์มของตัวเอง

ปลายทางของวันนี้: MQTT Explorer เห็นค่าจากบอร์ดทุก 5 วินาที และเราพิมพ์คำสั่งจากคอมให้ LED บนบอร์ดสลับสถานะได้

คาบนี้เป็นคาบแรกที่ข้อมูลของเรา ออกจากบอร์ด ไปอยู่ในมือของโปรแกรมอื่น

ปลายทางของคาบนี้ — บอร์ดรายงานตัวขึ้น broker

หน้าจอจากการรัน examples/s10/03_connect_and_publish.py↗ บน BENTO Emulator ที่ 800x480 เท่าจอของทั้งสองบอร์ด — ไม่ใช่ภาพวาด ไม่ใช่ mock-up และไม่ใช่ภาพถ่ายจากบอร์ด · หน้าจอของไฟล์เฉลย (ไฟสี่ดวง · ลูกบิด · สามใบล่าสุด · ปุ่มเริ่ม/หยุดส่ง) ยังไม่ได้ถ่าย
  • การ์ดบนคือ บันไดสามขั้นก่อนส่งได้ — 1) WiFi ได้ IP · 2) broker ต่อแล้ว · 3) publish กำลังส่ง — ขั้นที่ผ่านแล้วต้องเห็นด้วยตา
  • การ์ดล่างนับใบที่ส่งไปแล้ว พร้อมแถบความคืบหน้าของ 10 ใบ — ส่งทุก 2 วินาที ดูเลขเดินขึ้น
  • ผลลัพธ์จริงของคาบนี้ไปโผล่ที่ เครื่องอื่น — จอนี้บอกได้แค่ว่าบอร์ด "สั่งส่ง" แล้ว

ถ้าจอบอกว่าส่งแล้ว แต่ฝั่งรับไม่เห็นอะไร แปลว่ายังไม่จบ — คาบนี้ต้องดูสองจอพร้อมกัน

ทบทวนคาบ 9 — เราหยุดไว้ตรงไหน

wifi.scan() รู้ว่ามีใครอยู่แถวนี้ wifi.connect() เข้าเครือข่ายได้ wifi.ip() มีที่อยู่เป็นของตัวเอง wifi.ping() ออกไปข้างนอกถึง วันนี้ mqtt.publish() mqtt.subscribe() คาบ 9 ต่อท่อได้ · คาบ 10 เริ่มมีของไหลในท่อ ping พิสูจน์แค่ว่า "เส้นทางถึง" — ไม่ได้แปลว่ามีโปรแกรมไหนรอรับข้อมูลของเราอยู่

สองอย่างจากคาบที่แล้วที่ต้องเอามาใช้วันนี้ทั้งคู่ — wifi.connect() บล็อกได้นานถึงราว 85 วินาทีถ้ารหัสผิด จึงต้องต่อ WiFi ให้ได้ก่อนเสมอแล้วค่อยเริ่มเรื่อง MQTT และ wifi.ping() รับเฉพาะหมายเลข IP ไม่รับชื่อโฮสต์ — เดี๋ยวเราจะใช้มันตรวจว่าเครื่องที่รัน broker อยู่ในระยะที่คุยได้จริงก่อนจะเสียเวลาต่อ

ถ้า wifi.is_connected() เป็น False อย่าเพิ่งไปหาสาเหตุที่ MQTT — ปัญหายังไม่เดินทางมาถึงชั้นนั้น

client–server กับ pub/sub ต่างกันตรงไหน

ซ้าย — ภาพ: Michel Bakni / Wikimedia Commons — CC BY-SA 4.0 · ขวา — ภาพ: Mathieu.clabaut / Wikimedia Commons — CC BY-SA 4.0

ซ้าย คือแบบที่เราคุ้น (client–server) เว็บเบราว์เซอร์ต้องรู้ชื่อเซิร์ฟเวอร์ ต้องถามก่อนถึงจะได้คำตอบ ผู้ถามกับผู้ตอบ ผูกกันตรง ๆ ถ้าอีกฝั่งย้ายที่อยู่ ทุกฝั่งต้องแก้ตาม

ขวา คือ publish/subscribe ผู้เขียน (writer) โยนข้อมูลใส่ ชื่อเรื่อง ผู้อ่าน (reader) ขอรับตาม ชื่อเรื่อง — ทั้งสองฝั่งไม่เคยรู้จักกัน รู้จักแค่ชื่อเรื่องเดียวกัน

ผลที่ตามมาสามข้อ ซึ่งเป็นเหตุผลที่ IoT เลือกแบบหลัง

  1. บอร์ดไม่ต้องมี IP ที่คนอื่นเข้าถึงได้ — บอร์ดเป็นฝ่ายวิ่งออกไปหา broker เอง จึงอยู่หลัง NAT หรือหลังไฟร์วอลล์ได้
  2. เพิ่มผู้รับได้โดยไม่แตะโค้ดบนบอร์ด — พรุ่งนี้อยากให้แดชบอร์ดตัวที่สองดูข้อมูลเดียวกัน แค่ subscribe เพิ่ม
  3. ผู้รับล่มไม่ทำให้ผู้ส่งล่ม — บอร์ดยิงเข้า broker เหมือนเดิม ไม่มีใครค้างรอใคร

คำที่ต้องจำ: pub/sub แยกผู้ส่งออกจากผู้รับ ทั้งในเชิงพื้นที่และเชิงเวลา

broker อยู่ตรงกลาง — และมีคำสั่งอยู่แค่ไม่กี่คำ

ภาพ: Brivadeneira / Wikimedia Commons — CC BY-SA 4.0

CONNECT เข้าไปแนะนำตัว → SUBSCRIBE ขอรับ topic ที่สนใจ → PUBLISH ส่งข้อมูล → DISCONNECT บอกลา
สี่คำนี้คือทั้งหมดที่ผู้เรียนต้องใช้วันนี้ ที่เหลือ broker จัดการให้เอง

อะไรคือ MQTT ? — code maow maow | โค้ดแมวแมว · 8 นาที 26 วินาที · ไทย — ดูเพื่อเห็นภาพรวมของ broker/publisher/subscriber เป็นภาษาไทยก่อนลงมือ

broker ไม่ใช่ฐานข้อมูล มันคือ ที่ทำการไปรษณีย์ — รับเข้ามาแล้วส่งต่อทันที ไม่เก็บไว้ให้ (เว้นแต่สั่งให้เก็บ)

topic คือที่อยู่ — และมันเป็นต้นไม้ ไม่ใช่ชื่อแบน ๆ

ภาพ: Ademant / Wikimedia Commons — CC BY-SA 4.0 — ในภาพมีทั้งลำดับชั้น topic และ wildcard ทั้งสองแบบ

ในภาพ ปลั๊กสองตัวส่ง /plug1/voltage, /plug2/current ส่วนแล็ปท็อป subscribe /plug1/# และมือถือ subscribe /+/current
# = ทุกอย่างที่อยู่ใต้ลงไปกี่ชั้นก็ได้ (ต้องเป็นตัวสุดท้าย) · + = แทนที่ หนึ่งชั้นพอดี

MQTT Essentials Part 6 — Topic Best Practices — HiveMQ · 5 นาที 50 วินาที · อังกฤษ — ดูเพื่อเข้าใจว่าทำไม topic ที่ออกแบบดีทำให้ระบบขยายได้โดยไม่ต้องแก้โค้ดฝั่งอุปกรณ์

topic ไม่ต้องประกาศล่วงหน้า — พิมพ์ publish ไปที่ชื่อไหน ชื่อนั้นก็เกิดขึ้นเดี๋ยวนั้น จึงต้องมีวินัยตั้งชื่อเอง

เข้าใจฮาร์ดแวร์ · MQTT วิ่งอยู่ตรงไหนบนบอร์ด

CM33_NS — คอร์ที่รัน MicroPython โค้ด Python ของเรา ลูป publish/poll งานเครือข่าย TCP บัฟเฟอร์ 2048 ไบต์ ไดรเวอร์ WiFi — วิทยุตัวเดียวของทั้งบอร์ด MPY heap 64 KB CM55 — คอร์ที่วาดจอและอ่านเซนเซอร์ อ่าน CapSense / pot ให้เรา · BMI270 ด้วยบน Eva วาดผลบนจอผ่าน LVGL ไม่แตะเครือข่ายเลยแม้แต่นิดเดียว ค่าเซนเซอร์เดินข้ามมาทาง IPC ก่อนจะถูก publish ข้อความขาเข้าถูกวางในช่องรับโดยงานเครือข่าย ไม่ว่าโค้ด Python ของเราจะอยู่บรรทัดไหนก็ตาม โมดูล mqtt ต่อได้เฉพาะพอร์ตข้อความเปล่า — ทั้ง Eva และ Dev Kit นี่คือเหตุผลที่คาบนี้เป็นพอร์ต 1883 ล้วน ยังไม่ใช่ 8883/8884 — TLS อยู่คาบหน้าในโมดูล tesaiot

ทั้ง WiFi และ MQTT ทำงานอยู่บน CM33_NS คอร์เดียวกับที่รันโค้ด Python ของเรา ส่วน CM55 ที่วาดจอกับอ่านเซนเซอร์ไม่แตะเครือข่ายเลย — ทั้งสองบอร์ดจงใจอย่างนั้น เพราะสองคอร์แย่งอุปกรณ์ตัวเดียวกันเคยทำให้เครื่องล้มมาแล้ว · ภาพนี้จริงทั้ง Eva Kit และ Dev Kit เพราะโค้ด Wi-Fi/MQTT เป็นชุดเดียวกัน ต่างกันแค่ใครอ่าน IMU (Eva: คอร์จออ่านให้ · Dev Kit: CM33 อ่านเองจาก I2C) · เหตุผลที่คาบนี้ใช้ 1883 ไม่ใช่เรื่องหน่วยความจำ แต่เพราะโมดูล mqtt ส่งข้อมูลรับรอง TLS เป็นค่าว่างเสมอ (ดูตารางหกชื่อ) จึงต่อได้เฉพาะพอร์ตข้อความเปล่าบนบอร์ดไหนก็ตาม

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

เครือข่ายกับหน้าจออยู่คนละคอร์ — ถ้าจอค้าง MQTT อาจยังวิ่งอยู่ และในทางกลับกันด้วย

เกร็ด: หัวข้อความสองไบต์ กับมาตรฐานที่ใช้เวลาสิบห้าปี

ภาพ: Blacktron / Wikimedia Commons — CC BY-SA 4.0 — โครงสร้างแพ็กเก็ต PUBLISH

ดูแถวบนสุดของภาพ: 16 บิต = 2 ไบต์ นั่นคือส่วนหัวคงที่ของ MQTT ทั้งหมด — ชนิดแพ็กเก็ต, ธง DUP, ระดับ QoS, ธง RETAIN และความยาว อัดอยู่ในสองไบต์นั้น

เทียบกับ HTTP ที่ส่วนหัวเป็นข้อความยาวหลายร้อยไบต์ต่อคำขอหนึ่งครั้ง ("GET /… HTTP/1.1", "Host:", "User-Agent:", …) ความต่างนี้ไม่ใช่เรื่องความสวยงาม แต่คือ ค่าไฟกับค่าเน็ตของอุปกรณ์ที่ต้องส่งข้อมูลทุกห้าวินาที เป็นปี

มาตรฐานเองก็ไม่ได้เกิดในวันเดียว: MQTT 3.1.1 ถูกอนุมัติเป็นมาตรฐาน OASIS เมื่อ 2014-10-29 และ 5.0 เมื่อ 2019-03-07 เวอร์ชันที่อุปกรณ์ฝังตัวใช้กันแพร่หลายที่สุดจนถึงวันนี้ยังเป็น 3.1.1

เชื่อมกับวันนี้: payload ของเราจะยาวราว 80 ไบต์ ส่วนหัว MQTT กินเพิ่มแค่หลักหน่วยไบต์ — ถ้าเราเลือก HTTP แทน ข้อมูลจริงจะกลายเป็นส่วนน้อยของสิ่งที่ส่งออกไป และแบตเตอรี่จะหมดไปกับการส่ง "ชื่อหัวข้อ" ของข้อมูล ไม่ใช่ตัวข้อมูล

กลไกหลักของคาบ — get_message() มีช่องรับเพียงช่องเดียว

สถานการณ์: สองข้อความมาถึงติด ๆ กัน ก่อนที่เราจะเรียก get_message() ข้อความ A {"cmd":"on"} ข้อความ B {"cmd":"off"} ช่องรับ 1 ช่อง ≤ 255 ไบต์ เขียนทับเสมอ B ทับ A เงียบ ๆ ไม่มี error ไม่มีธงบอก get_message() ได้ B อย่างเดียว A หายไปโดยไม่มีใครรู้ ทางแก้ที่ใช้ได้จริง: poll ให้ถี่กว่าคนพิมพ์คำสั่ง — sleep_ms(100) ในลูป ไม่ใช่ sleep(5) เหมาะกับคำสั่งจังหวะคนกด · ไม่เหมาะกับสตรีมข้อมูลที่ไหลตลอด

นี่คือข้อจำกัดที่เราต้อง สอน ไม่ใช่ซ่อน เพราะมันไม่ส่งเสียงเวลาเกิด

ค่า เพดานจริง เกินแล้วเป็นอย่างไร
ช่องรับข้อความ 1 ข้อความ ข้อความใหม่ทับของเก่าทันที ไม่มีสัญญาณเตือน
payload ขาเข้า 255 ไบต์ ถูกตัดเงียบ ๆ — JSON ที่ถูกตัดจะ parse ไม่ผ่าน
topic ขาเข้า 127 ไบต์ ถูกตัดเงียบ
client_id / username / password 31 ตัวอักษร ถูกตัดเงียบ → แพลตฟอร์มหาอุปกรณ์ไม่เจอ → ปฏิเสธการเชื่อมต่อ

ความล้มเหลวที่แย่ที่สุดสำหรับผู้เรียนคือความล้มเหลวที่เงียบ — จำสี่แถวนี้ไว้ แล้วจะประหยัดเวลาดีบักไปทั้งคาบ

QoS 0/1/2 — ราคาของคำว่า "แน่ใจ"

ผู้ส่ง broker QoS 0 — ส่งแล้วแล้วกัน PUBLISH ครั้งเดียว · เน็ตหลุด = หายไปเลย · ถูกที่สุด QoS 1 — อย่างน้อยหนึ่งครั้ง PUBLISH + PUBACK · ถ้า ack หาย จะส่งซ้ำ — ผู้รับอาจได้ซ้ำ QoS 2 — ครั้งเดียวเป๊ะ สี่ขั้นตอนไป-กลับ · ช้าและกินหน่วยความจำที่สุด

mqtt.publish(topic, payload, qos) และ mqtt.subscribe(topic, qos) รับ qos เป็น อาร์กิวเมนต์ตำแหน่ง ไม่ใช่คีย์เวิร์ด
คาบนี้ใช้ QoS 0 สำหรับ telemetry ทุก 5 วินาที (ค่าถัดไปมาใน 5 วิอยู่แล้ว หายหนึ่งใบไม่เสียหาย) และควรใช้ QoS 1 สำหรับคำสั่ง เพราะคำสั่งที่หายคือคำสั่งที่ผู้ใช้กดแล้วไม่เกิดอะไรขึ้น

MQTT Essentials Part 7 — QoS — HiveMQ · 5 นาที 41 วินาที · อังกฤษ — ดูเพื่อเห็นลำดับแพ็กเก็ตของ QoS 1 และ 2 แบบเต็ม

เลือก QoS จากคำถามเดียว: "ถ้าข้อความนี้หายไปหนึ่งใบ ใครเดือดร้อน"

งบข้อมูลต่อรอบ — ทำไมเป็น 5 วินาที ไม่ใช่ 100 มิลลิวินาที

งบขาออก — payload ของเราเทียบกับเพดานที่ปลอดภัย 1000 ไบต์ 80 ไบต์ — เหลือที่ว่างอีกมาก เพิ่มฟิลด์ได้สบาย งบขาเข้า — คำสั่งของเราเทียบกับเพดานแข็ง 255 ไบต์ 26 ไบต์ — เกิน 255 เมื่อไร ถูกตัดกลางคันเงียบ ๆ แล้ว JSON พัง ส่งถี่ขึ้นสองเท่า ข้อมูลโตสองเท่า — บนเครือข่ายมือถือหรือแบตเตอรี่ นี่คือค่าใช้จ่ายจริง

ปริมาณข้อมูล≈ขนาด payloadTpublish80 B5 s=16 B/spayload ขาเข้า≤255 B\text{ปริมาณข้อมูล} \approx \frac{\text{ขนาด payload}}{T_{\text{publish}}} \qquad \frac{80\ \text{B}}{5\ \text{s}} = 16\ \text{B/s} \qquad \text{payload ขาเข้า} \le 255\ \text{B}

ตัวเลข 5 วินาทีไม่ได้มาจากความรู้สึก แต่มาจากคำถามว่า ค่าที่เราวัดเปลี่ยนเร็วแค่ไหน อุณหภูมิห้องเปลี่ยนช้ามาก ส่งทุก 5 วินาทีก็ละเอียดเกินพอ ส่วนความสั่นของมอเตอร์เปลี่ยนใน 10 มิลลิวินาที — ค่าแบบนั้นไม่ควรส่งดิบ ๆ ขึ้น MQTT แต่ควรให้บอร์ดสรุปก่อน (ค่าสูงสุด ค่า RMS หรือจำนวนครั้งที่เกินเกณฑ์ในช่วงนั้น) แล้วค่อยส่ง

"ส่งให้ถี่ที่สุดเท่าที่ทำได้" เป็นการออกแบบที่แย่เสมอ — ถามก่อนว่าใครจะใช้ค่านี้ทำอะไร

ออกแบบ topic และ payload ของทีม

แบบที่ 1 — broker สาธารณะ : กันชนด้วยชื่อทีม bento/ team03/telemetry team03/cmd/led publish ทุก 5 วิ subscribe รอคำสั่ง แบบที่ 2 — TESAIoT CE : แพลตฟอร์มล็อกรูปแบบให้ device/team03/telemetry device/team03/commands ช่องที่สองต้องเท่ากับ device_id เป๊ะ ไม่งั้น ACL ปฏิเสธ payload: ส่งแค่ก้อน data ก็พอ — บริดจ์เติม device_id และ timestamp ให้เอง {"ax": 0.12, "ay": -9.75, "az": 0.31, "pot": 48.2} ค่าซ้อนกัน {"accel":{"x":1}} ถูกแบนเป็น accel_x เฉพาะค่าตัวเลขเท่านั้นที่กลายเป็นเส้นกราฟ

กติกาการตั้งชื่อ topic ที่ใช้ได้ทั้งชีวิตการทำงาน: เรียงจากกว้างไปแคบ (bento/team03/telemetry ไม่ใช่ telemetry/team03/bento) · ห้ามขึ้นต้นด้วย / เพราะจะได้ช่องว่างเปล่าเป็นชั้นแรก · ห้ามใส่ช่องว่างหรือภาษาไทย · และ อย่าใส่ค่าที่เปลี่ยนบ่อยลงในชื่อ topic (เช่น .../temp/25.4) เพราะผู้รับจะ subscribe ไม่ถูก

สตริงที่เป็นข้อความ เช่น {"status":"ok"} ส่งขึ้นไปได้และเก็บได้ แต่ จะไม่กลายเป็นเส้นกราฟ เพราะกราฟต้องการตัวเลข ถ้าอยากให้เห็นบนกราฟ ให้แปลงเป็นตัวเลขก่อน เช่น {"ok": 1}

ชื่อ topic กับรูปร่าง payload คือ สัญญาระหว่างทีมเรากับทุกคนที่จะใช้ข้อมูลนี้ต่อ เปลี่ยนทีหลังแปลว่าพังทุกฝั่งพร้อมกัน

เรื่องที่เราให้ 70% น้อง ๆ เขียน 30%

70% — เฟิร์มแวร์ + แพลตฟอร์มทำให้แล้ว TCP/IP · แพ็กเก็ต MQTT · broker · ฐานข้อมูล · กราฟ 30% — งานของเรา ส่งอะไร ชื่ออะไร ถี่แค่ไหน โค้ดสั้นลง แต่การตัดสินใจหนักขึ้น — นี่คือหน้าตาของงานวิศวกรรมระบบ ไลบรารีตอบแทนไม่ได้ เพราะสี่คำถามนั้นเป็นคำถามเรื่องระบบ ไม่ใช่เรื่องโค้ด

สิ่งที่ทำให้แล้ว (70%)
สแตก TCP/IP และการต่อ WiFi · การเข้ารหัสแพ็กเก็ต MQTT ตามมาตรฐาน · การส่ง keepalive ให้เองเป็นระยะ · ฝั่งแพลตฟอร์ม: broker EMQX, บริดจ์ที่ subscribe device/+/telemetry รออยู่แล้ว, ฐานข้อมูลอนุกรมเวลา และกราฟที่สร้างจากชื่อคีย์ JSON อัตโนมัติ

สิ่งที่เป็นงานของเรา (30%)
ตัดสินใจว่า ส่งอะไร ตั้งชื่อว่าอะไร ถี่แค่ไหน และ ทำอะไรกับคำสั่งที่รับกลับมา — สี่คำถามนี้ไม่มีไลบรารีไหนตอบแทนได้ เพราะมันคือคำถามเรื่องระบบ ไม่ใช่เรื่องโค้ด

สังเกตว่าโค้ดของคาบนี้สั้นกว่าคาบ 8 มาก แต่การตัดสินใจหนักกว่า — นี่คือหน้าตาของงานวิศวกรรมระบบจริง

โค้ดที่สั้นลงไม่ได้แปลว่างานง่ายลง มันแปลว่าน้ำหนักย้ายไปอยู่ที่การออกแบบ

โมดูล mqtt มีหกชื่อ เท่านี้จริง ๆ — และเพดานของแต่ละตัว

เรียกอย่างไร คืนอะไร เพดานและกับดัก
mqtt.connect(broker, port=1883, client_id="psoc-edge", username=None, password=None, keepalive=60) True / False client_id ใช้ได้ 31 ตัวอักษร ที่เกินถูกตัดทิ้งเงียบ ๆ แล้ว broker ค่อยปฏิเสธทีหลัง · broker ยาวได้ 63 · username กับ password อย่างละ 31 · เขียน keep_alive ไม่ได้ ต้อง keepalive
mqtt.disconnect() None ตัดการเชื่อมต่อและ คืน client_id ให้ว่าง ต่อใหม่ด้วยชื่อเดิมได้ทันที · ไม่คืน True จึงห้ามใส่ใน if
mqtt.publish(topic, payload, qos=0) True / False ยังไม่ได้ต่อแล้วเรียก ได้ OSError ไม่ใช่ False · ต่ออยู่แต่ส่งไม่ผ่าน ได้ False · จึงมีสามทางออก ไม่ใช่สอง
mqtt.subscribe(topic, qos=0) True / False broker ปฏิเสธ (เช่นติด ACL) ก็คืน False เหมือนกัน ไม่โยน error · ยังไม่ได้ต่อจึงจะได้ OSError
mqtt.is_connected() True / False เปลี่ยนเป็น False เองเมื่อ broker ตัดเรา ไม่ต้องรอให้ publish พัง
mqtt.get_message() None หรือ tuple (topic, payload) ไม่เคยบล็อก · topic เป็น str สูงสุด 127 ไบต์ · payload เป็น bytes สูงสุด 255 ไบต์ ต้อง .decode() ก่อนใช้ · เกินเพดานถูกตัดกลางคันเงียบ ๆ

retain ไม่ใช่พารามิเตอร์ เอกสารในเครื่องมือบางที่เขียนว่า mqtt.publish(topic, payload, qos=0, retain=False) แต่ตัวจริงรับได้แค่สามอาร์กิวเมนต์ ใส่ retain=True ไปจะได้ TypeError ทันที และในซอร์สค่านั้นถูกตั้งเป็น false ตายตัวอยู่แล้ว

port ตั้งได้ก็จริง แต่ตัวจริงส่งข้อมูลรับรอง TLS เป็นค่าว่างเสมอ จึงต่อได้เฉพาะพอร์ตที่เปิดรับแบบข้อความเปล่า ชี้ไปพอร์ต TLS จริงแล้วจะล้มตอนจับมือ — คาบหน้าเราจะใช้คนละโมดูลกันไปเลยเพราะเหตุนี้

ครึ่งหลังของคาบ — แพลตฟอร์มที่เราติดตั้งเอง

ภาพหน้าจอ: TESAIoT Community Edition v1.1.8 — เอกสารของ repo (Apache-2.0)

เราจะไม่ยืมแดชบอร์ดของใคร — แต่ละทีมติดตั้ง TESAIoT Community Edition บนเครื่องของตัวเองด้วย Docker

  • make install (ธง PREBUILT=1 ดึงอิมเมจสำเร็จรูป) ใช้เวลาราว 15–30 นาที
  • ลำดับสำคัญ: make up ต้องมาก่อน make init-pki ถ้าสลับกันจะค้างตั้งแต่บูตแรก
  • RAM อย่างน้อย 8 GB ตามเอกสารไทยของ repo — น้อยกว่านี้ฐานข้อมูลถูกระบบฆ่าทิ้งกลางทาง
  • มีเอกสารภาษาไทยครบ — README.th.md และ docs/th/ อีก 15 ไฟล์

นี่คือครั้งแรกที่ทีมได้เป็นเจ้าของ ทั้งอุปกรณ์และแพลตฟอร์ม ไม่ใช่แค่ผู้ใช้บริการของใคร

กับดักพอร์ต 1883 — เอกสารบอกอย่าง ไฟล์ตั้งค่าทำอีกอย่าง

บอร์ดใน LAN 192.168.1.77 ยิงไปที่พอร์ต 1883 เครื่องที่รัน TESAIoT CE 127.0.0.1:11883 เห็นได้เฉพาะในเครื่อง 0.0.0.0:1883 เห็นได้จากทั้ง LAN broker EMQX ข้างในฟังที่ 0.0.0.0:1883 อยู่แล้ว ปัญหาอยู่ที่ชั้นเผยแพร่พอร์ตของ compose ต่อไม่ติดเลย ไม่ใช่รหัสผิด ไม่ใช่ ACL แก้บรรทัดเดียวใน docker-compose.yml — และย้อนกลับเมื่อสอนเสร็จ

เอกสารของ CE ทุกฉบับเขียนว่าพอร์ต 1883 แต่ docker-compose.yml เผยแพร่จริงเป็น 127.0.0.1:11883:1883 คือทั้งเลขพอร์ตต่างจากที่เขียนไว้ และผูกกับ loopback ทำให้ บอร์ดที่อยู่ใน LAN เดียวกันเข้าไม่ถึงเลย (คอมเมนต์ในไฟล์บอกว่าทำไว้เลี่ยงชนกับซอฟต์แวร์ตัวอื่นบนเครื่องผู้พัฒนา)

-      - "127.0.0.1:11883:1883"
+      - "0.0.0.0:1883:1883"     # classroom only - plaintext MQTT บน LAN

ใช้ docker compose up -d emqx เท่านั้น — restart จะไม่อ่านการตั้งค่าพอร์ตใหม่

เอกสารกับไฟล์ตั้งค่าขัดกันได้เสมอ — ไฟล์ตั้งค่าคือความจริง เพราะมันคือสิ่งที่เครื่องอ่าน

1883 ส่งรหัสผ่านเป็นข้อความเปล่า — พูดให้ตรง

ภาพ: Miraceti / Wikimedia Commons — CC BY-SA 3.0

พอร์ต 1883 ไม่มีการเข้ารหัส ทั้ง username, password และ payload ทุกไบต์ เดินทางบน WiFi ห้องเรียนในรูปข้อความอ่านออกได้ ใครที่ดักจับสัญญาณได้ ก็อ่านรหัสของทีมเราได้ และ ปลอมเป็นอุปกรณ์ของเราส่งข้อมูลปลอมเข้าแพลตฟอร์มได้ทันที

  • CE เองระบุไว้ในไฟล์ตั้งค่าว่าพอร์ต 1883 มีไว้สำหรับ local/dev เท่านั้น
  • API ของแพลตฟอร์มจะเขียน log เตือนทุกครั้งที่อุปกรณ์โหมด server_tls เข้ามาทางพอร์ต 1883
  • ดังนั้นสิ่งที่เราทำวันนี้คือ การฝึกในสนามซ้อมที่ปิดล้อม ไม่ใช่วิธีที่ใช้กับของจริง

คาบหน้า (คาบ 11) เราจะเปลี่ยนไปพอร์ต 8884 พร้อม TLS แล้วเทียบให้เห็นด้วยตาว่าสิ่งที่ดักได้ต่างกันอย่างไร — วันนี้จึงเป็นครึ่งแรกของบทเรียนเรื่องความปลอดภัย ไม่ใช่บทเรียนที่จบในตัว

จำประโยคนี้ให้ได้: "ต่อได้" กับ "ต่อได้อย่างปลอดภัย" เป็นคนละคำถาม และเราเพิ่งตอบข้อแรก

ขึ้นทะเบียนอุปกรณ์ก่อน แล้วค่อยต่อ

ภาพหน้าจอ: TESAIoT Community Edition v1.1.8 — เอกสารของ repo (Apache-2.0)

แพลตฟอร์มนี้ ไม่มีการลงทะเบียนอัตโนมัติ — อุปกรณ์ที่ไม่รู้จักถูกปฏิเสธตั้งแต่ตอน CONNECT

  1. Devices → Add device แล้ว ตั้ง device_id เองให้สั้น เช่น team03 (ระบบรับ 3–64 ตัว) — ถ้าปล่อยให้สุ่ม จะได้ UUID ยาว 36 ตัว ยาวเกิน 31 ตัวที่โมดูล mqtt รับได้ ถูกตัดเงียบ แล้วต่อไม่ติดโดยไม่บอกสาเหตุ
  2. ขอรหัส: POST /api/v1/devices/<id>/reset-mqtt-password คืน mqtt_username และรหัสผ่านมาตรง ๆ (อย่าใช้ /reset-password ซึ่งคืนคนละอย่าง)
  3. ตรวจว่าสถานะอุปกรณ์เป็น active และโหมดเป็น server_tls (ค่าเริ่มต้นอยู่แล้ว)
  4. กรอกลงโค้ดโดยให้ client_id == username == device_id ตรงกันเป๊ะ

ที่นี่ไม่ยอมรับ "เกือบตรง" — ผิดตัวเดียวใน device_id คือถูกปฏิเสธ และข้อความปฏิเสธไม่ได้บอกว่าผิดตรงไหน

แกะโค้ดจริง — ท่าที่ 1 ต่อ WiFi แล้วต่อ broker

# --- ท่าที่ 1: ต่อเน็ตให้ได้ก่อน แล้วค่อยแนะนำตัวกับ broker ---
wifi.connect(WIFI_SSID, WIFI_PASSWORD)
lcd.print("WiFi:", wifi.ip())

ok = mqtt.connect(BROKER, port=1883, client_id=DEVICE_ID,
                  username=DEVICE_ID, password=MQTT_PASS, keepalive=60)
lcd.print("<span class=ok>MQTT ต่อแล้ว</span>" if ok else "MQTT ต่อไม่ได้")

ชื่อคีย์เวิร์ดคือ keepalive ไม่ใช่ keep_alive — เอกสารช่วยเหลือใน IDE เขียนผิดจุดนี้ พิมพ์ตามแล้วได้ TypeError ทันที

keepalive=60 แปลว่าถ้าเราเงียบเกิน 60 วินาที broker จะตัดทิ้ง — ส่งทุก 5 วินาทีจึงปลอดภัย แต่ถ้าเปลี่ยนไปส่งทุก 5 นาที ต้องขยับค่านี้ตาม ไม่งั้นจะโดนตัดเป็นรอบ ๆ

mqtt.connect() คืน True/False ไม่โยน exception — ถ้าไม่ตรวจค่าที่คืน โปรแกรมจะวิ่งต่อทั้งที่ไม่มีการเชื่อมต่อ แล้ว publish ทุกใบหายเงียบ

ภาพ: Simon A. Eugster / Wikimedia Commons — CC BY-SA 4.0 · ภาพนี้มีธง retain ซึ่งโมดูล mqtt ของเราไม่มี

บรรทัดแรกที่ต้องเขียนหลัง connect() คือบรรทัดที่ บอกให้รู้ว่าต่อติดหรือไม่ติด ไม่ใช่บรรทัดถัดไปของงาน

แกะโค้ดจริง — ท่าที่ 2 ส่งค่าจริงทุก 5 วินาที

    # --- ท่าที่ 2: อ่านเซนเซอร์ → ประกอบ JSON → publish (ใน publish_telemetry) ---
    try:
        ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
        data = {"ax": round(ax, 2), "ay": round(ay, 2), "az": round(az, 2),
                "pot": round(sensors.pot.percent(), 1)}
    except OSError:
        return None                            # อ่านพลาดหนึ่งรอบ ข้ามไปรอบหน้า
    ...
    mqtt.publish(TOPIC_PUB, json.dumps(data))  # แบน ไม่ห่อ แพลตฟอร์มห่อให้เอง
ค่าจากเซนเซอร์ ax = 0.1234567 pot = 48.23456 ทศนิยมยาวไม่มีประโยชน์ round() ก่อน 0.12 48.2 payload สั้นลงเกือบครึ่ง json.dumps() {"ax":0.12, "ay":-9.75, "az":0.31, "pot":48.2} สตริงเดียว ~80 ไบต์ publish ขึ้น broker

sensors.bmi270.motion() คืนหกค่าในการอ่านครั้งเดียว — อ่านทีเดียวดีกว่าเรียกหลายฟังก์ชัน เพราะค่าทั้งหกมาจากช่วงเวลาเดียวกันจริง · บรรทัดที่อ่านค่าอยู่ใน try เพราะอ่านพลาดหนึ่งรอบต้องไม่ทำให้ทั้งโปรแกรมตาย

round() ไม่ใช่เรื่องความสวยงาม: 0.1234567890 กิน 12 ไบต์ ส่วน 0.12 กิน 4 ไบต์ — คูณด้วยจำนวนฟิลด์และรอบต่อวันแล้วคือค่าเน็ตที่ประหยัดได้ฟรี · json.dumps() แปลง dict เป็นสตริงที่ทุกภาษาอ่านออก คือจุดที่ข้อมูลเลิกเป็นของ Python

ส่งแบน ไม่ต้องห่อ บริดจ์เป็นคนห่อให้เอง แล้วเติม device_id (จาก topic) กับ timestamp ให้ด้วย
ห่อเองซ้ำจะได้ชื่อวัดขึ้นต้น data_ แล้วตารางหน่วยฝั่งเซิร์ฟเวอร์หาไม่เจอ

แกะโค้ดจริง — ท่าที่ 3 รับคำสั่งกลับมาสั่ง LED

mqtt.subscribe(TOPIC_CMD)               # ท่าที่ 3: subscribe ครั้งเดียว แล้ว poll ถี่ ๆ ในลูปเดียวกับ publish
...
    msg = mqtt.get_message()            # ไม่มีข้อความ = None
    if msg is not None:
        try:
            cmd = json.loads(msg[1].decode())   # msg[1] คือ payload เป็น bytes
        except ValueError:
            cmd = {}                    # คนส่งมั่วได้เสมอ โปรแกรมต้องไม่ตาย
        if cmd.get("cmd") == "toggle":
            led_on = not led_on         # จำสถานะเอง
            lamp.value(1 if led_on else 0)      # lamp = led_named("RGB_GREEN", "LED2")
            led_remote.value(1 if led_on else 0)
    ...
    time.sleep_ms(100)
ลูปเดียว ทำสองหน้าที่ — จับเวลาเองว่าถึงรอบ publish หรือยัง poll poll poll publish (ครบ 5 วิ) poll poll poll poll poll ทุก ๆ 100 มิลลิวินาทีเราถามหนึ่งครั้ง — คนกดปุ่มบนคอมไม่มีทางกดเร็วกว่านี้จนข้อความทับกัน ถ้าเขียน time.sleep(5) แล้วค่อย poll ครั้งเดียว — คำสั่งที่ส่งมาระหว่างนั้นจะเหลือแค่ใบสุดท้าย

get_message() คืน tuple (topic, payload) หรือ None ต้องตรวจ is not None ก่อนเสมอ · payload เป็น bytes จึงต้อง .decode() ก่อน json.loads() · ต้อง จำสถานะ LED ในตัวแปร Python เอง เพราะ gpio.led().value() ตอบแค่ระดับของขา ณ วินาทีที่ถาม · lamp ถูกเลือกตามชื่อ — led_named("RGB_GREEN", "LED2") หาดวงสีเขียวจาก gpio.board_info()["led_names"]: Dev Kit ได้ RGB_GREEN · Eva ได้ LED2 เพราะเลขดัชนีต่างกันตามบอร์ด และ led(0)/led(1) บน Dev Kit อยู่บน SoM ซึ่งยังไม่มีใครยืนยันว่ามองเห็นบนบอร์ดประกอบ

คำสั่งจากภายนอกคือ ข้อมูลที่เราไม่ได้เขียนเอง — ตรวจก่อนใช้เสมอ ทั้งว่ามีจริงไหม แปลงได้ไหม และอยู่ในชุดค่าที่เรารู้จักไหม

ท่าที่ 4 ที่ไม่มีในโครง — mqtt.disconnect() และเหตุผลที่ควรเรียก

# --- จบงานแล้วบอก broker ให้รู้ ไม่ใช่หายไปเฉย ๆ ---
mqtt.disconnect()                 # คืน None ไม่ใช่ True
print(mqtt.is_connected())        # False ทันที ไม่ต้องรอ keepalive หมดอายุ
ไม่เรียก แล้วกดรันใหม่ทันที บอร์ดหายไปเฉย ๆ broker ยังไม่รู้ client_id เดิมยังถูกจองอยู่ราวหนึ่งนาที รันใหม่ด้วยชื่อเดิม broker เตะตัวเก่าออก เห็นเป็นอาการ "ต่อติดแล้วหลุดสลับกัน" เสียเวลาไล่หาสาเหตุที่ไม่ได้อยู่ในโค้ด เรียกก่อนจบเสมอ broker รู้ทันทีว่าเราไปแล้ว client_id ว่างทันที ต่อใหม่ชื่อเดิมได้เลย is_connected() ตอบ False ตรงกับความจริง รันซ้ำได้ต่อเนื่องโดยไม่ต้องรอ ในห้องเรียนที่รันวันละสิบรอบ ต่างกันมาก

โค้ดหลักของคาบนี้วนลูปไม่รู้จบ จึงไม่เคยเดินมาถึงบรรทัด disconnect() แต่พอทีมกด Ctrl-C หรือกดรันใหม่ บอร์ดหายไปโดยที่ broker ยังนับว่าเรายังอยู่ เพราะฝั่งนั้นรอจนครบ keepalive ก่อนถึงจะยอมรับว่าเราไปแล้ว ระหว่างนั้นชื่อ client_id เดิมยังถูกจองอยู่

disconnect() จึงมีค่าที่สุดตอน เลิกใช้งานตามตั้งใจ ไม่ใช่ตอนพัง เขียนไว้ท้ายไฟล์ หรือใน except KeyboardInterrupt: แล้วอาการ "ต่อติดแล้วหลุดสลับกัน" ที่อยู่ในตารางกับดักจะหายไปเองครึ่งหนึ่ง

ลองไฟล์ examples/s10/07_disconnect_frees_id.py↗ — มันต่อ ส่ง ตัด แล้วต่อใหม่ด้วยชื่อเดิมทันที ให้เห็นว่าทำได้จริงเมื่อบอกลาอย่างถูกวิธี

ข้อมูลไหลไปทางไหน — ตั้งแต่เซนเซอร์ถึงกราฟ

เซนเซอร์ BMI270 · pot โค้ดของเรา json.dumps EMQX broker 1883 บริดจ์ subscribe ไว้แล้ว ฐานข้อมูล อนุกรมเวลา กราฟ อัปเดตสด ขาขึ้น — ผู้เรียนเขียนแค่สองกล่องแรก ที่เหลือแพลตฟอร์มต่อไว้ให้แล้ว ส่งถูก topic แล้วเห็นกราฟทันที โดยไม่ต้องสร้างแดชบอร์ดเอง ขาลง — คำสั่งจาก MQTT Explorer เดินย้อนเส้นเดียวกันกลับมาที่ LED ถ้าไม่เห็นข้อมูลบนกราฟ ให้ไล่จากซ้ายไปขวาทีละกล่อง อย่าเดาจากปลายทาง

จุดตรวจที่ใช้ได้จริงเวลาข้อมูลไม่ขึ้น: MQTT Explorer เห็นข้อความไหม ถ้าเห็น แปลว่าสองกล่องแรกและ broker ทำงานครบ ปัญหาอยู่ที่ topic ผิดรูปหรือ JSON ไม่ถูกต้อง (บริดจ์ทิ้ง JSON เสียเงียบ ๆ พร้อมเขียน log) ถ้าไม่เห็น ปัญหายังอยู่ฝั่งบอร์ด

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

วิธีรันบนบอร์ด

1 · เตรียมฝั่งเครื่อง CE ขึ้นแล้ว · แก้พอร์ต 1883 ขึ้นทะเบียน device_id สั้น จด IP ของเครื่องไว้ 2 · แก้ค่าบนหัวไฟล์ WIFI_SSID / WIFI_PASSWORD BROKER · DEVICE_ID · MQTT_PASS TOPIC_PUB · TOPIC_CMD 3 · รันแล้วดูสองจอ จอบอร์ด: สถานะการเชื่อมต่อ จอคอม: MQTT Explorer แล้วทดสอบขากลับ
  1. บนเครื่องที่รัน CE — ตรวจว่า docker compose ps เห็น emqx สถานะ up และพอร์ตขึ้นเป็น 0.0.0.0:1883 แล้วจริง
  2. หา IP ของเครื่องนั้นใน LAN (ไม่ใช่ localhost — บอร์ดอยู่คนละเครื่อง) แล้วทดสอบจากบอร์ดด้วย wifi.ping("192.168.1.50") ก่อน ถ้า ping ไม่ผ่าน อย่าเพิ่งเสียเวลากับ MQTT
  3. บนคอม เปิด MQTT Explorer ต่อไปที่ IP เดียวกัน พอร์ต 1883 ด้วยชื่อผู้ใช้/รหัสของทีม แล้ว subscribe device/# ไว้ล่วงหน้า
  4. บนบอร์ด เปิดหน้า BENTO Playground ค้างไว้ แล้วเปิด practise_codes/s10_mqtt_telemetry.py↗ แก้ค่าบนหัวไฟล์ให้เป็นของทีม
  5. เติมช่องว่าง pass ให้ครบ แล้วกด Program to Device — จากนั้นมองสองจอสลับกัน
  6. ทดสอบขากลับ: ใน MQTT Explorer พิมพ์ publish ไปที่ device/<device_id>/commands ด้วย payload {"cmd":"toggle"} แล้วดู LED

ตัวอย่างชุดคาบ 10 — สามไฟล์แรกคือชุดที่พา JSON ใบแรกขึ้น broker แล้วสั่งกลับมาได้

ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 35 นาที

ลำดับ · เรื่อง · เวลา ไฟล์ ลงมือทำอะไร แล้วจะเข้าใจอะไร
1 · ตั้งชื่อ topic ก่อนต่อ broker · 8 นาที examples/s10/01_topic_design.py↗ ตั้ง topic ของทีมที่ไม่ชนกับอีกเก้าทีม และรู้ว่า wildcard ใส่ใน publish ไม่ได้ · จะเข้าใจว่าชื่อ topic เป็นของที่ออกแบบก่อนต่อ broker ไม่ใช่ค่อยคิดตอนโค้ดพร้อมส่งแล้ว
2 · บันไดสามขั้น WiFi → TCP → MQTT · 15 นาที examples/s10/03_connect_and_publish.py↗ ส่ง JSON ใบแรกขึ้น broker ได้ และรู้ว่าพังขั้นไหนเมื่อมันพัง · จะเห็นว่าการต่อคือบันไดสามขั้น WiFi → TCP → MQTT ที่ล้มได้คนละแบบ
3 · สั่งกลับจากคอมพิวเตอร์ · 12 นาที examples/s10/04_subscribe_command.py↗ ทำครึ่งหลังของ MVP — รับ {"cmd":"toggle"} แล้วสลับ LED จริง · จะเข้าใจว่าการสั่งกลับมาที่บอร์ดคือฝั่ง subscribe ไม่ใช่ฝั่ง publish

ติดตรงไหน เปิดอันนี้

อาการที่เจอ ไฟล์ที่ตอบอาการนั้น
ส่งได้ทุก 5 วินาทีแล้ว แต่กดสั่งจากคอมทีไรบอร์ดไม่ตอบ examples/s10/05_send_every_5s_still_listen.py↗ — นับเวลาถึงกำหนดด้วยนาฬิกา แทนการหลับรอ ไฟล์นี้ไม่ต้องแก้อะไรก่อนกด Run
ไม่แน่ใจว่าจะใส่ฟิลด์อะไรลง payload examples/s10/02_payload_shape.py↗ — เทียบสามรูปแบบที่ยาวไม่เท่ากันทั้งที่บอกเรื่องเดียวกัน
โค้ดบอกว่าส่งแล้ว แต่ MQTT Explorer ไม่เห็นอะไรเลย examples/s10/06_sent_is_not_delivered.py↗ — นับที่ปลายทาง ไม่ใช่นับที่ต้นทาง

อ่านเสริมนอกเวลา — เรื่องนี้เป็นเนื้อหาของคาบ 11 ไม่ใช่เกณฑ์ผ่านของวันนี้: examples/s11/06_secure_publish_loop.py↗ คือลูปเดียวกันนี้ แต่วิ่งบนช่องที่เข้ารหัสไปยังแพลตฟอร์ม วันนี้ยังรันไม่ได้จนกว่าทีมจะได้ device_id ของตัวเอง เปิดอ่านเทียบโครงได้ แต่อย่าเพิ่งพยายามรัน

ทีมละหนึ่งบอร์ดหนึ่ง device_id — ห้ามสองทีมใช้ client_id เดียวกัน เพราะ broker จะเตะเครื่องเก่าออกทุกครั้งที่เครื่องใหม่ต่อเข้ามา แล้วทั้งสองทีมจะหลุดสลับกันเป็นลูป

MVP checkpoint — ผ่านคาบนี้เมื่อ

สองทาง — publish JSON เซนเซอร์จริงทุก 5 วินาที + สั่ง toggle LED จาก MQTT Explorer ผ่าน topic ของทีม

แปลเป็นสิ่งที่ตรวจได้จริง:

  • [ ] MQTT Explorer เห็นข้อความเข้ามาที่ topic ของทีม ห่างกัน 5 วินาที ต่อเนื่องอย่างน้อย 1 นาที
  • [ ] payload เป็น JSON ที่ถูกต้อง มีค่าจากเซนเซอร์จริงอย่างน้อย 3 ฟิลด์ และค่าจะเปลี่ยนเมื่อขยับบอร์ด
  • [ ] พิมพ์ {"cmd":"toggle"} จาก MQTT Explorer แล้ว LED บนบอร์ดสลับสถานะ ได้ทั้งติดและดับ
  • [ ] ข้อมูลขึ้นกราฟใน Device Details → Telemetry ของ TESAIoT CE ที่ทีมติดตั้งเอง
  • [ ] อธิบายได้ว่า client_id, username, device_id และช่องที่สองของ topic ต้องสัมพันธ์กันอย่างไร
  • [ ] บันทึกภาพหน้าจอทั้งสองฝั่งลง worksheet

ข้อที่สามคือหัวใจ — ถ้าขาดข้อนี้ เราสร้างได้แค่ เครื่องส่งข้อมูล ยังไม่ใช่ อุปกรณ์ที่สั่งได้

กับดักที่เจอบ่อย

อาการ สาเหตุที่แท้จริง วิธีแก้
connect() คืน False ทุกครั้ง ทั้งที่รหัสถูก CE เผยแพร่พอร์ตเป็น 127.0.0.1:11883 บอร์ดใน LAN เข้าไม่ถึง แก้ compose เป็น 0.0.0.0:1883:1883 แล้ว up -d emqx
ต่อไม่ติด และไม่มีข้อความบอกสาเหตุ device_id เป็น UUID 36 ตัว ถูกตัดเหลือ 31 ตั้ง device_id เองให้สั้น แล้วขึ้นทะเบียนใหม่
TypeError: unexpected keyword argument พิมพ์ keep_alive ตามเอกสารใน IDE ซึ่งเขียนผิด ใช้ keepalive
publish ไม่ error แต่แพลตฟอร์มไม่มีข้อมูล ช่องที่สองของ topic ไม่ตรงกับ device_id → ACL ปฏิเสธ ใช้ device/<device_id>/telemetry ตรงตัวอักษร
ข้อมูลเข้า แต่ไม่มีเส้นกราฟ ค่าเป็นสตริง — กราฟรับเฉพาะตัวเลข เปลี่ยนเป็นตัวเลข เช่น {"ok": 1}
กดคำสั่งรัว ๆ แล้วได้ผลแค่ครั้งสุดท้าย ช่องรับมีช่องเดียว ข้อความใหม่ทับของเก่า poll ทุก 100 ms และอย่าใช้ time.sleep(5) คร่อมทั้งลูป
คำสั่งยาว ๆ ทำให้ json.loads พัง payload ขาเข้าเกิน 255 ไบต์ ถูกตัดกลางคัน คำสั่งต้องสั้น และมี try/except
สองทีมหลุดสลับกันเป็นจังหวะ ใช้ client_id ซ้ำกัน broker เตะตัวเก่าออก หนึ่งทีมหนึ่ง client_id เสมอ
ต่อได้ตอนแรก แล้วหลุดทุก ๆ ราวหนึ่งนาที เงียบนานเกิน keepalive ส่งถี่กว่าค่า keepalive หรือขยับค่านั้นขึ้น
TypeError ตอนใส่ retain=True retain มีในเอกสารแต่ไม่มีในตัวจริง รับได้แค่ 3 อาร์กิวเมนต์ ตัด retain ออก ถ้าต้องการค่าคงค้างต้องตั้งฝั่ง broker
ชื่อ topic ขาเข้ายาว ๆ อ่านแล้วแยกไม่ออกว่ามาจากใคร topic ขาเข้าถูกตัดที่ 127 ไบต์ เงียบ ๆ ตั้งชื่อ topic ให้สั้น อย่าซ้อนลึกเกินสี่ชั้น
OSError ตอนเรียก publish() ทั้งที่โค้ดเดิมเคยผ่าน ลิงก์หลุดไปก่อนแล้ว publish() ตอนไม่ได้ต่อโยน error ไม่ได้คืน False เช็ก is_connected() ก่อน และครอบ publish() ด้วย try/except OSError

สิบเอ็ดจากสิบสองแถวนี้ ไม่ส่ง error ที่ตรงกับสาเหตุ — จึงต้องอ่านตารางนี้ก่อนเจอปัญหา ไม่ใช่หลังเจอ

ลงมือทำ — เติมช่องว่างในไฟล์ฝึก

s10_mqtt_telemetry.py pass ท่า 1 — mqtt.connect(...) (1 จุด) pass ท่า 2 — ประกอบ dict + publish (2 จุด) pass ท่า 3 — subscribe + get_message + LED (3 จุด) รวม 6 จุด — เติมทีละจุด แล้วรันทุกครั้ง

เปิด practise_codes/s10_mqtt_telemetry.py↗ มีช่องว่างให้เติม 6 จุด

# เติม: ok = mqtt.connect(BROKER, port=1883, client_id=DEVICE_ID, username=DEVICE_ID, password=MQTT_PASS, keepalive=60)
pass
        # เติม: data = {"ax": round(ax, 2), "ay": round(ay, 2), "az": round(az, 2), "pot": round(sensors.pot.percent(), 1)}
        pass
    # เติม: mqtt.publish(TOPIC_PUB, json.dumps(data))
    pass
# เติม: mqtt.subscribe(TOPIC_CMD)
pass
    # เติม: msg = mqtt.get_message()
    pass
            # เติม: lamp.value(1 if led_on else 0)
            pass

หนึ่ง แก้ค่าเจ็ดบรรทัดบนหัวไฟล์ สอง เติมท่าที่ 1 แล้วรันจนเห็นว่าต่อแล้ว สาม เติมท่า 2-3 ทีละจุด

การเติมครบทั้งหกจุดแล้วค่อยรัน คือวิธีที่ทำให้ไม่มีทางรู้ว่าอะไรพัง — บนเครือข่ายยิ่งจริงกว่าเดิม เพราะจุดที่พังได้มีมากขึ้น

ปิดวงจร — เอาเลขแต่งออก ใส่เซนเซอร์จริงเข้าไป

เจ็ดไฟล์ที่ผ่านมาส่งเลขที่เราแต่งขึ้นเอง โดยตั้งใจ — กลไก MQTT มีเรื่องให้ผิดพลาดมากพออยู่แล้ว · ตอนนี้กลไกแน่นแล้ว examples/s10/08_real_sensor_leaves_the_board.py↗ เอาเลขแต่งออก แล้วต่อ ชิปบนบอร์ด → WiFi → broker ให้ครบวง

ค่าที่ไฟล์นี้ส่งคือ อุณหภูมิ อ่านผ่านฟังก์ชัน read_temp() ในไฟล์ — sensors.snapshot() ไม่มีช่องอุณหภูมิบนบอร์ดไหนเลย ไฟล์จึงถามก่อนว่าบอร์ดมี sensors.sht40 ไหม: บน Dev Kit ได้อุณหภูมิห้องจริงจาก SHT40 ส่วน บน Eva ไม่มีเซนเซอร์อุณหภูมิ ลูกบิดจึงเล่นบทแทน (0–100 % = 15–45 °C) และ console บอกไว้ตั้งแต่รอบแรกว่าค่ามาจากไหน — หมุนลูกบิดแล้วเห็นเส้นวิ่งทั่วกราฟช่วง 20–35 °C ได้ในห้องเรียน ไฟล์นี้ยังไม่มีเกณฑ์ตัดสิน (เงื่อนไข 0.3 องศาคือการบ้านท้ายไฟล์)

ตัวเลขที่ต้องแยกให้ออก ในไฟล์นี้ ทำไม
รอบวัด ทุก 200 ms การวัดไม่กวนใคร วัดถี่ได้ตามใจ
รอบส่ง ทุก 2000 ms การส่งกวน broker และกวนเพื่อนร่วมห้อง

เลขสองตัวนี้ไม่เท่ากัน และไม่ควรเท่ากัน — ถ้าส่งทุก 200 ms คือ 5 ข้อความต่อวินาทีต่อบอร์ด สิบห้าโต๊ะก็ 75 ข้อความต่อวินาทีเข้า broker ตัวเดียว นั่นคือวิธีทำให้ห้องเรียนล่มโดยไม่มีใครเขียนโค้ดผิดสักบรรทัด

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

ตาคุณ — เพิ่มเงื่อนไข "ส่งเฉพาะเมื่อค่าเปลี่ยนเกิน 0.3 องศา" แล้วดูว่าจำนวนครั้งที่ส่งลดลงเท่าไร โดยที่คนดูปลายทางยังเห็นภาพเดิมทุกประการ

เชื่อมโยงรากฐาน + สรุปคาบ

ฝั่งสมองกลฝังตัว บัฟเฟอร์ขนาดจำกัด · การ poll งบหน่วยความจำเป็นข้อจำกัดจริง เครือข่ายกับจอคนละคอร์ ฝั่ง Python และเครือข่าย dict → JSON · bytes vs str pub/sub · topic · QoS try/except กับข้อมูลจากภายนอก ฝั่งออกแบบระบบ สัญญาระหว่างระบบ (topic/schema) แลกความถี่กับต้นทุน ความล้มเหลวที่เงียบ ต้องรู้ล่วงหน้า

วันนี้เราได้: ส่งข้อมูลออกจากบอร์ดไปให้โปรแกรมอื่นใช้ · รับคำสั่งจากภายนอกมาสั่งฮาร์ดแวร์จริง · ติดตั้งแพลตฟอร์ม IoT ด้วยตัวเองและเห็นข้อมูลขึ้นกราฟโดยไม่ต้องสร้างแดชบอร์ด · และรู้ข้อจำกัดจริงสี่ข้อของโมดูล mqtt ที่ไม่ส่งเสียงเวลาเกิน

สิ่งที่ติดตัวไปแม้เปลี่ยนภาษาและเปลี่ยนบอร์ด: การออกแบบชื่อ topic และรูปร่าง payload ในฐานะ สัญญา ที่แก้ทีหลังแล้วพังทั้งระบบ · การเลือกความถี่การส่งจากคำถามว่าใครใช้ค่านี้ทำอะไร · และนิสัยตรวจค่าที่รับมาจากภายนอกก่อนใช้เสมอ

งานทำเอง 30%: เลือกทำ 1 ข้อจากสี่ข้อในสไลด์ต่อยอด บันทึกลง worksheet ข้อ 8

คาบหน้า: เปลี่ยนจากพอร์ต 1883 ไปเป็น 8884 พร้อม TLS แล้วดักจับสัญญาณเทียบกันให้เห็นด้วยตาว่าสิ่งที่คนดักได้ต่างกันอย่างไร

วันนี้บอร์ดพูดได้และฟังเป็นแล้ว คาบหน้าเราจะทำให้ คนอื่นแอบฟังไม่ได้

เฉลย s10_mqtt_telemetry.py↗ — ส่วนที่หนึ่ง

WIFI_SSID = "AIoT-Class"
WIFI_PASSWORD = "<รหัสผ่านของห้องเรียน>"
BROKER = "192.168.1.50"                # IP ของเครื่องที่รัน TESAIoT CE ในแลน (ไม่ใช่ localhost)
DEVICE_ID = "team03"                   # ต้องตรงกับ device_id ที่ขึ้นทะเบียน · <= 31 ตัวอักษร
MQTT_PASS = "bento"                    # broker ฝึกไม่ตรวจ แต่ของห้องเรียนจะตรวจ
TOPIC_PUB = "device/team03/telemetry"
TOPIC_CMD = "device/team03/commands"
...
try:
    sensors.bmi270.motion()            # อุ่นเครื่องหนึ่งครั้ง ให้การรอไปเกิดก่อนต่อเน็ต
except OSError:
    print("อ่านเซนเซอร์รอบแรกยังไม่ได้ - ลองใหม่ตอนส่ง")
...
wifi.connect(WIFI_SSID, WIFI_PASSWORD)
lcd.print("WiFi:", wifi.ip())
ค่าที่ต้องแก้ทั้งหมด อยู่บนหัวไฟล์ 7 บรรทัด ไม่ต้องแก้อะไรข้างล่างอีก คนมาแก้ทีหลังหาเจอทันที DEVICE_ID โผล่ 3 ที่ client_id · username · topic จึงประกาศเป็นตัวแปรตัวเดียว ทำให้ไม่มีทางไม่ตรงกัน ไม่ต้องเรียก sensors.init() Eva: เรียกแล้วได้ OSError Dev Kit: ปลุกไว้ตั้งแต่บูตแล้ว Eva: อ่านครั้งแรกอาจรอถึง 16 วิ

บน Eva Kit คอร์จอ (CM55) เป็นเจ้าของบัสเซนเซอร์ sensors.init() และ sensors.scan() จึงถูกปฏิเสธด้วย OSError ส่วน sensors.bmi270.* capsense.* pot.* เรียกได้ทันทีผ่าน snapshot ที่คอร์จอเก็บไว้ให้ · บน Dev Kit init() ทำงานได้จริง แต่ก็ไม่ต้องเรียก เพราะเฟิร์มแวร์ปลุกเซนเซอร์ไว้ตั้งแต่บูต — โค้ดเดียวกันจึงรันได้ทั้งสองบอร์ด · sensors.snapshot() คืน dict รูปเดียวกันทั้งสองบอร์ด (คีย์ bmi270 capsense pot) และไม่มีคีย์อุณหภูมิ

เขียนค่าที่ต้องตรงกันไว้ที่เดียว แล้วบั๊กประเภท "ลืมแก้ที่หนึ่ง" จะหายไปทั้งตระกูล

เฉลย — แผงเฝ้าลิงก์ สร้าง ก่อน ต่อเน็ต

ui.screen()
time.sleep_ms(200)
ui.Label("MQTT Telemetry - คาบ 10", x=24, y=36, color=COL_TEXT, value=20)
led_wifi = ui.Led(x=328, y=16, w=48, h=48, color=COL_OK, value=0)
...
led_mqtt = ui.Led(x=440, y=16, w=48, h=48, color=COL_OK, value=0)
...
led_stale = ui.Led(x=552, y=16, w=48, h=48, color=COL_WARN, value=0)
...
led_remote = ui.Led(x=664, y=16, w=48, h=48, color=COL_RUN, value=0)
...
ui.Panel(x=24, y=112, w=232, h=280, color=COL_CARD, min=COL_DIM, max=12, value=1)
...
lbl_pot = ui.Label("- %", x=40, y=160, color=COL_TEXT, value=24)
bar_pot = ui.Bar(x=40, y=204, w=200, h=12, color=0x4A9EFF, min=0, max=100, value=0)
sc_pot = ui.Scale(x=40, y=224, w=200, h=44, color=COL_TEXT, min=0, max=100)
sc_pot.ticks(11, 5)
...
seg_sent = ui.Seg7("0", x=40, y=324, w=200, h=48, color=COL_TEXT)
...
lst_sent = ui.List(x=288, y=160, w=208, h=168)
...
btn_go = ui.Button("เริ่มส่ง", x=544, y=240, w=88, h=88, color=0x30A46C, value=20)
btn_hold = ui.Button("หยุดส่ง", x=664, y=240, w=88, h=88, color=0x3A4150, value=20)
ui.poll()

wifi.connect(WIFI_SSID, WIFI_PASSWORD)
lcd.print("WiFi:", wifi.ip())
led_wifi.value(1 if wifi.is_connected() else 0)

จอถูกสร้าง ก่อน wifi.connect() โดยตั้งใจ — การต่อเน็ตคือช่วงที่น่าดูที่สุดของโปรแกรมนี้ ถ้าสร้างจอทีหลัง ช่วงนั้นผ่านไปโดยไม่มีใครเห็น · ไฟสี่ดวงไล่ตาม เส้นทางจริงของข้อมูล: WiFi → MQTT → "ค่าค้าง" → หลอดจริงที่คนอีกห้องสั่งได้ · ui.Scale ใต้ ui.Bar ทำให้ค่า pot มีพิสัยกำกับ — ตัวเลข 14.6 ที่มีไม้บรรทัด 0–100 อยู่ใต้มันบอกทันทีว่าสูงไหม · การ์ดสร้างก่อนของที่วางบนมัน (LVGL วาดตามลำดับสร้าง) · ปุ่มสองปุ่มสูง 88 = เป้าสัมผัสตามเกณฑ์ และจบที่ y=328 เพราะมุมขวาล่างเป็นของปุ่ม Console

ui.List ไม่ใช่ ui.Label เรียงกัน เพราะรายการนี้ถูกล้างแล้วเขียนใหม่ทุกห้าวินาที ถ้าทำด้วย Label ต้องนับพิกเซลใหม่ทุกครั้งที่ข้อความยาวไม่เท่าเดิม

เฉลย — ส่วนที่สอง: ลูปหลัก และทำไมเรียงสามท่าแบบนี้

mqtt.subscribe(TOPIC_CMD)                  # ต่อจากท่าที่ 1 ที่ connect แล้ว
...
while True:
    if sending and time.ticks_diff(time.ticks_ms(), t_last) >= SEND_MS:
        d = publish_telemetry()            # ท่าที่ 2 ทั้งท่าอยู่ในฟังก์ชันนี้
        if d is not None:
            sent += 1
            t_good = time.ticks_ms()
            ...
            lbl_pot.text(str(pot) + " %")  # จอขยับทุก 5 วินาที ไม่ใช่ทุกรอบลูป
            bar_pot.value(int(pot))
            seg_sent.text(str(sent))
            ...
        t_last = time.ticks_ms()
    ...
ท่า 1 — ต่อได้จริง ท่า 2 — ข้อมูลออกได้ ท่า 3 — คำสั่งเข้าได้ ครบสองทาง แต่ละท่ายืนยันท่าก่อนหน้า

ท่า 2 ส่งออกก่อนรับเข้า เพราะขาส่งตรวจง่ายกว่า — ถ้าถึงท่า 3 แล้วไม่ทำงาน รู้แน่ว่าปัญหาอยู่ที่ topic ของคำสั่ง

เฉลย — ส่วนที่สอง (ต่อ): ครึ่งหลังของลูป — สามนาฬิกาในลูปเดียว

    ...
    stale = time.ticks_diff(time.ticks_ms(), t_good) >= STALE_MS
    if stale != stale_shown:               # เขียนเฉพาะตอนเปลี่ยน
        stale_shown = stale
        led_stale.value(1 if stale else 0)

    msg = mqtt.get_message()               # ถามทุกรอบลูป
    if msg is not None:
        ...                                # แปลง JSON แล้วสั่งไฟ - เหมือนท่าที่ 3
    if time.ticks_diff(time.ticks_ms(), t_ui) >= 200:
        t_ui = time.ticks_ms()
        for ev in ui.poll():
            ...                            # ปุ่มเริ่มส่ง / หยุดส่ง
    time.sleep_ms(100)

สังเกตว่าลูปนี้มีนาฬิกา สามเรือน ไม่ใช่เรือนเดียว: เรือนของการส่ง (5 วินาที) เรือนของนิ้ว (200 ms) และเรือนของ get_message() ซึ่งถามทุกรอบ ถ้ายุบทั้งสามให้เดินจังหวะเดียวกัน จะได้โปรแกรมที่ไม่ตอบคำสั่งหรือไม่ก็ยิงจอทิ้งเปล่า

if stale != stale_shown: ไม่ได้เขียนไว้ให้สวย — คิวคำสั่งของจอมีก้นถัง พอมันเต็ม เฟิร์มแวร์ทิ้งคำสั่งเปลี่ยนข้อความก่อนเป็นอย่างแรก (ipc_ui.c ระบุ SET_TEXT ว่าเป็นคำสั่งที่ทิ้งได้) โปรแกรมที่ยิงคำสั่งจอสิบครั้งต่อวินาที จึงเห็นตัวเลขค้างเป็นบางครั้งโดยไม่มี error สักบรรทัด แก้ที่ยิงให้น้อยลง ไม่ใช่ยิงซ้ำให้มากขึ้น

try/except มีไว้เพราะคำสั่งมาจากคนพิมพ์ — โปรแกรมต้องไม่ตายเพราะคนอื่นพิมพ์ผิด

เชื่อมจุดให้เห็นภาพ — วันนี้อยู่ตรงไหนของเส้นทาง

คาบ 1-5 สั่งฮาร์ดแวร์ ในบอร์ดของเราเอง คาบ 6-9 อ่านเซนเซอร์ วาดกราฟ แล้วต่อเน็ตได้ วันนี้ · คาบ 10 ข้อมูลออกไปนอกบอร์ด และคำสั่งเดินกลับเข้ามา คาบ 11-12 ทำให้ปลอดภัย ประกอบเป็นสินค้า วันนี้คือจุดที่ "โครงงานบนโต๊ะ" กลายเป็น "ระบบที่มีหลายเครื่อง"

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

ใช้จริงที่ไหน — สี่มุมที่ MQTT ทำงานอยู่ตอนนี้

โรงงาน · สายการผลิตหลายร้อยจุด เซนเซอร์แต่ละตัว publish ขึ้น topic ของสายผลิตตัวเอง ระบบซ่อมบำรุง subscribe ด้วย wildcard ครั้งเดียว เพิ่มเครื่องจักรใหม่ ไม่ต้องแก้โปรแกรมฝั่งใดเลย อาคาร · ระบบควบคุมส่วนกลาง แอร์ ไฟ ม่าน รับคำสั่งผ่าน topic ของห้องตัวเอง สั่งทั้งชั้นพร้อมกันได้ด้วยการ publish ครั้งเดียว นี่คือขากลับแบบเดียวกับ toggle LED ของเราวันนี้ เกษตร · แปลงที่สัญญาณไม่ดี ส่วนหัวสองไบต์ทำให้ส่งผ่านลิงก์แคบ ๆ ได้ ส่งวันละไม่กี่ครั้ง แบตอยู่ได้เป็นฤดูกาล เลือกความถี่จากธรรมชาติของค่าที่วัด ขนส่ง · รถที่วิ่งอยู่ตลอดเวลา สัญญาณหลุดเป็นเรื่องปกติ ไม่ใช่ข้อยกเว้น QoS 1 สำหรับสิ่งที่หายไม่ได้ · QoS 0 สำหรับพิกัด การเลือก QoS คือการเลือกว่าอะไร "หายได้"

ทั้งสี่มุมนี้ใช้คำสั่งชุดเดียวกับที่เราเพิ่งเขียนวันนี้ ต่างกันแค่จำนวนอุปกรณ์และความสำคัญของข้อมูล

ดูเพิ่มเติมนอกเวลา — วิดีโอที่ตรวจแล้วว่าเปิดได้

เรียนรู้ MQTT และควบคุมอุปกรณ์ IoTs จากทุกมุมโลกด้วย MQTT เข้าใจง่าย — IT around U · 21 นาที 26 วินาที · ไทย — ตัวเลือกภาษาไทยที่ครบที่สุด: ตั้ง broker เอง สาธิต pub/sub และคุมอุปกรณ์จริง (ข้ามส่วนติดตั้งได้ที่นาทีที่ 10)

MQTT Essentials Part 1 — What is MQTT — HiveMQ · 6 นาที 26 วินาที · อังกฤษ — อธิบายว่าทำไม IoT ไม่เลือก HTTP ตั้งแต่ต้น เป็นตอนแรกของซีรีส์ที่เราหยิบตอน 6 และ 7 มาใช้แล้ว

อ่านต่อสำหรับคนอยากรู้ลึก

คลิปเหล่านี้ไม่อยู่ในเกณฑ์ผ่าน แต่คนที่ดูจะเข้าใจคาบ 11 เรื่อง TLS ได้เร็วกว่าเพื่อน

ต่อยอด — คิดต่อเอง (เลือกทำ 1 ข้อ)

1 · ฟังทั้งห้อง device/+/telemetry แล้วเทียบ schema ทุกทีม 2 · คำสั่งชุดใหญ่ คุม LED ทุกดวงของบอร์ด แล้วส่งสถานะกลับ 3 · วัดเพดาน 255 ไบต์ ยาวขึ้นทีละ 10 ไบต์ จนหาจุดที่มันเริ่มพัง 4 · ส่งเมื่อเปลี่ยน แทนการส่งตามเวลา วัดว่าลดไปกี่เปอร์เซ็นต์

ข้อ 1 · ฟังทั้งห้องด้วย wildcard — subscribe device/+/telemetry แล้วบันทึกว่าแต่ละทีมส่งฟิลด์อะไร ทำตารางเทียบ แล้วเสนอว่า ถ้าทั้งห้องต้องใช้ schema เดียวกัน ควรมีฟิลด์ใดบ้างและชื่อว่าอะไร พร้อมเหตุผล

ข้อ 2 · คำสั่งชุดใหญ่ขึ้น — ขยายคำสั่งเป็น {"cmd":"set","led":"RGB_GREEN","on":1} ให้คุม LED ได้ทุกดวงของบอร์ดแยกกัน (gpio.num_leds() ดวง — 3 บน Eva, 5 บน Dev Kit — ระบุดวงด้วยชื่อจาก board_info()["led_names"] ไม่ใช่เลข เพราะเลขเดียวกันคือคนละหลอดบนคนละบอร์ด) และให้บอร์ด publish สถานะกลับ ไปที่ .../status ทุกครั้งที่เปลี่ยน เพื่อให้ฝั่งคอมไม่ต้องเดา

ข้อ 3 · วัดเพดาน 255 ไบต์ด้วยมือตัวเอง — ส่งคำสั่งที่ยาวขึ้นทีละ 10 ไบต์จนโปรแกรมเริ่มพัง บันทึกว่าพังที่ความยาวเท่าไรและอาการเป็นอย่างไร แล้วเขียนวิธีป้องกันที่ดีกว่าการเดา

ข้อ 4 · ส่งเมื่อค่าเปลี่ยน แทนการส่งตามเวลา — เปลี่ยนเงื่อนไขเป็น "ค่าเปลี่ยนเกินเกณฑ์ หรือ ครบ 60 วินาทีแล้วยังไม่ได้ส่ง" แล้ววัดว่าจำนวนข้อความต่อนาทีลดลงกี่เปอร์เซ็นต์ พร้อมอธิบายว่าทำไมยังต้องมีเงื่อนไขข้อหลัง

เขียนคำตอบลง worksheet ข้อ 8 แล้วเอามาเล่าให้เพื่อนฟังต้นคาบหน้า

หน้าจอของทุกไฟล์ในคาบนี้ (1/3)

01 ออกแบบชื่อ topic ก่อนเขียนโค้ดส่ง · 02 รูปร่างของ payload ตัดสินว่าฝั่งรับทำงานง่ายหรือยาก · 03 ต่อ broker แล้วส่งค่าขึ้นไปหนึ่งชุด
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

หน้าจอของทุกไฟล์ในคาบนี้ (2/3)

04 รับคำสั่งจากข้างนอก แล้วทำตาม · 05 ส่งทุก 5 วินาที แต่ยังรับคำสั่งได้ทุก 100 ms · 06 publish คืน True แปลว่าอะไร และไม่แปลว่าอะไร
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

หน้าจอของทุกไฟล์ในคาบนี้ (3/3)

07 บอกลา broker ให้ถูกวิธี แล้วต่อใหม่ด้วยชื่อเดิมได้ทันที · 08 ค่าที่วัดได้จริงบนโต๊ะนี้ ออกไปหาคนอื่น
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

อ้างอิงและเครดิต

มาตรฐานและเอกสารโพรโทคอล

วิดีโอ

ภาพ (ทุกไฟล์เก็บไว้ใน slides/img/ ไม่ได้ลิงก์ข้ามเว็บ) — จาก Wikimedia Commons:

  • s10_mqtt_publish_flow.png (Brivadeneira, CC BY-SA 4.0) · s10_mqtt_topic_wildcards.svg (Ademant, CC BY-SA 4.0) · s10_mqtt_session_flow.svg (Simon A. Eugster, CC BY-SA 4.0)
  • s10_mqtt_publish_packet.svg (Blacktron, CC BY-SA 4.0) · s10_clientserver_sequence.png (Michel Bakni, CC BY-SA 4.0) · s10_pubsub_topic_decoupling.svg (Mathieu.clabaut, CC BY-SA 4.0) · s10_mitm_attack.svg (Miraceti, CC BY-SA 3.0)
  • s10_ce_devices.png, s10_ce_dashboard.png — ภาพหน้าจอ TESAIoT Community Edition v1.1.8, docs/images/screenshots/ (Apache-2.0)

ข้อเท็จจริงของเฟิร์มแวร์และแพลตฟอร์ม

ข้อจำกัดของโมดูล mqtt (ช่องรับ 1 ข้อความ, payload ขาเข้า 255 ไบต์, client_id/username 31 ตัวอักษร, ไม่มี retain, คีย์เวิร์ด keepalive) ตรวจจากซอร์ส modmqtt.c ใน BENTO-TESAIoT-libraries ซึ่งเป็นโค้ดร่วมของทั้ง Eva Kit และ TESAIoT Dev Kit (เพดานทุกตัวจึงเท่ากันสองบอร์ด) · การที่ sensors.init() ถูกปฏิเสธบน Eva Kit และค่าทั้งหมดมาจาก sensors.snapshot() ของคอร์จอ ตรวจจาก modsensors.c (sensors_init() บรรทัด 549 และ sensors_snapshot() บรรทัด 825) · บน Dev Kit snapshot() คืน dict รูปเดียวกัน (IMU อ่านตรงจาก CM33, CapSense/pot จากคอร์จอ) และไม่มีคีย์อุณหภูมิบนบอร์ดไหน — ห้าไฟล์ของคาบ 9–12 จึงอ่านอุณหภูมิผ่าน read_temp() (SHT40 บน Dev Kit / ลูกบิดแทนบน Eva) · ข้อเท็จจริงของ TESAIoT CE (พอร์ต 127.0.0.1:11883, listener 0.0.0.0:1883, reset-mqtt-password, username == client_id == device_id, ACL, การเติม device_id/timestamp และการแบนค่าซ้อนเป็น accel_x) ตรวจจาก repo tesaiot/tesaiot-community-edition v1.3.1 เมื่อ 2026-08-12

ทุกตัวเลขบนสไลด์นี้สืบกลับไปที่ซอร์สโค้ดหรือเอกสารต้นทางได้ ถ้าเจอที่ไม่ตรง บอกผู้สอนได้เลย

fit-css

VIDEO-SLOT: คลิป 60-90 วินาที ถ่ายสองจอพร้อมกัน — จอซ้ายคือ MQTT Explorer บนคอม เห็นข้อความ JSON ไหลเข้ามาทุก 5 วินาทีที่ topic ของทีม จอขวาคือบอร์ด แล้วผู้สอนพิมพ์ {"cmd":"toggle"} ส่งจาก MQTT Explorer ให้เห็น LED บนบอร์ดติดทันที ปิดท้ายด้วยการชี้ว่าสองเครื่องนี้ไม่ได้ต่อสายถึงกันเลย

หน่วยความจำ: บน Eva TLS ต้องการราว 8.8 KB ขณะที่ตอนจับมือมีที่ว่างจริงราว 7 KB ส่วนบน Dev Kit heap ของ MicroPython ถูกย้ายไปอยู่ใน SOCMEM จึงไม่บีบเท่ากัน — แต่นั่นไม่ใช่เหตุผลที่คาบนี้ใช้ 1883

VIDEO-SLOT: คลิป 30-40 วินาที ถ่ายตอนผ่าน MVP — เริ่มที่ MQTT Explorer เห็น JSON เข้ามาสามใบติดกันห่างกัน 5 วินาที (ให้เห็นนาฬิกาหรือ timestamp ในภาพ) แล้วสลับไปหน้า Device Details → Telemetry ที่กราฟกำลังขยับ จากนั้นพิมพ์คำสั่ง toggle สองครั้งให้เห็น LED ติดและดับ ใช้เป็นภาพมาตรฐานของคำว่าผ่านให้ทุกทีมเทียบ

☰ สารบัญ