Extra — เครือข่ายเซนเซอร์ห้อง

หนึ่ง topic ครอบทุกบอร์ดด้วย wildcard +

โปรเจกต์เสริม (ต่อยอดจากคาบ 13 WiFi + MQTT)
หลายบอร์ดส่งค่าของตัวเอง · บอร์ดกลางหนึ่งตัวรวมมาโชว์เป็นกราฟแท่ง

คาบ 13 เราส่งคะแนนหากันสองบอร์ด วันนี้เราจะขยับขึ้นอีกขั้น ให้ "ทั้งห้อง" ส่งค่าเข้ากระดานเดียว แล้วบอร์ดกลางสรุปให้เห็นพร้อมกันหมด

เป้าหมาย + use case จริง

เราจะสร้างเครือข่ายเซนเซอร์เล็ก ๆ แบบที่อาคารจริงใช้กัน หัวใจอยู่ที่การแยกบทบาทเป็นสองฝั่ง

node (หลายบอร์ด)
วัดค่าของตัวเอง แล้ว publish ขึ้น env/<board>/temp
แต่ละบอร์ดตั้งชื่อไม่ซ้ำกัน
aggregator (บอร์ดกลาง 1 ตัว)
subscribe("env/+/temp") รับจากทุกบอร์ด
เก็บค่าล่าสุดใน dict วาดกราฟแท่งบนจอ

หลาย node (เขียว) วัดค่าแล้ว publish เข้า env/<board>/temp ของตัวเอง — aggregator (น้ำเงิน) ตัวเดียว subscribe env/+/temp รับครบทุกบอร์ด เพิ่ม node กี่ตัวก็ไม่ต้องแก้โค้ดฝั่งกลาง

โลกจริงที่หน้าตาเหมือนกันเป๊ะ: เซนเซอร์อุณหภูมิทุกห้องในตึกส่งค่าเข้า dashboard กลาง, เครื่องจักรในโรงงานส่งสถานะเข้าระบบมอนิเตอร์ (Industry 4.0) — โครงสร้าง pub/sub + wildcard อันเดียวกันนี้เลย

นี่คือ node ของจริง ในงานภาคสนาม — แต่ละกล่องคือบอร์ดหนึ่งตัว ตั้งกระจายกันในพื้นที่ มีเซนเซอร์ของตัวเอง วัดค่าแล้วส่งขึ้นเครือข่ายไปรวมที่ส่วนกลาง โครงเหมือนกับที่น้องกำลังจะทำเป๊ะ ต่างแค่กล่องของเราคือบอร์ด Bento บนโต๊ะ ส่วนของจริงทนแดดทนฝนได้

ที่มา: "CSIRO ScienceImage 3719 CSIROs Fleck wireless sensor network technology" — Stephan Brosnan, CSIRO, CC BY 3.0, Wikimedia Commons

ไดอะแกรม pub/sub + ทำไมต้องมี wildcard

ทุกบอร์ดส่งเข้า topic คนละชื่อ (env/board-01/temp, env/board-02/temp, ...) แต่ aggregator ขอฟังด้วย env/+/temp ครั้งเดียว เครื่องหมาย + แทน "หนึ่งระดับ ชื่ออะไรก็ได้"

board-01 env/board-01/temp board-02 env/board-02/temp board-03 env/board-03/temp Broker test.mosquitto.org:1883 aggregator env/+/temp + จับครบทุกชื่อ

ข้อดี: เพิ่มบอร์ดใหม่กี่ตัวก็ได้ aggregator ไม่ต้องแก้โค้ดเลย — + รับให้อัตโนมัติ

topic เป็นต้นไม้ — + จับทั้งกิ่งในระดับเดียว

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

env/+/temp จับ env/board-01/temp, env/board-02/temp, ... ครบทุกบอร์ด แต่ ไม่ ลงไปจับสองระดับ เช่น env/board-01/cpu/temp (นั่นต้องใช้ #) — + คือ "หนึ่งช่อง ชื่ออะไรก็ได้", # คือ "ตั้งแต่ตรงนี้ลงไปกี่ระดับก็เอา"

โปรโตคอลจริงทำงานยังไง — ลำดับ publish/subscribe

ก่อนลงโค้ด ขอเปิดภาพลำดับข้อความจริงของ MQTT ให้ดูสักครั้ง จะเห็นว่าทุกฝั่ง CONNECT เข้า broker ก่อน ฝั่งที่อยากได้ค่า SUBSCRIBE ไว้ พอมีใคร PUBLISH เข้า topic เดียวกัน broker จะ "ส่งต่อ" ให้ทุก subscriber เอง — ผู้ส่งกับผู้รับไม่เคยรู้จักกันตรง ๆ เลย คุยผ่าน broker ล้วน ๆ

นี่คือหัวใจของ pub/sub: แยกผู้ส่ง (node) ออกจากผู้รับ (aggregator) อย่างเด็ดขาด

  • node แค่ PUBLISH ขึ้น topic ของตัวเอง ไม่ต้องรู้ว่าใครฟังอยู่บ้าง
  • aggregator แค่ SUBSCRIBE topic ที่สนใจ ไม่ต้องรู้ว่ามีบอร์ดกี่ตัว
  • broker เป็นตัวกลางจับคู่ให้ — เพิ่ม/ถอดบอร์ดได้ตลอดโดยอีกฝั่งไม่กระทบ

ในภาพใช้ topic temperature/roof, temperature/floor ซึ่งโครงเหมือน env/<board>/temp ของเราเป๊ะ ต่างกันแค่ชื่อ

ที่มา: "MQTT protocol example without QoS" — Simon A. Eugster, CC BY-SA 4.0, Wikimedia Commons

เดินโค้ดฝั่ง node — publish ค่าของตัวเอง

ฝั่ง 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) มาคูณ-บวกเป็นเลขที่เหมือนอุณหภูมิ:

T25+10ay(ay หน่วย g,  วางราบ ay0T25)T \approx 25 + 10\,a_y \qquad (a_y \text{ หน่วย } g,\; \text{วางราบ } a_y\approx 0 \Rightarrow T\approx 25)

วางบอร์ดราบ aya_y เกือบ 0 ได้ราว 25 องศา เอียงไปทางเดียว aya_y เป็นบวก ค่าสูงขึ้น เอียงกลับทางเป็นลบ ค่าต่ำลง — เลข 25 กับ 10 เป็นแค่ค่าที่เลือกให้เห็นการขยับชัด ๆ ไม่ใช่ฟิสิกส์จริงของอุณหภูมิ

กราฟของสมการนี้เป็น เส้นตรง — จุดสีเขียวคือ "วางราบ" (ay0T25a_y\approx 0 \Rightarrow T\approx 25) เลื่อนไปทางขวา (เอียงไปทาง) ค่าก็ไต่ขึ้นเป็นเส้นตรง เลื่อนไปทางซ้ายก็ลดลง ความชัน 10 คือตัวคูณที่ทำให้เอียงนิดเดียวแท่งก็ขยับให้เห็นชัด การ map เส้นตรงแบบนี้ (input ช่วงหนึ่ง -> output อีกช่วง) คือพื้นฐานของ sensor conversion ทุกตัว

เชื่อมโยงรากฐาน — โค้ดสั้น ๆ นี้ร้อยสี่รากฐานเข้าด้วยกัน: (embedded) อ่าน BMI270 ผ่านบัส I2C/I3C ของ PSoC Edge E84 ได้ค่าดิบสามแกน · (algorithm) แปลงค่าเซนเซอร์ดิบเป็นเลขที่ใช้งานได้ด้วยสมการ map ข้างบน — สเต็ปเดียวกับงาน sensor conversion จริงทุกตัว · (python) ห่อ logic ไว้ใน def read_value() แล้วเรียกซ้ำในลูป คือการใช้ฟังก์ชันเพื่อไม่ให้โค้ดซ้ำ · ต่อยอดเป็นผลิตภัณฑ์จริงก็แค่เปลี่ยนสมการนี้เป็นสูตรของเซนเซอร์อุณหภูมิตัวจริง โครงที่เหลือไม่ต้องแตะ

เดินโค้ดฝั่ง aggregator — wildcard + กราฟแท่ง

ฝั่งกลาง 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 จึงไม่บวมไม่ว่ามีกี่บอร์ด

จากตัวเลข -> ความยาวแท่ง (map ค่าเป็นภาพ)

ตอนวาดกราฟ เราต้องเปลี่ยน "ตัวเลขอุณหภูมิ" ให้เป็น "ความยาวแท่งเป็นพิกเซล" โค้ดจริงในฝั่ง aggregator ใช้บรรทัดนี้:

    length = max(10, int((v - 15.0) * 12))   # map ค่า -> ความยาวแท่ง

อ่านเป็นสมการคือ — ลบฐาน 15 ออกก่อน (15 องศา = แท่งสั้นสุด) แล้วคูณ 12 เพื่อขยายให้ตาเห็นง่าย สุดท้ายกันด้วย max(10, ...) ไม่ให้แท่งสั้นกว่า 10 px หรือหดติดลบเวลาเจอค่าน้อย ๆ

L=max ⁣(10,  (v15)×12)  พิกเซลL = \max\!\big(10,\; (v - 15)\times 12\big) \;\text{พิกเซล}

  • เส้นตรงชันขึ้น = ยิ่งร้อน แท่งยิ่งยาว (ความสัมพันธ์เชิงเส้น)
  • ช่วงล่างซ้ายเป็นเส้นแบน = max(10, …) คุมพื้นขั้นต่ำไว้
  • ค่า 25.3 องศา map ได้ราว 123 px (จุดสีน้ำเงิน)

การ map ช่วงค่าหนึ่งไปอีกช่วง (เซนเซอร์ -> พิกเซล) คือเทคนิคพื้นฐานที่ใช้ซ้ำได้ทั้ง gauge, progress bar, และการ scale ค่าเข้ากราฟทุกชนิด

วิธีรัน + ต่อยอดสู่โลกจริง

ต้องใช้กี่บอร์ด: อย่างน้อย 2 ตัว — 1 ตัวรัน aggregator, ที่เหลือรัน node (ยิ่งหลายตัวยิ่งเห็นกราฟหลายแท่ง)

  1. บอร์ด node: เปิด ex_b2_node.py แก้ BOARD ให้ไม่ซ้ำ แล้วกด Program to Device
  2. บอร์ดกลาง: เปิด ex_b2_aggregator.py กด Program to Device — กราฟแท่งจะขึ้นเอง
  3. ลองเอียงบอร์ด node แล้วดูแท่งของมันขยับบนจอ aggregator

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

ต่อยอดที่อยากชวนน้อง ๆ ลอง:

  • เปลี่ยน topic เป็น env/+/+ เพื่อรับทั้ง temp และ humidity ในคราวเดียว
  • เพิ่ม timestamp ในกราฟ ถ้าบอร์ดไหนเงียบเกิน 10 วิ ให้เปลี่ยนแท่งเป็นสีแดง (เหมือน dashboard เฝ้าระวังจริง)
  • ส่งค่าขึ้น cloud dashboard (Node-RED / Grafana) — โครง pub/sub เดิม ต่อกับระบบ Industry 4.0 ได้ทันที

เครือข่ายเซนเซอร์ที่น้องทำวันนี้ คือหัวใจเดียวกับระบบ smart building และ smart factory จริง ต่างกันแค่จำนวน node เท่านั้นเอง

อยากเข้าใจ pub/sub ให้ลึกขึ้น

การออกแบบ topic ให้ดีคือหัวใจของเครือข่ายเซนเซอร์หลาย node — ตั้งชื่อ topic เป็นลำดับชั้น แล้ว subscribe ด้วย wildcard ทีเดียวรับได้ทุกตัว คลิปนี้จาก HiveMQ สรุปแนวทางตั้ง topic ที่ใช้ได้จริง

MQTT Topic Best Practices | MQTT Essentials Part 6 — HiveMQ

วิธีออกแบบ topic เป็นลำดับชั้น และใช้ wildcard subscribe หลาย node พร้อมกัน ตรงกับเครือข่ายเซนเซอร์ที่เราทำวันนี้

fit-css

← Extra Index