תכנות מיקרו-בקרים בשפת MicroPython · פרק 16 — פרוטוקול MQTT⌂📚🏠
פרק 16

פרוטוקול MQTT

The MQTT Protocol
4 שעות לימוד

בשני הפרקים הקודמים הלוח פנה לכתובת מסוימת וחיכה לתשובה. זה לא מתאים לחיישן קטן ששולח את הקריאה שלו כל כמה שניות למי שמעוניין בה, וגם לא ללוח שממתין לפקודה שאינו יודע מתי תגיע. בפרק הזה נלמד דרך אחרת: כל לוח מדבר עם גורם אחד בלבד, המתווך, והוא מעביר כל הודעה למי שביקש אותה.

בפרק הזה:
  • נסביר את תפקידם של המתווך, הנושא, הפרסום וההרשמה, ונבחר שמות מסודרים לנושאים.
  • נפרסם קריאה בעזרת הספרייה umqtt.simple, נקבל פקודה ונבחין בין check_msg() ל-wait_msg().
  • נשים נתונים בהודעת JSON, ונטפל בהודעה פגומה בלי שהתוכנית תיעצר.
  • נבחר איכות שירות, ונבין מהן הודעה שמורה, keepalive וצוואה אחרונה.
  • נתאושש מניתוק, ונחבר שני לוחות דרך מתווך אחד.
הערה: בבניית הספר כל התוכניות בפרק הזה רצות מול מתווך מדומה וב-ESP32 בלבד, ולא ברשת אמיתית, ולכן מה שמופיע על המסך אינו מוכיח שהודעה עברה באוויר. השורות שהמדמה מוסיף אחרי התוכנית מראות מה פורסם. הן אינן מזכירות מספרי הדקים, ולכן מתאימות גם ל-Pico W אחרי התקנת הספרייה.

16.1 מתווך במקום חיבור ישיר (Publish and Subscribe)

ב-HTTP מי שפונה מכיר את הכתובת של מי שהוא פונה אליו, והצד השני צריך להיות מוכן באותו רגע. MQTT מפריד בין שני הצדדים: הלוח שמודד אינו יודע מי קורא, ומי שקורא אינו יודע מי מדד.

מתווך: תוכנית שרצה על שרת קבוע. כל ההתקנים מתחברים אליה, והיא מפיצה ביניהם את ההודעות. שמה באנגלית broker.
נושא: כתובת טקסט שנכתבת על כל הודעה, למשל school/lab1/temp. שמו באנגלית topic.
פרסום והרשמה: פרסום הוא שליחת הודעה לנושא. הרשמה היא בקשה מן המתווך לקבל כל הודעה שמגיעה לנושא מסוים. אותו התקן יכול להיות גם מפרסם וגם מנוי.
ESP32מפרסם: טמפרטורהPico Wמפרסם: לחותהמתווךbrokerRaspberry Piמנוי: שניהםטלפוןמנוי: הטמפרטורהפרסוםפרסוםמסירהמסירה
לכל התקן יש חיבור אחד, אל המתווך. המפרסם אינו יודע כמה מנויים יש.

ההודעה ב-MQTT היא רצף בייטים שהפרוטוקול אינו קובע את משמעותו, ואנחנו קוראים לה מטען. כל התקן מתחבר למתווך דרך TCP והחיבור נשאר פתוח, ולכן המתווך יכול להעביר פקודה ללוח בכל רגע, בלי שהלוח ישאל עליה.

דוגמה 16.1
במערכת יש 6 חיישנים ו-3 מסכים, וכל מסך מציג את כל החיישנים. כמה חיבורים דרושים אם כל חיישן מדבר ישירות עם כל מסך? וכמה עם מתווך?
פתרון:

נסמן ב-Ns את מספר החיישנים וב-Nd את מספר המסכים. בחיבור ישיר יש חיבור לכל זוג של חיישן ומסך.

Ndirect = Ns · Nd
Ndirect = 6 · 3
Ndirect = 18

עם מתווך, לכל התקן יש חיבור אחד.

Nbroker = Ns + Nd
Nbroker = 6 + 3
Nbroker = 9
Nbroker = 9
שאלה 16.1: המתווך הפסיק לעבוד. האם שני לוחות סמוכים יכולים להחליף הודעה דרך MQTT? מה מסיקים מכך?

16.2 שמות נושאים ותווים כלליים (Topics and Wildcards)

הנושא הוא נתיב שהקווים האלכסוניים מפרידים בין דרגותיו, ואנחנו בוחרים את המבנה שלו. הכלל שלנו בספר הזה: מהכללי אל הפרטי, כלומר school/desk1/pot, באותיות אנגליות קטנות, בלי רווחים ובלי קו אלכסוני בתחילתו. הפקודה והמצב הם שני נושאים נפרדים: כותבים .../led/set לפקודה ו-.../led/state למצב בפועל.

רק בהרשמה אפשר להשתמש בשני תווים כלליים:

התומשמעותודוגמה להרשמהמתאים ללא מתאים ל
+דרגה אחת בדיוקschool/+/tempschool/lab1/tempschool/lab1/door/temp
#כל מה שנשאר, ומופיע רק בסוף השםschool/lab1/#school/lab1/door/tempschool/lab2/temp
נרשמים ל-school/+/temp ומפרסמים לארבעה נושאים. חוזר אלינו רק מה שמתאים להרשמה.
import time
from umqtt.simple import MQTTClient
import wifi

TOPICS = (b"school/lab1/temp", b"school/lab2/temp",
          b"school/lab1/hum", b"school/lab1/door/temp")


def on_message(topic, msg):
    print("  received:", topic.decode(), "=", msg.decode())


wifi.connect()
client = MQTTClient("desk1-wild", "broker.hivemq.com")
client.set_callback(on_message)
client.connect()
client.subscribe(b"school/+/temp")

for t in TOPICS:
    print("publish:", t.decode())
    client.publish(t, b"21")
    time.sleep(1)
    client.check_msg()

client.disconnect()
ESP32 + Pico W · MicroPython 1.29 · simulated board
publish: school/lab1/temp
  received: school/lab1/temp = 21
publish: school/lab2/temp
  received: school/lab2/temp = 21
publish: school/lab1/hum
publish: school/lab1/door/temp

התוכנית מדברת עם עצמה דרך המתווך, וזו דרך טובה לבדוק כל הרשמה. שימו לב ל-check_msg() אחרי כל פרסום: ההודעה מגיעה לפונקציה רק אם מבקשים אותה. נחזור לזה בסעיף 16.4.

אזהרה: הנושא מבחין בין אותיות גדולות לקטנות: School/lab1 ו-school/lab1 הם שני נושאים שונים. טעות באות אחת אינה מפיקה הודעת שגיאה: ההודעות הולכות לנושא שאיש אינו קורא.
שאלה 16.2: נרשמנו ל-school/lab1/#. אילו מארבעת הנושאים בתוכנית מגיעים אלינו?

16.3 פרסום קריאה (Publishing)

נשתמש בספרייה umqtt.simple מתוך micropython-lib. קובצי ההגדרה הרשמיים של ESP32 מזכירים אותה בתוך הקושחה, וקובצי Pico W אינם מזכירים אותה. לכן קודם ננסה from umqtt.simple import MQTTClient. אם מופיעה שגיאה שהמודול אינו קיים, נתחבר לרשת ונתקין אותו פעם אחת בפונקציה mip.install('umqtt.simple'), כפי שראינו בפרק 11.

כל התוכניות בפרק הזה צריכות קודם כול רשת, ולשם כך אנחנו משתמשים בספרייה wifi.py מפרק 14.

קריאת הפוטנציומטר כל 5 שניות, באחוזים.
import time
from umqtt.simple import MQTTClient
import board
import wifi
from convert import to_percent

BROKER = "broker.hivemq.com"
TOPIC = b"school/desk1/pot"

pot = board.analog_in(board.POT)

wifi.connect()
client = MQTTClient("desk1-pub", BROKER)
client.connect()
print("connected to", BROKER)

while True:
    percent = to_percent(pot.read_u16())
    client.publish(TOPIC, str(percent).encode())
    print("sent", percent)
    time.sleep(5)
ESP32 + Pico W · MicroPython 1.29 · simulated board
connected to broker.hivemq.com
sent 25
sent 50
sent 74
[mqtt_pub]
school/desk1/pot 25
school/desk1/pot 50
school/desk1/pot 74
...
פקודהמה היא עושה
MQTTClient(client_id, server)יוצרים את הלקוח. עדיין לא מתבצע שום חיבור.
client.connect()פותחים חיבור למתווך, כברירת מחדל בשער 1883.
client.publish(topic, msg)שולחים את המטען לנושא. מי קורא? זה לא ענייננו.
דוגמה 16.2
מה גודל ההודעה שהתוכנית שולחת בכל פעם, וכמה בייטים ביום? הנושא school/desk1/pot, והמטען בן שני בייטים. להודעה קטנה באיכות שירות 0 יש כותרת קבועה של שני בייטים, ועוד שני בייטים לאורך הנושא.
פתרון:

Lt הוא אורך הנושא ו-Lp הוא אורך המטען.

B = 2 + 2 + Lt + Lp
B = 2 + 2 + 16 + 2
B = 22 byte
n = 864005
n = 17280
Bday = n · B
Bday = 17280 · 22
Bday = 380160 byte

זה כ-380 kB ביום, בלי הכותרות של TCP/IP.

Bday = 380160 byte
אזהרה: לכל לקוח שמחובר למתווך יש מזהה משלו, מזהה לקוח. אם שני לוחות מתחברים עם אותו מזהה, המתווך מנתק את הראשון, ושניהם מתחלפים בניתוק בלי סיבה נראית. כשתוכנית אחת רצה על לוחות רבים, בונים את המזהה מ-machine.unique_id().
שאלה 16.3: למה התוכנית אינה צריכה את check_msg()?

16.4 הרשמה וקבלת פקודה (Subscribing)

הקליטה עוברת שלושה שלבים, והסדר שלהם חשוב: נותנים לספרייה פונקציה שתיקרא כשמגיעה הודעה, set_callback, אחר כך מתחברים, ואחר כך נרשמים. אחרי זה צריך לתת לספרייה הזדמנות לקרוא את מה שהגיע, באחת משתי פונקציות:

check_msg()wait_msg()
התנהגותבודקת וחוזרת מידממתינה עד שמגיעה הודעה
מתי משתמשיםכשהתוכנית עושה גם עבודה אחרתכשאין לתוכנית עבודה מלבד המתנה לפקודות
הסכנהאם שכחנו לקרוא לה, לא תגיע שום הודעההתוכנית לא זזה בזמן ההמתנה
הפקודה מגיעה בשנייה 1, 2 ו-3, מסימולציה של המתווך. המצב מתפרסם לנושא אחר.
import time
from machine import Pin
from umqtt.simple import MQTTClient
import board
import wifi

CMD = b"school/desk1/led/set"
STATE = b"school/desk1/led/state"

led = Pin(board.LED, Pin.OUT)
client = MQTTClient("desk1-led", "broker.hivemq.com")


def on_message(topic, msg):
    if msg == b"on":
        led.on()
    elif msg == b"off":
        led.off()
    else:
        print("unknown command:", msg)
        return
    client.publish(STATE, msg)
    print("led is now", led.value())


wifi.connect()
client.set_callback(on_message)
client.connect()
client.subscribe(CMD)

while True:
    client.check_msg()
    time.sleep(0.1)
ESP32 + Pico W · MicroPython 1.29 · simulated board
led is now 1
unknown command: b'blink'
led is now 0
[mqtt_pub]
school/desk1/led/state on
school/desk1/led/state off
...
אזהרה: הספרייה מעבירה לפונקציה את הנושא ואת המטען מסוג bytes. לכן התנאי msg == "on" שגוי תמיד, בלי שום הודעת שגיאה, שום דבר לא קורה ואנחנו מחפשים את התקלה ברשת. הנכון הוא msg == b"on".
הערה: מפרידים בין נושא הפקודה לנושא המצב משני טעמים: אילו פרסמנו את המצב לנושא הפקודה, הוא היה חוזר אלינו כפקודה חדשה, ונושא המצב מראה מה קרה בפועל בלוח ולא מה ביקשו ממנו.
שאלה 16.4: בתוכנית הפקודות משנים את time.sleep(0.1) ל-time.sleep(10). האם הפקודות הולכות לאיבוד?

16.5 הודעת JSON

כשיש ערך אחד, מספיק טקסט קצר. כשיש יותר, כותבים את המטען בפורמט JSON שראינו בפרק 12: הטלפון והשרת קוראים אותו בקלות, והוא מראה את משמעותו של כל ערך. כל התקן יכול לשלוח לנו כל דבר, ולכן מגינים על הקריאה בעזרת try.

פקודה תקינה משנה את זמן המחזור של השליחה, והודעה פגומה אינה עוצרת את התוכנית.
import json
import time
from umqtt.simple import MQTTClient
import board
import wifi
from convert import to_percent

CMD = b"school/desk1/cmd"
DATA = b"school/desk1/data"

pot = board.analog_in(board.POT)
period = 4


def on_message(topic, msg):
    global period
    try:
        period = json.loads(msg)["period"]
        print("new period:", period)
    except (ValueError, KeyError):
        print("ignored:", msg)


wifi.connect()
client = MQTTClient("desk1-json", "broker.hivemq.com")
client.set_callback(on_message)
client.connect()
client.subscribe(CMD)

elapsed = 0
while True:
    client.check_msg()
    if elapsed >= period:
        elapsed = 0
        data = {"pot": to_percent(pot.read_u16())}
        client.publish(DATA, json.dumps(data).encode())
    time.sleep(1)
    elapsed += 1
ESP32 + Pico W · MicroPython 1.29 · simulated board
new period: 2
ignored: b'hello'
[mqtt_pub]
school/desk1/data {"pot": 50}
school/desk1/data {"pot": 50}
school/desk1/data {"pot": 50}
school/desk1/data {"pot": 50}
...
דוגמה 16.3
כמה בייטים עולה הבהירות של JSON? נשווה את הטקסט 24.5 לטקסט {"temp": 24.5, "hum": 60}.
פתרון:

סופרים את התווים בכל טקסט. כל תו אנגלי הוא בייט אחד.

D = Ljson − Lplain
D = 25 − 4
D = 21 byte

הבייטים האלה נושאים את הערך השני ואת שמות הערכים.

D = 21 byte
שאלה 16.5: להודעה {"speed": 3} הגיעה לתוכנית. מה היא מדפיסה? ולמה היא לא נעצרת?

16.6 QoS, הודעה שמורה, keepalive וצוואה (QoS, Retain, Keepalive, Last Will)

איכות שירות, ושמה QoS, היא מה שאנחנו דורשים ממסירת ההודעה:

הרמהההבטחההמחירב-umqtt.simple
0לכל היותר פעם אחת: ייתכן שתלך לאיבודהקלה מכולןנתמכת
1לפחות פעם אחת: ייתכן שתחזוראישור לכל הודעה, והמתנה לונתמכת
2פעם אחת בדיוקארבע הודעות לכל הודעהאינה נתמכת

קריאה שחוזרת כל 5 שניות מסתפקת ב-0: הקריאה הבאה תפצה. פקודה שלא באה אחריה פקודה אחרת ראויה ל-1, ואז התוכנית צריכה לעמוד בכך שהיא עלולה להגיע פעמיים. עוד שלושה רעיונות משלימים את התמונה:

לא הורץ בבניית הספר: במדמה אין set_last_will, והוא אינו שומר הודעות.
import time
from umqtt.simple import MQTTClient
import wifi

STATUS = b"school/desk1/status"

wifi.connect()
client = MQTTClient("desk1", "broker.hivemq.com", keepalive=60)
client.set_last_will(STATUS, b"offline", retain=True, qos=1)
client.connect()
client.publish(STATUS, b"online", retain=True, qos=1)

while True:
    client.ping()
    client.check_msg()
    time.sleep(30)

השורה החשובה היא set_last_will, והיא באה לפני connect() לפי תיעוד הספרייה. בעת ההתחברות מפרסמים online כהודעה שמורה באותו נושא, וכך מי שנרשם בכל זמן יודע את המצב עכשיו: online או offline.

דוגמה 16.4
הגדרנו keepalive=60. לפי מפרט הפרוטוקול, המתווך מנתק את החיבור אחרי שתיקה שאורכה פעם וחצי מהזמן הזה. מתי זה קורה?
פתרון:
t = 1.5 · T
t = 1.5 · 60
t = 90 s

לכן התוכנית קוראת ל-ping() כל 30 שניות, עם מרווח נוח.

t = 90 s
שאלה 16.6: למה מציבים retain=True על הודעת המצב, ולא על פקודת הפעלת המנוע?

16.7 התאוששות מניתוק (Reconnecting)

הלוח יעבוד ימים, והרשת תתנתק. בלי טיפול, התוכנית נעצרת בשגיאת OSError הראשונה ואינה חוזרת. הרעיון פשוט: תופסים את השגיאה, מנסים שוב, וממתינים בין הניסיונות זמן שמוכפל, כדי לא להציף את המתווך כשהוא חוזר.

בסבב השלישי אנחנו מנתקים את החיבור בעצמנו כדי לראות את הטיפול, ובמציאות העולם מנתק אותו.
import time
from umqtt.simple import MQTTClient
import wifi

TOPIC = b"school/desk1/count"
client = MQTTClient("desk1-safe", "broker.hivemq.com")


def reconnect(tries=4):
    wait = 1
    for _ in range(tries):
        try:
            client.connect()
            print("connected")
            return True
        except OSError:
            print("no broker, waiting", wait, "s")
            time.sleep(wait)
            wait *= 2
    return False


def send(msg):
    try:
        client.publish(TOPIC, msg)
    except OSError:
        print("link lost")
        if reconnect():
            client.publish(TOPIC, msg)


wifi.connect()
if reconnect():
    for i in range(4):
        if i == 2:
            client.disconnect()
        send(str(i).encode())
        time.sleep(1)
else:
    print("giving up")
ESP32 + Pico W · MicroPython 1.29 · simulated board
connected
link lost
connected
אזהרה: אחרי חיבור מחדש צריך להירשם מחדש: עם clean_session=True, שהוא ברירת המחדל, המתווך שוכח את ההרשמות שלנו. תוכנית שרק מפרסמת אינה מראה זאת, אבל לוח פקודות נשאר מחובר ושותק לנצח, אם שכחנו.
הערה: קיימת ספרייה umqtt.robust שמוסיפה חיבור מחדש אוטומטי מעל umqtt.simple, לפי התיעוד שלה. המדמה אינו מייצג את החיבור מחדש שלה, ולכן אנחנו כותבים כאן את הטיפול בעצמנו, וכך יודעים מה קורה.
שאלה 16.7: מה התוכנית מדפיסה אם המתווך כבוי מלכתחילה? וכמה שניות היא ממתינה לפני שהיא מוותרת?

16.8 שני בקרים דרך מתווך (Two Boards)

בפרויקט הזוגי בקר אחד צריך לקבל פקודה מבקר אחר. עם מתווך, אף לוח אינו צריך את הכתובת של האחר: מספיק להסכים על שני נושאים. לוח A מפרסם פקודה כשלוחצים על הלחצן, ולוח B מבצע אותה ומפרסם את המצב. 192.168.1.10 היא כתובת לדוגמה של המתווך ברשת המקומית.

A: ESP32לוח הלחצןהמתווךRaspberry PiB: Pico Wלוח הנוריתschool/pair1/led/setschool/pair1/led/state
שני נושאים שהוסכם עליהם. הפקודה הולכת מ-A אל B, והמצב חוזר מ-B אל A.
לוח A: ממתין ללחצן ומאזין למצב גם יחד, ולכן משתמש ב-check_msg().
import time
from machine import Pin
from umqtt.simple import MQTTClient
import board
import wifi

BROKER = "192.168.1.10"
CMD = b"school/pair1/led/set"
STATE = b"school/pair1/led/state"

btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
lit = False


def on_message(topic, msg):
    print("B reports:", msg.decode())


wifi.connect()
client = MQTTClient("pair1-a", BROKER)
client.set_callback(on_message)
client.connect()
client.subscribe(STATE)

last = 1
while True:
    now = btn.value()
    if last == 1 and now == 0:
        lit = not lit
        client.publish(CMD, b"on" if lit else b"off")
        print("pressed, asking for", lit)
    last = now
    client.check_msg()
    time.sleep(0.05)
ESP32 + Pico W · MicroPython 1.29 · simulated board
pressed, asking for True
B reports: on
pressed, asking for False
B reports: off
[mqtt_pub]
school/pair1/led/set on
school/pair1/led/set off
...
לוח B: אין לו מה לעשות מלבד להמתין, ולכן wait_msg() מספיקה לו.
from machine import Pin
from umqtt.simple import MQTTClient
import board
import wifi

BROKER = "192.168.1.10"
CMD = b"school/pair1/led/set"
STATE = b"school/pair1/led/state"

led = Pin(board.LED, Pin.OUT)
client = MQTTClient("pair1-b", BROKER)


def on_message(topic, msg):
    led.value(1 if msg == b"on" else 0)
    client.publish(STATE, msg)
    print("led:", led.value())


wifi.connect()
client.set_callback(on_message)
client.connect()
client.subscribe(CMD)

while True:
    client.wait_msg()
ESP32 + Pico W · MicroPython 1.29 · simulated board
led: 1
led: 0
[mqtt_pub]
school/pair1/led/state on
school/pair1/led/state off
...

בבנייה כל לוח רץ בסימולציה משלו, ומה שמגיע אליו מהצד השני כתוב מראש בסימולציה. על שולחן העבודה מריצים את שתי התוכניות על שני לוחות אמיתיים, וכל אחד מהם מגיע אל האחר דרך המתווך, בלי לשנות אף שורה.

שאלה 16.8: רוצים לוח שלישי שמציג את מצב הנורית על מסך. מה משנים ב-A וב-B?
פעילות: עם חבר: הסכימו על שני נושאים וכתבו אותם לפני שאתם כותבים אפילו שורה אחת. אחר כך נתקו את לוח B מההזנה בזמן העבודה, רשמו מה A מדפיס, ומה אתם מציעים כדי ש-A יידע ש-B חסר.

16.9 היכן רץ המתווך (Where the Broker Runs)

טעות נפוצה: למתווך ציבורי מתחברים יחד איתנו לוחות רבים. לכל לקוח יש מזהה ייחודי, ואם שני לקוחות מתחברים עם אותו מזהה, המתווך מנתק את הראשון. לכן מחליפים את desk1 בתוכניות בשם משלנו, וגם שמות הנושאים מתחילים בשם משלנו. בלוח אמיתי ההודעה חוזרת רגע אחרי הפרסום, ולכן שורת הקבלה עשויה להופיע במסך אחרי השורה הבאה.

לניסוי הראשון מספיק מתווך ציבורי באינטרנט, כמו broker.hivemq.com, שבו השתמשנו כאן, והודעה עברה דרכו הלוך ושוב מלוח אמיתי. מתווכים ציבוריים עלולים להפסיק לעבוד בלי התראה, ולכן אם הוא לא עונה מנסים אחר, כמו broker.emqx.io. הוא לניסוי בלבד, מכמה סיבות ברורות:

בפרויקט אמיתי מפעילים מתווך משלנו על Raspberry Pi 4/5 באותה רשת, בתוכנה Mosquitto. הפקודות הבאות לא הורצו בבניית הספר. אחרי ההתקנה אומרים למתווך להאזין לרשת: מגרסה 2.0 הוא מקבל כברירת מחדל רק חיבורים מההתקן עצמו.

ההתקנה על Raspberry Pi 4/5. לא הורצה בבניית הספר.
sudo apt update
sudo apt install mosquitto mosquitto-clients
קובץ ההגדרות, כותבים אותו בהרשאות מנהל. לא נעשה בו שימוש בבניית הספר.
# /etc/mosquitto/conf.d/lab.conf
listener 1883
allow_anonymous true
פותחים שני חלונות: הפקודה השנייה ממתינה ומדפיסה, והשלישית מפרסמת מהחלון האחר. לא הורצה בבניית הספר.
sudo systemctl restart mosquitto
mosquitto_sub -h localhost -t 'school/#' -v
mosquitto_pub -h localhost -t school/pair1/led/set -m on
שימו לב: allow_anonymous true מתירה לכל התקן ברשת להתחבר בלי סיסמה. מקבלים אותה בניסוי ברשת פרטית, ולא מקבלים אותה בפרויקט שפועל באמת.

סיסמאות, הפעלה רציפה של המתווך וחיבורו למסד נתונים: זה נושא פרק 23.

שאלה 16.9: למה מספיק לשנות שורה אחת בתוכנית הלוח כדי לעבור מהמתווך הציבורי למתווך שלנו?

עיקרי הפרק:

פקודות הפרק:

פקודהמה היא עושה
MQTTClient(id, server, keepalive=60)יוצרים לקוח.
client.set_callback(f)הפונקציה f(topic, msg) מקבלת את ההודעות.
client.set_last_will(t, m)הצוואה האחרונה, לפני connect().
client.connect()מתחברים למתווך.
client.subscribe(topic)נרשמים לנושא, והוא יכול להכיל תו כללי.
client.publish(t, m, retain, qos)מפרסמים הודעה.
client.check_msg()בודקים אם הגיעה הודעה, וחוזרים מיד.
client.wait_msg()ממתינים עד שמגיעה הודעה.
client.ping()דופק keepalive.
client.disconnect()פרידה מסודרת; הצוואה האחרונה אינה מתפרסמת איתה.

מילון מונחים:

עבריתEnglishערבית
מתווךBrokerالوسيط
נושאTopicالموضوع
פרסוםPublishالنشر
הרשמהSubscribeالاشتراك
מטעןPayloadالحمولة
תו כלליWildcardالرمز الشامل
מזהה לקוחClient IDمعرّف العميل
איכות שירותQuality of Serviceجودة الخدمة
הודעה שמורהRetained Messageالرسالة المحفوظة
keepaliveKeepaliveنبضة البقاء
צוואה אחרונהLast Willالوصية الأخيرة
התשובות הסופיות לשאלות הפרק
16.1 לא. כל הדרכים עוברות דרכו. לכן המתווך צריך לרוץ על התקן קבוע ואמין, וכל לוח צריך לנהוג בבטחה כשהמתווך חסר.
16.2 school/lab1/temp, school/lab1/hum ו-school/lab1/door/temp, אבל לא school/lab2/temp.
16.3 כי היא רק מפרסמת ואינה נרשמת לשום דבר, ולכן אין הודעות שמגיעות אליה. כך כתוב בתיעוד הספרייה.
16.4 בדרך כלל לא, כי הן ממתינות בחיבור, אבל כל פקודה מתעכבת עד 10 שניות. הפגם כאן הוא בזמן התגובה.
16.5 היא מדפיסה ignored ואת ההודעה: ה-JSON תקין, אבל המפתח period חסר, ולכן מתרחשת KeyError ואנחנו תופסים אותה.
16.6 המצב נכון גם למי שנרשם באיחור. לעומת זאת פקודה שמורה מתבצעת שוב בכל פעם שהלוח נרשם אחרי הפעלה מחדש, והיא עלולה להפעיל את המנוע בלי שמישהו ביקש.
16.7 היא מדפיסה:
no broker, waiting 1 s
no broker, waiting 2 s
no broker, waiting 4 s
no broker, waiting 8 s
giving up
סך ההמתנה 1 + 2 + 4 + 8 = 15 s.
16.8 שום דבר. הלוח השלישי נרשם ל-school/pair1/led/state בלבד. זה היתרון של המתווך.
16.9 כי הלוח מכיר מן המתווך רק את הכתובת שלו: משנים את הערך של BROKER, והנושאים והתוכנית נשארים כמות שהם.

שאלות לתרגול:

רמה ראשונה — בסיסית
1.
מה תפקיד המתווך? והאם המפרסם יודע מי קורא את ההודעה שלו?
תשובות: הוא מקבל את ההודעות ומפיץ אותן למנויים. המפרסם אינו יודע מי קורא.
2.
האם ההרשמה home/+/temp מתאימה לנושא home/kitchen/temp? ולנושא home/temp?
תשובות: הראשון כן, והשני לא: התו + תופס דרגה אחת בדיוק.
3.
מתי משתמשים ב-wait_msg(), ומתי ב-check_msg()?
תשובות: הראשונה כשאין לתוכנית עבודה אחרת, והשנייה כשיש עבודה אחרת.
4.
הודעה שהנושא שלה בן 20 תווים והמטען שלה 10 בייטים, באיכות שירות 0. מה גודלה לפי החישוב בסעיף 16.3?
תשובות: 2 + 2 + 20 + 10 = 34 בייטים.
רמה שנייה — יישום
5.
שנו את תוכנית הפרסום כך שתקרא גם את הנגד התלוי באור ותפרסם כל קריאה לנושא נפרד.
תשובות: קוראים את board.LDR באותה דרך, ומפרסמים לשני נושאים: school/desk1/pot ו-school/desk1/ldr.
6.
תוכנית נרשמת לנושא ואינה מקבלת דבר. תנו שלוש סיבות אפשריות.
תשובות: למשל: שכחנו את check_msg(); טעות בשם הנושא; השווינו את המטען לטקסט במקום ל-bytes.
7.
העברנו את לוח הפקודות לתוכנית ההתאוששות. מה צריך להוסיף ב-reconnect?
תשובות: אחרי connect() קוראים שוב ל-subscribe, כי המתווך שכח את ההרשמה.
8.
תכננו נושאים לבית עם שני חדרים, ובכל אחד חיישן טמפרטורה ונורית. איך מסך הטמפרטורה קורא את שני החדרים בהרשמה אחת?
תשובות: למשל home/room1/temp ו-home/room1/led/set, והמסך נרשם ל-home/+/temp.
רמה שלישית — אתגר
9.
לוח מפרסם כל 10 שניות הודעה שגודלה 50 בייטים. כמה בייטים בשבוע?
תשובות: 60480 הודעות בשבוע, כלומר 3024000 בייטים.
10.
הוסיפו ללוח B מצב offline שמתפרסם כשהוא מתנתק. מה כותבים? ומה התנאי שיופיע גם למי שנרשם באיחור?
תשובות: set_last_will(topic, b"offline", retain=True) לפני connect(), ובעת ההתחברות מפרסמים online עם retain=True. התנאי: ההודעה שמורה.
11.
פקודת הפעלת מנוע באיכות שירות 1 עלולה להגיע פעמיים. איך הופכים את הביצוע לבטוח?
תשובות: שולחים מצב מבוקש on/off ולא פקודת החלפה: ביצוע שלה פעמיים נותן אותה תוצאה.