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ชุดบทเรียนถัดไป (บทเรียน 4.7–4.9) เราจะเปลี่ยนไปพอร์ต 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: แล้วอาการ "ต่อติดแล้วหลุดสลับกัน" ที่อยู่ในตารางกับดักจะหายไปเองครึ่งหนึ่ง
ลองไฟล์
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/# ไว้ล่วงหน้าs10_mqtt_telemetry.py แก้ค่าบนหัวไฟล์ให้เป็นของคุณpass ให้ครบ แล้วกด Program to Device — จากนั้นมองสองจอสลับกันdevice/<device_id>/commands ด้วย payload {"cmd":"toggle"} แล้วดู LEDต้องทำในบทเรียน · เปิดตามลำดับนี้ ทั้งชุดราว 35 นาที
| ลำดับ · เรื่อง · เวลา | ไฟล์ | ลงมือทำอะไร แล้วจะเข้าใจอะไร |
|---|---|---|
| 1 · ตั้งชื่อ topic ก่อนต่อ broker · 8 นาที | 01_topic_design.py |
ตั้ง topic ที่ไม่ชนกับผู้เรียนคนอื่นบน broker เดียวกัน และรู้ว่า wildcard ใส่ใน publish ไม่ได้ · จะเข้าใจว่าชื่อ topic เป็นของที่ออกแบบก่อนต่อ broker ไม่ใช่ค่อยคิดตอนโค้ดพร้อมส่งแล้ว |
| 2 · บันไดสามขั้น WiFi → TCP → MQTT · 15 นาที | 03_connect_and_publish.py |
ส่ง JSON ใบแรกขึ้น broker ได้ และรู้ว่าพังขั้นไหนเมื่อมันพัง · จะเห็นว่าการต่อคือบันไดสามขั้น WiFi → TCP → MQTT ที่ล้มได้คนละแบบ |
| 3 · สั่งกลับจากคอมพิวเตอร์ · 12 นาที | 04_subscribe_command.py |
ซ้อมครึ่งหลังของ MVP — รับ {"cmd":"beep"} กับ {"cmd":"count"} แล้วบอร์ดตอบทันที (คำสั่ง toggle ที่สลับ LED จริงอยู่ในไฟล์ฝึก s10_mqtt_telemetry.py ของบทเรียน 4.6) · จะเข้าใจว่าการสั่งกลับมาที่บอร์ดคือฝั่ง subscribe ไม่ใช่ฝั่ง publish |
ติดตรงไหน เปิดอันนี้
| อาการที่เจอ | ไฟล์ที่ตอบอาการนั้น |
|---|---|
| ส่งได้ทุก 5 วินาทีแล้ว แต่กดสั่งจากคอมทีไรบอร์ดไม่ตอบ | 05_send_every_5s_still_listen.py — นับเวลาถึงกำหนดด้วยนาฬิกา แทนการหลับรอ ไฟล์นี้ไม่ต้องแก้อะไรก่อนกด Run |
| ไม่แน่ใจว่าจะใส่ฟิลด์อะไรลง payload | 02_payload_shape.py — เทียบสามรูปแบบที่ยาวไม่เท่ากันทั้งที่บอกเรื่องเดียวกัน |
| โค้ดบอกว่าส่งแล้ว แต่ MQTT Explorer ไม่เห็นอะไรเลย | 06_sent_is_not_delivered.py — นับที่ปลายทาง ไม่ใช่นับที่ต้นทาง |
อ่านเสริมนอกเวลา — เรื่องนี้เป็นเนื้อหาของบทเรียน 4.7–4.9 ไม่ใช่เกณฑ์ผ่านของวันนี้: 06_secure_publish_loop.py คือลูปเดียวกันนี้ แต่วิ่งบนช่องที่เข้ารหัสไปยังแพลตฟอร์ม วันนี้ยังรันไม่ได้จนกว่าทีมจะได้ device_id ของตัวเอง เปิดอ่านเทียบโครงได้ แต่อย่าเพิ่งพยายามรัน
หนึ่งบอร์ดหนึ่ง
device_id— ห้ามสองบอร์ดใช้client_idเดียวกัน เพราะ broker จะเตะเครื่องเก่าออกทุกครั้งที่เครื่องใหม่ต่อเข้ามา แล้วทั้งสองเครื่องจะหลุดสลับกันเป็นลูป · ไฟล์ 04 06 07 ต่อ broker ฝึกสาธารณะbroker.hivemq.comที่คนอื่นก็ใช้ ก่อนรันให้แก้team03ทั้งในDEVICE_IDและใน topic เป็นรหัสของคุณ เช่นnok4821