| คำถาม | คำตอบของคาบนี้ | อยู่ช่วงไหน | |
|---|---|---|---|
| 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 เราพิสูจน์ว่าสายดี · คาบนี้เราหาคนที่ปลายสายเจอ และคุยกันได้สองทาง
mqtt.connect() / publish() / subscribe() / get_message() ได้ถูกต้องตามข้อจำกัดจริงของเฟิร์มแวร์ปลายทางของวันนี้: MQTT Explorer เห็นค่าจากบอร์ดทุก 5 วินาที และเราพิมพ์คำสั่งจากคอมให้ LED บนบอร์ดสลับสถานะได้
คาบนี้เป็นคาบแรกที่ข้อมูลของเรา ออกจากบอร์ด ไปอยู่ในมือของโปรแกรมอื่น

examples/s10/03_connect_and_publish.py↗ บน BENTO Emulator ที่ 800x480 เท่าจอของทั้งสองบอร์ด — ไม่ใช่ภาพวาด ไม่ใช่ mock-up และไม่ใช่ภาพถ่ายจากบอร์ด · หน้าจอของไฟล์เฉลย (ไฟสี่ดวง · ลูกบิด · สามใบล่าสุด · ปุ่มเริ่ม/หยุดส่ง) ยังไม่ได้ถ่ายถ้าจอบอกว่าส่งแล้ว แต่ฝั่งรับไม่เห็นอะไร แปลว่ายังไม่จบ — คาบนี้ต้องดูสองจอพร้อมกัน
สองอย่างจากคาบที่แล้วที่ต้องเอามาใช้วันนี้ทั้งคู่ — wifi.connect() บล็อกได้นานถึงราว 85 วินาทีถ้ารหัสผิด จึงต้องต่อ WiFi ให้ได้ก่อนเสมอแล้วค่อยเริ่มเรื่อง MQTT และ wifi.ping() รับเฉพาะหมายเลข IP ไม่รับชื่อโฮสต์ — เดี๋ยวเราจะใช้มันตรวจว่าเครื่องที่รัน broker อยู่ในระยะที่คุยได้จริงก่อนจะเสียเวลาต่อ
ถ้า
wifi.is_connected()เป็น False อย่าเพิ่งไปหาสาเหตุที่ MQTT — ปัญหายังไม่เดินทางมาถึงชั้นนั้น
ซ้าย คือแบบที่เราคุ้น (client–server) เว็บเบราว์เซอร์ต้องรู้ชื่อเซิร์ฟเวอร์ ต้องถามก่อนถึงจะได้คำตอบ ผู้ถามกับผู้ตอบ ผูกกันตรง ๆ ถ้าอีกฝั่งย้ายที่อยู่ ทุกฝั่งต้องแก้ตาม
ขวา คือ publish/subscribe ผู้เขียน (writer) โยนข้อมูลใส่ ชื่อเรื่อง ผู้อ่าน (reader) ขอรับตาม ชื่อเรื่อง — ทั้งสองฝั่งไม่เคยรู้จักกัน รู้จักแค่ชื่อเรื่องเดียวกัน
ผลที่ตามมาสามข้อ ซึ่งเป็นเหตุผลที่ IoT เลือกแบบหลัง
คำที่ต้องจำ: pub/sub แยกผู้ส่งออกจากผู้รับ ทั้งในเชิงพื้นที่และเชิงเวลา

CONNECT เข้าไปแนะนำตัว → SUBSCRIBE ขอรับ topic ที่สนใจ → PUBLISH ส่งข้อมูล → DISCONNECT บอกลา
สี่คำนี้คือทั้งหมดที่ผู้เรียนต้องใช้วันนี้ ที่เหลือ broker จัดการให้เอง
อะไรคือ MQTT ? — code maow maow | โค้ดแมวแมว · 8 นาที 26 วินาที · ไทย — ดูเพื่อเห็นภาพรวมของ broker/publisher/subscriber เป็นภาษาไทยก่อนลงมือ
broker ไม่ใช่ฐานข้อมูล มันคือ ที่ทำการไปรษณีย์ — รับเข้ามาแล้วส่งต่อทันที ไม่เก็บไว้ให้ (เว้นแต่สั่งให้เก็บ)
ในภาพ ปลั๊กสองตัวส่ง /plug1/voltage, /plug2/current ส่วนแล็ปท็อป subscribe /plug1/# และมือถือ subscribe /+/current
# = ทุกอย่างที่อยู่ใต้ลงไปกี่ชั้นก็ได้ (ต้องเป็นตัวสุดท้าย) · + = แทนที่ หนึ่งชั้นพอดี
MQTT Essentials Part 6 — Topic Best Practices — HiveMQ · 5 นาที 50 วินาที · อังกฤษ — ดูเพื่อเข้าใจว่าทำไม topic ที่ออกแบบดีทำให้ระบบขยายได้โดยไม่ต้องแก้โค้ดฝั่งอุปกรณ์
topic ไม่ต้องประกาศล่วงหน้า — พิมพ์ publish ไปที่ชื่อไหน ชื่อนั้นก็เกิดขึ้นเดี๋ยวนั้น จึงต้องมีวินัยตั้งชื่อเอง
ทั้ง 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 อาจยังวิ่งอยู่ และในทางกลับกันด้วย
ดูแถวบนสุดของภาพ: 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() มีช่องรับเพียงช่องเดียวนี่คือข้อจำกัดที่เราต้อง สอน ไม่ใช่ซ่อน เพราะมันไม่ส่งเสียงเวลาเกิด
| ค่า | เพดานจริง | เกินแล้วเป็นอย่างไร |
|---|---|---|
| ช่องรับข้อความ | 1 ข้อความ | ข้อความใหม่ทับของเก่าทันที ไม่มีสัญญาณเตือน |
| payload ขาเข้า | 255 ไบต์ | ถูกตัดเงียบ ๆ — JSON ที่ถูกตัดจะ parse ไม่ผ่าน |
| topic ขาเข้า | 127 ไบต์ | ถูกตัดเงียบ |
client_id / username / password |
31 ตัวอักษร | ถูกตัดเงียบ → แพลตฟอร์มหาอุปกรณ์ไม่เจอ → ปฏิเสธการเชื่อมต่อ |
ความล้มเหลวที่แย่ที่สุดสำหรับผู้เรียนคือความล้มเหลวที่เงียบ — จำสี่แถวนี้ไว้ แล้วจะประหยัดเวลาดีบักไปทั้งคาบ
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 วินาทีไม่ได้มาจากความรู้สึก แต่มาจากคำถามว่า ค่าที่เราวัดเปลี่ยนเร็วแค่ไหน อุณหภูมิห้องเปลี่ยนช้ามาก ส่งทุก 5 วินาทีก็ละเอียดเกินพอ ส่วนความสั่นของมอเตอร์เปลี่ยนใน 10 มิลลิวินาที — ค่าแบบนั้นไม่ควรส่งดิบ ๆ ขึ้น MQTT แต่ควรให้บอร์ดสรุปก่อน (ค่าสูงสุด ค่า RMS หรือจำนวนครั้งที่เกินเกณฑ์ในช่วงนั้น) แล้วค่อยส่ง
"ส่งให้ถี่ที่สุดเท่าที่ทำได้" เป็นการออกแบบที่แย่เสมอ — ถามก่อนว่าใครจะใช้ค่านี้ทำอะไร
กติกาการตั้งชื่อ topic ที่ใช้ได้ทั้งชีวิตการทำงาน: เรียงจากกว้างไปแคบ (bento/team03/telemetry ไม่ใช่ telemetry/team03/bento) · ห้ามขึ้นต้นด้วย / เพราะจะได้ช่องว่างเปล่าเป็นชั้นแรก · ห้ามใส่ช่องว่างหรือภาษาไทย · และ อย่าใส่ค่าที่เปลี่ยนบ่อยลงในชื่อ topic (เช่น .../temp/25.4) เพราะผู้รับจะ subscribe ไม่ถูก
สตริงที่เป็นข้อความ เช่น {"status":"ok"} ส่งขึ้นไปได้และเก็บได้ แต่ จะไม่กลายเป็นเส้นกราฟ เพราะกราฟต้องการตัวเลข ถ้าอยากให้เห็นบนกราฟ ให้แปลงเป็นตัวเลขก่อน เช่น {"ok": 1}
ชื่อ topic กับรูปร่าง payload คือ สัญญาระหว่างทีมเรากับทุกคนที่จะใช้ข้อมูลนี้ต่อ เปลี่ยนทีหลังแปลว่าพังทุกฝั่งพร้อมกัน
สิ่งที่ทำให้แล้ว (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 บนเครื่องของตัวเองด้วย Docker
make install (ธง PREBUILT=1 ดึงอิมเมจสำเร็จรูป) ใช้เวลาราว 15–30 นาทีmake up ต้องมาก่อน make init-pki ถ้าสลับกันจะค้างตั้งแต่บูตแรกREADME.th.md และ docs/th/ อีก 15 ไฟล์นี่คือครั้งแรกที่ทีมได้เป็นเจ้าของ ทั้งอุปกรณ์และแพลตฟอร์ม ไม่ใช่แค่ผู้ใช้บริการของใคร
เอกสารของ 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 ไม่มีการเข้ารหัส ทั้ง username, password และ payload ทุกไบต์ เดินทางบน WiFi ห้องเรียนในรูปข้อความอ่านออกได้ ใครที่ดักจับสัญญาณได้ ก็อ่านรหัสของทีมเราได้ และ ปลอมเป็นอุปกรณ์ของเราส่งข้อมูลปลอมเข้าแพลตฟอร์มได้ทันที
server_tls เข้ามาทางพอร์ต 1883คาบหน้า (คาบ 11) เราจะเปลี่ยนไปพอร์ต 8884 พร้อม TLS แล้วเทียบให้เห็นด้วยตาว่าสิ่งที่ดักได้ต่างกันอย่างไร — วันนี้จึงเป็นครึ่งแรกของบทเรียนเรื่องความปลอดภัย ไม่ใช่บทเรียนที่จบในตัว
จำประโยคนี้ให้ได้: "ต่อได้" กับ "ต่อได้อย่างปลอดภัย" เป็นคนละคำถาม และเราเพิ่งตอบข้อแรก

แพลตฟอร์มนี้ ไม่มีการลงทะเบียนอัตโนมัติ — อุปกรณ์ที่ไม่รู้จักถูกปฏิเสธตั้งแต่ตอน CONNECT
device_id เองให้สั้น เช่น team03 (ระบบรับ 3–64 ตัว) — ถ้าปล่อยให้สุ่ม จะได้ UUID ยาว 36 ตัว ยาวเกิน 31 ตัวที่โมดูล mqtt รับได้ ถูกตัดเงียบ แล้วต่อไม่ติดโดยไม่บอกสาเหตุPOST /api/v1/devices/<id>/reset-mqtt-password คืน mqtt_username และรหัสผ่านมาตรง ๆ (อย่าใช้ /reset-password ซึ่งคืนคนละอย่าง)server_tls (ค่าเริ่มต้นอยู่แล้ว)client_id == username == device_id ตรงกันเป๊ะที่นี่ไม่ยอมรับ "เกือบตรง" — ผิดตัวเดียวใน
device_idคือถูกปฏิเสธ และข้อความปฏิเสธไม่ได้บอกว่าผิดตรงไหน
# --- ท่าที่ 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 ทุกใบหายเงียบ
บรรทัดแรกที่ต้องเขียนหลัง
connect()คือบรรทัดที่ บอกให้รู้ว่าต่อติดหรือไม่ติด ไม่ใช่บรรทัดถัดไปของงาน
# --- ท่าที่ 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)) # แบน ไม่ห่อ แพลตฟอร์มห่อให้เอง
sensors.bmi270.motion() คืนหกค่าในการอ่านครั้งเดียว — อ่านทีเดียวดีกว่าเรียกหลายฟังก์ชัน เพราะค่าทั้งหกมาจากช่วงเวลาเดียวกันจริง · บรรทัดที่อ่านค่าอยู่ใน try เพราะอ่านพลาดหนึ่งรอบต้องไม่ทำให้ทั้งโปรแกรมตาย
round() ไม่ใช่เรื่องความสวยงาม: 0.1234567890 กิน 12 ไบต์ ส่วน 0.12 กิน 4 ไบต์ — คูณด้วยจำนวนฟิลด์และรอบต่อวันแล้วคือค่าเน็ตที่ประหยัดได้ฟรี · json.dumps() แปลง dict เป็นสตริงที่ทุกภาษาอ่านออก คือจุดที่ข้อมูลเลิกเป็นของ Python
ส่งแบน ไม่ต้องห่อ บริดจ์เป็นคนห่อให้เอง แล้วเติม
device_id(จาก topic) กับtimestampให้ด้วย
ห่อเองซ้ำจะได้ชื่อวัดขึ้นต้นdata_แล้วตารางหน่วยฝั่งเซิร์ฟเวอร์หาไม่เจอ
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)
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 ซึ่งยังไม่มีใครยืนยันว่ามองเห็นบนบอร์ดประกอบ
คำสั่งจากภายนอกคือ ข้อมูลที่เราไม่ได้เขียนเอง — ตรวจก่อนใช้เสมอ ทั้งว่ามีจริงไหม แปลงได้ไหม และอยู่ในชุดค่าที่เรารู้จักไหม
mqtt.disconnect() และเหตุผลที่ควรเรียก# --- จบงานแล้วบอก broker ให้รู้ ไม่ใช่หายไปเฉย ๆ ---
mqtt.disconnect() # คืน None ไม่ใช่ True
print(mqtt.is_connected()) # False ทันที ไม่ต้องรอ keepalive หมดอายุ
โค้ดหลักของคาบนี้วนลูปไม่รู้จบ จึงไม่เคยเดินมาถึงบรรทัด disconnect() แต่พอทีมกด Ctrl-C หรือกดรันใหม่ บอร์ดหายไปโดยที่ broker ยังนับว่าเรายังอยู่ เพราะฝั่งนั้นรอจนครบ keepalive ก่อนถึงจะยอมรับว่าเราไปแล้ว ระหว่างนั้นชื่อ client_id เดิมยังถูกจองอยู่
disconnect() จึงมีค่าที่สุดตอน เลิกใช้งานตามตั้งใจ ไม่ใช่ตอนพัง เขียนไว้ท้ายไฟล์ หรือใน except KeyboardInterrupt: แล้วอาการ "ต่อติดแล้วหลุดสลับกัน" ที่อยู่ในตารางกับดักจะหายไปเองครึ่งหนึ่ง
ลองไฟล์
examples/s10/07_disconnect_frees_id.py↗ — มันต่อ ส่ง ตัด แล้วต่อใหม่ด้วยชื่อเดิมทันที ให้เห็นว่าทำได้จริงเมื่อบอกลาอย่างถูกวิธี
จุดตรวจที่ใช้ได้จริงเวลาข้อมูลไม่ขึ้น: MQTT Explorer เห็นข้อความไหม ถ้าเห็น แปลว่าสองกล่องแรกและ broker ทำงานครบ ปัญหาอยู่ที่ topic ผิดรูปหรือ JSON ไม่ถูกต้อง (บริดจ์ทิ้ง JSON เสียเงียบ ๆ พร้อมเขียน log) ถ้าไม่เห็น ปัญหายังอยู่ฝั่งบอร์ด
ระบบยาวหกกล่อง แต่การดีบักไม่เคยยาวกว่า "หาให้เจอว่ากล่องสุดท้ายที่ยังเห็นข้อมูลคือกล่องไหน"
docker compose ps เห็น emqx สถานะ up และพอร์ตขึ้นเป็น 0.0.0.0:1883 แล้วจริงlocalhost — บอร์ดอยู่คนละเครื่อง) แล้วทดสอบจากบอร์ดด้วย wifi.ping("192.168.1.50") ก่อน ถ้า ping ไม่ผ่าน อย่าเพิ่งเสียเวลากับ MQTTdevice/# ไว้ล่วงหน้าpractise_codes/s10_mqtt_telemetry.py↗ แก้ค่าบนหัวไฟล์ให้เป็นของทีมpass ให้ครบ แล้วกด Program to Device — จากนั้นมองสองจอสลับกันdevice/<device_id>/commands ด้วย payload {"cmd":"toggle"} แล้วดู LEDต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 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 จะเตะเครื่องเก่าออกทุกครั้งที่เครื่องใหม่ต่อเข้ามา แล้วทั้งสองทีมจะหลุดสลับกันเป็นลูป
สองทาง — publish JSON เซนเซอร์จริงทุก 5 วินาที + สั่ง toggle LED จาก MQTT Explorer ผ่าน topic ของทีม
แปลเป็นสิ่งที่ตรวจได้จริง:
{"cmd":"toggle"} จาก MQTT Explorer แล้ว LED บนบอร์ดสลับสถานะ ได้ทั้งติดและดับclient_id, username, device_id และช่องที่สองของ topic ต้องสัมพันธ์กันอย่างไรข้อที่สามคือหัวใจ — ถ้าขาดข้อนี้ เราสร้างได้แค่ เครื่องส่งข้อมูล ยังไม่ใช่ อุปกรณ์ที่สั่งได้
| อาการ | สาเหตุที่แท้จริง | วิธีแก้ |
|---|---|---|
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 ที่ตรงกับสาเหตุ — จึงต้องอ่านตารางนี้ก่อนเจอปัญหา ไม่ใช่หลังเจอ
เปิด 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 องศา" แล้วดูว่าจำนวนครั้งที่ส่งลดลงเท่าไร โดยที่คนดูปลายทางยังเห็นภาพเดิมทุกประการ
วันนี้เราได้: ส่งข้อมูลออกจากบอร์ดไปให้โปรแกรมอื่นใช้ · รับคำสั่งจากภายนอกมาสั่งฮาร์ดแวร์จริง · ติดตั้งแพลตฟอร์ม 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())
บน 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()
...
ท่า 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มีไว้เพราะคำสั่งมาจากคนพิมพ์ — โปรแกรมต้องไม่ตายเพราะคนอื่นพิมพ์ผิด
คำถามคิดต่อ: ถ้าเน็ตห้องเราหลุดไปสองนาที ข้อมูลช่วงนั้นควรหายไปเลย หรือบอร์ดควรเก็บไว้ส่งทีหลัง · ใครควรเป็นคนตัดสินใจว่าค่าไหนผิดปกติ ระหว่างบอร์ดกับแพลตฟอร์ม · ถ้ามีอุปกรณ์ 500 ตัวส่งทุก 5 วินาที broker ตัวเดียวรับไหวไหม และเราจะรู้ได้อย่างไรก่อนจะสาย
ทั้งสี่มุมนี้ใช้คำสั่งชุดเดียวกับที่เราเพิ่งเขียนวันนี้ ต่างกันแค่จำนวนอุปกรณ์และความสำคัญของข้อมูล
เรียนรู้ 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 · ฟังทั้งห้องด้วย 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 แล้วเอามาเล่าให้เพื่อนฟังต้นคาบหน้า



มาตรฐานและเอกสารโพรโทคอล
วิดีโอ
ภาพ (ทุกไฟล์เก็บไว้ใน 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 ติดและดับ ใช้เป็นภาพมาตรฐานของคำว่าผ่านให้ทุกทีมเทียบ