+ จับทั้งกิ่งในระดับเดียวถ้ามอง topic เป็นต้นไม้ จะเห็นชัดว่า + ไปยืนอยู่ตรง "ระดับชื่อบอร์ด" แล้วจับทุกกิ่งในระดับนั้น (หนึ่งระดับเท่านั้น) — บอร์ดใหม่งอกกิ่งเพิ่ม + ก็จับให้เองโดยที่เราไม่ต้องไปแตะโค้ด aggregator

env/+/temp จับ env/board-01/temp, env/board-02/temp, ... ครบทุกบอร์ด แต่ ไม่ ลงไปจับสองระดับ เช่น env/board-01/cpu/temp (นั่นต้องใช้ #) — + คือ "หนึ่งช่อง ชื่ออะไรก็ได้", # คือ "ตั้งแต่ตรงนี้ลงไปกี่ระดับก็เอา"
ก่อนลงโค้ด ขอเปิดภาพลำดับข้อความจริงของ MQTT ให้ดูสักครั้ง จะเห็นว่าทุกฝั่ง CONNECT เข้า broker ก่อน ฝั่งที่อยากได้ค่า SUBSCRIBE ไว้ พอมีใคร PUBLISH เข้า topic เดียวกัน broker จะ "ส่งต่อ" ให้ทุก subscriber เอง — ผู้ส่งกับผู้รับไม่เคยรู้จักกันตรง ๆ เลย คุยผ่าน broker ล้วน ๆ

นี่คือหัวใจของ pub/sub: แยกผู้ส่ง (node) ออกจากผู้รับ (aggregator) อย่างเด็ดขาด
PUBLISH ขึ้น topic ของตัวเอง ไม่ต้องรู้ว่าใครฟังอยู่บ้างSUBSCRIBE topic ที่สนใจ ไม่ต้องรู้ว่ามีบอร์ดกี่ตัวในภาพใช้ topic temperature/roof, temperature/floor ซึ่งโครงเหมือน env/<board>/temp ของเราเป๊ะ ต่างกันแค่ชื่อ
ที่มา: "MQTT protocol example without QoS" — Simon A. Eugster, CC BY-SA 4.0, Wikimedia Commons
ฝั่ง node ต่อ WiFi → ต่อ broker → วน publish ค่าทุก 2 วินาที payload ใช้รูปแบบเดียวกันทั้งห้องคือ "ชื่อ:ค่า"
BOARD = "board-01" # แต่ละบอร์ดตั้งไม่ซ้ำกัน
TOPIC = "env/%s/temp" % BOARD # env/board-01/temp
sensors.init() # ปลุกเซนเซอร์ครั้งเดียว
def read_value():
ax, ay, az = sensors.bmi270.acceleration() # อ่านความเร่ง 3 แกน
return round(25.0 + ay * 10.0, 1) # เอียง -> ค่าราว 25 องศา
while True:
value = read_value()
payload = "%s:%.1f" % (BOARD, value) # "board-01:25.3"
mqtt.publish(TOPIC, payload) # ส่งขึ้นกระดานกลาง
time.sleep(2) # อย่าส่งถี่เกิน
บอร์ดนี้ไม่มีเซนเซอร์อุณหภูมิจริง เราจึงประมาณค่าจากความเอียง (BMI270) — เอียงบอร์ดแล้วค่าขยับเห็นชัด เหมาะกับการสาธิตแนวคิด pub/sub
บรรทัด read_value() คือการ แปลงค่าเซนเซอร์เป็นค่าที่สื่อความหมาย เราหยิบความเร่งแกน y (หน่วย g) มาคูณ-บวกเป็นเลขที่เหมือนอุณหภูมิ:
วางบอร์ดราบ เกือบ 0 ได้ราว 25 องศา เอียงไปทางเดียว เป็นบวก ค่าสูงขึ้น เอียงกลับทางเป็นลบ ค่าต่ำลง — เลข 25 กับ 10 เป็นแค่ค่าที่เลือกให้เห็นการขยับชัด ๆ ไม่ใช่ฟิสิกส์จริงของอุณหภูมิ

กราฟของสมการนี้เป็น เส้นตรง — จุดสีเขียวคือ "วางราบ" () เลื่อนไปทางขวา (เอียงไปทาง) ค่าก็ไต่ขึ้นเป็นเส้นตรง เลื่อนไปทางซ้ายก็ลดลง ความชัน 10 คือตัวคูณที่ทำให้เอียงนิดเดียวแท่งก็ขยับให้เห็นชัด การ map เส้นตรงแบบนี้ (input ช่วงหนึ่ง -> output อีกช่วง) คือพื้นฐานของ sensor conversion ทุกตัว
เชื่อมโยงรากฐาน — โค้ดสั้น ๆ นี้ร้อยสี่รากฐานเข้าด้วยกัน: (embedded) อ่าน BMI270 ผ่านบัส I2C/I3C ของ PSoC Edge E84 ได้ค่าดิบสามแกน · (algorithm) แปลงค่าเซนเซอร์ดิบเป็นเลขที่ใช้งานได้ด้วยสมการ map ข้างบน — สเต็ปเดียวกับงาน sensor conversion จริงทุกตัว · (python) ห่อ logic ไว้ใน
def read_value()แล้วเรียกซ้ำในลูป คือการใช้ฟังก์ชันเพื่อไม่ให้โค้ดซ้ำ · ต่อยอดเป็นผลิตภัณฑ์จริงก็แค่เปลี่ยนสมการนี้เป็นสูตรของเซนเซอร์อุณหภูมิตัวจริง โครงที่เหลือไม่ต้องแตะ
ฝั่งกลาง subscribe ด้วย + ครั้งเดียว แล้ววน get_message() (ไม่บล็อก) เก็บค่าล่าสุดของแต่ละบอร์ดลง dict แล้ววาดแท่ง
mqtt.subscribe("env/+/temp") # + = รับทุกชื่อบอร์ด
values = {} # {"board-01": 25.3, ...}
while True:
if game.keys().back: # กด BACK = ออก
break
msg = mqtt.get_message() # (topic, bytes) หรือ None
if msg is not None:
topic, data = msg
text = data.decode() # bytes ต้อง decode ก่อน
name, sval = text.split(":", 1) # แยก "ชื่อ:ค่า"
values[name] = float(sval) # เก็บค่าล่าสุด
draw() # วาดกราฟแท่งใหม่
time.sleep(0.1)
values[name] = ... ทับค่าเดิมของบอร์ดเดิมเสมอ จึงเห็นเฉพาะค่าล่าสุดของทุกบอร์ดพร้อมกัน

ไล่ตามลูปในภาพ: ทุกหนึ่งรอบเรา ถาม get_message() แบบไม่รอ (non-blocking) — ถ้าได้ None ก็ sleep(0.1) วนใหม่ จอจึงยังลื่นและกด BACK ออกได้ตลอด · ถ้ามีข้อความเข้ามาค่อย decode() + split(":") แล้วยัดลง dict ที่คีย์เป็นชื่อบอร์ด — คีย์ซ้ำจึง ทับค่าเดิม เก็บแค่ค่าล่าสุด หนึ่งคีย์ต่อหนึ่งบอร์ด dict จึงไม่บวมไม่ว่ามีกี่บอร์ด
ตอนวาดกราฟ เราต้องเปลี่ยน "ตัวเลขอุณหภูมิ" ให้เป็น "ความยาวแท่งเป็นพิกเซล" โค้ดจริงในฝั่ง aggregator ใช้บรรทัดนี้:
length = max(10, int((v - 15.0) * 12)) # map ค่า -> ความยาวแท่ง
อ่านเป็นสมการคือ — ลบฐาน 15 ออกก่อน (15 องศา = แท่งสั้นสุด) แล้วคูณ 12 เพื่อขยายให้ตาเห็นง่าย สุดท้ายกันด้วย max(10, ...) ไม่ให้แท่งสั้นกว่า 10 px หรือหดติดลบเวลาเจอค่าน้อย ๆ

max(10, …) คุมพื้นขั้นต่ำไว้การ map ช่วงค่าหนึ่งไปอีกช่วง (เซนเซอร์ -> พิกเซล) คือเทคนิคพื้นฐานที่ใช้ซ้ำได้ทั้ง gauge, progress bar, และการ scale ค่าเข้ากราฟทุกชนิด
ต้องใช้กี่บอร์ด: อย่างน้อย 2 ตัว — 1 ตัวรัน aggregator, ที่เหลือรัน node (ยิ่งหลายตัวยิ่งเห็นกราฟหลายแท่ง)
ex_b2_node.py แก้ BOARD ให้ไม่ซ้ำ แล้วกด Program to Deviceex_b2_aggregator.py กด Program to Device — กราฟแท่งจะขึ้นเอง
บนจอ aggregator เปิด node ทีละตัว แท่งใหม่จะ "งอก" ขึ้นมาเองทุกครั้งที่มีบอร์ดเริ่มส่ง โดยที่เราไม่ได้แตะโค้ด aggregator เลยสักบรรทัด — นั่นแหละพลังของ + ที่จับทุกชื่อบอร์ดให้อัตโนมัติ
ต่อยอดที่อยากชวนน้อง ๆ ลอง:
env/+/+ เพื่อรับทั้ง temp และ humidity ในคราวเดียวเครือข่ายเซนเซอร์ที่น้องทำวันนี้ คือหัวใจเดียวกับระบบ smart building และ smart factory จริง ต่างกันแค่จำนวน node เท่านั้นเอง
การออกแบบ topic ให้ดีคือหัวใจของเครือข่ายเซนเซอร์หลาย node — ตั้งชื่อ topic เป็นลำดับชั้น แล้ว subscribe ด้วย wildcard ทีเดียวรับได้ทุกตัว คลิปนี้จาก HiveMQ สรุปแนวทางตั้ง topic ที่ใช้ได้จริง
MQTT Topic Best Practices | MQTT Essentials Part 6 — HiveMQ
วิธีออกแบบ topic เป็นลำดับชั้น และใช้ wildcard subscribe หลาย node พร้อมกัน ตรงกับเครือข่ายเซนเซอร์ที่เราทำวันนี้
fit-css