9.19. Python’da MQTT

Ağa bağlı her OpenMV kameraya dahil edilen mqtt modülü, MQTT tel protokolünü tek bir sınıfa, mqtt.MQTTClient sınıfına sarmalar. Sınıf, TCP soketini açar, CONNECT el sıkışmasını gerçekleştirir, bayt düzeyindeki paketleri paketler ve açar, PINGREQ keepalive’ı yönetir ve gelen PUBLISH mesajlarını bir geri çağırmaya (callback) yönlendirir. Uygulama kodu connect(), publish(), subscribe() ve wait_msg() / check_msg() çağrılarını yapar.

9.19.1. On beş satırda bir yayıncı

En küçük yararlı program tek bir yayındır. Bağlan, bir mesaj yayınla, bağlantıyı kes:

from mqtt import MQTTClient

client = MQTTClient(
    client_id='yard-cam',
    server='test.mosquitto.org',
    port=1883,
)
client.connect()
client.publish(b'yard-cam/motion', b'detected at 14:02', qos=0)
client.disconnect()

test.mosquitto.org, Eclipse Mosquitto projesi tarafından işletilen genel test aracısıdır (broker). 1883 portunda kimlik bilgisi olmadan düz TCP bağlantılarını kabul eder. Ciddi hiçbir şey için kullanmayın; gizlilik garantisi yoktur ve konu (topic) ad alanı internetteki diğer her test kullanıcısıyla paylaşılır.

client_id her aracı bağlantısı için benzersiz olmalıdır; aracı bunu oturumları izlemek için kullanır. Konular ve mesaj yükleri bayttır; daha kullanışlıysa str geçirebilirsiniz, istemci bunu UTF-8 olarak kodlayacaktır.

9.19.2. TLS üzerinden bağlanma

Hızlı denemelerin ötesindeki her şey için, TLS üzerinden MQTT yalnızca bir ek argümandır. ssl_params sözlüğü ssl.wrap_socket() fonksiyonuna iletilir, dolayısıyla orada çalışan her şey burada da çalışır:

import ssl

client = MQTTClient(
    client_id='yard-cam',
    server='broker.example.com',
    port=8883,                          # TLS-MQTT default port
    ssl_params={'server_hostname': 'broker.example.com'},
    user='yard-cam',
    password=load_token(),
)

8883 portu, IANA tarafından ayrılmış TLS-MQTT portudur. server_hostname, SNI’yi etkinleştirir; böylece paylaşılan bir IP arkasındaki aracılar doğru sertifikaya yönlendirebilir – HTTPS’in kullandığı mekanizmanın aynısı. user / password, CONNECT paketinin kullanıcı adı/parola alanlarına eşlenir; aracı, bu kimlik bilgilerinin belirli konulara yayınlama veya abone olma haklarını verip vermediğine karar verir.

9.19.3. Abone olma ve mesaj alma

Mesaj almak için bir istemci bir geri çağırma (callback) sağlar ve subscribe() çağrısını yapar. Geri çağırma iki bayt argümanı alır: konu ve yük:

def on_message(topic, msg):
    print('received on', topic.decode(), ':', msg.decode())

client = MQTTClient(
    client_id='dashboard',
    server='test.mosquitto.org',
    port=1883,
    callback=on_message,
)
client.connect()
client.subscribe(b'yard-cam/motion', qos=0)
while True:
    client.wait_msg()

wait_msg(), bir MQTT paketi gelene kadar bloklar, onu ayrıştırır, abone olunan bir konuda bir PUBLISH ise geri çağırmayı çağırır ve döner. Abone olunan geri çağırmalar bu çağrının içinden tetiklenir; arka planda bir iş parçacığı yoktur.

Başka işler yapmaya devam etmesi gereken etkileşimli bir kamera döngüsü için, check_msg() aynı mantığın bloklamayan biçimidir. 50 ms zaman aşımıyla select.select() kullanır ve bekleyen bir şey yoksa hemen döner:

while True:
    client.check_msg()
    run_frame()                  # capture + processing
    check_motion_threshold()

9.19.4. Düzgün bir şekilde yeniden bağlanma

Uzun süre çalışan herhangi bir MQTT istemcisinin kopan bağlantıları işlemesi gerekir. Wi-Fi kesintileri, aracı yeniden başlatmaları, NAT zaman aşımları veya basitçe trafik olmadan keepalive süresini aşmak, soketin sona ermesine neden olur. Dahil edilen istemci, kopmayı fark eden çağrıdan OSError (veya aracının dönüş koduyla birlikte çıplak bir istisna) yükseltir ve standart kalıp bir yeniden deneme döngüsüdür:

import time

def keep_publishing(client, topic, get_message):
    while True:
        try:
            client.connect()
            while True:
                client.publish(topic, get_message())
                time.sleep(5)
        except OSError:
            print('connection lost, reconnecting in 5s')
            time.sleep(5)

Abonelikler, istemci bağlanırken clean_session=False geçirmediği sürece yeniden bağlanmalar arasında kalıcı değildir; bu nedenle içteki connect çağrısı, yayınlama döngüsüne girmeden önce tüm subscribe() çağrılarını da yeniden yapmalıdır.

9.19.5. Son vasiyet (last-will) kancası

Durum bildiren bir kamera, bağlantı beklenmedik şekilde koparsa aracının kamera adına hangi mesajı göndereceğini ona söylemelidir. Vasiyeti connect() çağrısından önce ayarlayın:

client = MQTTClient(
    client_id='yard-cam',
    server='broker.example.com',
    port=8883,
    ssl_params={'server_hostname': 'broker.example.com'},
)
client.set_last_will(
    b'yard-cam/status',
    b'offline',
    retain=True,
    qos=0,
)
client.connect()
client.publish(b'yard-cam/status', b'online', retain=True)

Artık yard-cam/status konusuna abone olan herhangi bir kontrol paneli, kamera bağlandığı anda online görür ve aracı kameranın koptuğunu fark ettiğinde offline görür. Saklanan offline mesajı aracıda kalıcı olur; böylece on dakika sonra bağlanan bir kontrol paneli yine de doğru güncel durumu görür.

9.19.6. MQTT’nin HTTP yerine ne zaman seçileceği

Web sunucuları bölümü, kameranın bir HTTP sunucusu olarak ve buluta yükleme sayfasında, sabit bir URL’ye JPEG gönderen bir HTTP istemcisi olarak davranmasını kapsar. Her ikisinin de bir yeri vardır. Bunun yerine MQTT’ye başvurmanın doğru zamanı:

  • Aynı verinin, kameranın listeyi önceden bilmesine gerek kalmadan birkaç dinleyiciye (bir kontrol paneli, bir bildirim hizmeti, bir kaydedici) gitmesi gerektiğinde.

  • Dinleyiciler, kamera yeniden başlatılmadan gelip gidebileceğinde.

  • Kameranın abone olmak istediğinde – bir denetleyiciden komutlar almak için – ki bir HTTP istemcisi bunu uzun yoklama (long polling) veya bir geri çağırma URL’sine gönderim yapan bir sunucu olmadan yapamaz.

  • Bağlantının uzun boşta kalma sürelerini ucuza atlatması gerektiğinde.

HTTP’de kalmanın doğru zamanı: tek kamera, tek sunucu, tek bir MQTT konusu için fazla büyük olan bir gövdeye sahip sabit bir istek/yanıt kalıbı (MQTT üzerinden JPEG çerçeveler çalışır ancak aracıya karşı kabadır; HTTP POST doğal uyumdur).

Çapraz bağlantı: web sunucuları bölümündeki buluta yükleme sayfası, “kamera → bulut arşivi” işleminin HTTP sürümünü gösterir. Aynı sorunun MQTT sürümü, kamerayı arşivin URL’sinden ayrık tutar ve ikinci bir tüketicinin (örneğin bir telefon uyarı uygulaması) aynı akıştan yararlanmasına izin verir.