From ec186fec2b7f7bd7179f4058ad6db3b51006f4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A1dudvari=20=C3=81kos?= <nadudvari.akos@hallgato.ppke.hu> Date: Sun, 27 Apr 2025 22:45:20 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20eredm=C3=A9nyek=20init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nix/flake-module.nix | 8 +- src/contents/5-elozmenyek.tex | 1 + src/contents/6-tervezes.tex | 43 ++- src/contents/7-eredmenyek.tex | 556 +++++++++++++++++++++++++++++ src/contents/melleklet.tex | 2 +- src/hivatkozasok.bib | 62 ++++ src/images/statemachine.drawio.svg | 4 + src/szakdolgozat.tex | 8 +- 8 files changed, 661 insertions(+), 23 deletions(-) create mode 100644 src/images/statemachine.drawio.svg diff --git a/nix/flake-module.nix b/nix/flake-module.nix index e4924af..39daa17 100644 --- a/nix/flake-module.nix +++ b/nix/flake-module.nix @@ -29,7 +29,8 @@ in bookmark url csquotes listings listings-ext sourcecodepro silence biblatex-ieee ly1 metafont transparent catchfile microtype l3kernel l3packages texcount moreverb pdfpages pdflscape - tabularray ninecolors; + tabularray ninecolors minted fvextra latex2pydata + newfloat pdftexcmds pgfkeyx pgfopts upquote lineno; }); }; document.font = mkOption { @@ -63,7 +64,8 @@ in packages.document = pkgs.stdenvNoCC.mkDerivation { name = "latex-document"; src = config.document.source; - buildInputs = [ pkgs.coreutils pkgs.inkscape config.document.texlive config.document.font locales ]; + buildInputs = (with pkgs; [ coreutils inkscape latexminted ]) ++ + [ config.document.texlive config.document.font locales ]; phases = [ "unpackPhase" "buildPhase" "installPhase" ]; buildPhase = '' set -o errexit @@ -92,7 +94,7 @@ in let doc-watcher = pkgs.writeShellApplication { name = "doc-watcher"; - runtimeInputs = with pkgs; [ watchexec coreutils inkscape ] ++ [ config.document.texlive ]; + runtimeInputs = with pkgs; [ watchexec coreutils inkscape latexminted ] ++ [ config.document.texlive ]; text = '' export OSFONTDIR="${config.document.font}/share/fonts" watchexec -r --print-events -- \ diff --git a/src/contents/5-elozmenyek.tex b/src/contents/5-elozmenyek.tex index a200780..ad3213a 100644 --- a/src/contents/5-elozmenyek.tex +++ b/src/contents/5-elozmenyek.tex @@ -425,6 +425,7 @@ stabilitását és biztonságát a modern DIY rendszerek nyitottságával és rugalmasságával igyekszik ötvözni. \section{Biztonságtechnikai kérdések} +\label{bizt-kerd} \paragraph{} Egy riasztórendszer megalkotásához elengedhetetlen a tervezőnek ismermie a biztonságtechnika alapjait, illetve tisztában lennie a diff --git a/src/contents/6-tervezes.tex b/src/contents/6-tervezes.tex index 62cbb70..2b0db55 100644 --- a/src/contents/6-tervezes.tex +++ b/src/contents/6-tervezes.tex @@ -182,6 +182,7 @@ 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. @@ -323,6 +324,7 @@ implementálásakor, amivel potenciálisan azonnal a flash memóriába lehet maj 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 @@ -351,8 +353,9 @@ 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 két mód elérhető: a kapu két szárnyát egyszerre nyitó gomb, -csak az egyik szárnyat nyitó gomb, és akár egy állapot visszajelző szenzor. +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), amiben azokra automatizmusokat is lehet létrehozni. Egy automatizmus három @@ -377,22 +380,26 @@ ha nincs mozgás legalább 10 percen át, akkor kapcsolódjon le a lámpa. 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ó és a fejlesztő -számára is. MQTT-n keresztül lehetőség van entitások és eszközök telepítésére -manuális konfiguráció vagy az úgynevezett ``MQTT-discovery'' 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 az 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 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. Ennek realitását +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'' +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. 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. \clearpage % Ez azért kell, hogy nehogy képek átcsússzanak a következő fejezethez diff --git a/src/contents/7-eredmenyek.tex b/src/contents/7-eredmenyek.tex index 5da6996..3ef2a40 100644 --- a/src/contents/7-eredmenyek.tex +++ b/src/contents/7-eredmenyek.tex @@ -16,6 +16,59 @@ } \section{Hardver} +\label{hardware} + +\paragraph{} A megvalósítást a hardver megépítésével kezdtem. Legelőször a +tápellátást helyeztem üzembe. Egy 12 voltos akkumulátort és hozzá egy töltés +vezérlőt szereztem, melynek kimenetére egy buck-boost átalakító modult +helyeztem. Így az akkumulátor feszültségét $3,3\ V$-ra tudtam illeszteni, +az ESP32 számára. Ezután a fejlesztőpanel megfelelő lábaira kötöttem a +perifériákat. A firmware fejlesztésének idejéig a PIR szenzorok helyett egyszerű +nyomógombokat használtam. Ezzel könnyedén tudtam szimulálni a mozgás észlelését. +Hasonlóan a sziréna kimenete helyett először egy LED-et hajtottam meg a +vonallal, természetesen egy ellenálláson keresztül. + +\Aref{esp}. fejezetben megismert beépített RMII interfészt szerettem volna +használni az Ethernet perifériához, melyet nem sikerült működésre bírni a +tesztüzem során. A modul egy olcsó Wiznet IP101GRI interfészt használ, bár +nem találtam hozzá hivatalos datasheet-et, csak az IP101 meghajtó chipről. +\cite{ip101} A hiba forrását próbáltam több úton kideríteni, de sajnos nem +találtam meg. Ellenőriztem a csatlakozások helyességét -- rendben voltak, +a szoftveres használatot próbáltam újraimplementálni példák alapján -- +eredménytelenül, illetve másik gyártó által adott példaprogram sem működött. +\cite{ip101-example} Ezzel arra tudtam következtetni, hogy maga az Ethernet +vezérlővel volt probléma, így más utat kellett, hogy válasszak. Szerencsére a +periféria az RMII interfészen kívül támogatott SPI buszon való kommunikációt +is, úgyhogy ezzel próbálkoztam -- sikeresen. Viszont a nehézség itt az volt, +hogy az sajátos protokollon kommunikált, melyhez szintén nem találtam sem +drivert, sem annak működéséről dokumentációt. Azt a döntést hoztam, hogy a végső +megvalósításban egy új, kizárólag SPI interfészt támogató perifériát használok, +melyhez beépített támogatás van az ESP-IDF fejlesztőkörnyezetünkben. A Wiznet +W5500 chip ezeknek megfelel és az ehhez választott perifériám ezen a chipen +alapul. Innentől kedzve ezzel a megoldással dolgoztam. \cite{w5500-datasheet} + +Miután a szoftvert sikerült arra a szintre hozni, hogy a perifériákkal tudott +a központi egységem kommunikálni, a rendszert elhelyeztem tesztüzem jelleggel +az otthonomban, ahol onnantól kezdve távolról tudtam frissíteni a firmware-t +-- melynek működéséről \aref{firmware}. fejezetben számolok be. Az átmenetileg +elhelyezett nyomógombokat és LED-et eltávolítottam, és azokat a valós PIR +szenzorokra és a szirénára kötöttem. Otthonomban ezelőtt korábban volt egy +riasztórendszer telepítve, így abból újra fel tudtam használni ezeket a +perifériákat. Az így kapott végleges hardveres bekötést \aref{diag:hardware}. +ábrán rajzoltam le. A központi egység egy fém szerelődobozban került +elhelyezésre, ahonnan minden vezeték a falon keresztül jut el a perifériákhoz, +illetve a tápellátás sem könnyedén megszüntethető -- bár itt az elismerés az +előző rendszer telepítőjét illeti, nem nyúltam hozzá a kábelezéshez, mivel +\aref{bizt-kerd}. fejezet mechanikai biztonsági kérdései szerint megfelelőnek +találtam. Első beszereléskor a tápellátás megszűntetésével kipróbáltam, hogy a +rendszer jól működik-e önerőből -- valóban, elegendő volt az akkumulátor az új +központi egység és a telepített perifériák meghajtásához. + +Reflektálva \aref{feladat}. fejezetben elvártakra, a rendszer hardveres +felépítését -- apró korrigálás után -- sikeresen megalkottam. \Aref{bizt-kerd}. +fejezetben definiált rendszer modelljére illeszkedik, annak biztonsági kérdéseit +betartja. Ezek után a szoftverre koncentrálok; a riasztórendszer logikáját +implementálom, majd az integrációkat. \begin{figure}[htbp!] \ttfamily @@ -26,7 +79,510 @@ \end{figure} \section{Firmware} +\label{firmware} + +A firmware forráskódját GitHub-on tettem közzé, mely az alábbi címen érhető el: +\\ +\extlink{https://github.com/akosnad/rusty-esp-alarm}{akosnad/rusty-esp-alarm} + +\paragraph{} A szoftver fejlesztése előtt a fejlesztői környezetet alakítottam +ki. A Rust beépített csomagkezelőjével (\textit{cargo}) \cite{cargo} létrehoztam +az \textit{esp-idf-template} \cite{esp-idf-template} sablon alapján a +mappastruktúrát. A sablonban előre vannak konfigurálva a környezethez bizonyos +szükséges és kényelmi beállítások. Ezek közül megemlítendő: + +\begin{itemize} + \item Az \textit{esp-idf-svc} könyvtár függőségként fel van véve a projekthez + a \verb|Cargo.toml| leíró fájlban. + Ez a könyvtár tartalmazza \aref{esp}. és \ref{rust-env}. fejezetben + látott ESP-IDF framework Rust oldali alacsony és magas szintű API-jait. + \item A Rust fordító és linker optimizációs szintje ``s'' értékre van állítva, ami az eredmény + bináris méretének csökkentésére helyezi a hangsúlyt. + \item A framework által elvárt egyéb fordító és linker beállítások alkalmazva vannak + a\\ \verb|.cargo/config.toml| fájlban. + \item Az \verb|sdkconfig.defaults| fájlban az ESP-IDF framework beállításai találhatóak, + ehhez ésszerű alapértelmezéseket állít be. + \item A szokásos ``hello world'' példaprogramot tartalmazza a \verb|main.rs| fő forrásfájl. +\end{itemize} + +Ezek után nekiláttam a perifériák vezérlésének implementálásához. A \verb|main| +függvény valahány utasítása az a perifériák inicializálását végzi, utána az +egyes feladatkörök elindítása több szálon (thread). A legnagyobb kihívás +itt az ethernet vezérlő működésre bírása volt. Ahogy \aref{hardware}. fejezetben +is említettem, végül a Wiznet W5500 típusú SPI interfész alapú chip vezérlését +kellett implelentáljam. A fizikai réteg inicializálása egyszerűen történik, mely +így néz ki a forráskódban: + +\noindent\extlink{https://github.com/akosnad/rusty-esp-alarm/blob/1d3073a0cbd3b98af0f6a14bdf625732938f78e2/src/main.rs\#L103}{main.rs} +\begin{minted}[linenos,firstnumber=103]{rust} +let eth = Box::leak(Box::new(esp_idf_svc::eth::EspEth::wrap( + esp_idf_svc::eth::EthDriver::new_spi( + SpiDriver::new( + peripherals.spi2, + pins.gpio18, + pins.gpio19, + Some(pins.gpio23), + &SpiDriverConfig::new().dma(Dma::Auto(4096)), + )?, + pins.gpio26, + Some(pins.gpio5), + Some(pins.gpio33), + esp_idf_svc::eth::SpiEthChipset::W5500, + 20.MHz().into(), + Some(&[0x02, 0x00, 0x00, 0xfc, 0x18, 0x01]), + None, + sysloop.clone(), + )?, +)?)); +\end{minted} + +Először egy \mintinline{rust}/SpiDriver/ -t hozok létre, ahol megadom, hogy +milyen kivezetések felelnek meg az SCLK, MISO, MOSI vezetékeknek, illetve +bekonfigurálom, hogy az interfész használjon egy automatikusan kiosztott DMA +csatornát maximum 4096 szavas puffermérettel. A driver azonnal átadásra kerül az +\mintinline{rust}/EthDriver/ inicializálásához, hiszen más eszköz nincsen az SPI +buszon. Így a forráskódban sincs hozzáférés ezentúl a nyers SPI buszhoz, hanem +azt csak a W5500 driver fogja tudni használni. A busz driveren kívül megadom a +maradék W5500 chiphez specifikus kivezetések pin beosztását, a chip típusát, az +SPI buszon üzemeltetett órajel frekvenciáját, illetve egy beégetett MAC címet. +\cite{esp-idf-svc} Így egy szépen enkapszulált \mintinline{rust}/eth/ változónk +van, mely reprezentálja az Ethernet interfészünket. Ezt a futás során később +átadom a hálózat kezelésére lértehozott modulnak: + +\begin{minted}[linenos,firstnumber=216]{rust} +// Network stack +network::init(eth, sysloop.clone(), timer, status_tx.clone(), &mut tasks)?; +\end{minted} + +A \mintinline{rust}/network/ modulban a hálózat teljeskörű kezelését az \mintinline{rust}/eth_task/ függvény végzi: + +%TC:ignore +% texcountnak nem tetszik ez a minted env, hibát dob... +\noindent\extlink{https://github.com/akosnad/rusty-esp-alarm/blob/1d3073a0cbd3b98af0f6a14bdf625732938f78e2/src/network.rs\#L60}{network.rs} +\begin{minted}[linenos,firstnumber=60]{rust} +async fn eth_task<T>( + mut eth: AsyncEth<&mut EspEth<'_, T>>, + status_tx: mpsc::Sender<StatusEvent>, +) -> ! { + loop { + // ... +\end{minted} +%TC:endignore +A logikája a következő lépésekből áll: +\begin{enumerate} + \item A fizikai link kezelése: várakozás amíg nincs link, illetve annak megszűnésekor visszatérés ide + \item IP réteg kezelése: várakozás amíg a DHCP-n keresztüli konfiguráció megtörténik + \item MQTT kliens inicializálása, elindítása konkurrensen +\end{enumerate} + +Láthatjuk, hogy a függvény az \mintinline{rust}/async/ kulcsszóval van +ellátva, és egyes függvényhívások az \mintinline{rust}/.await/ operátor postfix +notációval vannak írva. Rustban így lehet kooperatív többszálú programozást +végezni. Igyekeztem ennek támogatását kihasználni az \textit{esp-idf-svc} +könyvtárban. A legtöbb műveletből létezik blokkoló és nem blokkoló (async) +változat. Az aszinkron megközelítés effektívebb tud lenni egy ilyen rendszernél, +hiszen a kevés processzor erőforrást ki kell tudni minden pillanatban +kihasználni -- a blokkoló műveletre várakozás helyett más feladatot futtatunk. +A preemptív megközelítés is működik, ahol fix időbeosztás van a futás jogára, +bár úgy gondolom egy ilyen rendszernél a teljes kontroll jobb ha nálunk van, +hiszen a sűrű kontextusváltás is befolyásolhatja a teljesítményt kevés erőforrás +mellett. Ezt az állításomat nem ellenőriztem a gyakorlatban, csupán így +közelítettem meg az implementációt. + +Az MQTT kliens működését az \mintinline{rust}/mqtt_task/ és az utána következő segédfüggvények végzik. +A fő eseményfeldolgozó ciklus így néz ki: + +%TC: ignore +\noindent\extlink{https://github.com/akosnad/rusty-esp-alarm/blob/1d3073a0cbd3b98af0f6a14bdf625732938f78e2/src/network.rs\#L136}{network.rs} +\begin{minted}[linenos,firstnumber=136]{rust} +fn mqtt_task( + status_tx: mpsc::Sender<StatusEvent>, + mqtt_client_config: MqttClientConfiguration<'_>, +) -> anyhow::Result<()> { + info!("Starting MQTT..."); + let (client, mut connection) = + EspMqttClient::new_with_conn(MQTT_ENDPOINT, &mqtt_client_config)?; + let mut client = Some(client); + let mut ota = None; + + while let Some(msg) = connection.next() { + match msg { + Err(e) => /* ... */, + Ok(msg) => { + // ... +\end{minted} +%TC: endignore + +Az \mintinline{rust}/msg/ változó az MQTT kliens fogadott eseményeit +fogja tartalmazni, ami lehet a csatlakozás esemény, lecsatlakozás esemény, +feliratkozott topic-ra érkezett üzenet. A \mintinline{rust}/handle_mqt_message/ +függvény kezeli a beérkezett üzeneteket feliratkozott topic-ra. Az eszköz +lényegében egyetlen dologra kíváncsi: a firmware frissítésre -- Over the +Air Update (OTA). Ezt egy erre dedikált MQTT topic-on tudja megkapni, az új +firmware tartalmát ide lehet publikálni bináris formátumban. Ennek a kezelése a +legbonyolultabb az egész firmware kódjában -- nem is az alapfeladat része, így +csak egy kivonatát mutatom itt be: + +%TC: ignore +\noindent\extlink{https://github.com/akosnad/rusty-esp-alarm/blob/1d3073a0cbd3b98af0f6a14bdf625732938f78e2/src/network.rs\#L222}{network.rs} +\begin{minted}{rust} +fn handle_ota_message(msg: MessageImpl, ota: &mut Option<OtaUpdate>) -> anyhow::Result<()> { + let data = msg.data(); + if let Some(mut in_progress_ota) = ota.take() { + match msg.details() { + Details::InitialChunk(_) => { + // hiba -- folyamatban lévő OTA közben érkezett egy újabb darabolt folyam első szelete + // ... + } + Details::SubsequentChunk(SubsequentChunkData { + current_data_offset, + total_data_size, + }) => { + // tovább írás a flash memóriára + // ... + } + Details::Complete => { + // ekkor a teljes új firmware ki van írva az inaktív partícióra, + // újraindítjuk a rendszert, erre átváltva + // ... + } + } + } else { + // OTA megkezdése + // a flash memórián megkeressük az inaktív partíciót, elkezdünk arra írni + // ... + } +} +\end{minted} +%TC: endignore + +Miután sikeresen tudtam az MQTT brokerrel kapcsolatot létesíteni, illetve a +perifériákkal való kapcsolat is működött, áttértem a riasztórendszer logikájának +implementálására. A logika elvét először \aref{diag:alarm-statemachine}. ábrán rajzoltam le, majd +egy új modulban hoztam létre az alábbi adatstruktúrákat: + +\begin{figure}[htbp!] + \ttfamily + {\scriptsize \includesvg[width=\columnwidth]{images/statemachine.drawio.svg} } + \rmfamily + \caption{A firmware belső állapotgép-modellje} + \label{diag:alarm-statemachine} +\end{figure} + +%TC:ignore +\noindent\extlink{https://github.com/akosnad/rusty-esp-alarm/blob/1d3073a0cbd3b98af0f6a14bdf625732938f78e2/src/alarm.rs\#L8}{alarm.rs} +\begin{minted}[linenos,firstnumber=8]{rust} +#[derive(Debug)] +pub enum AlarmEvent { + MotionDetected(HAEntity), + MotionCleared(HAEntity), + AlarmStateChanged((HAEntity, AlarmState)), +} + +#[derive(Clone, PartialEq, Debug)] +pub enum AlarmState { + Disarmed, + Arming(Instant), + Armed(Instant), + Pending(Instant), + Triggered, +} + +#[derive(Clone, PartialEq)] +pub enum AlarmCommand { + Arm, + ArmInstantly, + Disarm, + ManualTrigger, + Untrigger, +} +\end{minted} +%TC:endignore +Az \mintinline{rust}/AlarmEvent/ a rendszer által kiváltott események +közzétételéért felel. Az \mintinline{rust}/AlarmState/ az állapotgép belső +állapotát képviseli. Végül, az \mintinline{rust}/AlarmCommand/ a felhasználó +számára elérhető műveletek variánsai (állapotátmenetek). A control flow pedig +nagyvonalakban így néz ki: + +\noindent\extlink{https://github.com/akosnad/rusty-esp-alarm/blob/1d3073a0cbd3b98af0f6a14bdf625732938f78e2/src/alarm.rs\#L42}{alarm.rs} +\begin{minted}{rust} +pub fn alarm_task( + queue: VecDeque<AlarmEvent>, + motion_sensors: Vec<AlarmMotionEntity>, + command_rx: Receiver<AlarmCommand>, + // ... +) -> ! { + let mut alarm_state = AlarmState::Disarmed; + + const ARMING_TIMEOUT: Duration = Duration::from_secs(90); + const PENDING_TIMEOUT: Duration = Duration::from_secs(30); + + loop { + let mut motion_detected = false; + for s in motion_sensors.iter_mut() { + let sensor_motion = s.pin_driver.is_high(); + if sensor_motion = s.motion { + continue; + } + + s.motion = motion; + if motion { + motion_detected = true; + queue.push_back(AlarmEvent::MotionDetected(s.entity)); + } + } + + let last_state = alarm_state.clone(); + + match command_rx.try_recv() { + Ok(AlarmCommand::Arm) => { + if alarm_state == AlarmState::Disarmed { + alarm_state = AlarmState::Arming(Instant::now()); + } + } + Ok(AlarmCommand::ArmInstantly) => { + if alarm_state == AlarmState::Disarmed { + alarm_state = AlarmState::Armed(Instant::now()); + } + } + Ok(AlarmCommand::Disarm) => { + alarm_state = AlarmState::Disarmed; + } + Ok(AlarmCommand::ManualTrigger) => { + if let AlarmState::Armed(_) = alarm_state { + alarm_state == AlarmState::Triggered; + } + } + Ok(AlarmCommand::Untrigger) => match alarm_state { + AlarmState::Triggered | AlarmState::Pending(_) => { + alarm_state = AlarmState::Armed(Instant::now()); + } + _ => {} + } + Err(e) => { + // ... + } + } + + match alarm_state { + AlarmState::Disarmed = >{} + AlarmState::Arming(start) => { + if start.elapsed() >= ARMING_TIMEOUT { + alarm_state = AlarmState::Armed(Instant::now()); + } + } + AlarmState::Armed(_) => { + if motion_detected { + alarm_state = AlarmState::Pending(Instant::now()); + } + } + AlarmState::Pending(start) => { + if start.elapsed() >= PENDING_TIMEOUT { + alarm_state = AlarmState::Triggered; + } + } + AlarmState::Triggered => { + siren_pin.set_low(); + } + } + + if last_state != alarm_state { + if last_state == AlarmState::Triggered { + siren_pin.set_high(); + } + + queue.push_back(AlarmEvent::AlarmStateChanged(( + alarm_entity.clone(), + alarm_state.clone(), + ))); + } + } +} +\end{minted} + +A kód teszteléséhez először a parancsokat egyszerű MQTT üzenetek segítségével +tudtam küldeni az központi egység felé. Egy hét tesztelés után úgy ítéltem meg, +hogy az állapotgép implementációja helyesen működött. \Aref{feladat}. fejezetben +megfogalmazott alapfeladatot ezennel a rendszer teljesíteni tudja -- kivéve +az okosotthon integrációt. \Aref{bizt-kerd}. fejezet szerinti behatolásjelzés +funkcióját betölti. Ebben az állapotban a rendszer még kényelmetlen volt +a rendes használatra, hiszen semmilyen kezelőfelületet nem létesítettem. A +következő fejezetben a Home Assistant integráció fejlesztését dokumentálom, ami +a feladat maradék részét meg fogja oldani. Ezután fogunk tudni valós tesztelést +végezni. \section{Integráció} +\paragraph{} \Aref{hass}. fejezet írása során megismerkedtem a Home Assistant +architektúrájával, és az MQTT integrációs lehetőségeiről. Eldöntöttem, hogy az +autodiscovery használatával fogom megvalósítani az integrációt. Ehhez először +a firmware-ben ki kellett alakítanom adatstruktúrákat, amik segítenek a Home +Assistant fogalmait absztrahálni. A forráskódban ezek a segédobjektumok a +\extlink{https://github.com/akosnad/rusty-esp-alarm/blob/1d3073a0cbd3b98af0f6a14bdf625732938f78e2/ha_types/src/lib.rs}{ha\_types/src/lib.rs} +fájlban találhatóak. + +Ezeket felhasználva készítettem el az MQTT csatornán a valós kommunikáció +logikáját. Az inicializáció a következőképpen néz ki: + +\noindent\extlink{https://github.com/akosnad/rusty-esp-alarm/blob/1d3073a0cbd3b98af0f6a14bdf625732938f78e2/src/scheduler.rs\#L113}{scheduler.rs} +\begin{minted}[linenos,firstnumber=113]{rust} +fn init_mqtt( + client: &mut EspMqttClient<'_, ConnState<MessageImpl, EspError>>, + entities: &[HAEntity], +) -> anyhow::Result<()> { + const AVAILABILITY_TOPIC: &str = env!("ESP_AVAILABILITY_TOPIC"); + const OTA_TOPIC: &str = env!("ESP_OTA_TOPIC"); + + // send entity config messages + for entity in entities.iter() { + let entity = HAEntity { + availability: Some(HADeviceAvailability { + payload_available: Some("online".to_string()), + payload_not_available: Some("offline".to_string()), + topic: AVAILABILITY_TOPIC.to_string(), + value_template: None, + }), + ..entity.clone() + }; + let topic = format!( + "{}/{}/{}/config", + "homeassistant", entity.variant, entity.unique_id + ); + let entity_out: HAEntityOut = entity.into(); + let payload = serde_json::to_string(&entity_out).unwrap(); + client.publish(&topic, QoS::AtLeastOnce, true, payload.as_bytes())?; + + if let Some(command_topic) = entity_out.command_topic { + client.subscribe(&command_topic, QoS::ExactlyOnce)?; + } + } + + // birth message + client.publish(AVAILABILITY_TOPIC, QoS::AtLeastOnce, true, b"online")?; + + // subscribe to ota + client.subscribe(OTA_TOPIC, QoS::ExactlyOnce)?; + + Ok(()) +} +\end{minted} + +Lényegében három dolog történik: +\begin{enumerate} + \item A riasztórendszer entitásainak konfigurációját egyesével legeneráljuk + (alarm panel és mozgásérzékelők) a segéd struktúrákkal, majd JSON formátumba konvertáljuk, + és azonnal el is küldjük a Home Assistant számára az autodiscovery protokoll szerint. + \item Az entitások elérhetőségét (availability) egyetlen közös topic-ra állítottuk az előző lépésben (hiszen mindegyik + a központi egység elérhetőségétől függ), így egyetlen üzenetet kiküldve ``online'' állapotba hozzuk. + \item Feliratkozunk a firmware frissítés (OTA) topic-jára -- melynek működését láttuk az előző fejezetben. +\end{enumerate} + +Ezzel még nem vagyunk kész, hiszen a szenzorok állapotát még nem küldjük és a +riasztópanel parancsait még nem fogadjuk. Ezek implementációját így oldottam +meg: + +\noindent\extlink{https://github.com/akosnad/rusty-esp-alarm/blob/1d3073a0cbd3b98af0f6a14bdf625732938f78e2/src/scheduler.rs\#L168}{scheduler.rs} +\begin{minted}[linenos,firstnumber=168]{rust} +fn send_alarm_state_change( + state: &AlarmState, + entity: &HAEntity, + client: &mut EspMqttClient<'_, ConnState<MessageImpl, EspError>>, +) -> anyhow::Result<()> { + let payload = match state { + AlarmState::Disarmed => "disarmed", + AlarmState::Arming(_) => "arming", + AlarmState::Armed(_) => "armed_away", + AlarmState::Pending(_) => "pending", + AlarmState::Triggered => "triggered", + }; + client.publish( + &entity.state_topic, + QoS::AtLeastOnce, + true, + payload.as_bytes(), + )?; + Ok(()) +} + +fn handle_alarm_command( + payload: &str, + alarm_command_tx: &Sender<AlarmCommand>, +) -> anyhow::Result<()> { + let command = match payload { + "ARM_AWAY" => AlarmCommand::Arm, + "ARM_CUSTOM_BYPASS" => AlarmCommand::ArmInstantly, + "DISARM" => AlarmCommand::Disarm, + "TRIGGER" => AlarmCommand::ManualTrigger, + "UNTRIGGER" => AlarmCommand::Untrigger, + _ => { + log::warn!("Unknown command: {}", payload); + return Ok(()); + } + }; + alarm_command_tx.send(command)?; + Ok(()) +} +\end{minted} + +Ezek a segédfüggvények teszik lehetővé az állapotgép és az MQTT csatornán +küldött/fogadott adatok közötti egyértelmű átjárhatóságot. Amikor az +állapotgép generál egy \mintinline{rust}/AlarmEvent/ eseményt, azt egy +\mintinline{rust}/VecDeque<AlarmEvent>/ típusú objektumba helyezi, melyhez az +MQTT kezelő szál is hozzáfér. Ez egy double-ended queue, amit egy sima FIFO-ként +használok az események feldolgozásához. Ugyanilyen adatszerkezetet használok +az ellenkező irányban is -- a parancsok fogadására. Mindkét queue esetén a +konkurrens hozzáférést egy \mintinline{rust}/Mutex<>/ biztosítja. + +\section{Tesztelés, validálás} + +\paragraph{} Az implementáció készen van, az alap feladatot megvalósítottam +(\ref{feladat}. fejezet). Ahhoz, hogy megbizonyosodjunk a rendszer szoftverének +helyes működéséről és biztonságtechnikai helyességéről, tesztelés és validálásra +van szükség. + +Miután lefrissítettem a firmware-t a kész verzióra a próbapanelen, a Home +Assistant felületét és az eszköz log folyamát figyeltem. 5 újraindítás +keretében gyűjtöttem statisztikákat. A reset gomb elengedése pillanatától a +Home Assistantban megjelenő entitások online állapotára kerüléséig eltelt idő +átlagosan 3.8 másodperc volt. A legrosszabb minta: 6.2 másodperc, a legjobb: +3 másodperc volt. Ezt kimagasló eredménynek tartom, hiszen bekapcsolás után +szinte azonnal használható lesz a rendszer. \Aref{kereskedelmi-megbizhatosag}. +fejezetben láttuk a kereskedelmi rendszerek használhatóságát, kifejezetten +a DIY rendszerek onboarding élményét. A látottak alapján a saját rendszerem +első indításra -- miután a rendszer telepítve lett -- az Ethernet adapter +csatlakoztatásával szinte azonnal láthatóvá válik és használható lesz a Home +Assistant felületén. Jó kompromisszumnak tartom a megoldásomat. A hagyományos +rendszerek telepítési igényeit ellensúlyozza a szinte erőfeszítés nélküli +onboarding élmény. Hozzá kell tennem, hogy a mérés körülményei ideálisak voltak: +a hálózat két eszközből állt (ESP és laptop), így a zavaró tényezők minimálisak +voltak. Az onboarding élmény összehasonlítását más rendszerekkel a dolgozat +keretében nem teszem, de az üzem alatti tapasztalatokat tudom mérni. Ahhoz hogy +lássuk az eszköz valós környezetbeli teljesítményét, beszereltem az otthonomban +hosszútávú tesztelésre. A dolgozat írásakor a rendszer nagyjából 8 hónapja +üzemben van. Ez idő alatt jegyeztem a felmerülő hibákat, észrevételeket: + +\begin{itemize} + \item Az első 2 hónap alatt ha elég sokáig futott a firmware (nagyjából + 4-5 nap hossza), hirtelen nem reagált semmire. A hiba egy deadlock + szituációból adódott, melyet kijavítottam. A hiba bekövetkezése és az eltelt idő látszólagos + korrelációja valójában hamis volt, mert a deadlock bekövetkezéséhez + két eseménynek kellett egyszerre történnie: a riasztó élesedése és + egy mozgásérzékelő jelet észlelése. Javítás után a hiba nem jelentkezett többször. + \item Az MQTT broker leállása és újraindulása után a firmware nem csatlakozott + újra, így használhatatlan maradt a manuális újraindításig. Ez azért volt, + mert csak az Ethernet link megszakadását kezeltem le. Ha a link + sértetlen maradt, és csak a socket szakadt meg, azt figyelmen kívül + hagyta az implementáció. Javítás után teszteltem a hibatűrést. + A brokert újraindítottam és az Ethernet linket leválasztottam véletlenszerű + időpontokban egy héten keresztül. Minden alkalommal, akár több nap után + is képes volt visszacsatlakozni az eszköz. A hibát kijavítottnak tekintettem. +\end{itemize} + +Összesítve a hibák sűrűsége alacsony volt, de azok kritikusak. Itt látszik jól +a Rust erőssége és az aranymondás róla igazolódik: \textsl{``Ha a kód lefordul, + akkor biztos lehetsz benne, hogy az úgy működni fog.''} \cite{rust-compiles-1} +\cite{rust-compiles-2} \cite{rust-compiles-3} Bizonyos keretekig ez az állítás +igaz; a fordító garantálja azt a működést amit enged, hogy leforduljon. Azt, +hogy a programozó logikai hibát vétett, azt már nem tudja kijavítani helyette. + \clearpage % Ez azért kell, hogy nehogy képek átcsússzanak a következő fejezethez diff --git a/src/contents/melleklet.tex b/src/contents/melleklet.tex index 1b1d08e..405fa13 100644 --- a/src/contents/melleklet.tex +++ b/src/contents/melleklet.tex @@ -26,7 +26,7 @@ nélküli leütések számát jelenti. \url{http://mirrors.ctan.org/support/texcount/doc/TeXcount.pdf}} { - \small + \footnotesize \verbatiminput{./build/charcount.tex} } diff --git a/src/hivatkozasok.bib b/src/hivatkozasok.bib index 94efd7b..62ce919 100644 --- a/src/hivatkozasok.bib +++ b/src/hivatkozasok.bib @@ -656,3 +656,65 @@ urldate = {2025-04-22}, url = {https://www.home-assistant.io/integrations/mqtt/}, urldate = {2025-04-26}, } + +@manual{ip101, + title = {Single Port 10/100 MII/RMII/TP/Fiber Fast Ethernet Transciever - + IP101G Data Sheet}, + organization = {IC Plus Corp.}, + url = { + https://www.lcsc.com/datasheet/lcsc_datasheet_2008201637_IC-Plus-IP101GRI_C703537.pdf + }, + urldate = {2025-04-26}, +} + +@online{ip101-example, + title = {ESP32-Ethernet-Kit V1.2 Getting Started Guide}, + url = { + https://docs.espressif.com/projects/esp-idf/en/v4.4/esp32/hw-reference/esp32/get-started-ethernet-kit.html + }, + urldate = {2025-04-26}, +} + +@manual{w5500-datasheet, + title = {W5500 Datasheet - Version 1.1.0}, + url = {https://docs.wiznet.io/img/products/w5500/W5500_ds_v110e.pdf}, + urldate = {2025-04-26}, +} + +@online{esp-idf-template, + title = {Rust on ESP-IDF "Hello, World" template}, + url = {https://github.com/esp-rs/esp-idf-template}, + urldate = {2025-04-27}, +} + +@online{cargo, + title = {The Cargo Book}, + url = {https://doc.rust-lang.org/cargo/}, + urldate = {2025-04-27}, +} + +@online{rust-compiles-1, + title = {Opinion: Rust code typically works once compiled, why?}, + organization = {Rust users forum}, + url = { + https://users.rust-lang.org/t/opinion-rust-code-typically-works-once-compiled-why/95126 + }, + urldate = {2025-04-27}, +} + +@online{rust-compiles-2, + title = {Am I safe, if Rust program has successfully compiled?}, + organization = {Reddit}, + url = { + https://www.reddit.com/r/rust/comments/113bm7a/am_i_safe_if_rust_program_has_successfully/ + }, + urldate = {2025-04-27}, +} + +@online{rust-compiles-3, + title = {It's true that Rust approaches the "if it compiles, it works" + property that Hask...}, + organization = {Hacker News}, + url = {https://news.ycombinator.com/item?id=8392945}, + urldate = {2025-04-27}, +} diff --git a/src/images/statemachine.drawio.svg b/src/images/statemachine.drawio.svg new file mode 100644 index 0000000..87975d0 --- /dev/null +++ b/src/images/statemachine.drawio.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Do not edit this file with editors other than draw.io --> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" style="background: transparent; background-color: transparent; color-scheme: light dark;" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="439px" height="429px" viewBox="-0.5 -0.5 439 429" content="<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0" version="26.2.14"> <diagram name="Page-1" id="S62LkRPGwtLC0qFBxN4L"> <mxGraphModel dx="596" dy="967" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0"> <root> <mxCell id="0" /> <mxCell id="1" parent="0" /> <mxCell id="33Sh5yp_UrR39phBk1Qc-3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="33Sh5yp_UrR39phBk1Qc-1" target="33Sh5yp_UrR39phBk1Qc-2"> <mxGeometry relative="1" as="geometry" /> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-10" value="Arm" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="33Sh5yp_UrR39phBk1Qc-3"> <mxGeometry x="0.2007" y="4" relative="1" as="geometry"> <mxPoint x="-24" y="14" as="offset" /> </mxGeometry> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0.25;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="33Sh5yp_UrR39phBk1Qc-1" target="33Sh5yp_UrR39phBk1Qc-4"> <mxGeometry relative="1" as="geometry"> <Array as="points"> <mxPoint x="330" y="790" /> <mxPoint x="470" y="790" /> </Array> </mxGeometry> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-21" value="ArmInstantly" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="33Sh5yp_UrR39phBk1Qc-20"> <mxGeometry x="0.1064" y="2" relative="1" as="geometry"> <mxPoint y="-17" as="offset" /> </mxGeometry> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-1" value="Disarmed" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> <mxGeometry x="300" y="560" width="120" height="60" as="geometry" /> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1"> <mxGeometry relative="1" as="geometry"> <mxPoint x="530" y="740" as="sourcePoint" /> <mxPoint x="530" y="860" as="targetPoint" /> </mxGeometry> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-12" value="\textit{90s}" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="33Sh5yp_UrR39phBk1Qc-5"> <mxGeometry x="-0.0241" y="-1" relative="1" as="geometry"> <mxPoint x="21" y="2" as="offset" /> </mxGeometry> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1"> <mxGeometry relative="1" as="geometry"> <mxPoint x="330" y="500" as="sourcePoint" /> <mxPoint x="330" y="560" as="targetPoint" /> </mxGeometry> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-25" value="Disarm" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="33Sh5yp_UrR39phBk1Qc-24"> <mxGeometry x="-0.2254" y="-3" relative="1" as="geometry"> <mxPoint x="23" y="-7" as="offset" /> </mxGeometry> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-2" value="Arming" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> <mxGeometry x="440" y="680" width="120" height="60" as="geometry" /> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-7" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="33Sh5yp_UrR39phBk1Qc-4" target="33Sh5yp_UrR39phBk1Qc-6"> <mxGeometry relative="1" as="geometry" /> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-14" value="\textit{motion\_detected}" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="33Sh5yp_UrR39phBk1Qc-7"> <mxGeometry x="-0.0236" y="-1" relative="1" as="geometry"> <mxPoint y="-9" as="offset" /> </mxGeometry> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-4" value="Armed" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> <mxGeometry x="440" y="860" width="120" height="60" as="geometry" /> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-9" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1"> <mxGeometry relative="1" as="geometry"> <mxPoint x="250" y="860" as="sourcePoint" /> <mxPoint x="250" y="740" as="targetPoint" /> </mxGeometry> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-13" value="\textit{30s}" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="33Sh5yp_UrR39phBk1Qc-9"> <mxGeometry x="0.1769" y="-2" relative="1" as="geometry"> <mxPoint x="8" y="11" as="offset" /> </mxGeometry> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-6" value="Pending" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> <mxGeometry x="160" y="860" width="120" height="60" as="geometry" /> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-8" value="Triggered" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> <mxGeometry x="160" y="680" width="120" height="60" as="geometry" /> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-26" value="ManualTrigger" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.159;entryY=-0.036;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1"> <mxGeometry y="40" relative="1" as="geometry"> <mxPoint x="180" y="620" as="sourcePoint" /> <mxPoint x="180.07999999999998" y="680" as="targetPoint" /> <Array as="points"> <mxPoint x="180" y="662.16" /> <mxPoint x="180" y="662.16" /> </Array> <mxPoint as="offset" /> </mxGeometry> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-28" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1"> <mxGeometry relative="1" as="geometry"> <mxPoint x="190" y="740" as="sourcePoint" /> <mxPoint x="190" y="860" as="targetPoint" /> </mxGeometry> </mxCell> <mxCell id="33Sh5yp_UrR39phBk1Qc-29" value="Untrigger" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="33Sh5yp_UrR39phBk1Qc-28"> <mxGeometry x="0.1769" y="-2" relative="1" as="geometry"> <mxPoint x="-28" y="-10" as="offset" /> </mxGeometry> </mxCell> </root> </mxGraphModel> </diagram> </mxfile> "><defs/><g><g data-cell-id="0"><g data-cell-id="1"><g data-cell-id="33Sh5yp_UrR39phBk1Qc-3"><g><path d="M 282 97 L 362 97 L 362 180.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/><path d="M 362 185.88 L 358.5 178.88 L 362 180.63 L 365.5 178.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(0, 0, 0), rgb(255, 255, 255)); stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-10"><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 133px; margin-left: 342px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">Arm</div></div></div></foreignObject><text x="342" y="136" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">Arm</text></switch></g></g></g></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-20"><g><path d="M 192 127 L 192 297 L 332 297 L 332 360.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/><path d="M 332 365.88 L 328.5 358.88 L 332 360.63 L 335.5 358.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(0, 0, 0), rgb(255, 255, 255)); stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-21"><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 278px; margin-left: 232px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">ArmInstantly</div></div></div></foreignObject><text x="232" y="282" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">ArmInstantly</text></switch></g></g></g></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-1"><g><rect x="162" y="67" width="120" height="60" fill="#ffffff" stroke="#000000" pointer-events="all" style="fill: light-dark(#ffffff, var(--ge-dark-color, #121212)); stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 97px; margin-left: 163px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Disarmed</div></div></div></foreignObject><text x="222" y="101" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="12px" text-anchor="middle">Disarmed</text></switch></g></g></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-5"><g><path d="M 392 247 L 392 360.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/><path d="M 392 365.88 L 388.5 358.88 L 392 360.63 L 395.5 358.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(0, 0, 0), rgb(255, 255, 255)); stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-12"><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 308px; margin-left: 412px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">\textit{90s}</div></div></div></foreignObject><text x="412" y="311" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">\textit{90s}</text></switch></g></g></g></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-24"><g><path d="M 192 7 L 192 60.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/><path d="M 192 65.88 L 188.5 58.88 L 192 60.63 L 195.5 58.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(0, 0, 0), rgb(255, 255, 255)); stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-25"><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 24px; margin-left: 212px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">Disarm</div></div></div></foreignObject><text x="212" y="27" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">Disarm</text></switch></g></g></g></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-2"><g><rect x="302" y="187" width="120" height="60" fill="#ffffff" stroke="#000000" pointer-events="all" style="fill: light-dark(#ffffff, var(--ge-dark-color, #121212)); stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 217px; margin-left: 303px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Arming</div></div></div></foreignObject><text x="362" y="221" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="12px" text-anchor="middle">Arming</text></switch></g></g></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-7"><g><path d="M 302 397 L 148.37 397" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/><path d="M 143.12 397 L 150.12 393.5 L 148.37 397 L 150.12 400.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(0, 0, 0), rgb(255, 255, 255)); stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-14"><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 387px; margin-left: 224px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">\textit{motion\_detected}</div></div></div></foreignObject><text x="224" y="391" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">\textit{motion\_detected}</text></switch></g></g></g></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-4"><g><rect x="302" y="367" width="120" height="60" fill="#ffffff" stroke="#000000" pointer-events="all" style="fill: light-dark(#ffffff, var(--ge-dark-color, #121212)); stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 397px; margin-left: 303px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Armed</div></div></div></foreignObject><text x="362" y="401" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="12px" text-anchor="middle">Armed</text></switch></g></g></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-9"><g><path d="M 112 367 L 112 253.37" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/><path d="M 112 248.12 L 115.5 255.12 L 112 253.37 L 108.5 255.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(0, 0, 0), rgb(255, 255, 255)); stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-13"><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 308px; margin-left: 122px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">\textit{30s}</div></div></div></foreignObject><text x="122" y="311" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">\textit{30s}</text></switch></g></g></g></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-6"><g><rect x="22" y="367" width="120" height="60" fill="#ffffff" stroke="#000000" pointer-events="all" style="fill: light-dark(#ffffff, var(--ge-dark-color, #121212)); stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 397px; margin-left: 23px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Pending</div></div></div></foreignObject><text x="82" y="401" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="12px" text-anchor="middle">Pending</text></switch></g></g></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-8"><g><rect x="22" y="187" width="120" height="60" fill="#ffffff" stroke="#000000" pointer-events="all" style="fill: light-dark(#ffffff, var(--ge-dark-color, #121212)); stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 217px; margin-left: 23px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Triggered</div></div></div></foreignObject><text x="82" y="221" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="12px" text-anchor="middle">Triggered</text></switch></g></g></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-26"><g><path d="M 42 127 L 42 169.17 L 42.05 180.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/><path d="M 42.07 185.88 L 38.54 178.9 L 42.05 180.63 L 45.54 178.87 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(0, 0, 0), rgb(255, 255, 255)); stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 157px; margin-left: 82px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">ManualTrigger</div></div></div></foreignObject><text x="82" y="160" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">ManualTrigger</text></switch></g></g></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-28"><g><path d="M 52 247 L 52 360.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/><path d="M 52 365.88 L 48.5 358.88 L 52 360.63 L 55.5 358.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(0, 0, 0), rgb(255, 255, 255)); stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g><g data-cell-id="33Sh5yp_UrR39phBk1Qc-29"><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 308px; margin-left: 22px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">Untrigger</div></div></div></foreignObject><text x="22" y="312" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">Untrigger</text></switch></g></g></g></g></g></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.drawio.com/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Text is not SVG - cannot display</text></a></switch></svg> \ No newline at end of file diff --git a/src/szakdolgozat.tex b/src/szakdolgozat.tex index 785a6cd..2802d18 100644 --- a/src/szakdolgozat.tex +++ b/src/szakdolgozat.tex @@ -121,7 +121,7 @@ % Kódrészletek \usepackage{listings} -\usepackage{sourcecodepro} % egy jó betűtípus +% \usepackage{sourcecodepro} % egy jó betűtípus \lstset{captionpos=b, numberbychapter=false, basicstyle=\ttfamily, showstringspaces=false, columns=fullflexible} % Kódrészletek magyar stílusú számozása @@ -244,6 +244,12 @@ \usepackage{moreverb} \immediate\write18{texcount -char -tex -merge -sum ./\jobname.tex > ./build/charcount.tex} +% syntax highlighthoz +\usepackage{minted} +\setminted{style=colorful,fontsize=\footnotesize,breaklines=true} + +%TC:envir minted [] other + % ------------- Dokumentum legenerálása ----------------- \begin{document} -- GitLab