Uygulamalı Eğitimler

Flask ile MQTT (flask-mqtt ve paho)

MQTT'yi flask-mqtt veya paho-mqtt 2.x ile bir Flask uygulamasına entegre edin, reloader ve Gunicorn thread tuzaklarından kaçının, SSE ile canlı dashboard kurun.

Güncellendi · 9 dk okuma

Flask ile MQTT (flask-mqtt ve paho)

Flask'ın kendi event loop'u yoktur ve her isteği bir thread'de işler; bu yüzden Flask'ta MQTT, web sunucusunun yanında bir MQTT istemcisi çalıştırmak demektir: arka planda bir ağ thread'i mesajları alır, istek handler'ları da aynı istemci üzerinden publish eder. Bunu flask-mqtt eklentisiyle ya da doğrudan paho-mqtt 2.x ile yapabilirsiniz. İkisi de çalışır ve ikisi de aynı thread ve çoklu süreç tuzaklarını paylaşır — bu rehberin asıl konusu da bunlar.

Her iki seçeneği kuracak, "her mesaj iki kez geliyor" sorununu çözecek ve MQTT verilerini Server-Sent Events ile tarayıcıya aktaran küçük bir canlı dashboard ile bitireceğiz.

Seçenek 1: flask-mqtt

flask-mqtt, paho-mqtt'yi sarar ve ayarlarını app.config'den okur:

Kurulum
pip install flask flask-mqtt
app_flask_mqtt.py
import os
from flask import Flask
from flask_mqtt import Mqtt

app = Flask(__name__)
app.config["MQTT_BROKER_URL"] = "broker.connectmq.com"
app.config["MQTT_BROKER_PORT"] = 1883
app.config["MQTT_USERNAME"] = os.getenv("MQTT_USER", "")
app.config["MQTT_PASSWORD"] = os.getenv("MQTT_PASS", "")
app.config["MQTT_KEEPALIVE"] = 30
app.config["MQTT_TLS_ENABLED"] = False
mqtt = Mqtt(app)

@mqtt.on_connect()
def handle_connect(client, userdata, flags, rc):
    mqtt.subscribe("testmqtt/flask/#")

@mqtt.on_message()
def handle_message(client, userdata, message):
    print(message.topic, message.payload.decode())

@app.post("/ping")
def ping():
    mqtt.publish("testmqtt/flask/ping", "pong")
    return {"ok": True}

Kurulumu hızlıdır, ama iki şeyi aklınızda tutun. Eklentinin bakımı zayıftır ve eski sürümleri paho-mqtt 1.x callback API'sine göre yazılmıştır — paho-mqtt 2.0 kurulduktan sonra callback imzası hataları alırsanız daha yeni bir flask-mqtt sürümü arayın ya da paho-mqtt<2 ile sabitleyin. Ayrıca Mqtt(app) çalıştığı anda bağlanır; yani uygulamayı import eden her süreç bir bağlantı açar (aşağıda ayrıntısı var).

Seçenek 2: doğrudan paho-mqtt 2.x

paho'yu doğrudan kullanmak birkaç satır daha ister; karşılığında güncel API'yi, otomatik yeniden bağlanmayı ve tam kontrolü alırsınız. İstemciyi kendi modülüne koyun:

mqtt_service.py
import os
import queue
import threading
import uuid
import paho.mqtt.client as mqtt

TELEMETRY = "testmqtt/flask/+/telemetry"


class MqttService:
    def __init__(self, host="broker.connectmq.com", port=1883):
        self.host, self.port = host, port
        self.latest: dict[str, str] = {}
        self.listeners: list[queue.Queue] = []
        self.lock = threading.Lock()
        self.client = mqtt.Client(
            mqtt.CallbackAPIVersion.VERSION2,
            client_id=f"testmqtt-flask-{uuid.uuid4().hex[:8]}",  # süreç başına benzersiz
        )
        if os.getenv("MQTT_USER"):
            self.client.username_pw_set(os.environ["MQTT_USER"], os.getenv("MQTT_PASS"))
        self.client.reconnect_delay_set(min_delay=1, max_delay=30)
        self.client.on_connect = self._on_connect
        self.client.on_message = self._on_message

    def start(self):
        self.client.connect_async(self.host, self.port, keepalive=30)
        self.client.loop_start()          # ağ thread'i, kendiliğinden yeniden bağlanır

    def _on_connect(self, client, userdata, flags, reason_code, properties):
        if reason_code.is_failure:
            print("MQTT connect failed:", reason_code)
            return
        client.subscribe(TELEMETRY, qos=1)  # her yeniden bağlanmadan sonra tekrar çalışır

    def _on_message(self, client, userdata, msg):
        item = {"topic": msg.topic, "payload": msg.payload.decode(errors="replace")}
        with self.lock:
            self.latest[msg.topic] = item["payload"]
            listeners = list(self.listeners)
        for q in listeners:
            try:
                q.put_nowait(item)
            except queue.Full:
                pass                        # yavaş tarayıcı: at, ağ thread'ini asla bloklama

    def join(self) -> queue.Queue:
        q: queue.Queue = queue.Queue(maxsize=100)
        with self.lock:
            self.listeners.append(q)
        return q

    def leave(self, q: queue.Queue):
        with self.lock:
            self.listeners.remove(q)

    def publish(self, topic: str, payload: str, qos: int = 1) -> bool:
        if not self.client.is_connected():
            return False
        return self.client.publish(topic, payload, qos=qos).rc == mqtt.MQTT_ERR_SUCCESS

connect_async() ile loop_start(), broker erişilemez olsa bile Flask'ın başlamasını sağlar; paho, reconnect_delay_set() ile belirlenen backoff ile arka planda denemeye devam eder. on_connect içinde abone olmak, aboneliği her yeniden bağlanmadan sonra geri yükler. paho API'sinin temelleri için Python'da MQTT yazısına bakın.

Thread kuralları

Bu kurulumda üç thread MQTT verisine dokunur; birkaç kural her şeyi güvende tutar:

KuralNeden
on_message'ı kısa tutunpaho'nun tek ağ thread'inde çalışır. Yavaş bir handler tüm mesajları geciktirir ve keep-alive ping'lerini geciktirebilir.
Paylaşılan durumu bir lock ile koruyunİstek thread'leri latest'i okurken ağ thread'i ona yazar.
İstek handler'larından publish() sorun değilpaho'nun publish()'i thread-safe'tir; önce is_connected()'ı kontrol edin, değilse 503 dönün.
Callback'lerde app context yokcurrent_app, g ve Flask-SQLAlchemy oturumları callback içinde with app.app_context(): gerektirir.
wait_for_publish()'i asla timeout'suz beklemeyinBağlantısı kopmuş bir broker istek thread'ini sonsuza dek askıda bırakır.

Ağır işler — veritabanı yazımları, diğer servislere HTTP çağrıları — on_message'a değil, bir worker thread'in ya da Celery veya RQ gibi bir görev kuyruğunun tükettiği bir kuyruğa aittir.

"Her mesaj iki kez" sorunu

Flask'ta en yaygın MQTT hatası: mesajlar iki kez işlenir ya da log, istemcinin bir döngü içinde bağlanıp koptuğunu gösterir. Nedenler ve çözümler:

  • Debug reloader. app.run(debug=True) modülünüzü hem izleyici süreçte hem de isteklere hizmet eden süreçte çalıştırır; iki MQTT istemciniz olur. use_reloader=False kullanın ya da MQTT'yi yalnızca hizmet veren süreçte başlatın: if os.environ.get("WERKZEUG_RUN_MAIN") == "true" or not app.debug:.
  • Birden fazla Gunicorn worker'ı. -w 4 dört istemci demektir. Dashboard için sorun değil (her worker'ın kendi tarayıcıları için tüm mesajlara ihtiyacı var), ama yalnızca bir kez olması gereken her şey — bir veritabanı insert'i, bir alarm — shared subscription kullanmalıdır; böylece broker her mesajı tek bir worker'a verir. Bkz. shared subscription'lar.
  • Sabit client ID. Tüm bu süreçler aynı client ID'yi kullanırsa broker her yeni bağlantıda eskisini düşürür ve süreçler birbirini sonsuza dek atar. Rastgele sonekler bunu önler — bkz. yinelenen client ID.
  • gunicorn --preload. Uygulama fork'tan önce master'da import edilir, ama paho thread'i fork() sonrasında hayatta kalmaz; worker'lar ağ döngüsü olmayan bir istemci nesnesiyle kalır. Preload kullanmayın ya da istemciyi bir post_fork hook'unda başlatın.

Basit bir canlı dashboard

Şimdi servisi Flask'a bağlayın: bir JSON anlık görüntüsü, bir komut endpoint'i, bir SSE akışı ve küçük bir HTML sayfası.

app.py
import json
import queue
import re
from flask import Flask, Response, abort, jsonify, request
from mqtt_service import MqttService

app = Flask(__name__)
mqtt_service = MqttService()
mqtt_service.start()
DEVICE_ID = re.compile(r"^[A-Za-z0-9_-]{1,64}$")

PAGE = """<!doctype html><title>MQTT dashboard</title>
<h1>Canlı telemetri</h1><table id="t"></table>
<script>
  const rows = {};
  new EventSource("/stream").onmessage = (e) => {
    const { topic, payload } = JSON.parse(e.data);
    let row = rows[topic];
    if (!row) {
      row = rows[topic] = document.getElementById("t").insertRow();
      row.insertCell().textContent = topic;
      row.insertCell();
    }
    row.cells[1].textContent = payload;
  };
</script>"""


@app.get("/")
def index():
    return PAGE


@app.get("/api/latest")
def latest():
    with mqtt_service.lock:
        return jsonify(mqtt_service.latest)


@app.post("/api/devices/<device_id>/command")
def command(device_id):
    if not DEVICE_ID.match(device_id):
        abort(400)                          # doğrulanmamış girdiyi asla topic'e koymayın
    body = json.dumps(request.get_json(force=True))
    if not mqtt_service.publish(f"testmqtt/flask/{device_id}/command", body):
        abort(503)
    return jsonify(queued=True), 202


@app.get("/stream")
def stream():
    q = mqtt_service.join()

    def events():
        try:
            while True:
                try:
                    yield f"data: {json.dumps(q.get(timeout=15))}\n\n"
                except queue.Empty:
                    yield ": keep-alive\n\n"  # proxy'lerin akışı kapatmasını önler
        finally:
            mqtt_service.leave(q)

    return Response(events(), mimetype="text/event-stream", headers={"Cache-Control": "no-cache"})


if __name__ == "__main__":
    app.run(debug=True, use_reloader=False, threaded=True)

Açık her dashboard bir SSE bağlantısı tutar ve thread'li sunucuda bu, tarayıcı başına bir thread demektir — birkaç izleyicili dahili bir dashboard için yeterli. Üretimde gunicorn -w 1 --threads 16 app:app çalıştırın (ya da yukarıdaki kurallara uyarak daha fazla worker). Çok sayıda eşzamanlı izleyici için asenkron bir framework daha uygundur; bkz. FastAPI ile MQTT ya da tarayıcıların canlı dashboard eğitiminde gösterildiği gibi doğrudan broker'a bağlanmasına izin verin.

Test edin

  1. python app.py komutunu çalıştırın ve http://localhost:5000 adresini açın.
  2. Online MQTT istemcisinde broker.connectmq.com'a bağlanın ve testmqtt/flask/greenhouse/telemetry topic'ine {"temp": 23.1} publish edin. Dashboard'da hemen bir satır görünmelidir.
  3. Online istemcide testmqtt/flask/#'e abone olun ve bir komut gönderin:
HTTP → MQTT
curl -X POST localhost:5000/api/devices/greenhouse/command \
  -H 'content-type: application/json' -d '{"fan": "on"}'
  1. curl localhost:5000/api/latest komutunun topic başına son değeri döndürdüğünü kontrol edin.
  2. Broker'ı durdurun ya da ağı kesin: komut endpoint'i 503 dönmeli, paho yeniden bağlandığında dashboard kendiliğinden devam etmelidir.

Flask dashboard'unuza test telemetrisi gönderin

Projeniz Flask yerine Django kullanıyorsa aynı kalıplar, uzun ömürlü subscriber olarak bir management command ile geçerlidir — bkz. Django ile MQTT.

Flask'ta MQTT, onu gerçekte olduğu şey olarak ele aldığınızda iyi çalışır: arka plan thread'inde, süreç başına bir tane uzun ömürlü istemci. Onu bir kez başlatın, callback'leri kısa ve thread-safe tutun, her sürece benzersiz bir client ID verin ve daha fazla worker eklemeden önce tüm döngüyü — cihazdan tarayıcıya ve tarayıcıdan cihaza — test edin.

Sıkça sorulan sorular

Flask uygulamam neden her MQTT mesajını iki kez alıyor?

Genellikle iki MQTT istemcisi çalışıyordur: debug reloader kodunuzu iki süreçte çalıştırır ya da Gunicorn birden fazla worker başlatmıştır. Reloader'ı kapatın (use_reloader=False) veya MQTT'yi yalnızca isteklere hizmet eden süreçte başlatın ve her sürece benzersiz bir client ID verin.

flask-mqtt mi kullanmalıyım, doğrudan paho-mqtt mi?

flask-mqtt, ayarlarını app.config'den okuyan ve decorator'lar sunan ince bir sarmalayıcıdır. Küçük uygulamalar için pratiktir ama bakımı zayıftır; paho-mqtt 2.x ile uyumluluğunu kontrol edin. Doğrudan paho-mqtt kullanmak tam kontrol ve güncel callback API'sini sağlar.

MQTT callback'i içinde Flask-SQLAlchemy kullanabilir miyim?

Evet, ancak MQTT callback'leri herhangi bir isteğin dışında, paho'nun ağ thread'inde çalışır; dolayısıyla uygulama bağlamı (app context) yoktur. Veritabanı kodunu with app.app_context(): içine alın ve işi kısa tutun ya da bir kuyruğa devredin.