9.19. MQTT i Python¶
Den medföljande modulen mqtt på varje nätverksansluten OpenMV-kamera kapslar in MQTT-trådprotokollet i en klass, mqtt.MQTTClient. Klassen öppnar TCP-socketen, utför CONNECT-handskakningen, packar och packar upp paketen på bytenivå, hanterar PINGREQ-keepalive och dirigerar inkommande PUBLISH-meddelanden till ett återanrop. Applikationskoden anropar connect(), publish(), subscribe() och wait_msg() / check_msg().
9.19.1. En publicerare på femton rader¶
Det minsta användbara programmet är en enda publicering. Anslut, publicera ett meddelande, koppla från:
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 är den publika testmäklaren som drivs av Eclipse Mosquitto-projektet. Den accepterar oklara TCP-anslutningar på port 1883 utan inloggningsuppgifter. Använd den inte för något seriöst; den har inga sekretessgarantier och ämnesnamnrymden delas med alla andra testare på internet.
client_id måste vara unik per mäklaranslutning – mäklaren använder den för att spåra sessioner. Ämnen och meddelandenyttolaster är byte; skicka str om det är bekvämare så kodar klienten den som UTF-8.
9.19.2. Ansluta över TLS¶
För allt utöver snabba experiment är MQTT över TLS ett extra argument. Ordlistan ssl_params vidarebefordras till ssl.wrap_socket(), så allt som fungerar där fungerar hä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(),
)
Port 8883 är den IANA-reserverade TLS-MQTT-porten. server_hostname slår på SNI så att mäklare bakom en delad IP kan dirigera till rätt certifikat – samma mekanism som HTTPS använder. user / password mappar till CONNECT-paketets användarnamns- och lösenordsfält; mäklaren avgör om dessa inloggningsuppgifter ger rättigheter att publicera eller prenumerera på specifika ämnen.
9.19.3. Prenumerera och ta emot¶
För att ta emot meddelanden tillhandahåller en klient ett återanrop och anropar subscribe(). Återanropet tar emot två byte-argument, ämnet och nyttolasten:
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() blockerar tills ett MQTT-paket anländer, tolkar det, anropar återanropet om det var en PUBLISH på ett prenumererat ämne, och återvänder. Prenumererade återanrop utlöses inifrån det anropet – det finns ingen bakgrundstråd.
För en interaktiv kameraloop som behöver fortsätta göra annat arbete är check_msg() samma logik i icke-blockerande form. Den använder select.select() med en 50 ms timeout och återvänder omedelbart om inget väntar:
while True:
client.check_msg()
run_frame() # capture + processing
check_motion_threshold()
9.19.4. Återansluta snyggt¶
Varje långkörande MQTT-klient måste hantera tappade anslutningar. Wi-Fi kopplar från, mäklaren startar om, NAT-timeouter, eller helt enkelt att köra förbi keepalive utan trafik avslutar alla socketen. Den medföljande klienten kastar OSError (eller ett naket undantag med mäklarens returkod) från det anrop som märkte tappet, och standardmönstret är en återförsöksloop:
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)
Prenumerationer bevaras inte över återanslutningar om inte klienten skickade clean_session=False vid anslutning, så det inre connect bör också göra om alla subscribe()-anrop innan den faller in i publiceringsloopen.
9.19.5. Last-will-kroken¶
En kamera som rapporterar status bör tala om för mäklaren vilket meddelande den ska skicka för kamerans räkning om anslutningen oväntat dör. Ställ in viljan innan connect()
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)
Nu ser varje instrumentpanel som prenumererar på yard-cam/status online i samma ögonblick som kameran ansluter och offline så snart mäklaren märker att kameran tappade anslutningen. Det bevarade offline-meddelandet kvarstår på mäklaren så att en instrumentpanel som ansluter tio minuter senare fortfarande ser det korrekta aktuella tillståndet.
9.19.6. När man ska välja MQTT framför HTTP¶
Kapitlet om webbservrar täcker kameran som agerar HTTP-server och, på sidan om molnuppladdning, som HTTP-klient som postar JPEG-bilder till en fast URL. Båda har sin plats. Rätt tillfälle att istället ta till MQTT:
Samma data behöver gå till flera lyssnare (en instrumentpanel, en aviseringstjänst, en inspelare) utan att kameran känner till listan i förväg.
Lyssnare kan komma och gå utan att kameran startas om.
Kameran vill prenumerera – för att ta emot kommandon från en styrenhet – vilket en HTTP-klient inte kan göra utan långpolling eller en server som pushar på en återanrops-URL.
Anslutningen måste överleva långa inaktiva perioder billigt.
Rätt tillfälle att hålla sig till HTTP: en kamera, en server, ett fast begäran/svar-mönster med en kropp som är för stor för ett enda MQTT-ämne (JPEG-bildrutor över MQTT fungerar men är oförskämt mot mäklaren; HTTP POST är den naturliga passformen).
Korslänk: sidan om molnuppladdning i kapitlet om webbservrar visar HTTP-versionen av ”kamera → molnarkiv”. MQTT-versionen av samma problem håller kameran frikopplad från arkivets URL och låter en andra konsument (säg en larmapp för telefon) ansluta till samma ström.