برمجة المتحكّمات بلغة مايكروبايثون · الفصل 16 — بروتوكول MQTTפרוטוקול MQTT · תכנות מיקרו-בקרים בשפת MicroPython⌂📚🏠
الفصل 16

بروتوكول MQTT

פרוטוקול MQTT – The MQTT Protocol
4 ساعات تعليمية

في الفصلين السابقين كانت اللوحة تكلّم عنوانا محدّدا، وتنتظر جوابه. هذا لا يناسب حسّاسا صغيرا يرسل قراءته كل بضع ثوان إلى من يهمّه الأمر، ولا لوحة تنتظر أمرا لا تعرف متى يصل. في هذا الفصل نتعلّم طريقة أخرى: كل لوحة تكلّم طرفا واحدا فقط هو الوسيط، وهو يوصل كل رسالة إلى من طلبها.

في هذا الفصل:
  • نشرح دور الوسيط والموضوع والنشر والاشتراك، ونختار أسماء موضوعات منظّمة.
  • ننشر قراءة بمكتبة umqtt.simple، ونستقبل أمرا ونفرّق بين check_msg() وwait_msg().
  • نضع البيانات في رسالة JSON، ونتعامل مع رسالة فاسدة دون أن يتوقف البرنامج.
  • نختار جودة الخدمة، ونفهم الرسالة المحفوظة ونبضة البقاء والوصية الأخيرة.
  • نتعافى من الانقطاع، ونربط لوحتين عبر وسيط واحد.
ملاحظة: كل البرامج في هذا الفصل تعمل في بناء الكتاب على وسيط مُحاكى وعلى 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

حين تكون القيمة واحدة يكفي نص قصير. وحين تزيد، نكتب الحمولة بصيغة 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()نبضة بقاء.
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 لا أمر تبديل: تنفيذها مرتين يعطي النتيجة نفسها.