<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Тестовий сайт</title>
    <description>Тіпа, блоґ.</description>
    <link>https://test.de.co.ua</link>
    <atom:link href="https://test.de.co.ua/rss.xml" rel="self" type="application/rss+xml" />
    <copyright>© dk487</copyright>
    <lastBuildDate>Sun, 29 Mar 2026 17:56:07 +0300</lastBuildDate>
    <pubDate>Sun, 29 Mar 2026 17:56:07 +0300</pubDate>
    <ttl>21600</ttl>

    <item>
      <title>RTSP та WebRTC</title>
      <description><![CDATA[

<p>У мене вже був пост <a href="/2025/10/20/rtmp.html">про RTMP</a>, тепер буде про RTSP.</p>

<p>Це я відкрив для себе офігезний проект <a href="https://mediamtx.org/">MediaMTX</a>. Там в документації вже є все потрібне. Але про всяк випадок зберу докупи свої знахідки, щоб не забути.</p>

<p>Загальна концепція: є передавач відеопотоку, є отримувач, між ними є медіасервер. Це може бути три різних компа. Може два, якщо медіасервер запустити там же, де й передавач. Для перших експериментів, нехай це буде один localhost.</p>

<p>Моя мета: <em>дивитися через веб-камеру на блимаючий світлодіод</em>, ха-ха.</p>

<h2 id="запуск-сервера">Запуск сервера</h2>

<p>Запускаю <a href="https://mediamtx.org/docs/kickoff/install#docker-image">MediaMTX в Docker</a>, якось так, як описано в документації. (Звісно, замість <code class="language-plaintext highlighter-rouge">192.168.xx.xx</code> вказую свою IP-адресу в локальній мережі). Це достатньо надійний варіант.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--rm</span> <span class="nt">-it</span> <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">MTX_RTSPTRANSPORTS</span><span class="o">=</span>tcp <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">MTX_WEBRTCADDITIONALHOSTS</span><span class="o">=</span>192.168.xx.xx <span class="se">\</span>
  <span class="nt">-p</span> 8554:8554 <span class="se">\</span>
  <span class="nt">-p</span> 1935:1935 <span class="se">\</span>
  <span class="nt">-p</span> 8888:8888 <span class="se">\</span>
  <span class="nt">-p</span> 8889:8889 <span class="se">\</span>
  <span class="nt">-p</span> 8890:8890/udp <span class="se">\</span>
  <span class="nt">-p</span> 8189:8189/udp <span class="se">\</span>
  bluenviron/mediamtx:1
</code></pre></div></div>

<p>Або, якщо можу собі дозволити, то запускаю ось так:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--rm</span> <span class="nt">-it</span> <span class="nt">--network</span><span class="o">=</span>host bluenviron/mediamtx:1
</code></pre></div></div>

<p>Також я запускав <a href="https://github.com/bluenviron/mediamtx/releases">готові бінарники MediaMTX</a> на своїй старенькій Raspberry Pi Model 2. Ок норм.</p>

<h2 id="трансляція-rtsp">Трансляція RTSP</h2>

<p>Штатна <a href="https://mediamtx.org/docs/publish/generic-webcams">документація пропонує</a> запускати <code class="language-plaintext highlighter-rouge">ffmpeg</code> прямо під час запуску MediaMTX. Мені більше до смаку тримати медіасервер постійно запущеним, а трансляції розпочинати окремою командою.</p>

<p>Ось варіант команди, який транслює відео з веб-камери, використовуючи кодек H.264:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg <span class="nt">-f</span> v4l2 <span class="nt">-framerate</span> 30 <span class="nt">-video_size</span> 640x480 <span class="nt">-i</span> /dev/video0 <span class="se">\</span>
  <span class="nt">-pix_fmt</span> yuv420p <span class="nt">-c</span>:v libx264 <span class="nt">-preset</span> ultrafast <span class="nt">-b</span>:v 600k <span class="se">\</span>
  <span class="nt">-tune</span> zerolatency <span class="nt">-bf</span> 0 <span class="nt">-g</span> 15 <span class="nt">-keyint_min</span> 30 <span class="se">\</span>
  <span class="nt">-f</span> rtsp rtsp://localhost:8554/mystream
</code></pre></div></div>

<p>Або те саме з апаратним прискоренням через <a href="https://en.wikipedia.org/wiki/Video_Acceleration_API">VAAPI</a>:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg <span class="nt">-f</span> v4l2 <span class="nt">-framerate</span> 30 <span class="nt">-video_size</span> 640x480 <span class="nt">-i</span> /dev/video0 <span class="se">\</span>
  <span class="nt">-c</span>:v h264_vaapi <span class="nt">-vaapi_device</span> /dev/dri/renderD128 <span class="se">\</span>
  <span class="nt">-vf</span> <span class="s1">'format=nv12,hwupload'</span> <span class="nt">-qp</span> 24 <span class="nt">-quality</span> 1 <span class="nt">-bf</span> 0 <span class="nt">-g</span> 30 <span class="se">\</span>
  <span class="nt">-f</span> rtsp rtsp://localhost:8554/mystream
</code></pre></div></div>

<p>Тут всюди 640×480, бо звичайні веб-камери майже ніколи не дають 30 кадрів на секунду на більших розмірах картинки. Чесно, мені для моїх задач той HD чи FullHD не дуже потрібний, а от відсутність гальм справді важлива.</p>

<p>Звісно, замість <code class="language-plaintext highlighter-rouge">localhost</code> може бути потрібна IP-адреса. Також шлях <code class="language-plaintext highlighter-rouge">/mystream</code> — це на ваш вибір, щоб якось вказати даний конкретний відеопотік. Через один медіасервер може одночасно йти декілька різних відеопотоків з різними шляхами.</p>

<p>Можна робити трансляцію з VP8, але не сьогодні, бо моя відеокарта не робе апаратне прискорення кодування саме для VP8 (ну або я не знайшов як), а софтварний енкодер жере проц. Після експериментів і роздумів, я вирішив зупинитися на H.264 як на оптимальному варіанті.</p>

<h2 id="перегляд-rtsp">Перегляд RTSP</h2>

<p>Ну, наприклад, можна якось так:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffplay <span class="se">\</span>
  <span class="nt">-fflags</span> nobuffer <span class="se">\</span>
  <span class="nt">-flags</span> low_delay <span class="se">\</span>
  <span class="nt">-framedrop</span> <span class="se">\</span>
  <span class="nt">-probesize</span> 32 <span class="se">\</span>
  <span class="nt">-analyzeduration</span> 0 <span class="se">\</span>
  <span class="nt">-sync</span> ext <span class="se">\</span>
  <span class="nt">-rtsp_transport</span> udp <span class="se">\</span>
  rtsp://localhost:8554/mystream
</code></pre></div></div>

<p>Або так:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mpv rtsp://localhost:8554/mystream <span class="nt">--profile</span><span class="o">=</span>low-latency <span class="nt">--untimed</span>
</code></pre></div></div>

<h2 id="перегляд-webrtc">Перегляд WebRTC</h2>

<p>Просто відкриваю в браузері <code class="language-plaintext highlighter-rouge">http://localhost:8889/mystream/</code> і бачу там відео. Магія!</p>

<p>Є певний нюанс з кодеками. Chrome/Chromium нормально грає H.264, а Firefox його грати не хоче. Треба полізти в <code class="language-plaintext highlighter-rouge">about:config</code>, знайти там <code class="language-plaintext highlighter-rouge">media.webrtc.hw.h264.enabled</code> і увімкнути його. Саме через це я експериментував з VP8, але вирішив що не на часі.</p>

<h2 id="вбудувати-стрім-у-довільну-веб-сторінку">Вбудувати стрім у довільну веб-сторінку</h2>

<p>Теж все робиться просто і зрозуміло, <a href="https://mediamtx.org/docs/other/embed-streams-in-a-website">як в документації</a>:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;iframe</span> <span class="na">src=</span><span class="s">"http://localhost:8889/mystream"</span> <span class="na">scrolling=</span><span class="s">"no"</span><span class="nt">&gt;&lt;/iframe&gt;</span>
</code></pre></div></div>

<p>Можна додати CSS за смаком:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;iframe</span>
 <span class="na">src=</span><span class="s">"http://localhost:8889/mystream"</span>
 <span class="na">style=</span><span class="s">"border: none;
        aspect-ratio: 4 / 3;
        width: 100%;
        height: 100%;
        max-width: 100vw;
        max-height: 100vh"</span>
 <span class="na">scrolling=</span><span class="s">"no"</span><span class="nt">&gt;&lt;/iframe&gt;</span>
</code></pre></div></div>

<p>Для дашборда Node-RED можна створити UI Template node і прямо туди це все покласти. Власне, саме задля цього я занурився у ці нетрі: дуже захотілося поєднати телеметрію та елементи керування моїх саморобних IoT проектів з живим відео. І це заслуговує окремого поста.</p>

<h2 id="додаткова-краса">Додаткова краса</h2>

<p>Коли трансляція не ведеться, можна <a href="https://mediamtx.org/docs/other/always-available">показувати</a> щось красиве, наприклад телевізійну тест-таблицю <a href="https://en.wikipedia.org/wiki/SMPTE_color_bars">SMPTE</a> чи <a href="https://en.wikipedia.org/wiki/Philips_circle_pattern">Philips</a>. Для цього треба з картинки зробити відео довжиною, гммм, 1 секунду.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget https://upload.wikimedia.org/wikipedia/commons/6/66/SMPTE_Color_Bars.svg
rsvg-convert SMPTE_Color_Bars.svg <span class="nt">-w</span> 640 <span class="nt">-h</span> 480 <span class="o">&gt;</span> smpte.png
ffmpeg <span class="nt">-loop</span> 1 <span class="nt">-framerate</span> 30 <span class="nt">-t</span> 1 <span class="nt">-i</span> smpte.png <span class="nt">-c</span>:v libx264 <span class="nt">-pix_fmt</span> yuv420p smpte.mp4
</code></pre></div></div>

<p>Або так:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget https://upload.wikimedia.org/wikipedia/commons/a/aa/Philips_PM5544.svg
rsvg-convert Philips_PM5544.svg -w 640 -h 480 &gt; pm5544.png
ffmpeg -loop 1 -framerate 30 -t 1 -i pm5544.png -c:v libx264 -pix_fmt yuv420p pm5544.mp4
</code></pre></div></div>

<p>Власне, далі треба буде лізти в конфігураційний файл <code class="language-plaintext highlighter-rouge">mediamtx.yml</code>, і якщо у вас MediaMTX все ще запускається через Docker, то вам доведеться з цим розбиратися окремо.</p>

<p>Просто дописую в кінець таку секцію:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>paths:
  mystream:
    alwaysAvailable: true
    alwaysAvailableFile: "pm5544.mp4"
</code></pre></div></div>

<h2 id="проброс-портів">Проброс портів</h2>

<p>Якщо є бажання поділитися своєю трансляцією в WebRTC з усім світом, то треба буде десь на роутері налаштувати port forwarding:</p>

<ul>
  <li>8889/TCP → 192.168.xx.xx:8889</li>
  <li>8189/UDP → 192.168.xx.xx:8189</li>
</ul>

<p>Далі дивлюся трансляцію так само, тільки в якості хоста вказую реальну IP-адресу.</p>

<h2 id="відкриті-питання">Відкриті питання</h2>

<p>Треба буде розібратися з питаннями безпеки:</p>

<ol>
  <li>Обмежити можливість трансляції, дозволити лише «кому треба»</li>
  <li>Обмежити можливість перегляду (JWT?)</li>
  <li>Убезпечити транспорт WebRTC від MitM (https?)</li>
</ol>

<p>Треба більше RTFM.</p>

<p>Треба всерйоз подумати, як би то його точніше виміряти затримку. Візуально бачу що вона відрізняється для різних конфігурацій, зокрема для різних кодеків і різних транспортів. Можливо, параметри запуску <code class="language-plaintext highlighter-rouge">ffmpeg</code> та <code class="language-plaintext highlighter-rouge">ffplay</code> можна ще вдосконалити.</p>

<h2 id="висновки">Висновки</h2>

<p>Маю офігенний інструмент в своєму арсеналі, от!</p>


      ]]></description>
      <link>https://test.de.co.ua/2026/03/29/mediamtx.html</link>
      <guid>https://test.de.co.ua/2026/03/29/mediamtx.html</guid>
      <pubDate>Sun, 29 Mar 2026 17:47:58 +0300</pubDate>
    </item>

    <item>
      <title>Привіт, світе!</title>
      <description><![CDATA[

<p>Дійшли руки написати перший пост в 2026 році.</p>

<p>Сказати мені нічого. Ну тобто насправді багато чого можу сказати, але 
не хочу. У мене в житті останнім часом багато подій, турбулентність.</p>

<p>Закинув свої хобі-проекти.</p>

<p>Одна з останніх штук, яку я колупав заради розваги та душевного спокою, 
була якась <a href="https://github.com/kastaneda/matchbox">«читалка» тексту</a> на мікроскопічних екранчиках SSD1306, 
типу 128×32 пікселів чи щось подібне; наразі з цієї «читалки» мало що 
є, окрім шрифта. (Насправді це мав бути переважно апаратний проект, але 
якось не склалося). <a href="https://mastodon.social/@dk487/115695108525799226">Є фото</a> читалки у мене в Mastodon. Може колись 
я продовжу цей проект.</p>

<p>Колись один мій друг зробив сам для себе <a href="https://github.com/diggya/bookreader">книжкочиталку на J2ME</a>. 
Він читав усякі книжки на чорно-білих кнопкових телефонах в ті часи, 
коли смартфони ще не винайшли, а КПК були дорогими і незручними. Мій 
проект частково натхненний тим його досвідом.</p>


      ]]></description>
      <link>https://test.de.co.ua/2026/01/18/hello-world.html</link>
      <guid>https://test.de.co.ua/2026/01/18/hello-world.html</guid>
      <pubDate>Sun, 18 Jan 2026 20:48:49 +0200</pubDate>
    </item>

    <item>
      <title>Типу, привіт</title>
      <description><![CDATA[

<p>Давно не писав. Занурився у повсякденні дрібниці.</p>

<p>По приколу пишу ці рядки через вбудовану у GitHub веб-версію VS Code, та що запускається по натисканню на кнопку <kbd>.</kbd> і це по-своєму прикольно.</p>

<p>Хоча і тупо. Я міг би без гальм і без споживання зайвого трафіку написати цей текст у Vim на найменшому зі своїх ноутів, і теж було б непогано.</p>

<div lang="en">
  <blockquote>
    <p><em>Braxton:</em>
Fucking grown man wearing a Timex, dude.
He was fucking pathetic. Look at that.
See that?..
It’s worth more than he made in a year.
What do you think he’d say about this?</p>

    <p><em>Christian:</em>
He would say it’s the same time on his watch.</p>
  </blockquote>
</div>

<p>Гм. Треба буде якось написати пост про економію ресурсів. Щось у цьому є таке, що складно висловити словами. Мені не важко запустити VSCode в браузері і не важко запустити його локально. Зазвичай я пишу текст і код в Geany. Але часто буваю в Vim. Чому так?</p>

<p>Це не тільки питання зручності. Я відчуваю, що Vim мене може врятувати, умовно кажучи, «на пустельному острові». (Або навіть не повноцінний потужний Vim, а його злий брат-близнюк <code class="language-plaintext highlighter-rouge">busybox vi</code>, у якого не все добре з Unicode). О, до речі, про свої експерименти з Debian-Live теж треба написати.</p>

<p>Колись про все це напишу.</p>

<p><em>Оновлення 12:07.</em> Щодо економії: іноді кількість переходить в якість. Можливість досягати мети різними шляхами, в тому числі короткими і мінімалістичними, дає свободу вибору.</p>

      ]]></description>
      <link>https://test.de.co.ua/2025/12/03/hi-there.html</link>
      <guid>https://test.de.co.ua/2025/12/03/hi-there.html</guid>
      <pubDate>Wed, 03 Dec 2025 10:00:00 +0200</pubDate>
    </item>

    <item>
      <title>RTMP</title>
      <description><![CDATA[

<p>Привіт світ. Продовжую вивчати прості і всім відомі штуки. Сьогодні взнав як зробити собі трансляцію по RTMP, і напишу про це (переважно сам для себе) короткий брудний howto.</p>

<p>Одразу уточнюю: 1) не через OBS Studo, 2) не на YouTube.</p>

<p>Я просто хочу дивитися відео зі своєї веб-камери з мінімальною затримкою. Комп з камерою і комп для перегляду обидва знаходяться в локальній мережі. Ніякого інтерактивного GUIшного софта, тільки ffmpeg та інші scriptable консольні програми.</p>

<p>Раніше я знав лише один супер-простий метод, про який <a href="/2022/08/05/ssh-webcam.html">писав колись</a>:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-Y</span> foohost mplayer <span class="nt">-vo</span> x11 tv://
</code></pre></div></div>

<p>(Не робіть так, це страшенні гальма + великий overhead).</p>

<h2 id="як-можна-зробити-краще">Як можна зробити краще</h2>

<p>Погнали.</p>

<p>В принципі у нас може бути три різних хоста: комп з камерою, комп для перегляду і проміжний хост з RTMP-сервером. В моєму випадку RTMP-сервер може бути там же, де знаходиться камера.</p>

<h3 id="сервер">Сервер</h3>

<p>Перше, що нам треба — запустити сервер RTMP. Можна зробити RTMP-сервер через Nginx та <code class="language-plaintext highlighter-rouge">mod_rtmp</code>. А у найпростіших випадках (і щоб погратися) достатньо взяти готовий контейнер:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--rm</span> <span class="nt">-p</span> 1935:1935 tiangolo/nginx-rtmp 
</code></pre></div></div>

<p>Йому треба лише 1935 порт.</p>

<p>Припустимо, що це хост з адресою <code class="language-plaintext highlighter-rouge">192.168.0.99</code>, тоді адреса нашої трансляції буде <code class="language-plaintext highlighter-rouge">rtmp://192.168.0.99/live/123</code> (де <code class="language-plaintext highlighter-rouge">/live/123</code> це довільний обраний мною шлях).</p>

<h3 id="камера">Камера</h3>

<p>Мені потрібна трансляція без звуку. Нехай це камера <code class="language-plaintext highlighter-rouge">/dev/video0</code>. Тоді на компі з камерою запускаємо таку магію, де крім <code class="language-plaintext highlighter-rouge">/dev/video0</code> та адреси трансляції для початку краще нічого не чіпати:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg <span class="nt">-f</span> v4l2 <span class="nt">-i</span> /dev/video0 <span class="nt">-c</span>:v libx264 <span class="se">\</span>
  <span class="nt">-pix_fmt</span> yuv420p <span class="nt">-framerate</span> 15 <span class="nt">-g</span> 30 <span class="nt">-b</span>:v 500k <span class="se">\</span>
  <span class="nt">-preset</span> ultrafast <span class="nt">-tune</span> zerolatency <span class="se">\</span>
  <span class="nt">-f</span> flv rtmp://192.168.0.99/live/123
</code></pre></div></div>

<p>Я нагуглив <a href="https://www.baeldung.com/linux/ffmpeg-webcam-stream-video">перший-ліпший випадковий howto</a> і прибрав згадки про звук. Якщо вам буде треба звук (і якщо у вас ALSA), то йдіть подивіться оригінальний пост.</p>

<h3 id="перегляд">Перегляд</h3>

<p>Ну найпростіше, що можна зробити, це щось типу такого:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mplayer rtmp://192.168.0.99/live/123
</code></pre></div></div>

<p>Воно працює. Але ну його до біса! Затримка більше 10 секунд мені абсолютно не підходить. Ось кращий варіант:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mpv <span class="nt">--no-cache</span> <span class="nt">--untimed</span> <span class="nt">--no-demuxer-thread</span> <span class="se">\</span>
  <span class="nt">--video-sync</span><span class="o">=</span>audio <span class="nt">--vd-lavc-threads</span><span class="o">=</span>1 <span class="se">\</span>
  rtmp://192.168.0.99/live/123
</code></pre></div></div>

<p>або такий:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffplay <span class="nt">-autoexit</span> <span class="nt">-flags</span> low_delay <span class="nt">-framedrop</span> <span class="se">\</span>
  <span class="nt">-strict</span> experimental <span class="nt">-vf</span> <span class="nv">setpts</span><span class="o">=</span>0 <span class="nt">-tcp_nodelay</span> 1 <span class="se">\</span>
  rtmp://192.168.0.99/live/123
</code></pre></div></div>

<p>І от тут, нарешті, я починаю бачити в цьому всьому профіт. Я можу дивитися в камеру+екран як в дзеркало і не відчувати затримку.</p>

<p>P.S. Що я не знайшов, так це нормального рішення для перегляду стріму на Android. Запускаю там VLC, воно все працює, але лаг 15 секунд (upd.: <a href="https://code.videolan.org/videolan/vlc/-/issues/15597">вони про це знають і їм похуй</a>). Може хтось порадить рішення краще.</p>

<p>Upd.: ще я нагуглив якийсь стрьомний <a href="https://play.google.com/store/apps/details?id=com.softvelum.sldp_player">Larix Player</a>, це дуже мерзенний freemium з обмеженням у 5 хвилин перегляду відео на безкоштовній версії. З того, що я спробував і що вдалося змусити працювати на burner phone, у цього додатку поки що виявився найменший лаг. Треба ще пошукати.</p>


      ]]></description>
      <link>https://test.de.co.ua/2025/10/20/rtmp.html</link>
      <guid>https://test.de.co.ua/2025/10/20/rtmp.html</guid>
      <pubDate>Mon, 20 Oct 2025 13:17:41 +0300</pubDate>
    </item>

    <item>
      <title>Темна ніч та функція show_source()</title>
      <description><![CDATA[

<p>Швидкий брудний пост. Я під час розробки на PHP іноді користуюсь <a href="https://www.php.net/manual/uk/function.show-source.php"><code class="language-plaintext highlighter-rouge">show_source()</code></a>. Ну, знаєте, буває треба показати код і при цьому <em>хоч якось</em> підсвітити синтаксис, не встановлюючи тонну зовнішніх залежностей.</p>

<p>Так, це анахронізм часів PHP4. І так, за замовчанням кольори там розраховані на світле тло. Якщо у вас сайт з темним фоном, то можна <a href="https://www.php.net/manual/uk/misc.configuration.php#ini.syntax-highlighting">поміняти кольори</a> на щось відповідне, але це все трошки бісить.</p>

<p>А що як у нас сучасна адаптивна кольорова схема?</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"color-scheme"</span> <span class="na">content=</span><span class="s">"light dark"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>Тоді у нас взагалі не може бути фіксованих кольорів, бо має одночасно існувати і варіант для темної схеми, і варіант для світлої.</p>

<p>Спробував через <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/var">змінні CSS</a>. Погнали.</p>

<p>Пишемо в <code class="language-plaintext highlighter-rouge">php.ini</code> щось таке:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">highlight.default</span> <span class="p">=</span> <span class="s">"var(--php-default)"</span>
<span class="py">highlight.html</span> <span class="p">=</span> <span class="s">"var(--php-html)"</span>
<span class="py">highlight.keyword</span> <span class="p">=</span> <span class="s">"var(--php-keyword)"</span>
<span class="py">highlight.string</span> <span class="p">=</span> <span class="s">"var(--php-string)"</span>
<span class="py">highlight.comment</span> <span class="p">=</span> <span class="s">"var(--php-comment)"</span>
</code></pre></div></div>

<p>Або, якщо змінювати <code class="language-plaintext highlighter-rouge">php.ini</code> незручно, робимо це в runtime в PHP:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">foreach</span> <span class="p">([</span><span class="s1">'default'</span><span class="p">,</span> <span class="s1">'html'</span><span class="p">,</span> <span class="s1">'keyword'</span><span class="p">,</span> <span class="s1">'string'</span><span class="p">,</span> <span class="s1">'comment'</span><span class="p">]</span> <span class="k">as</span> <span class="nv">$key</span><span class="p">)</span> <span class="p">{</span>
    <span class="nb">ini_set</span><span class="p">(</span><span class="s1">'highlight.'</span> <span class="mf">.</span> <span class="nv">$key</span><span class="p">,</span> <span class="s1">'var(--php-'</span> <span class="mf">.</span> <span class="nv">$key</span> <span class="mf">.</span> <span class="s1">')'</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>І тепер нам треба приблизно отакий CSS:</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">:root</span> <span class="p">{</span>
  <span class="py">--php-comment</span><span class="p">:</span> <span class="m">#f80</span><span class="p">;</span>
  <span class="py">--php-default</span><span class="p">:</span> <span class="m">#00b</span><span class="p">;</span>
  <span class="py">--php-html</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
  <span class="py">--php-keyword</span><span class="p">:</span> <span class="m">#070</span><span class="p">;</span>
  <span class="py">--php-string</span><span class="p">:</span> <span class="m">#d00</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">@media</span> <span class="p">(</span><span class="n">prefers-color-scheme</span><span class="p">:</span> <span class="n">dark</span><span class="p">)</span> <span class="p">{</span> 
  <span class="nd">:root</span> <span class="p">{</span>
    <span class="py">--php-comment</span><span class="p">:</span> <span class="m">#860</span><span class="p">;</span>
    <span class="py">--php-default</span><span class="p">:</span> <span class="m">#66f</span><span class="p">;</span>
    <span class="py">--php-html</span><span class="p">:</span> <span class="m">#ccc</span><span class="p">;</span>
    <span class="py">--php-keyword</span><span class="p">:</span> <span class="m">#0f3</span><span class="p">;</span>
    <span class="py">--php-string</span><span class="p">:</span> <span class="m">#fc0</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Всьо, пофарбували, порядок :)</p>


      ]]></description>
      <link>https://test.de.co.ua/2025/10/16/temna-nich-ta-funktsiia-show_source.html</link>
      <guid>https://test.de.co.ua/2025/10/16/temna-nich-ta-funktsiia-show_source.html</guid>
      <pubDate>Thu, 16 Oct 2025 03:48:26 +0300</pubDate>
    </item>

    <item>
      <title>Інвертований світлодіод</title>
      <description><![CDATA[

<p>На платі Raspberry Pi Pico є вбудований світлодіод (GPIO25), і на платах ESP8266 теж є вбудований світлодіод (GPIO2). Але є важлива різниця.</p>

<p>Вбудований світлодіод Raspberry Pi Pico світиться, якщо подати на відповідний GPIO високий рівень сигналу, і не світиться якщо встановити низький рівень сигналу. Поведінка ESP8266 протилежна: високий рівень сигналу там <em>вимикає</em> світлодіод.</p>

<p>Тобто, клас <code class="language-plaintext highlighter-rouge">BlinkingLED</code> з <a href="/2025/10/09/python-blinking-led.html">попередьного посту</a> в режимі «не блимати» має різну поведінку на Raspberry Pi Pico та ESP8266: для RP2 вимикання режиму блимання робить світлодіод неактивним, а для ESP8266 вимикання режиму блимання врубає світлодіод на повну. (В режимі «блимати» вони все ж виглядають однаково). Тож треба з цим щось зробити, чи не так?</p>

<p>Окей, давайте і тут зробимо щось сумнівне…</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">InvertedPinOut</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">pin</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">pin</span> <span class="o">=</span> <span class="n">pin</span>

    <span class="k">def</span> <span class="nf">value</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">new_value</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">pin</span><span class="p">.</span><span class="n">value</span><span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">new_value</span><span class="p">)</span>
</code></pre></div></div>

<p>Об’єкт цього класу можна передавати в <code class="language-plaintext highlighter-rouge">BlinkingLED</code> замість <code class="language-plaintext highlighter-rouge">machine.Pin</code>. Норм чи крінж?</p>

<p>Альтернативно, ми можемо додати в клас <code class="language-plaintext highlighter-rouge">BlinkingLED</code> властивість <code class="language-plaintext highlighter-rouge">is_inverted</code> і додаткову логіку:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code>     async def main(self):
         while True:
<span class="gd">-            self.pin.value(int(self.blinking))
</span><span class="gi">+            self.pin.value(int(self.blinking ^ self.is_inverted))
</span>             await asyncio.sleep_ms(500)
<span class="gd">-            self.pin.value(0)
</span><span class="gi">+            self.pin.value(int(self.is_inverted)
</span>             await asyncio.sleep_ms(500)
</code></pre></div></div>

<p>Це скоріше крінж.</p>

<p>Або так: зробити в класі властивості <code class="language-plaintext highlighter-rouge">value_on</code> та <code class="language-plaintext highlighter-rouge">value_off</code> (за замовчанням (для звичайних світлодіоів), значення 1 та 0, відповідно). Ще довше, але у певному сенсі наочніше.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code>     async def main(self):
         while True:
<span class="gd">-            self.pin.value(int(self.blinking))
</span><span class="gi">+            self.pin.value(self.value_on if self.blinking else self.value_off)
</span>             await asyncio.sleep_ms(500)
<span class="gd">-            self.pin.value(0)
</span><span class="gi">+            self.pin.value(self.value_off)
</span>             await asyncio.sleep_ms(500)
</code></pre></div></div>

<p>Зовсім крінж. Як же краще?</p>

<p>Увага, правильна відповідь, щойно випадково знайдена в документації: можна використати вже наявний в стандартній бібліотеці <a href="https://docs.micropython.org/en/latest/library/machine.Signal.html">клас <code class="language-plaintext highlighter-rouge">machine.Signal</code></a>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">led</span> <span class="o">=</span> <span class="n">machine</span><span class="p">.</span><span class="n">Signal</span><span class="p">(</span><span class="n">machine</span><span class="p">.</span><span class="n">Pin</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="n">machine</span><span class="p">.</span><span class="n">Pin</span><span class="p">.</span><span class="n">OUT</span><span class="p">),</span> <span class="n">invert</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>

<p>Яка проста і прикольна штука!</p>


      ]]></description>
      <link>https://test.de.co.ua/2025/10/11/inverted-pin.html</link>
      <guid>https://test.de.co.ua/2025/10/11/inverted-pin.html</guid>
      <pubDate>Sat, 11 Oct 2025 23:15:28 +0300</pubDate>
    </item>

    <item>
      <title>MicroPython та блимаючий світлодіод</title>
      <description><![CDATA[

<p>Привіт, світ! Сьогодні я <a href="/2025/03/23/pro-svitlodiody.html">знову</a> буду блимати світлодіодом.</p>

<p>Цього разу я звернуся до класичного жанру, одного з найжорстокіших та найбезглуздіших дисциплін «спеціальної олімпіади» з програмування: це DRY, «<a href="https://uk.wikipedia.org/wiki/Don%27t_repeat_yourself">don’t repeat yourself</a>». Міф про універсальний код, який чудово буде працювати всюди без модифікацій. Як на мене, гонитва за цим DRY буває навіть гіршою за синдромом «<a href="https://uk.wikipedia.org/wiki/%D0%92%D0%B8%D0%BD%D0%B0%D0%B9%D0%B4%D0%B5%D0%BD%D0%BE_%D0%BD%D0%B5_%D0%BD%D0%B0%D0%BC%D0%B8">not invented here</a>».</p>

<p>Оскільки я все це роблю задля своєї розваги, то можу собі дозволити такі брутальні експерименти.</p>

<h2 id="опис-завдання">Опис завдання</h2>

<p>За моїм задумом, жменька простого, тупого (місцями тривіального) коду на Python має працювати більш-менш однаково на двох достатньо різних платформах:</p>

<ul>
  <li>на якійсь ESP8266 (WeMos D1 mini) через MQTT + Wi-Fi, та</li>
  <li>на Raspberry Pi Pico через <code class="language-plaintext highlighter-rouge">/dev/ttyACM0</code>, shell-скрипти та <code class="language-plaintext highlighter-rouge">socat</code></li>
</ul>

<p>Що має робити цей код? Те саме, <a href="/2024/03/18/mqtt-oop.html">що я вже робив</a> півтора роки тому:</p>

<ul>
  <li>блимати світлодіодом</li>
  <li>вмикати/вимикати блимання світлодіодом по команді, отриманій з MQTT</li>
  <li>вмикати/вимикати блимання через натискання на кнопку локально
    <ul>
      <li>в тому числі при втраті зв’язку з брокером MQTT</li>
    </ul>
  </li>
  <li>передавати брокеру MQTT поточний стан (блимає/не блимає)</li>
</ul>

<p>Додатковий бонус для ESP8266:</p>

<ul>
  <li>звітувати брокеру MQTT про свою наявність онлайн</li>
  <li>звітувати (через last will) про перехід в офлайн</li>
  <li>використовувати флаг retain для індикації стану онлайн/офлайн</li>
</ul>

<p>Заради цього мені знадобиться купа допоміжного коду. Ну шо, погнали.</p>

<h2 id="реалізація">Реалізація</h2>

<p>У мене було декілька спроб зробити це нормально. Я починав з налаштування мережі, комунікації з брокером MQTT, і виходило якось кострубато і некрасиво.</p>

<p>Але потім я підійшов з іншого боку. Взагалі відклав всю цю возню з мережею. У центрі мають бути не технічні деталі підключення до брокера, а внутрішні процеси і зв’язки. А, забув сказати, це має бути асинхронний код. Тож архітектура важлива.</p>

<p>Почнемо з головного, зі світлодіода.</p>

<h3 id="чернетка">Чернетка</h3>

<p>Просто поблимаємо світлодіодом. Повільно і спокійно, на частоті 1 Гц, з затримкою 0,5 секунди.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">asyncio</span>
<span class="kn">import</span> <span class="nn">machine</span>

<span class="k">class</span> <span class="nc">BlinkingLED</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">gpio</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">gpio</span> <span class="o">=</span> <span class="n">gpio</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">is_blinking</span> <span class="o">=</span> <span class="bp">True</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
            <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep_ms</span><span class="p">(</span><span class="mi">500</span><span class="p">)</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">gpio</span><span class="p">.</span><span class="n">value</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">is_blinking</span><span class="p">))</span>
            <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep_ms</span><span class="p">(</span><span class="mi">500</span><span class="p">)</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">gpio</span><span class="p">.</span><span class="n">value</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

<span class="k">try</span><span class="p">:</span>
    <span class="c1"># WeMos D1 mini: built-in LED = pin D4 = GPIO2
</span>    <span class="n">led</span> <span class="o">=</span> <span class="n">machine</span><span class="p">.</span><span class="n">Pin</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="n">machine</span><span class="p">.</span><span class="n">Pin</span><span class="p">.</span><span class="n">OUT</span><span class="p">)</span>
    
    <span class="c1"># Raspberry Pi Pico: built-in LED = GPIO25
</span>    <span class="c1">#led = machine.Pin(25, machine.Pin.OUT)
</span>
    <span class="n">led_task</span> <span class="o">=</span> <span class="n">BlinkingLED</span><span class="p">(</span><span class="n">led</span><span class="p">)</span>
    <span class="n">asyncio</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">led_task</span><span class="p">.</span><span class="n">main</span><span class="p">())</span>
<span class="k">except</span> <span class="nb">KeyboardInterrupt</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'Stopped'</span><span class="p">)</span>
</code></pre></div></div>

<p>Окей. Це один процес. Мені знадобиться декілька різних процесів, які будуть щось робити: блимати світлодіодом, зчитувати стан кнопки, комунікувати з зовнішнім світом. Кожен з цих процесів — якийсь об’єкт з методом <code class="language-plaintext highlighter-rouge">main</code>, всередині якого нескінченний цикл.</p>

<p>Додамо ще один процес, який кожні 5 секунд буде вмикати або вимикати режим блимання цього світлодіода. І ще додамо якийсь клас, який запускатиме декілька різних процесів разом. І, звісно, зміниться шматок кода для запуска цього. Але клас <code class="language-plaintext highlighter-rouge">BlinkingLED</code> з попереднього лістингу кода залишається без змін.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">DemoScenario</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">led_task</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">led_task</span> <span class="o">=</span> <span class="n">led_task</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
            <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">led_task</span><span class="p">.</span><span class="n">is_blinking</span> <span class="o">=</span> <span class="ow">not</span> <span class="bp">self</span><span class="p">.</span><span class="n">led_task</span><span class="p">.</span><span class="n">is_blinking</span>

<span class="k">class</span> <span class="nc">App</span><span class="p">:</span>
    <span class="n">tasks</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">tasks</span> <span class="o">=</span> <span class="p">[</span><span class="n">task</span><span class="p">.</span><span class="n">main</span><span class="p">()</span> <span class="k">for</span> <span class="n">task</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">tasks</span><span class="p">]</span>
        <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">gather</span><span class="p">(</span><span class="o">*</span><span class="n">tasks</span><span class="p">)</span>

<span class="k">try</span><span class="p">:</span>
    <span class="n">led</span> <span class="o">=</span> <span class="n">machine</span><span class="p">.</span><span class="n">Pin</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="n">machine</span><span class="p">.</span><span class="n">Pin</span><span class="p">.</span><span class="n">OUT</span><span class="p">)</span>
    <span class="n">led_task</span> <span class="o">=</span> <span class="n">BlinkingLED</span><span class="p">(</span><span class="n">led</span><span class="p">)</span>
    <span class="n">demo</span> <span class="o">=</span> <span class="n">DemoScenario</span><span class="p">(</span><span class="n">led_task</span><span class="p">)</span>
    <span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">()</span>
    <span class="n">app</span><span class="p">.</span><span class="n">tasks</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">led_task</span><span class="p">)</span>
    <span class="n">app</span><span class="p">.</span><span class="n">tasks</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">demo</span><span class="p">)</span>
    <span class="n">asyncio</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">app</span><span class="p">.</span><span class="n">main</span><span class="p">())</span>
<span class="k">except</span> <span class="nb">KeyboardInterrupt</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'Stopped'</span><span class="p">)</span>
</code></pre></div></div>

<p>Вже цікавіше! Але тут я бачу одну <em>слабку</em> ділянку архітектури: <em>сильну</em> зв’язаність між об’єктами класів <code class="language-plaintext highlighter-rouge">DemoScenario</code> та <code class="language-plaintext highlighter-rouge">BlinkingLED</code>. Клас сценарію напряму керує станом світлодіода. Хочу, щоб керування це відбувалося не напряму, а через повідомлення, як з брокером MQTT.</p>

<h3 id="про-повідомлення-mqtt">Про повідомлення MQTT</h3>

<p>Вся ця фігня задумана для використання з красивими дашбордами на Node-RED. Має бути вимикач на дашборді, в який можна ткнути мишкою.</p>

<p>Кожен пристрій має своє ім’я, наприклад <code class="language-plaintext highlighter-rouge">board98</code> та <code class="language-plaintext highlighter-rouge">board99</code>. Всі повідомлення в MQTT, що стосуються певної плати, повинні мати топік з певним префіксом — <code class="language-plaintext highlighter-rouge">dev/board99</code>, <code class="language-plaintext highlighter-rouge">home/kitchen/board99</code> або просто <code class="language-plaintext highlighter-rouge">board99</code>.</p>

<p>Традиційно, вся комунікація з <em>одним</em> світлодіодом має відбуватися через <em>два</em> різних топіки: назвемо їх <code class="language-plaintext highlighter-rouge">board99/led</code> та <code class="language-plaintext highlighter-rouge">board99/led/set</code>.</p>

<table>
  <thead>
    <tr>
      <th>Topic</th>
      <th>Payload</th>
      <th>Напрямок передачі повідомлення</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">board99/led</code></td>
      <td><code class="language-plaintext highlighter-rouge">0</code> або <code class="language-plaintext highlighter-rouge">1</code></td>
      <td>звіт пристрою про стан</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">board99/led/set</code></td>
      <td><code class="language-plaintext highlighter-rouge">0</code> або <code class="language-plaintext highlighter-rouge">1</code></td>
      <td>команда від брокера до пристрою</td>
    </tr>
  </tbody>
</table>

<p>Префікс потрібний для комунікації з зовнішнім світом, а внутрішні зв’язки (наприклад, між кнопкою і світлодіодом) обходяться без префіксу.</p>

<h3 id="локальний-хаб-повідомлень">Локальний хаб повідомлень</h3>

<p>Задум такий: без всякого зовнішнього брокера MQTT передавати повідомлення всім зацікавленим процесам. Кожен з процесів, в свою чергу, може щось з цим зробити на свій розсуд.</p>

<p>Цього разу всі класи змінилися, включаючи <code class="language-plaintext highlighter-rouge">BlinkingLED</code>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">asyncio</span>
<span class="kn">import</span> <span class="nn">machine</span>

<span class="k">class</span> <span class="nc">App</span><span class="p">:</span>
    <span class="n">tasks</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">listeners</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="k">def</span> <span class="nf">add</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj</span><span class="p">):</span>
        <span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="s">'main'</span><span class="p">):</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">tasks</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>
        <span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="s">'handle'</span><span class="p">):</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">listeners</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">tasks</span> <span class="o">=</span> <span class="p">[</span><span class="n">task</span><span class="p">.</span><span class="n">main</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="k">for</span> <span class="n">task</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">tasks</span><span class="p">]</span>
        <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">gather</span><span class="p">(</span><span class="o">*</span><span class="n">tasks</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">topic</span><span class="p">,</span> <span class="n">payload</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">listener</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">listeners</span><span class="p">:</span>
            <span class="n">listener</span><span class="p">.</span><span class="n">handle</span><span class="p">(</span><span class="n">topic</span><span class="p">,</span> <span class="n">payload</span><span class="p">)</span>

<span class="k">class</span> <span class="nc">BlinkingLED</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">gpio</span><span class="p">,</span> <span class="n">topic</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">gpio</span> <span class="o">=</span> <span class="n">gpio</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">is_blinking</span> <span class="o">=</span> <span class="bp">True</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">topic</span> <span class="o">=</span> <span class="n">topic</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">app</span><span class="p">):</span>
        <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
            <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep_ms</span><span class="p">(</span><span class="mi">500</span><span class="p">)</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">gpio</span><span class="p">.</span><span class="n">value</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">is_blinking</span><span class="p">))</span>
            <span class="n">app</span><span class="p">.</span><span class="n">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">topic</span><span class="p">,</span> <span class="nb">int</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">is_blinking</span><span class="p">))</span>
            <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep_ms</span><span class="p">(</span><span class="mi">500</span><span class="p">)</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">gpio</span><span class="p">.</span><span class="n">value</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
            <span class="n">app</span><span class="p">.</span><span class="n">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">topic</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">topic</span><span class="p">,</span> <span class="n">payload</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">topic</span> <span class="o">==</span> <span class="bp">self</span><span class="p">.</span><span class="n">topic</span><span class="o">+</span><span class="s">'/set'</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">is_blinking</span> <span class="o">=</span> <span class="nb">bool</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">payload</span><span class="p">))</span>

<span class="k">class</span> <span class="nc">DemoScenario</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">led_topic</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">led_topic</span> <span class="o">=</span> <span class="n">led_topic</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">app</span><span class="p">):</span>
        <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
            <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
            <span class="n">app</span><span class="p">.</span><span class="n">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">led_topic</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
            <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
            <span class="n">app</span><span class="p">.</span><span class="n">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">led_topic</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>

<span class="k">class</span> <span class="nc">DebugMessages</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">topic</span><span class="p">,</span> <span class="n">payload</span><span class="p">):</span>
        <span class="k">print</span><span class="p">(</span><span class="n">topic</span><span class="p">,</span> <span class="n">payload</span><span class="p">)</span>

<span class="k">try</span><span class="p">:</span>
    <span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">()</span>
    <span class="n">led</span> <span class="o">=</span> <span class="n">machine</span><span class="p">.</span><span class="n">Pin</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="n">machine</span><span class="p">.</span><span class="n">Pin</span><span class="p">.</span><span class="n">OUT</span><span class="p">)</span>
    <span class="n">app</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">BlinkingLED</span><span class="p">(</span><span class="n">led</span><span class="p">,</span> <span class="s">'led'</span><span class="p">))</span>
    <span class="n">app</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">DemoScenario</span><span class="p">(</span><span class="s">'led/set'</span><span class="p">))</span>
    <span class="n">app</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">DebugMessages</span><span class="p">())</span>
    <span class="n">asyncio</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">app</span><span class="p">.</span><span class="n">main</span><span class="p">())</span>
<span class="k">except</span> <span class="nb">KeyboardInterrupt</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'Stopped'</span><span class="p">)</span>
</code></pre></div></div>

<p>Є певна асиметричність у поведінці цих класів. Клас <code class="language-plaintext highlighter-rouge">BlinkingLED</code> як приймає повідомлення, так і відправляє. Клас <code class="language-plaintext highlighter-rouge">DemoScenario</code> лише відправляє повідомлення. Клас <code class="language-plaintext highlighter-rouge">DebugMessages</code> лише приймає, і у нього немає метода <code class="language-plaintext highlighter-rouge">main</code>.</p>

<p>У попередній версії я намагався на рівні класа <code class="language-plaintext highlighter-rouge">App</code> визначати, які процеси мають отримати кожне окреме повідомлення. Цього разу я відправляю всі повідомлення всім слухачам, а вони розбираються, що їм треба. Наприклад, клас <code class="language-plaintext highlighter-rouge">DebugMessages</code> опрацьовує всі повідомлення.</p>

<p>Коли я в IDE Thonny запускаю на мікроконтролері цей код, то бачу в консолі щось таке:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>led 1
led 0
led 1
led 0
led 1
led 0
led 1
led 0
led 1
led/set 0
led 0
led 0
led 0
led 0
</code></pre></div></div>

<p>Спочатку блимали світлодіодом, потім не блимаємо. Видно як стан світлодіода, так і команду, яка переменула режим.</p>

<p>Дико переускладнений варіант: 64 рядки асинхронного коду там, де того ж самого результату можна було б досягти в три рядки і два цикли :) але, на мою думку, це має бути більш-менш правильний підхід, з точки зору мого задуму. Бо далі буде більше коду.</p>

<h3 id="зєднання-з-зовнішнім-світом">З’єднання з зовнішнім світом</h3>

<p>Це буде розповідь про ESP8266, яку ми любимо за її Wi-Fi.</p>

<p>Припустимо, весь попередній код ми писали в <code class="language-plaintext highlighter-rouge">main.py</code>, що є цілком традиційним підходом. Нехай у нас також є <code class="language-plaintext highlighter-rouge">boot.py</code>, і там ми напишемо пароль від вайфаю:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import network

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
    wlan.connect('ssid', 'password')
</code></pre></div></div>

<p>Оцей от <code class="language-plaintext highlighter-rouge">wlan.connect</code> лише <em>розпочинає</em> процес з’єднання. Тобто весь цей фрагмент коду не заблокує нам виконання програми на декілька секунд.</p>

<p>Тепер в основному <code class="language-plaintext highlighter-rouge">main.py</code> додамо ось такий клас:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">umqtt.simple</span>

<span class="k">class</span> <span class="nc">WirelessMQTT</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">server</span><span class="p">,</span> <span class="n">prefix</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">wlan</span> <span class="o">=</span> <span class="n">network</span><span class="p">.</span><span class="n">WLAN</span><span class="p">(</span><span class="n">network</span><span class="p">.</span><span class="n">STA_IF</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">prefix</span> <span class="o">=</span> <span class="n">prefix</span><span class="o">+</span><span class="s">'/'</span>
        <span class="n">kwargs</span><span class="p">[</span><span class="s">'keepalive'</span><span class="p">]</span> <span class="o">=</span> <span class="mi">2</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">mq</span> <span class="o">=</span> <span class="n">umqtt</span><span class="p">.</span><span class="n">simple</span><span class="p">.</span><span class="n">MQTTClient</span><span class="p">(</span><span class="n">prefix</span><span class="p">,</span> <span class="n">server</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">mq</span><span class="p">.</span><span class="n">set_last_will</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">prefix</span><span class="o">+</span><span class="s">'online'</span><span class="p">,</span> <span class="sa">b</span><span class="s">'0'</span><span class="p">,</span> <span class="n">retain</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">mq</span><span class="p">.</span><span class="n">set_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">mqtt_callback</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">nodup_t</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">nodup_p</span> <span class="o">=</span> <span class="bp">None</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">app</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">app</span> <span class="o">=</span> <span class="n">app</span>
        <span class="k">while</span> <span class="ow">not</span> <span class="bp">self</span><span class="p">.</span><span class="n">wlan</span><span class="p">.</span><span class="n">isconnected</span><span class="p">():</span>
            <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">mq</span><span class="p">.</span><span class="n">connect</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">mq</span><span class="p">.</span><span class="n">publish</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">prefix</span><span class="o">+</span><span class="s">'online'</span><span class="p">,</span> <span class="sa">b</span><span class="s">'1'</span><span class="p">,</span> <span class="n">retain</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">mq</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">prefix</span><span class="o">+</span><span class="s">'+/set'</span><span class="p">)</span>
        <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">gather</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">ping</span><span class="p">(),</span> <span class="bp">self</span><span class="p">.</span><span class="n">check_msg</span><span class="p">())</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">ping</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">mq</span><span class="p">.</span><span class="n">ping</span><span class="p">()</span>
            <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">check_msg</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">mq</span><span class="p">.</span><span class="n">check_msg</span><span class="p">()</span>
            <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">mqtt_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">topic</span><span class="p">,</span> <span class="n">payload</span><span class="p">):</span>
        <span class="n">topic</span><span class="p">,</span> <span class="n">payload</span> <span class="o">=</span> <span class="n">topic</span><span class="p">.</span><span class="n">decode</span><span class="p">(),</span> <span class="n">payload</span><span class="p">.</span><span class="n">decode</span><span class="p">()</span>
        <span class="k">if</span> <span class="n">topic</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">prefix</span><span class="p">):</span>
            <span class="n">topic</span> <span class="o">=</span> <span class="n">topic</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">prefix</span><span class="p">):]</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">nodup_t</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">nodup_p</span> <span class="o">=</span> <span class="n">topic</span><span class="p">,</span> <span class="n">payload</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">app</span><span class="p">.</span><span class="n">handle</span><span class="p">(</span><span class="n">topic</span><span class="p">,</span> <span class="n">payload</span><span class="p">)</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">nodup_t</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">nodup_p</span> <span class="o">=</span> <span class="bp">None</span>

    <span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">topic</span><span class="p">,</span> <span class="n">payload</span><span class="p">):</span>
        <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">mq</span> <span class="ow">and</span> <span class="bp">self</span><span class="p">.</span><span class="n">mq</span><span class="p">.</span><span class="n">sock</span><span class="p">:</span>
            <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="nb">int</span><span class="p">):</span>
                <span class="n">payload</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">topic</span> <span class="o">!=</span> <span class="bp">self</span><span class="p">.</span><span class="n">nodup_t</span> <span class="ow">or</span> <span class="n">payload</span> <span class="o">!=</span> <span class="bp">self</span><span class="p">.</span><span class="n">nodup_p</span><span class="p">:</span>
                <span class="bp">self</span><span class="p">.</span><span class="n">mq</span><span class="p">.</span><span class="n">publish</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">prefix</span><span class="o">+</span><span class="n">topic</span><span class="p">,</span> <span class="n">payload</span><span class="p">)</span>
</code></pre></div></div>

<p>І в кінці файлу, там де ми ініціалізуємо всі процеси і складаємо їх в один клас <code class="language-plaintext highlighter-rouge">App</code>, додаємо виклик цього класу:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">app</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">WirelessMQTT</span><span class="p">(</span><span class="s">'192.168.0.123'</span><span class="p">,</span> <span class="s">'board99'</span><span class="p">))</span>
</code></pre></div></div>

<p>Коли ESPшка з’єднається з мережею і підключиться до брокера MQTT, то всі наші повідомлення почнуть передаватися назовні. І, відповідно, зовнішні повідомлення передаватимуться класам, таким як <code class="language-plaintext highlighter-rouge">BlinkingLED</code>.</p>

<p>Цей код не дуже елегантний, але він працює.</p>

<h3 id="зєднання-з-зовнішнім-світом-через-кабель">З’єднання з зовнішнім світом <em>через кабель</em></h3>

<p>Тепер черга для Raspberry Pi Pico, до якої можна дістатися через <code class="language-plaintext highlighter-rouge">/dev/ttyACM0</code> за допомогою <a href="/2024/03/18/mqtt-oop.html">програми <code class="language-plaintext highlighter-rouge">socat</code> та моїх скриптів</a>. Тут все інакше і дикіше.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">select</span>

<span class="k">class</span> <span class="nc">StdioConnector</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">prefix</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">prefix</span> <span class="o">=</span> <span class="n">prefix</span><span class="o">+</span><span class="s">'/'</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">nodup_t</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">nodup_p</span> <span class="o">=</span> <span class="bp">None</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">app</span><span class="p">):</span>
        <span class="n">poller</span> <span class="o">=</span> <span class="n">select</span><span class="p">.</span><span class="n">poll</span><span class="p">()</span>
        <span class="n">poller</span><span class="p">.</span><span class="n">register</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">stdin</span><span class="p">,</span> <span class="n">select</span><span class="p">.</span><span class="n">POLLIN</span><span class="p">)</span>
        <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
            <span class="n">res</span> <span class="o">=</span> <span class="n">poller</span><span class="p">.</span><span class="n">poll</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">res</span><span class="p">:</span>
                <span class="n">line</span> <span class="o">=</span> <span class="n">sys</span><span class="p">.</span><span class="n">stdin</span><span class="p">.</span><span class="n">readline</span><span class="p">().</span><span class="n">strip</span><span class="p">()</span>
                <span class="k">if</span> <span class="n">line</span><span class="p">:</span>
                    <span class="n">topic</span><span class="p">,</span> <span class="n">payload</span> <span class="o">=</span> <span class="n">line</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">' '</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
                    <span class="k">if</span> <span class="n">topic</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">prefix</span><span class="p">):</span>
                        <span class="n">topic</span> <span class="o">=</span> <span class="n">topic</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">prefix</span><span class="p">):]</span>
                        <span class="bp">self</span><span class="p">.</span><span class="n">nodup_t</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">nodup_p</span> <span class="o">=</span> <span class="n">topic</span><span class="p">,</span> <span class="n">payload</span>
                        <span class="n">app</span><span class="p">.</span><span class="n">handle</span><span class="p">(</span><span class="n">topic</span><span class="p">,</span> <span class="n">payload</span><span class="p">)</span>
                        <span class="bp">self</span><span class="p">.</span><span class="n">nodup_t</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">nodup_p</span> <span class="o">=</span> <span class="bp">None</span>
            <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    
    <span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">topic</span><span class="p">,</span> <span class="n">payload</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">topic</span> <span class="o">!=</span> <span class="bp">self</span><span class="p">.</span><span class="n">nodup_t</span> <span class="ow">or</span> <span class="n">payload</span> <span class="o">!=</span> <span class="bp">self</span><span class="p">.</span><span class="n">nodup_p</span><span class="p">:</span>
            <span class="k">print</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">prefix</span><span class="o">+</span><span class="n">topic</span><span class="p">,</span> <span class="n">payload</span><span class="p">)</span>
</code></pre></div></div>

<p>Отак.</p>

<h2 id="висновки">Висновки</h2>

<p>Я пропущу питання з кнопкою, тим більше що у мене нашвидкоруч зроблений варіант без debounce. Також пропущу незграбні моменти з тим, що onboard світлодіод на ESPшці інвертований, а на Pi Pico він нормальний. Просто обмежусь тим, що в «фінальній» версії (тобто фінальній на сьогодні) я блимаю світлодіодом у енергійному темпі, коли кнопка натиснена.</p>

<p>Головне: вся ця катавасія дійсно працює, як локально, так і з зовнішнім брокером MQTT. Як по Wi-Fi, так і по кабелю. Можна в Node-RED робити зв’язки однієї плати з іншою. Зашибісь.</p>

<ul>
  <li><a href="https://github.com/kastaneda/mpy_sandbox/blob/master/just_pep8/f/main.py">Код для ESP8266</a></li>
  <li><a href="https://github.com/kastaneda/mpy_sandbox/blob/master/just_pep8/f/main_rp2040.py">Код для RP2040</a></li>
</ul>

<p>Невелике відео демонстрації роботи на YouTube: <a href="https://youtu.be/_cbC2cjzg2k">https://youtu.be/_cbC2cjzg2k</a></p>


      ]]></description>
      <link>https://test.de.co.ua/2025/10/09/python-blinking-led.html</link>
      <guid>https://test.de.co.ua/2025/10/09/python-blinking-led.html</guid>
      <pubDate>Thu, 09 Oct 2025 02:40:59 +0300</pubDate>
    </item>

    <item>
      <title>Надлишковість</title>
      <description><![CDATA[

<p>Колись проект Firmata допоміг мені усвідомити одну важливу ідею: сигнал може бути надлишковим, і за певних умов це може бути нормально.</p>

<p>Якщо під’єднати якусь умовну кнопку до GPIO пристрою з прошивкою Firmata, сконфігурувати пристрій на режим читання цього GPIO, то після цього хост отримає потік сигналів зі станом кнопки. Просто рівномірний потік даних на стандартній швидкості 57600 bps. По духу як аналогова телеметрія.</p>

<p>Звичний для мене підхід — передавати лише зміни стану цієї клятої кнопки. Це економно, бо ми передаємо тільки те що треба і тільки тоді коли треба. Це зручно, бо легше на такі повідомлення дивитися людськими очіма під час відлагодження, бо нема «стіни» однакових рядків.</p>

<p>А з іншого боку, треба пам’ятати про <a href="https://wiki.c2.com/?PrematureOptimization">корінь усього зла</a>.</p>

<p>Прості і тупі рішення мають свої переваги.</p>


      ]]></description>
      <link>https://test.de.co.ua/2025/10/08/nadlyshkovist.html</link>
      <guid>https://test.de.co.ua/2025/10/08/nadlyshkovist.html</guid>
      <pubDate>Wed, 08 Oct 2025 07:19:13 +0300</pubDate>
    </item>

    <item>
      <title>45</title>
      <description><![CDATA[

<p>45</p>

<p>Cross-post: <a href="https://kastaneda.kiev.ua/2025/09/06/45.html">https://kastaneda.kiev.ua/2025/09/06/45.html</a><br />
Cross-post: <a href="https://kastaneda.dreamwidth.org/559424.html">https://kastaneda.dreamwidth.org/559424.html</a></p>

      ]]></description>
      <link>https://test.de.co.ua/2025/09/06/45.html</link>
      <guid>https://test.de.co.ua/2025/09/06/45.html</guid>
      <pubDate>Sat, 06 Sep 2025 04:56:07 +0300</pubDate>
    </item>

    <item>
      <title>Beats per minute</title>
      <description><![CDATA[

<p>Я слухаю дуже різну музику.</p>

<p>Іноді мені сильно не вистачає швидкої енергійної електронної музики з високим BPM. Тоді я щось шукаю, іноді щось знаходжу. Ось, наприклад, кілька років тому знайшовся цікавий трек <a href="https://www.youtube.com/watch?v=kYllWdvCr2Q">Kopophobia — Out Of Time</a>. Бадьоро, дуже непогано, але.</p>

<p>Але куди йому до божественного треку <a href="https://www.youtube.com/watch?v=qvlQn2Ckoa0&amp;list=PLCEF9A47DD876875E&amp;index=3">Lotus III Theme</a>.</p>


      ]]></description>
      <link>https://test.de.co.ua/2025/08/20/beats-per-minute.html</link>
      <guid>https://test.de.co.ua/2025/08/20/beats-per-minute.html</guid>
      <pubDate>Wed, 20 Aug 2025 13:07:41 +0300</pubDate>
    </item>

</channel>
</rss>
