Skip to content
Snippets Groups Projects
Verified Commit ec186fec authored by Nádudvari Ákos's avatar Nádudvari Ákos
Browse files

feat: eredmények init

parent d83f095c
Branches
No related tags found
No related merge requests found
Pipeline #2712 passed
...@@ -29,7 +29,8 @@ in ...@@ -29,7 +29,8 @@ in
bookmark url csquotes listings listings-ext sourcecodepro silence bookmark url csquotes listings listings-ext sourcecodepro silence
biblatex-ieee ly1 metafont transparent catchfile microtype biblatex-ieee ly1 metafont transparent catchfile microtype
l3kernel l3packages texcount moreverb pdfpages pdflscape l3kernel l3packages texcount moreverb pdfpages pdflscape
tabularray ninecolors; tabularray ninecolors minted fvextra latex2pydata
newfloat pdftexcmds pgfkeyx pgfopts upquote lineno;
}); });
}; };
document.font = mkOption { document.font = mkOption {
...@@ -63,7 +64,8 @@ in ...@@ -63,7 +64,8 @@ in
packages.document = pkgs.stdenvNoCC.mkDerivation { packages.document = pkgs.stdenvNoCC.mkDerivation {
name = "latex-document"; name = "latex-document";
src = config.document.source; 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" ]; phases = [ "unpackPhase" "buildPhase" "installPhase" ];
buildPhase = '' buildPhase = ''
set -o errexit set -o errexit
...@@ -92,7 +94,7 @@ in ...@@ -92,7 +94,7 @@ in
let let
doc-watcher = pkgs.writeShellApplication { doc-watcher = pkgs.writeShellApplication {
name = "doc-watcher"; name = "doc-watcher";
runtimeInputs = with pkgs; [ watchexec coreutils inkscape ] ++ [ config.document.texlive ]; runtimeInputs = with pkgs; [ watchexec coreutils inkscape latexminted ] ++ [ config.document.texlive ];
text = '' text = ''
export OSFONTDIR="${config.document.font}/share/fonts" export OSFONTDIR="${config.document.font}/share/fonts"
watchexec -r --print-events -- \ watchexec -r --print-events -- \
......
...@@ -425,6 +425,7 @@ stabilitását és biztonságát a modern DIY rendszerek nyitottságával és ...@@ -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. rugalmasságával igyekszik ötvözni.
\section{Biztonságtechnikai kérdések} \section{Biztonságtechnikai kérdések}
\label{bizt-kerd}
\paragraph{} Egy riasztórendszer megalkotásához elengedhetetlen a \paragraph{} Egy riasztórendszer megalkotásához elengedhetetlen a
tervezőnek ismermie a biztonságtechnika alapjait, illetve tisztában lennie a tervezőnek ismermie a biztonságtechnika alapjait, illetve tisztában lennie a
......
...@@ -182,6 +182,7 @@ célhardverekhez tartom jobban igazodónak, nem magas szintű IoT-barát ...@@ -182,6 +182,7 @@ célhardverekhez tartom jobban igazodónak, nem magas szintű IoT-barát
megoldásoknak. megoldásoknak.
\subsection{Rust nyelv és környezet} \subsection{Rust nyelv és környezet}
\label{rust-env}
\paragraph{} A Rust nyelv ötlete egy Mozilla-nál dolgozó fejlesztő fejéből \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. 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 ...@@ -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. framework; előre tervezés szempontjából hasznos tudásnak tartom.
\subsection{Home Assistant okosotthon} \subsection{Home Assistant okosotthon}
\label{hass}
\paragraph{} A Home Assistant egy nyílt platform, otthon-automatizációs \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 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; ...@@ -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 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 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 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, kapu vezérlőt, ahol az entitások a következők: a kapu két szárnyát egyszerre
csak az egyik szárnyat nyitó gomb, és akár egy állapot visszajelző szenzor. 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), 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 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. ...@@ -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 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 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ő interfészt ad. Ez kimagaslóan jobb élményt ad a végfelhasználó számára,
számára is. MQTT-n keresztül lehetőség van entitások és eszközök telepítésére mert egységes és konzisztens marad a frontend. A fejlesztő számára is jobb
manuális konfiguráció vagy az úgynevezett ``MQTT-discovery'' segítségével. élmény, mert az intgrációt így nem szükséges a Home Assistant forráskódján át
Mindkét esetben az eszközöknek és entitásoknak van egy deklaratív leíró sémája, implementálni Pythonban. MQTT-n keresztül lehetőség van entitások és eszközök
ami tartalmaz meta-adatokat az azok működtetésére. Manuális konfiguráció esetén telepítésére manuális konfigurációval vagy az úgynevezett ``MQTT-discovery''
ezt a Home Assistantban YAML formátumban kell megadni, autodiscovery során segítségével. Mindkét esetben az eszközöknek és entitásoknak van egy deklaratív
pedig egy előre meghatározott topic-on kell küldeni JSON formátumban a rendszer leíró sémája, ami tartalmaz meta-adatokat és leírást azok működtetésére.
felé. Látható, hogy az utóbbi esetben az azt támogató eszköz végfelhasználói Manuális konfiguráció esetén ezt a Home Assistantban YAML formátumban kell
onbarding élménye sokkal kényelmeseb. Például, az eszköz első indítása után megadni, autodiscovery során pedig egy előre meghatározott MQTT topic-on kell
az autodiscovery üzenet küldésével a Home Assistant frontend felületén azonnal küldeni JSON formátumban a rendszer felé. Látható, hogy az utóbbi esetben
látni fog a felhasználó egy jóváhagyandó üzenetet, hogy az készen áll a az azt támogató eszköz végfelhasználói onbarding élménye sokkal kényelmeseb.
használatra. Jóváhagyás után a meghirdetett entitások importálásra kerülnek a Például, az eszköz első indítása után az autodiscovery üzenet küldésével a Home
Home Assistantba. \cite{hass-mqtt} Célom úgy megvalósítani a saját rendszert, Assistant frontend felületén azonnal látni fog a felhasználó egy jóváhagyandó
hogy az alapból támogassa az autodiscovery-t. Így \aref{kereskedelmi}. üzenetet, hogy az készen áll a használatra. Jóváhagyás után a meghirdetett
fejezetben megismert DIY rendszereknél is potenciálisan jobb élményt tudna entitások importálásra kerülnek a Home Assistantba. \cite{hass-mqtt} Célom úgy
nyújtani, hiszen a kommunikáció vezetékes médiumon történne. Ennek realitását 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. szintén a gyakorlatban fogjuk tudni megállapítani.
\clearpage % Ez azért kell, hogy nehogy képek átcsússzanak a következő fejezethez \clearpage % Ez azért kell, hogy nehogy képek átcsússzanak a következő fejezethez
...@@ -16,6 +16,59 @@ ...@@ -16,6 +16,59 @@
} }
\section{Hardver} \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!] \begin{figure}[htbp!]
\ttfamily \ttfamily
...@@ -26,7 +79,510 @@ ...@@ -26,7 +79,510 @@
\end{figure} \end{figure}
\section{Firmware} \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ó} \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 \clearpage % Ez azért kell, hogy nehogy képek átcsússzanak a következő fejezethez
...@@ -26,7 +26,7 @@ nélküli leütések számát jelenti. ...@@ -26,7 +26,7 @@ nélküli leütések számát jelenti.
\url{http://mirrors.ctan.org/support/texcount/doc/TeXcount.pdf}} \url{http://mirrors.ctan.org/support/texcount/doc/TeXcount.pdf}}
{ {
\small \footnotesize
\verbatiminput{./build/charcount.tex} \verbatiminput{./build/charcount.tex}
} }
......
...@@ -656,3 +656,65 @@ urldate = {2025-04-22}, ...@@ -656,3 +656,65 @@ urldate = {2025-04-22},
url = {https://www.home-assistant.io/integrations/mqtt/}, url = {https://www.home-assistant.io/integrations/mqtt/},
urldate = {2025-04-26}, 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},
}
<?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="&lt;mxfile host=&quot;app.diagrams.net&quot; agent=&quot;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&quot; version=&quot;26.2.14&quot;&gt;&#10; &lt;diagram name=&quot;Page-1&quot; id=&quot;S62LkRPGwtLC0qFBxN4L&quot;&gt;&#10; &lt;mxGraphModel dx=&quot;596&quot; dy=&quot;967&quot; grid=&quot;1&quot; gridSize=&quot;10&quot; guides=&quot;1&quot; tooltips=&quot;1&quot; connect=&quot;1&quot; arrows=&quot;1&quot; fold=&quot;1&quot; page=&quot;1&quot; pageScale=&quot;1&quot; pageWidth=&quot;850&quot; pageHeight=&quot;1100&quot; math=&quot;0&quot; shadow=&quot;0&quot;&gt;&#10; &lt;root&gt;&#10; &lt;mxCell id=&quot;0&quot; /&gt;&#10; &lt;mxCell id=&quot;1&quot; parent=&quot;0&quot; /&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-3&quot; value=&quot;&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;33Sh5yp_UrR39phBk1Qc-1&quot; target=&quot;33Sh5yp_UrR39phBk1Qc-2&quot;&gt;&#10; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-10&quot; value=&quot;Arm&quot; style=&quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];&quot; vertex=&quot;1&quot; connectable=&quot;0&quot; parent=&quot;33Sh5yp_UrR39phBk1Qc-3&quot;&gt;&#10; &lt;mxGeometry x=&quot;0.2007&quot; y=&quot;4&quot; relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;mxPoint x=&quot;-24&quot; y=&quot;14&quot; as=&quot;offset&quot; /&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-20&quot; style=&quot;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;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;33Sh5yp_UrR39phBk1Qc-1&quot; target=&quot;33Sh5yp_UrR39phBk1Qc-4&quot;&gt;&#10; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;Array as=&quot;points&quot;&gt;&#10; &lt;mxPoint x=&quot;330&quot; y=&quot;790&quot; /&gt;&#10; &lt;mxPoint x=&quot;470&quot; y=&quot;790&quot; /&gt;&#10; &lt;/Array&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-21&quot; value=&quot;ArmInstantly&quot; style=&quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];&quot; vertex=&quot;1&quot; connectable=&quot;0&quot; parent=&quot;33Sh5yp_UrR39phBk1Qc-20&quot;&gt;&#10; &lt;mxGeometry x=&quot;0.1064&quot; y=&quot;2&quot; relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;mxPoint y=&quot;-17&quot; as=&quot;offset&quot; /&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-1&quot; value=&quot;Disarmed&quot; style=&quot;rounded=0;whiteSpace=wrap;html=1;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&#10; &lt;mxGeometry x=&quot;300&quot; y=&quot;560&quot; width=&quot;120&quot; height=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-5&quot; value=&quot;&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;&quot; edge=&quot;1&quot; parent=&quot;1&quot;&gt;&#10; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;mxPoint x=&quot;530&quot; y=&quot;740&quot; as=&quot;sourcePoint&quot; /&gt;&#10; &lt;mxPoint x=&quot;530&quot; y=&quot;860&quot; as=&quot;targetPoint&quot; /&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-12&quot; value=&quot;\textit{90s}&quot; style=&quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];&quot; vertex=&quot;1&quot; connectable=&quot;0&quot; parent=&quot;33Sh5yp_UrR39phBk1Qc-5&quot;&gt;&#10; &lt;mxGeometry x=&quot;-0.0241&quot; y=&quot;-1&quot; relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;mxPoint x=&quot;21&quot; y=&quot;2&quot; as=&quot;offset&quot; /&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-24&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot;&gt;&#10; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;mxPoint x=&quot;330&quot; y=&quot;500&quot; as=&quot;sourcePoint&quot; /&gt;&#10; &lt;mxPoint x=&quot;330&quot; y=&quot;560&quot; as=&quot;targetPoint&quot; /&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-25&quot; value=&quot;Disarm&quot; style=&quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];&quot; vertex=&quot;1&quot; connectable=&quot;0&quot; parent=&quot;33Sh5yp_UrR39phBk1Qc-24&quot;&gt;&#10; &lt;mxGeometry x=&quot;-0.2254&quot; y=&quot;-3&quot; relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;mxPoint x=&quot;23&quot; y=&quot;-7&quot; as=&quot;offset&quot; /&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-2&quot; value=&quot;Arming&quot; style=&quot;rounded=0;whiteSpace=wrap;html=1;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&#10; &lt;mxGeometry x=&quot;440&quot; y=&quot;680&quot; width=&quot;120&quot; height=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-7&quot; value=&quot;&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;33Sh5yp_UrR39phBk1Qc-4&quot; target=&quot;33Sh5yp_UrR39phBk1Qc-6&quot;&gt;&#10; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-14&quot; value=&quot;\textit{motion\_detected}&quot; style=&quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];&quot; vertex=&quot;1&quot; connectable=&quot;0&quot; parent=&quot;33Sh5yp_UrR39phBk1Qc-7&quot;&gt;&#10; &lt;mxGeometry x=&quot;-0.0236&quot; y=&quot;-1&quot; relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;mxPoint y=&quot;-9&quot; as=&quot;offset&quot; /&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-4&quot; value=&quot;Armed&quot; style=&quot;rounded=0;whiteSpace=wrap;html=1;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&#10; &lt;mxGeometry x=&quot;440&quot; y=&quot;860&quot; width=&quot;120&quot; height=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-9&quot; value=&quot;&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;&quot; edge=&quot;1&quot; parent=&quot;1&quot;&gt;&#10; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;mxPoint x=&quot;250&quot; y=&quot;860&quot; as=&quot;sourcePoint&quot; /&gt;&#10; &lt;mxPoint x=&quot;250&quot; y=&quot;740&quot; as=&quot;targetPoint&quot; /&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-13&quot; value=&quot;\textit{30s}&quot; style=&quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];&quot; vertex=&quot;1&quot; connectable=&quot;0&quot; parent=&quot;33Sh5yp_UrR39phBk1Qc-9&quot;&gt;&#10; &lt;mxGeometry x=&quot;0.1769&quot; y=&quot;-2&quot; relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;mxPoint x=&quot;8&quot; y=&quot;11&quot; as=&quot;offset&quot; /&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-6&quot; value=&quot;Pending&quot; style=&quot;rounded=0;whiteSpace=wrap;html=1;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&#10; &lt;mxGeometry x=&quot;160&quot; y=&quot;860&quot; width=&quot;120&quot; height=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-8&quot; value=&quot;Triggered&quot; style=&quot;rounded=0;whiteSpace=wrap;html=1;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&#10; &lt;mxGeometry x=&quot;160&quot; y=&quot;680&quot; width=&quot;120&quot; height=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-26&quot; value=&quot;ManualTrigger&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.159;entryY=-0.036;entryDx=0;entryDy=0;entryPerimeter=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot;&gt;&#10; &lt;mxGeometry y=&quot;40&quot; relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;mxPoint x=&quot;180&quot; y=&quot;620&quot; as=&quot;sourcePoint&quot; /&gt;&#10; &lt;mxPoint x=&quot;180.07999999999998&quot; y=&quot;680&quot; as=&quot;targetPoint&quot; /&gt;&#10; &lt;Array as=&quot;points&quot;&gt;&#10; &lt;mxPoint x=&quot;180&quot; y=&quot;662.16&quot; /&gt;&#10; &lt;mxPoint x=&quot;180&quot; y=&quot;662.16&quot; /&gt;&#10; &lt;/Array&gt;&#10; &lt;mxPoint as=&quot;offset&quot; /&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-28&quot; value=&quot;&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;&quot; edge=&quot;1&quot; parent=&quot;1&quot;&gt;&#10; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;mxPoint x=&quot;190&quot; y=&quot;740&quot; as=&quot;sourcePoint&quot; /&gt;&#10; &lt;mxPoint x=&quot;190&quot; y=&quot;860&quot; as=&quot;targetPoint&quot; /&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;33Sh5yp_UrR39phBk1Qc-29&quot; value=&quot;Untrigger&quot; style=&quot;edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];&quot; vertex=&quot;1&quot; connectable=&quot;0&quot; parent=&quot;33Sh5yp_UrR39phBk1Qc-28&quot;&gt;&#10; &lt;mxGeometry x=&quot;0.1769&quot; y=&quot;-2&quot; relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&#10; &lt;mxPoint x=&quot;-28&quot; y=&quot;-10&quot; as=&quot;offset&quot; /&gt;&#10; &lt;/mxGeometry&gt;&#10; &lt;/mxCell&gt;&#10; &lt;/root&gt;&#10; &lt;/mxGraphModel&gt;&#10; &lt;/diagram&gt;&#10;&lt;/mxfile&gt;&#10;"><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: &quot;Helvetica&quot;; 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="&quot;Helvetica&quot;" 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: &quot;Helvetica&quot;; 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="&quot;Helvetica&quot;" 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: &quot;Helvetica&quot;; 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="&quot;Helvetica&quot;" 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: &quot;Helvetica&quot;; 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="&quot;Helvetica&quot;" 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: &quot;Helvetica&quot;; 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="&quot;Helvetica&quot;" 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: &quot;Helvetica&quot;; 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="&quot;Helvetica&quot;" 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: &quot;Helvetica&quot;; 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="&quot;Helvetica&quot;" 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: &quot;Helvetica&quot;; 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="&quot;Helvetica&quot;" 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: &quot;Helvetica&quot;; 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="&quot;Helvetica&quot;" 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: &quot;Helvetica&quot;; 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="&quot;Helvetica&quot;" 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: &quot;Helvetica&quot;; 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="&quot;Helvetica&quot;" 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: &quot;Helvetica&quot;; 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="&quot;Helvetica&quot;" 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: &quot;Helvetica&quot;; 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="&quot;Helvetica&quot;" 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
...@@ -121,7 +121,7 @@ ...@@ -121,7 +121,7 @@
% Kódrészletek % Kódrészletek
\usepackage{listings} \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} \lstset{captionpos=b, numberbychapter=false, basicstyle=\ttfamily, showstringspaces=false, columns=fullflexible}
% Kódrészletek magyar stílusú számozása % Kódrészletek magyar stílusú számozása
...@@ -244,6 +244,12 @@ ...@@ -244,6 +244,12 @@
\usepackage{moreverb} \usepackage{moreverb}
\immediate\write18{texcount -char -tex -merge -sum ./\jobname.tex > ./build/charcount.tex} \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 ----------------- % ------------- Dokumentum legenerálása -----------------
\begin{document} \begin{document}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment