Select Git revision
6-tervezes.tex
6-tervezes.tex 33.28 KiB
\chapter{Tervezés}
\chapterintro{
Az előzetes kutatás szerint ismerkedem a projekthez illő technológiákkal,
megkeresem a megvalósításhoz használt eszközöket és megválasztom azokat az arra
vetett megkötéseinket figyelembe véve. Miután konkretizáltam a projekt kereteit,
a megvalósítandó rendszer tervezésével foglalkozom. A mikrokontroller az ESP32
hardver platfomon, Rust szoftveres környezetben valósul meg. Az okosotthon
integrációhoz a rendszer vezetékes hálózaton MQTT protokollon fog kommunikálni
a Home Assistant okosotthon szoftverrel. A választott technológiákkal ismerkedem
és megindoklom a választásaimat helyesség, biztonság, a fejlesztés kényelmessége
és a későbbi bővíthetőség szempontjai alapján.
}
\section{Döntések}
\label{dontesek}
\paragraph{} \Aref{feladat}. fejezetben definiáltuk a rendszer alapfeladatait.
A megvalósításához meg kell választanom a megfelelő hardveres platformot és
szoftveres környezetet, figyelve a megismert biztonságtechnikai kérdésekre. A
döntéseknél figyelembe veszem a már létező kereskedelmi rendszerek sajátosságait
és megoldásait; azok előnyei és hátrányai között keresem a kompromisszumot.
A központi egységhez az Espressif ESP32 platformcsaládját választom, a
szoftveres implementációt Rust programozási nyelven, a hivatalos Rust alapú
framework használatával fogom megvalósítani. Az ESP32 viszonylag olcsó,
általános célú mikrokontroller. Szoftveres támogatottsága az elmúlt néhány
évben növekvő figyelmet élvez, különösen a Rust programozási nyelv körében.
\cite{rust-iot} Ez a hardver és szoftver kombináció az iparágban meglehetősen
újkeletűnek mondható például egy Arduino és a hozzá tartozó széleskörű hardveres
és szoftveres támogatottsággal szemben. Vitatható, hogy egy biztonságkritikus
rendszer megvalósításában ésszerű döntés lenne, ha a gyártó által ajánlott
és szélesebb körben használatos C/C++ alapú ESP-IDF framework-öt használnám,
a fiatalabb, kevésbé elterjedt változattal szemben. Azért nem így döntöttem,
mert \aref{inf-bizt}. fejezetben a szoftverbiztonság fényében fontosnak
tartom a memóriabiztonságot. Legalább olyan fontosnak, hogy az már a tervezés
legelején kereteket szabjon. Ezért válaszottam a Rust-ot, ami fundamentálisan
a memóriabiztonságra épülő rendszerek programozására akalmas alacsony szintű
nyelv.
A rendszerem kizárólag vezetékes kommunikációt fog alkalmazni. Ez
\aref{kereskedelmi}. fejezetben megismert két kategória közül a hagyományos
rendszerekre jellemző. Láttuk, hogy előny az egyszerűbb architektúra, de
hátrány a rugalmatlanság. Célom, hogy ne adjunk esélyt a kommunikációs médiumon
való információ kiszivárgására, ezért ez a fizikai biztonságon fog múlni.
A rendszeren belül titkosításra emiatt nincs szükség, de a külső hálózaton
mindenképpen kell, ezért erre alkalmas integrációs protokollt fogok választani.
A külső hálózati interfész szerepében egy Ethernet vezérlőt választok,
ami például egy WiFi rádió modulhoz képest kevesebb hibaforrást jelent az
architektúra egyszerűsége miatt szoftver oldalon is. A látott rugalmatlanság itt
nem lesz jellemző -- az okosotthon integráció fogja ellensúlyozni.
Az okosotthon integrációhoz előszor meg kell választanom, milyen szoftverrel
kell tudnia a rendszernek kommunikálnia. Ha ezt megtettem, ezután a
kommunikációhoz használt protokollt pontosítom. Ahogy \aref{kereskedelmi}.
fejezetben láttuk, hogy a DIY rendszerek mindegyike önmagában egy IoT platform
is, a gyártó által kiadott zárt szoftverrel együtt. Illetve a nagy hátrányuk
az volt, hogy a legtöbb esetben előfizetés keretében volt elérhető a teljes
funkcionalitás, vagyis hosszútávú költségek magasak voltak. A mi rendszerünknek
azt a célt tűzöm ki, hogy egy nyílt ökoszisztéma legyen, azaz a hardveres
megoldások és a szoftver forrása nyílt és szabadon felhasználható legyen.
Ne legyenek egyáltalán hosszútávú költségek, illetve a rövidtávú költségek
keretében csak a rendszer anyagára legyen számottevő. Ezekkel a megkötésekkel
a rendszer mechanikai telepítése és a szoftveres telepítés időköltsége
kompromitál. A megkötést az integrációra is érvényesítem, ezért az okosotthon
megoldások körében a helyileg futtatott, ``self-hosted'', nyílt forrású és
szabadon felhasználható szoftverek körében szeretnék elsősorban integrációra
lehetőséget keresni. Ezeknek a Home Assistant okosotthon felel meg, ezzel
a szoftvercsomaggal fog integrációt élvezni a rendszerem, ami a feladatban
megfogalmazott feltételeknek megfelel. Továbbá a feladatban említett értesítési
módok mindegyikét lehetséges vele használni. \cite{hass-integrations} A
megfogalmazott feltételből felmerülő hátrány a következő: a rendszer ettől
kezdve feltételezi és elvárja, hogy a felhasználónak legyen egy működő
Home Assistant példánya, és nem pedig egy felhős megoldással integrálódik.
Láthatóan a kényelem árára célom növelni a biztonságot itt is azzal, hogy maga
a riasztórendszer számára nem lesz internetelérés, hanem kizárólag a belső
hálózaton lesz képes kommunikálni az okosotthon-rendszerrel. A Home Assistant
készítői közzé tettek egy teljeskörű telepítési útmutatót számos lehetőséggel.
\cite{hass-install} Ennek tudatában ezt nem tartom hátráltató tényezőnek az
onboarding élmény során.
Az okosotthon integráció megválasztása után a hálózati kommunikációra
fordítom a tervezés menetét. Választanom kell egy olyan protokollt, mely képes
titkosításra, a Home Assistant támogat és a Rust platformon van hozzá megbízható
implementáció. Ezeknek az MQTT protokoll felel meg leginkább, ezért választottam
azt. Egyetlen hátránynak tűnhet, hogy egy önálló szerver futtatását igényli,
az úgynevezett ``broker''-t. Azért lehet hátrány, mert ez plusz terhet helyezne
a végfelhasználóra alapesetben, de mivel a Home Assistant szoftvercsomagában
széleskörű támogatást élvez, sőt egy hivatalos kiegészítőként is működtethető,
nem vonatkozik esetünkre az állítás. A telepítés lépései könnyűek és rövidek.
\cite{hass-mqtt-broker} Miután elindul a broker, és a rendszer csatlakozik
rá, azonnal láthatóvá fog válni a Home Assistantban. Ez \aref{kereskedelmi}.
fejezetben látottakban kimagasló előnyt jelent a DIY rendszerekhez képest, mert
az onboarding élmény elméletben megbízható lesz a vezetékes kommunikáció miatt,
és a kifejlett protokoll és integráció miatt. A hibafaktor és a használhatóság
az a saját implementáláson fog leginkább múlni, melyet a gyakorlatban fogunk
tudni felmérni.
Végezetül, a megtervezett rendszert és a megválasztott komponenseket
\aref{diag:rendszer}. ábrán illusztráltam. A megválasztás során felületesen
szereztem tudást az adott technológiákról, megoldásokról. A következő fejezetben
részletesen adok ismertetést azokról.
\begin{figure}[htbp!]
\includesvg[width=\textwidth]{images/rendszer.drawio.svg}
\caption{A teljes rendszer magas szintű vázlata}
\label{diag:rendszer}
\end{figure}
\section{Megoldásom ismertetése}
\label{valasztott-tech}
\subsection{ESP32 platform}
\label{esp}
\paragraph{} Az Espressif más megközelítéssel tervezi a hardvereit, mint hasonló
platfomok. Egy alacsony fogyasztású beépített rendszer általában minimális számú
perifériával van ellátva, elterjedten a mikrokontroller tokozásában nincsenek
kiegészítő perifériák, csak a processzor, órajel vezérlés, debug interfész,
és egyéb alapvető komponensek. Ez jellemző például az STMicroelectronics
ARM mikroprocesszor alapú chipjeire. Ez ideális egy olyan rendszernél,
ahol a hardver tervezését is szeretnénk maximálisan magunk elvégezni minden
részletében. Ezzel a módszerrel a végeredmény nagyon optimalizált összetételt
fog eredményezni egy nagyon specifikus célra. Hátránya, hogy a későbbi
változtatás felmerülő igényével számolni nehezebb, és azt legtöbb esetben
nehezebb kivitelezni, ezért rugalmatlanabb, mint itt az ESP32 esetében. Az
analógia a következő: legyen egy komplett, jól megválasztott perifériákkal
ellátott alaprendszer a chipen (``Systen on a Chip'' -- SoC) a kezünknél. Ez a
hardver tervezésének fázisát segíti elő, illetve a később felmerülő bővítéssel
számolni legtöbb esetben triviális az integrált perifériák jelenlétében. Ez az
analógia jól illeszkedik IoT célú eszközök körében, hiszen beépített WiFi és
Bluetooth rádióval van ellátva, mellé a szoftveres támogatás is integrált, a
gyors tervezés és implementálást elősegítve. \cite{esp32-family} Ahogy láttuk
\aref{kereskedelmi}. fejezetben, egy hagyományos riasztórendszerhez az előbbi
megközelítés áll jobban, és a DIY rendszerekhez az ESP32 megközelítését tartom
ideálisabbnak. Ezért esett rá a választás, hiszen a dolgozat keretében egy
alapot tervezek készíteni egy bővíthető rendszer számára.
Az ESP32 családjában több mikroprocesszor-architektúra is megtalálható,
nevezetesen: Xtensa, ARM és RISC-V. A legtöbb modell esetében Xtensa
magok találhatóak, mely kizárólag az ESP-k körében használatos manapság. A
mikrokontroller tokozásának egy általános felépítését a gyártó dokumentációja
\aref{diag:esp32}. ábrán mutatja be. A mag 32 bites ALU-val és 16 darab 32 bites
általános célú regiszterrel rendelkezik, 16 bites utasításkészlet mellett (ISA).
Az alap ISA 82 RISC utasítást tartalmaz. \cite{xtensa} Az általam választott
ESP32 alapú fejlesztőpanel (ESP32-D1-MINI) az ESP32-WROOM-32 chipet használja.
A chip tartalmaz 4MB beépített SPI flash memóriát, 520KB SRAM-ot. Megítélésem
szerint ez elegendő erőforrás a fehasználásunkra, hiszen nem fogjuk használni a
beépített WiFi rádiót, ami működtetése szignifikáns ROM és RAM-ot felhasználna.
\cite{esp-wroom} A chip tartalmaz egy beépített MAC interfészt (Media Access
Controller), amely 17 vezetéken (MII -- Media Independent Interface) vagy
9 (RMII -- Reduced Media Independent Interface) vezetéken képes egy fizikai
médium vezérlővel (PHY) kommunikálni. \cite{esp} Ez esetünkben praktikus, mert
csupán egy Ethernet aljzat és PHY vezérlő elegendő Ethernet LAN hálózatokkal
való integráláshoz. Továbbá a chipen a MAC egyedi DMA (Direct Memory Access)
vezérlővel működik beépített adó és vevő oldali 512 szavas FIFO-val. Ezt
alkalmazva az adatátvitel akár $100\ Mbit/s$ sebességen tud működni. \cite{esp}
Számunkra nem a sebesség a fontos, hanem a stabilitás, megbízhatóság, melynek
ezek a beépített perifériák eleget tesznek. Az adatátvitel szoftveres kezelését
a gyártó által fejlesztett framework-ben implementált driver fogja végezni, mely
kevésbé rizikósabb, mint harmadik fél által fejlesztett megoldások.
\begin{figure}[htbp!]
\centering
\includesvg[width=0.6\linewidth]{images/esp32_blockdiagram.svg}
\caption{Az ESP32 mikrokontrollerek általános funkcionális blokkdiagramja \cite{esp}}
\label{diag:esp32}
\end{figure}
Az Espressif fenn tart ehhez az architektúrához készült fordítóprogramokat,
melyek az ESP-IDF (Espressif IoT Development Framework) szofverfejlesztői
csomagban használatosak. \cite{idf-tools} A framework alapvetően C/C++ nyelvekre
célzott. A Rust támogatás esete speciális, mely két különböző módon adott.
Az egyik mód keretében az ESP-IDF függvénykönyvtárának felhasználásával
implementált Rust standard library (std) segítségével tudunk fejleszteni
alkalmazást. Vagy a sokkal kisebb binárist eredményező és memóriahasználatú,
alacsonyabb szintű út: std és ESP-IDF nélkül (``\verb|no_std|''), úgynevezett
``baremetal Rust'' környezetben tudunk fejleszteni. \cite{esp-rust-approaches}
A dokumentáció jól szemlélteti a két megközelítés közötti különbségeket,
illetve mikor érdemes melyiket választani. A projektünkhöz az std használatát
választom az ESP-IDF kifejledt ökoszisztémája miatt, mellyel korábban pozitív
tapasztalataim voltak. A korábbi eszköz szoftverében a működése óta nem találtam hibát,
ami hasonlóan egy ESP32-WROVER-32 alapú rendszer volt titkosított adatközléssel
WiFi hálozat felett. \cite{onlab} Az ESP-IDF több biztonságot garantál a program
stabilitása érdekében, mint a bare metal megoldás, hiszen ott futásidőben
nincsen operációs rendszer, ezért minden a fejlesztőre van bízva -- nincsen
heap, ezért a memóriakezelés visszaesik ránk. \cite{rust-nostd} \Aref{inf-bizt}.
fejezetben indokoltak miatt a projekt keretében a bare metal használata nem
ideális. Több minden van a fejlesztőre bízva, több befolyást ad a kezünkbe,
de ezzel egyben biztonság kárára optimalizálnánk a program memóriahasználatát
és bináris méretét. A bare metal megközelítést inkább elszigetelt, beágyazott
célhardverekhez tartom jobban igazodónak, nem magas szintű IoT-barát
megoldásoknak.
\subsection{Rust nyelv és környezet}
\label{rust-env}
\paragraph{} A Rust nyelv ötlete egy Mozilla-nál dolgozó fejlesztő fejéből
pattant ki, amikor egy liftbe beszállt és az elromlott, hibás szoftver miatt.
Kiábrándítónak tartotta, hogy a modern információs technológia korában még
mindig előfordulhatnak ilyenek; helytelen szoftver futásából keletkező hibák.
\cite{rust-history} Ahogy \aref{inf-bizt}. fejezetben is láttuk, a legtöbb
hiba helytelen memóriakezelésből származik. A nyelv megálmodója azt a célt
tűzte ki a Rust számára, hogy a memóriakezelést tegye biztossá, biztonságossá
és egyértelművé egy új megközelítéssel. \textsl{``A programozási nyelvek
tervezésénél a magas szintű ergonómia és az alacsony szintű vezérlés gyakran
ellentétben áll egymással; a Rust szembemegy ezzel a konfliktussal.''}
\cite{rust-book} -- írja a nyelv hivatalos dokumentációja.
\Aref{tab:lang-compare}. táblázatban kigyűjtöttem összehasonlításra néhány
szempontot a Rust és valahány elterjedt programozási nyelv között.
A memóriakezelés programozási nyelvek körében általában futásidőben vagy
fordítási időben történik (pontosabban a fejlesztőre van bízva -- allokálás
és deallokálás saját kezűleg történik). Az előbbit az úgynevezett ``garbage
collector'' végzi, ami tipikusan az egyes memóriaterületekre mutatott
referenciák számát figyelve dönti el, hogy a mutatott memóriaterületet fel
lehet szabadítani vagy sem -- használatban van-e még vagy már nem. Ezzel a
feljesztő válláról leveszi a memória kezelésének terhét, hiszen azt a program
futtatókörnyezete teszi helyette. Ennek a megközelítésnek az a hátránya, hogy
több memóriát használ, mint amennyire szükség lenne. Egy embedded rendszer
kereteiben ez kritikus, ezért zártam ki a választáskor az ilyen nyelveket. Az
utóbbi eset; a fordítási időben való memóriakezelés már nem ilyen triviálisan
megoldható kérdés. A Rust is egy ilyen nyelv, és ehhez különböző megkötéseket és
módszertanokat vet be.
\begin{table}[htbp!]
\begin{tblr}{
vlines,
hlines,
colspec={X[c] X[c] X[c] X[c] X[c]},
}
\textbf{Nyelv} & \textbf{Előre fordított (AOT/JIT) / interpretált} & \textbf{Futásidejű virtuális gép (VM)} & \textbf{Garbage collector (GC)} & \textbf{Fordítási idő} \\ \hline
Javascript & JIT & igen (böngésző, Node.js) & igen & - \\
Typescript & JIT & igen (böngésző, Node.js) & igen & - \\
Python & interpretált & igen & igen (reference counting) & - \\
Java & JIT & igen (JVM) & igen (tracing) & gyorsabb \\
Go & AOT & nem & igen & gyorsabb \\
Rust & AOT & nem & nem & lassabb \\
C/C++ & AOT & nem & nem & átlagos \\
\end{tblr}
\begin{tblr}{
vlines,
hlines,
colspec={X[c] X[c] X[c] X[c] X[c]},
}
\textbf{Nyelv} & \textbf{Memóriabiztonság} & \textbf{Típusbiztonság} & \textbf{``Null'' biztonság} & \textbf{``Data race'' biztonság} \\ \hline
Javascript & van (GC) & gyenge & nincs & van (egyszálú) \\
Typescript & van (GC) & erős (strict mode alatt) & van & van (egyszálú) \\
Python & van (GC) & erős & nincs & van (egyszálú) \\
Java & van (GC) & erős & nincs & nincs \\
Go & van (GC) & erős & nincs & van \\
Rust & van (RAII, ownership) & erős & van & van \\
C++ & van, részben (RAII) & gyenge & nincs & nincs \\
C & nincs & gyenge & nincs & nincs \\
\end{tblr}
\caption{Elterjedt programozási nyelvek áttekintő összehasonlítása \cite{gh-lang-perf}}
\label{tab:lang-compare}
\end{table}
Az egyik alapvető különbség más statikus típusú, procedurális és imperatív
nyelvektől, hogy minden változó első hozzárendelése után az nem megváltoztatható
-- a változó ekkor ``immutable''. Többszöri hozzárendelés esetén a program nem
fordul, a fordító hibaként jelzi, hogy az immutable. Ha szükséges a változó
értékét változtatni a program élete során, akkor azt a \verb|mut| kulcsszóval
lehetséges ``mutable'' változóvá tenni. Ekkor megengedett a többszöri
hozzárendelés, de ez más megkötésekkel fog járni, ha a referenciát hozunk
létre a mutable változóra. \cite{rust-book} Minden változóra lehet referenciát
létrehozni, de egyszerre csak egy referencia létezhet egy változóra. Továbbá
a referenciák maguk is lehetnek immutable vagy mutable referenciák, és rájuk
is egyéb korlátozások vannak helyezve. Ezek a szabályok ugyanúgy a fordítás
idejében kerülnek ellenőrzésre. \cite{rust-book} Jól látható, hogy a helytelen
használatból fakadó hibaforrások sokkal hamarabb és kontrolláltabb módon
felszínelhetőek, mint egy futásidő alatt ellenőrzött módon.
Minden változónak van egy tulajdonosa és tulajdonosból kizárólag egyetlen
lehet. Amikor a tulajdonos kilép a hatóköréből, akkor a változó értéke is
el lesz dobva. Ha az a heap-en volt, akkor itt történik a deallokáció. Ez
az ``ownership'' fogalma a Rust-ban. Ennek segítségével tudja garantálni
a memóriabiztonságot fordítási időben, egy futás idejű garbage collector
használata nélkül. \cite{rust-book} Furcsa lehet első látásra, hogy minden
változónak egyetlen tulajdonosa lehet, amiből azt a következtetést lehet
levonni, hogy így lehetetlen konkurrens hozzáférést implementálni egy több szálú
programban. Ahhoz, hogy ez lehetséges legyen, a Rust bevezeti az ``interior
mutability'' paradigmáját. A standard library tartalmaz olyan építőkocka jellegű
adattípusokat, melyek enkapszulálják a mutable memóriaterületet. Hívó fél
számára elegendő egy immutable reference az adattípusra, mégis van lehetőség
a belső memória tartalmának változtatására. A következő bekezdésben ismertetem
ez hogyan lehetséges. Az ilyen építőkocka adattípusokkal vannak implementálva
például a mutexek, read-write lock-ok, atomic reference counted (Arc) pointer.
\cite{rust-book}
Alapesetben nincsenek nyers pointer típusok, amivel az ownership szigorú
modelljét ki lehetne kerülni. Felmerül a kérdés: így hogyan lehet alapvető
adatszerkezeteket optimálisan megvalósítani a nyelvben? Emiatt létezik az
úgynevezett ``unsafe Rust'', melynek célja, hogy mégis tudjunk saját kezűleg
optimális, manuálisan memóriakezelt kódot írni. Ez továbbra is megkötésekkel
jár, de sokkal több kontrollt áthelyez a programozóra (egyben felelősséget
is), de ennek árán nem tud memóriabiztosságot garantálni a fordító --
ezért ``unsafe''. Unsafe Rust kereteiben léteznek nyers pointer típusok, és
megengedett egy pointer dereferenciája. Így egy éles vonalat tudunk húzni egy
adatszerkezet belső implementációja és az azt használó fél között. Ez úton
lehetséges az interior mutability is. A belső implementációt jól optimizált
módon, unsafe kóddal végezhetjük, és az adatszerkezetet használó félnek nincs
szüksége onnantól kezdve unsafe kódot írnia. \cite{rust-book} Az unsafe kód
szintaktikailag ugyanúgy Rust forráskód, csak egy \verb|unsafe| kulcsszóval
nyitott kódblokkba kell helyezni, amivel a fordító felé jelezzük, hogy itt
szeretnénk úgymond átvenni a totál irányítást.
A dokumentáció által ajánlott az unsafe kód írását kerülni egy felhasználói
alkalmazás keretében, illetve ha komolyan szeretnénk venni a Rust
memóriagaranciáját, akkor ésszerű döntés. Igyekszem ezt betartani a központi
egység firmware írásában, kivéve ha más módon nem implementálható valami, vagy
ha optimálisnak látom annak használatát. Az utóbbi esetében jól dokumentálom
annak okát és helyes használatát. Ezzel a hozzáállással a szoftverbiztonság
garanciáját a nyelvi elemekre és a fordítóra helyezem át ahol csak lehet,
csökketve az emberi hibából fakadó helytelenségeket.
% TODO: async rust + ábra az async runtime működéséről
\subsection{MQTT protokoll}
\paragraph{} Az MQTT protokoll az OSI réteg tetején ül, az Application Layer-en,
TCP/IP hálózatok felett. \textsl{``Az MQTT célja, hogy egyszerű, nyílt,
könnyű és nagy sávszélesség-hatékonyságú legyen, így jó választás a gépek
kommunikációjára korlátozott környezetben.''} \cite{mqtt-vs-coap} Az MQTT egy
publish/subscribe modellre építő üzenetküldő protokoll, ami az IoT világában
jól alkalmazható. Egyetlen nagy előnye a megbízhatósága, mivel TCP felett
van implementálva. Emiatt minden átvitt adat rendezett és veszteségmentes.
\cite{mqtt-vs-coap}
A publish/subscribe vagy röviden pub/sub modell egy hierarchikus rendszer.
Minden üzenet egy adott kategóriába (``topic'') van sorolva. Ha egy üzenetet
küldünk (publish) egy adott topic-on belül, akkor az arrra feliratkozott
(subscribe) kliensek mind megkapják az üzenetet. Egy kliens akárhány topic-ra
iratkozhat fel, illetve küldhet üzenetet. A protokoll megköveteli azt az
architektúrai elemet, hogy a kliensek között legyen egyetlen szerver, elnevezés
szerint a ``broker''. A broker fogadja a kliensek csatlakozási, feliratkozási,
üzenetküldési, leválasztási és egyéb kérelmeit. Nyilván tartja a klienseket,
azok jogosultságait és a topic-on közzétett üzenetek továbbítását végzi.
Számos implementációja van mind kliens oldali és broker oldali szerepeknek.
\cite{mqtt-vs-coap}
Az architektúra modelljét \aref{diag:mqtt}. ábrán rajzoltam le szemléltetés
végett. Az első lépésben a kliens feliratkozik a ``topic1'' üzenet kategóriára.
A második lépésben egy másik kliens iratkozik fel egy másik kategóriára:
``topic2''. 3. lépésben egy harmadik kliens küld üzenetet a ``topic1''
kategórián, melyet fogad az első kliens. További lépésekben szemléltetem,
hogy bármilyen kliens feliratkozhat bármilyen kategóriára, illetve bármilyen
kategóriára küldhet üzenetet bármilyen kliens. Egy üzenetet akkor továbbít a
broker, ha annak kategóriájára van feliratkozott kliens.
\begin{figure}[htbp!]
{\footnotesize\includesvg[width=\textwidth]{images/mqtt.drawio.svg}}
\caption{Az MQTT protokoll architektúra-modell bemutatása 8 lépésben}
\label{diag:mqtt}
\end{figure}
Az architektúra előnye, hogy a komplex feldolgozás feladata az a broker-re van
hagyva és egy kliens működtetése minimális erőforrásokat igényel. További előnye
más megoldásokkal szemben, hogy beépített Quality of Service (QoS) megoldást
kínál az átvitt üzenetek fogadásának és célba érkezésének garanciájához. A TCP
önmaga ilyen garanciát nem tud adni -- csak integritás és rendezettséget --
illetve csomagok újraküldésével tud próbálkozni de ez még nem garancia az adott
üzenet célba érkezésére, hiszen alacsonyabb absztrakciós szinten tartózkodik.
Ezért ez a feladat magasabb egy szintű protokollra marad. \cite{tcp} Az MQTT
három szinten definiál QoS-t:
\begin{itemize}
\item \textbf{QoS 0} (``At most once delivery''): Az üzenetek átviteléről kvázi
nincs garancia, tehát itt nincs semmilyen újraküldésre tett kísérlet.
\item \textbf{QoS 1} (``At least one delivery''): Az üzenetküldés sikerességét jóvá kell hagyni,
ellenben az üzenet újra küldésre kerül. Ez a szint garantálja hogy
az üzenet célba fog érni, de lehetséges, hogy többször is.
\item \textbf{QoS 2} (``Exactly once delivery''): Az üzenetküldés maga egy
négy utas kézfogás (``four-way handshake'') első üzenete, ami
garantálja, hogy az üzenet pontosan egyszer fog célba érni.
\cite{mqtt-vs-coap}
\end{itemize}
\Aref{esp}. fejezetben megismert ESP-IDF környezet beépített implementációt
kínál MQTT kliens szerepben, Rust nyelven is. Az integrációt ezzel tervezem
megvalósítani. Minden QoS szintet támogat, illetve aszinkron (async, nem
blokkoló) változatú implementáció is van hozzá. Darabolt adatfogadásra
is van lehetőség, amivel nagyobb mennyiségű adatot is fel lehet dolgozni
kevés memóriával is. \cite{esp-idf-svc} Ez jól fog jönni firmware frissítés
implementálásakor, amivel potenciálisan azonnal a flash memóriába lehet majd
írni az újabb binárist. Bár ez nem az alapfeladat része, de jó alapokat ad a
framework; előre tervezés szempontjából hasznos tudásnak tartom.
\subsection{Home Assistant okosotthon}
\label{hass}
\paragraph{} A Home Assistant egy nyílt platform, otthon-automatizációs
célokra. Egy központi, egységes felületet ad a felhasználó számára, ahol
eléri az összes okos eszközét és azok funkcióit. A teljes szoftver moduláris,
bővíthető integrációk segítségével, Python nyelven. A nyílt forrású kód
és licensz jóvoltából számos gyártó által fejlesztett protokoll, eszköz és
architektúrával van integrálva, melyet a Home Assistant fejlesztői közössége
épít. \cite{hass-github} A GitHub felmérése szerint 2024-ben a Home Assisstant
volt a legnépszerűbb projekt a platformon, csupán a hozzájárulók száma
alapján. \cite{github-2024} A projekt támogatást élvez az Open Home Foundation
jóvoltából, kinek tevékenységét számos vállalat szponzorál -- köztük az
Espressif is. \cite{openhome}
Nem csupán a népszerűsége és nyíltsága miatt választottam ezt a megoldást, hanem
az egyedi architektúrai felépítése miatt is. A szoftver minden adatot entitások
formájában értelmez. Egy entitásnak van egy egyedi azonosítója, típusa (ún.
``domain''), neve, állapota, illetve egyéb attribútumai. Egy entitás domain-je
sokféle lehet, ezek közül megemlítendő: nyomógomb, kapcsoló (ki/be), bináris
szenzor (ki/be, igaz/hamis), szenzor (hőmérséklet, légnyomás, áramfogyasztás
stb.), lámpa, zár, kamera és természetesen riasztó vezérlőpanel, mozgásérzékelő.
\cite{hass-entities} Jól látható, hogy egyes entitások csupán információt
közölnek, mások pedig felhasználói interakciót engednek meg. Lehetőség van
adott entitásokat csoportosítani felhasználó által definiált területekre (area).
Ez például hasznos akkor, ha több lámpa van egy szobában és mindet egyszerre
szeretnénk irányítani. A szoftver definiálja az eszköz (device) fogalmát;
ami szintén egy entitások csoportosítása, de azt nem a felhasználó, hanem a
gyártó/megalkotó határozza meg -- például egy IP kamera, ahol van egy kamera
entitás, és egy nappali és éjszakai mód között váltó gomb. Vagy vegyünk egy okos
kapu vezérlőt, ahol az entitások a következők: a kapu két szárnyát egyszerre
nyitó gomb, csak az egyik szárnyat nyitó gomb, és akár egy állapot visszajelző
szenzor.
Az entitásokat a felhasználó eléri a Home Assistant webes felületén
(frontend). \Aref{diag:hass-demo}. ábrán látható egy példa a frontend
kinézetére. A felületen a felhasználó az entitások vezérlésén kívül azokra
automatizmusokat is tud létrehozni. Egy automatizmus három fázisból áll:
indító események (trigger), feltételek (conditions), és műveletek (actions).
A felhasználó megadja, hogy milyen események -- entitás állapotváltozások
-- hatására induljon el a műveletek listája. A feltételek megszabják, hogy a
kiváltott esemény mellett definiált megkötések keretében szabad-e futtatni a
műveleteket. \cite{hass-automations} A dokumentáció a következő példa mondattal
magyarázza a felépítést: \textsl{``Amikor Paulus hazaér és a nap már lement,
akkor kapcsolódjanak be a lámpák a nappaliban.''} \cite{hass-automations} A
riasztórendszer-kezelőpanel típusú entitások élesítésére és hatástalanítására is
van művelet. Ezzel a rendszerünk frontend oldalról is teljeskörűleg integrálható
lesz. Szabadságot tudunk adni a végfelhasználónak, hogy a saját igényei szerint
lehessen a rendszert működtetni -- automatizált módon is. Ha a mozgásérzékelő
típusú bináris érzékelőket a Home Assistant számára elérhetőve tesszük,
nem csak a biztonságtechnikai szerepet tudná betölteni a rendszer, hanem az
otthonautomatizáció szerepét is bővítené. Például, a következő két automatizmust
lehetne megvalósítani egy mozgásérzékelő entitás jelenlétével: Amikor mozgás
van a szobában és a rendszer hatástalanítva van és a nap már lement, akkor
kapcsolódjon fel a szobában lévő lámpa. Illetve ha nincs mozgás legalább 10
percen át, akkor kapcsolódjon le a lámpa.
\begin{figure}[htbp!]
\centering
\includegraphics[width=\textwidth]{images/hass-demo.png}
\caption{A Home Assistant frontend felülete - gyártói demo \cite{hass-demo}}
\label{diag:hass-demo}
\end{figure}
Az MQTT egy első osztályú integráció a Home Assistantban. Az MQTT minden QoS
szintjét támogatja, illetve a többi integrációhoz képest sokkal általánosabb
interfészt ad. Ez kimagaslóan jobb élményt ad a végfelhasználó számára,
mert egységes és konzisztens marad a frontend. A fejlesztő számára is jobb
élmény, mert az intgrációt így nem szükséges a Home Assistant forráskódján át
implementálni Pythonban. MQTT-n keresztül lehetőség van entitások és eszközök
telepítésére manuális konfigurációval vagy az úgynevezett ``MQTT-discovery''
vagy ``autodiscovery'' segítségével. Mindkét esetben az eszközöknek és
entitásoknak van egy deklaratív leíró sémája, ami tartalmaz meta-adatokat
és leírást azok működtetésére. Manuális konfiguráció esetén ezt a Home
Assistantban YAML formátumban kell megadni, autodiscovery során pedig egy
előre meghatározott MQTT topic-on kell küldeni JSON formátumban a rendszer
felé. Látható, hogy az utóbbi esetben az azt támogató eszköz végfelhasználói
onbarding élménye sokkal kényelmeseb. Például, az eszköz első indítása után
az autodiscovery üzenet küldésével a Home Assistant frontend felületén azonnal
látni fog a felhasználó egy jóváhagyandó üzenetet, hogy az készen áll a
használatra. Jóváhagyás után a meghirdetett entitások importálásra kerülnek a
Home Assistantba. \cite{hass-mqtt} Célom úgy megvalósítani a saját rendszert,
hogy az alapból támogassa az autodiscovery-t. Így \aref{kereskedelmi}.
fejezetben megismert DIY rendszereknél is potenciálisan jobb élményt tudna
nyújtani, hiszen a kommunikáció vezetékes médiumon történne, ahol az Ethernet
csatlakoztatásával azonnal a hálózatra kerül az eszköz, egyúton megjelenne a
felhasználó Home Assistant felületén. Ez elméletben kényelmesebb, mint például
a WiFi konfigurációja. Ennek realitását szintén a gyakorlatban fogjuk tudni
megállapítani.
Ezzel a Home Assistant belső architektúráját magas szinten kielemeztük.
Szemléltetés céljával lerajzoltam a modellt \aref{diag:hass-arch}. ábrán.
\begin{figure}[htbp!]
{\footnotesize\includesvg[width=\textwidth]{images/hass.drawio.svg}}
\caption{A Home Assistant architektúra-modellje \cite{hass-diagram}}
\label{diag:hass-arch}
\end{figure}
\clearpage % Ez azért kell, hogy nehogy képek átcsússzanak a következő fejezethez