konyahin https://konyahin.xyz/ Recent content on konyahin Hugo -- gohugo.io ru-ru Sat, 01 Oct 2022 19:57:44 +0300 sttemp - пишем простой менеджер шаблонов https://konyahin.xyz/blog/sttemp/ Sun, 25 Feb 2024 15:11:58 +0300 https://konyahin.xyz/blog/sttemp/ <p>Как-то раз, мне захотелось собрать свои заготовки мейкфайлов, скелеты для скриптов и man страниц, тексты лицензий и прочую ерунду в одной директории и научиться удобно их использовать. Я не люблю привязанные к редактору схемы, так что мне нужен был максимально простой консольный менеджер шаблонов. Ну а люблю я костыли и велосипеды, поэтому такой менеджер я решил написать себе сам.</p> <p>Первая версия была написана на C, использовала максимально простую реализацию и максимально уродский синтаксис для подстановки значений в шаблоны. Вот такой вот: <code>{|variable|}</code>.</p> <p>Программа искала шаблон по имени в захардкоженной директории, построчно обрабатывала его и, если находила подстановку сначала искала переменную окружения с таким именем, а потом, в случае если она отсутствовала, спрашивала значение у пользователя на stderr.</p> <p>Почему на stderr? Чтобы stdout можно было перенаправить в файл и всё равно увидеть, что прога чего-то от тебя хочет.</p> <p>Так она и работал, причем вполне неплохо, чуть больше двух лет, если верить <a href="https://git.konyahin.xyz/sttemp/log.html">истории коммитов</a>, пока я не решил переписать её на языке Shell. Я хотел упростить синтаксис подстановок, передать почти всю работу <a href="https://www.gnu.org/software/gettext/manual/html_node/envsubst-Invocation.html">envsubst</a> из состава GNU gettext utilities, и реализовать ещё несколько идей, которые позволили мне вокруг простой программы сделать такую же простую и маленькую экосистему использования.</p> <p>Основная идея не поменялась. Всё так же есть директория с шаблонами, правда она переехала в <code>$XDG_DATA_HOME/.local/share/sttemp</code>, в знак уважения к <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">free desktop</a>, в них есть подстановки, но уже более привычного вида, в духе <code>$VARIABLE</code> и мы либо находим их в переменных окружения, либо спрашиваем у пользователя на stderr.</p> <p>Shell и envsubst позволили мне сильно сократить размер программы, я удалил из репозитория 408 строк, вместе с .h и Makefile, и добавил 62, получив аналогичную программу. Она настолько проста, что не буду останавливаться на ней подробнее, приведу только одну функцию, которая будет иметь значение для дальнейшего повествования. А именно - <code>ask</code>. Всё что она делает, это принимает на вход имя подстановки из шаблона, запрашивает её значение у пользователя и возвращает текстом из себя.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ask <span class="o">()</span> <span class="o">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">echo</span> <span class="s2">&#34;Enter </span><span class="nv">$1</span><span class="s2">:&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span> </span></span><span class="line"><span class="cl"> <span class="nb">read</span> -r value </span></span><span class="line"><span class="cl"> <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$value</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl"><span class="o">}</span> </span></span></code></pre></div><p>Покончив с переписыванием, я двинулся к следующей идее. Я хотел иметь возможность использовать те же самые шаблоны, по точно такому сценарию, но уже в GUI приложениях. В редакторе, в браузере, где мне будет угодно. Так родился скрипт <code>dsttemp</code>.</p> <p>Главной проблемой было то, как же мне спросить у пользователя, что он хочет подставить в шаблон. На помощь пришла утилита <code>dmenu</code>, от ребят из <a href="https://suckless.org/">suckless</a>. Утилита невероятно простая и настолько же невероятно полезная. Всё что она делает, это читает со стандартного ввода список вариантов, разделенных переносом строки, а потом позволяет пользователю, с помощью ввода какой-то части варианта, выбрать нужный, который и возвращает. Если вы когда-либо использовали <a href="https://github.com/junegunn/fzf">fzf</a>, то должны понять как оно работает. При этом, если пользователь вводит что-то не из набора изначальных вариантов, то возвращен будет этот самый ввод. В итоге, мы можем переписать нашу функцию <code>ask</code> как-то так:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ask <span class="o">()</span> <span class="o">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">echo</span> <span class="k">$(</span>dmenu -p <span class="s2">&#34;Enter </span><span class="nv">$1</span><span class="s2">&#34;</span> &lt;/dev/null<span class="k">)</span> </span></span><span class="line"><span class="cl"><span class="o">}</span> </span></span></code></pre></div><p>Через <code>-p</code> мы задаем промпт для пользователя, а перенаправление <code>/dev/null</code> на вход нам нужно чтобы никаких вариантов пользователю не высвечивалось. Осталась одна проблема. Эта функция написана в одном файле - <code>dsttemp</code>, а вызывается в совсем другом, в <code>sttemp</code>.</p> <p>Для решения этой проблемы я засунул код функции в переменную окружения <code>STTEMP_ASK</code>, а в <code>sttemp</code> добавил следующий код:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="o">[</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">STTEMP_ASK</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">eval</span> <span class="s2">&#34;</span><span class="nv">$STTEMP_ASK</span><span class="s2">&#34;</span> </span></span></code></pre></div><p>Я уже говорил, что люблю костыли? В итоге, <code>dsttemp</code> принял следующий вид:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env sh </span></span></span><span class="line"><span class="cl"><span class="cp"></span> </span></span><span class="line"><span class="cl"><span class="nb">set</span> -eu </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">STTEMP_ASK</span><span class="o">=</span><span class="k">$(</span> </span></span><span class="line"><span class="cl"> cat <span class="s">&lt;&lt;&#39;EOF&#39; </span></span></span><span class="line"><span class="cl"><span class="s">ask () { </span></span></span><span class="line"><span class="cl"><span class="s"> echo $(dmenu -p &#34;Enter $1&#34; &lt;/dev/null) </span></span></span><span class="line"><span class="cl"><span class="s">} </span></span></span><span class="line"><span class="cl"><span class="s">EOF</span> </span></span><span class="line"><span class="cl"><span class="k">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nv">TEMPL_NAME</span><span class="o">=</span><span class="k">$(</span>sttemp -l <span class="p">|</span> dmenu<span class="k">)</span> </span></span><span class="line"><span class="cl"><span class="nv">TEMPL_TEXT</span><span class="o">=</span><span class="k">$(</span>sttemp <span class="s2">&#34;</span><span class="nv">$TEMPL_NAME</span><span class="s2">&#34;</span><span class="k">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$TEMPL_TEXT</span><span class="s2">&#34;</span> <span class="p">|</span> xclip -selection clipboard </span></span><span class="line"><span class="cl">xdotool key --clearmodifiers <span class="s2">&#34;Shift+Insert&#34;</span> </span></span></code></pre></div><p>Сначала мы засовываем в переменную окружения код, которым хотим заменить оригинальную, консольную, функцию <code>ask</code>. Потом, тоже через <code>dmenu</code> предлагаем пользователю выбрать шаблон из списка, который возвращает <code>sttemp -l</code>. Дождавшись, когда пользователь заполнит все нужные переменные и к нам попадет итоговый текст, мы передаем его в иксовый буфер обмена, из которого вставляем его туда, где стоит курсор пользователя. В иксах несколько буферов обмена, возможно конкретный буфер и конкретную комбинацию клавиш нужно будет настроить исходя из того, каким окружением вы пользуетесь. У меня в <code>dwm</code> всё работает как часы.</p> <p>Как видите, мы можем заскриптовать своё поведение в иксах с помощью консольных утилит, таких как <code>xclip</code> и <code>xdotool</code>. Это круто и это играет важную роль в следующем, и финальном на сегодня, скрипте.</p> <p>Я читаю достаточно много статей в браузере и иногда мне хочется скопировать часть текста и отправить в свои заметки, чтобы потом над этим текстом отдельно подумать. В общем, хотелось что-то вроде Pocket, но попроще и чтобы он работал на меня, а не на Mozilla Corporation. И конечно же я не хочу ни писать, ни искать браузерное расширение. Я предпочитаю расширять Unix, а не браузер. Браузер и так расширился дальше некуда.</p> <p>Для начала я набросал вот такой шаблон, назвав его <code>webnote</code>.</p> <pre tabindex="0"><code># $TITLE $BODY Source: $URL </code></pre><p>Заголовок я введу сам, чтобы понимать что и для чего я вообще решил сохранить, а вот текст заметки и её источник должны заполняться скриптом на основании того, что и на какой странице в браузере я выделил.</p> <p>С телом нет никаких проблем, просто считаем выделение в переменную.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">BODY</span><span class="o">=</span><span class="k">$(</span>xclip -o<span class="k">)</span> </span></span></code></pre></div><p>С источником заметки история чуть более хитрая. Мы опять заскриптуем своё поведение в иксах, а именно нажатие Ctrl + l, для выделения содержимого address bar (у меня Chromium btw), а потом скопируем его и заключим в очередную переменную.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">xdotool key --clearmodifiers <span class="s2">&#34;Ctrl+l&#34;</span> </span></span><span class="line"><span class="cl">xdotool key --clearmodifiers <span class="s2">&#34;Ctrl+c&#34;</span> </span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">URL</span><span class="o">=</span><span class="k">$(</span>xclip -o -selection primary<span class="k">)</span> </span></span><span class="line"><span class="cl">xdotool key --clearmodifiers <span class="s2">&#34;Escape&#34;</span> </span></span></code></pre></div><p>Escape мы нажмем просто чтобы вернуться к тому состоянию, в котором пользователь начал выполнение скрипта. Теперь у нас есть всё, кроме заголовка, который пользователь заполнит сам. Осталось просто вызвать <code>sttemp</code> и сохранить его вывод туда, куда мы хотим.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nv">FILE_NAME</span><span class="o">=</span><span class="k">$(</span>date <span class="s1">&#39;+%Y-%m-%d-%H%M%S&#39;</span><span class="k">)</span> </span></span><span class="line"><span class="cl">sttemp webnote &gt;&gt; <span class="s2">&#34;/home/anton/public_gopher/web/</span><span class="nv">$FILE_NAME</span><span class="s2">.md&#34;</span> </span></span></code></pre></div><p>Вот собственно и всё. Полный исходный код доступен как на <a href="https://github.com/konyahin/sttemp">Github</a>, так и на <a href="https://git.konyahin.xyz/sttemp/log.html">self host git</a>. Зачем я вам всё это рассказал? Меня очаровывает тот факт, что даже простые программы, если объединять их с другими простыми программами, могут давать крутые эффекты, и даже наши действия в GUI можно и нужно скриптовать. А очаровывает ли это вас?</p> Портируем brogue на OpenBSD https://konyahin.xyz/blog/brogue-to-openbsd/ Fri, 19 May 2023 21:31:16 +0300 https://konyahin.xyz/blog/brogue-to-openbsd/ <p>В университетские годы, практически сразу после перехода на Linux, я от таких игр как Gothic 2, Morrowind и Fallout, перешел к новым развлечениям: многопользовательским <a href="https://www.bylins.su/">текстовым MUDам</a> и рогаликам. Так как друзей, готовых наслаждаться текстовыми битвами, у меня не нашлось, а я был слишком робок чтобы влиться в коллектив уже играющих людей, то мады пришлось забросить и остановиться на оффлайновых развлечениях.</p> <p>Перепробовав несколько разных рогаликов я прикипел душой к самому красочному и простому из них - Brogue. Мне нравилось его изучать, разбираться как работают те или иные механики, нравилось исследовать этажи, ища новые свитки, оружие или хотя бы немного еды. Мне нравилось то, как игра своей случайностью создавала для меня уникальные игровые ситуации, из которых я так же уникально выбирался. Либо же уникально умирал.</p> <p>Так и не пройдя её полностью, я, тем не менее, провел за игрой какое-то количество часов и она оставила в моей памяти несколько приятных воспоминаний. Недавно я снова вспомнил о ней и обнаружил, что в OpenBSD игру не завезли. Я нашел <a href="https://github.com/tmewett/BrogueCE/">репозиторий с кодом</a> и убедился, что с малыми правками она прекрасно собирается и работает на моей системе, просто никто не оформил порт. Так почему бы не сделать это самому?</p> <h2 id="готовимся-к-работе">Готовимся к работе</h2> <p>Все новые порты делаются для <code>current</code> <a href="https://www.openbsd.org/faq/faq5.html#Flavors">версии OpenBSD</a>. Обновитесь хотя бы <a href="https://www.openbsd.org/faq/current.html">до снапшота</a>, чтобы ваша локальная система не сильно отличалась от той, в которую порт будут принимать.</p> <p>Для работы над новым портом вам нужно дерево уже существующих. Обычно его кладут в <code>/usr/ports</code>. Чтобы не собирать порты под рутом и не класть их в домашнюю директорию - добавьте себя в группу <code>wsrc</code>.</p> <pre tabindex="0"><code>doas user mod -G wsrc username </code></pre><p>Теперь вам нужно определиться с тем, с какого зеркала вы будете забирать себе изменения в дереве. Выбор достаточно большой, ознакомьтесь со списком <a href="https://www.openbsd.org/anoncvs.html">вот тут</a>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># замените на то, что выбрали выше</span> </span></span><span class="line"><span class="cl"><span class="nv">USER</span><span class="o">=</span>anoncvs </span></span><span class="line"><span class="cl"><span class="nv">HOST</span><span class="o">=</span>mirror.osn.de </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">cd</span> /usr </span></span><span class="line"><span class="cl">doas mkdir -m <span class="m">775</span> ports </span></span><span class="line"><span class="cl">doas chown root:wsrc ports/ </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># запускаем именно из /usr</span> </span></span><span class="line"><span class="cl">cvs -qd <span class="nv">$USER</span>@<span class="nv">$HOST</span>:/cvs checkout -P ports </span></span></code></pre></div><p>Чтобы сэкономить время и трафик, можно вместо выкачивания всех портов через cvs предзагрузить текущий срез с какого-нибудь <a href="https://www.openbsd.org/ftp.html">сервера</a>. Тогда скрипт для выкачки будет выглядеть как-то так.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># замените на то, что выбрали выше</span> </span></span><span class="line"><span class="cl"><span class="nv">HOST</span><span class="o">=</span>mirror.yandex.ru </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">cd</span> /tmp </span></span><span class="line"><span class="cl">wget <span class="nv">$HOST</span>/pub/OpenBSD/snapshots/ports.tar.gz </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">cd</span> /usr </span></span><span class="line"><span class="cl">doas tar xzf /tmp/ports.tar.gz </span></span><span class="line"><span class="cl">doas chown -R :wsrc ports/ </span></span><span class="line"><span class="cl">doas chmod -R <span class="m">775</span> ports/ </span></span></code></pre></div><p>У снапшотов <a href="https://marc.info/?l=openbsd-ports&amp;m=147586620109500&amp;w=2">нет .sig файла</a>, так что придется обойтись без проверки подписи.</p> <p>Чтобы cvs знал, откуда брать обновления, после скачивания портов выполните следующий скрипт, либо выставьте переменную окружения <code>CVSROOT</code>, либо используйте ключ командной строки <code>-qd</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># замените на то, что выбрали выше</span> </span></span><span class="line"><span class="cl"><span class="nv">USER</span><span class="o">=</span>anoncvs </span></span><span class="line"><span class="cl"><span class="nv">HOST</span><span class="o">=</span>mirror.osn.de </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">cd</span> /usr/ports </span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$USER</span><span class="s2">@</span><span class="nv">$HOST</span><span class="s2">:/cvs&#34;</span> &gt; CVS/Root </span></span></code></pre></div><p>Чтобы не возвращаться к этой теме потом, сразу замечу, что порты надо держать в актуальном состоянии, периодически выкачивая себе новые изменения. Делается этот вот так.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">cd</span> /usr/ports </span></span><span class="line"><span class="cl">cvs -q up -Pd -A </span></span></code></pre></div><h2 id="куда-положить-и-как-скачать">Куда положить и как скачать</h2> <p>В <code>/usr/ports</code> много директорий, и большая часть из них - категории, содержащие порты. Так как мы портируем игру, то тут с категориями все просто, мы берем <code>games</code> и создаем директорию <code>brogue</code> внутри. Давайте скопируем в неё шаблон <code>Makefile</code>, который мы будем потихоньку заполнять.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">mkdir -p /usr/ports/games/brogue </span></span><span class="line"><span class="cl">cp /usr/ports/infrastructure/templates/Makefile.template <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> /usr/ports/games/brogue/Makefile </span></span></code></pre></div><p>Большая часть строк в <code>Makefile</code> закомментирована, мы заполним всё обязательное и будем раскомментировать интересующие нас строки в процессе работы над портом.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-make" data-lang="make"><span class="line"><span class="cl"><span class="nv">COMMENT</span> <span class="o">=</span> roguelike game by Brian Walker with X11 support </span></span><span class="line"><span class="cl"><span class="nv">CATEGORIES</span> <span class="o">=</span> games </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nv">HOMEPAGE</span> <span class="o">=</span> https://sites.google.com/site/broguegame/ </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nv">MAINTAINER</span> <span class="o">=</span> Anton Konyahin &lt;me@konyahin.xyz&gt; </span></span></code></pre></div><p>Хоть <code>DISTNAME</code> и <code>MASTER_SITES</code> раскомментированы с самого начала, нам они не нужны, просто удалите эти строчки. Brogue живет на гитхабе и для него у нас есть очень удобный набор переменных, делающих ненужными эти две.</p> <p>Обратите внимание на то, что после знака равно стоит знак табуляции. Это стандарт оформления, так что лучше его придерживаться.</p> <p>Давайте укажем переменные, необходимые для скачивания исходников с github.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-make" data-lang="make"><span class="line"><span class="cl"><span class="nv">GH_ACCOUNT</span> <span class="o">=</span> tmewett </span></span><span class="line"><span class="cl"><span class="nv">GH_PROJECT</span> <span class="o">=</span> BrogueCE </span></span><span class="line"><span class="cl"><span class="nv">GH_TAGNAME</span> <span class="o">=</span> v1.12 </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nv">PKGNAME</span> <span class="o">=</span> brogue-<span class="si">${</span><span class="nv">GH_TAGNAME</span><span class="p">:</span><span class="nv">S</span><span class="p">/v//</span><span class="si">}</span> </span></span></code></pre></div><p>Названия переменных говорят сами за себя, мы последовательно заполняем имя аккаунта, имя проекта и тег, которым помечен последний релиз. Если апстрим не использует теги для релизов, можно сослаться на конкретный коммит, заполнив переменную <code>GH_COMMIT</code>.</p> <p>Здесь же мы указали имя пакета, добавив к названию игры версию из имени тега. Команда, использованная через двоеточие, заменяет (S - substitute) v на пустую строку, что в итоге даст нам просто <code>1.12</code>.</p> <p>Чтобы при сборке можно было проверить, что скачиваемый из интернета архив не подменили злоумышленники, в каждом порте хранится чексумма архива с исходниками. Давайте сохраним её и сразу же проверим.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">make makesum </span></span><span class="line"><span class="cl">make checksum </span></span></code></pre></div><p>Вы получите несколько предупреждений о том, что не всё заполнено в нашем <code>Makefile</code>, пока игнорируйте их. В конце вывода второй команды вы должны увидеть строку <code>&gt;&gt; (SHA256) BrogueCE-1.12.tar.gz: OK</code>, сигнализирующую о том, что с архивом (и подписью) всё в порядке.</p> <p>После выполнения <code>make makesum</code> в директории с портом должен появиться файл <code>distinfo</code>, в нем хранится SHA256 подпись архива и его размер.</p> <p>Теперь мы можем разархивировать архив командой <code>make extract</code>. Файлы будут сложены в <code>/usr/ports/pobj/ИМЯ ПАКЕТА/ИМЯ АРХИВА/</code>. Но искать его не нужно, просто сделайте <code>make show=WRKSRC</code>.</p> <p>Следующим важным вопросом является вопрос лицензий. Для разработчиков важно, чтобы лицензия как на само ПО, так и на используемые им ресурсы, позволяла опакечивание и распространение в бинарном виде. Если лицензия этого сделать не позволяет, то пользователю придется самому собирать приложение из портов.</p> <p>В случае с Brogue все достаточно просто: код распространяется под GNU Affero General Public License, а все игровые ресурсы - под Creative Commons Attribution-ShareAlike 4.0. Понять это можно ознакомившись с двумя <code>LICENSE.txt</code> файлами, один из которых лежит в корне проекта, а второй в <code>/bin/assets</code>. Эти лицензии позволяют опакечивание ПО, так что укажем это в нашем <code>Makefile</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-make" data-lang="make"><span class="line"><span class="cl"><span class="c"># Code: AGPLv3+ </span></span></span><span class="line"><span class="cl"><span class="c"># Assets: CC BY-SA 4.0 </span></span></span><span class="line"><span class="cl"><span class="c"></span><span class="nv">PERMIT_PACKAGE</span> <span class="o">=</span> Yes </span></span></code></pre></div><h2 id="собираем-и-патчим">Собираем и патчим</h2> <p>К счастью, Brogue для сборки использует обычный <code>Makefile</code> (хоть и содержащий GNU расширения). Собрать проект будет просто. Сразу укажем несколько зависимостей, проект не соберется без установленных пакетов sdl2 и sdl2-image.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-make" data-lang="make"><span class="line"><span class="cl"><span class="nv">USE_GMAKE</span> <span class="o">=</span> Yes </span></span><span class="line"><span class="cl"><span class="c"># в рассылке посоветовали указать, так как тестов в проекте нет </span></span></span><span class="line"><span class="cl"><span class="c"></span><span class="nv">NO_TEST</span> <span class="o">=</span> Yes </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c"># указываем, какую цель из Makefile будем собирать </span></span></span><span class="line"><span class="cl"><span class="c"></span><span class="nv">ALL_TARGET</span> <span class="o">=</span> bin/brogue </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nv">WANTLIB</span> <span class="o">=</span> c m SDL2 SDL2_image </span></span><span class="line"><span class="cl"><span class="nv">LIB_DEPENDS</span> <span class="o">=</span> devel/sdl2 devel/sdl2-image </span></span></code></pre></div><p>c это стандартная библиотека языка C, m - такая же математическая библиотека. В <code>WANTLIB</code> мы указываем библиотеки так, как указали бы их для линковки (<code>-lm -lSDL2_image</code>), а в <code>LIB_DEPENDS</code> пишем названия портов с категориями, необходимых для работы программы.</p> <p>Теперь, прямо в <em>директории порта</em>, набираем <code>make build</code>. Игра собирается и мы даже можем её запустить, набрав в директории с исходниками <code>./brogue</code>. Однако, при сборке мы видим некоторое количество варнингов, которые было бы неплохо исправить.</p> <p>Первым делом избавимся от предупреждения о неизвестной опции компилятора. Для этого мы создадим копию <code>Makefile</code> с расширением <code>.orig.port</code> и поправим оригинал.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">cd</span> <span class="sb">`</span>make <span class="nv">show</span><span class="o">=</span>WRKSRC<span class="sb">`</span> </span></span><span class="line"><span class="cl">cp Makefile Makefile.orig.port </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># убираем опцию из Makefile</span> </span></span><span class="line"><span class="cl">vi Makefile </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># возвращаемся директорию порта</span> </span></span><span class="line"><span class="cl"><span class="nb">cd</span> - </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">make update-patches </span></span></code></pre></div><p>Последняя команда автоматически создаст патч с правильным именем, поместит его в директорию <code>patches</code> и откроет на редактирование в выбранном вами <code>$EDITOR</code>. Не спешите закрывать файл, хорошим тоном считается указать в начале файла что и зачем вы редактировали. Например вот так.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-patch" data-lang="patch"><span class="line"><span class="cl"> - drop unknown warning -Wformat-overflow=0 </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="gh">Index: Makefile </span></span></span><span class="line"><span class="cl"><span class="gh"></span><span class="gd">--- Makefile.orig </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+++ Makefile </span></span></span><span class="line"><span class="cl"><span class="gi"></span>Дальше идет текст патча... </span></span></code></pre></div><p>Схожим образом можно поправить и остальные предупреждения компилятора. Но не все они чинятся так легко. Например, Brogue использует небезопасные функции для работы со строками в сотне мест, исправить их все будет крайне трудозатратно и это добавит в порт огромную кучу патчей. Так как проект старый и представляет собой всего лишь игру - я решил их не трогать, но для чего-то более серьезного стоило бы приложить усилия к исправлению (только не патчами в порте, а сразу в апстриме.</p> <h2 id="flavors-и-флаги-сборки">Flavors и флаги сборки</h2> <p>Если внимательно посмотреть на <code>Makefile</code> brogue, можно увидеть несколько флагов сборки. Самым интересным из них будет <code>DATADIR</code>, позволяющий указать, в какой директории игра будет искать свои ассеты. По умолчанию это текущая директория, что неприемлемо для порта, ведь исполняемый файл должен лежать в <code>/usr/local/bin/</code>, а необходимые приложению ресурсы в <code>/usr/local/share/&lt;name&gt;</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-make" data-lang="make"><span class="line"><span class="cl"><span class="nv">MAKE_FLAGS</span> <span class="o">+=</span> <span class="nv">DATADIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">PREFIX</span><span class="si">}</span><span class="s2">/share/brogue&#34;</span> </span></span></code></pre></div><p>Второе, на что стоит обратить внимание, это возможность собрать brogue как в графическом, так и в консольном варианте (ещё есть web версия, но её мы оставим где лежит). Порты позволяют нам собирать несколько вариантов пакета с помощью механизма flavors, и даже несколько отдельных пакетов (например для утилиты, для GUI под нее и для документации) с помощью multi packages. В данном случае нам нужен именно flavors, который мы, аналогично пакетам со схожими вариантами, назовем <code>no_x11</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-make" data-lang="make"><span class="line"><span class="cl"><span class="nv">FLAVORS</span> <span class="o">=</span> no_x11 </span></span><span class="line"><span class="cl"><span class="nv">FLAVOR</span> <span class="o">?=</span> </span></span></code></pre></div><p>Чтобы поменять флаги сборки мы будем ориентироваться на переменную <code>FLAVOR</code>. Для сравнения её с <code>no_x11</code> мы воспользуемся модификатором <code>M</code>, подробнее о котором, как и о упоминавшемся выше <code>S</code>, можно почитать в <code>man make</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-make" data-lang="make"><span class="line"><span class="cl"><span class="nv">WANTLIB</span> <span class="o">=</span> c m </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nf">.if ${FLAVOR</span><span class="o">:</span><span class="n">Mno_x</span>11} </span></span><span class="line"><span class="cl"><span class="nv">WANTLIB</span> <span class="o">+=</span> curses </span></span><span class="line"><span class="cl"><span class="nv">MAKE_FLAGS</span> <span class="o">+=</span> <span class="nv">TERMINAL</span><span class="o">=</span>YES <span class="nv">GRAPHICS</span><span class="o">=</span>NO </span></span><span class="line"><span class="cl"><span class="err">.else</span> </span></span><span class="line"><span class="cl"><span class="nv">WANTLIB</span> <span class="o">+=</span> SDL2 SDL2_image </span></span><span class="line"><span class="cl"><span class="nv">LIB_DEPENDS</span> <span class="o">+=</span> devel/sdl2 devel/sdl2-image </span></span><span class="line"><span class="cl"><span class="err">.endif</span> </span></span></code></pre></div><p>Обратите внимание, что мы поменяли <code>WANTLIB</code>, оставив там только библиотеки, общие для всех вариантов пакета, а <code>LIB_DEPENDS</code> перенесли внутрь условия, так как консольная версия не имеет дополнительных зависимостей.</p> <p>В начало добавим отдельное описание для варианта без поддержки иксов.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-make" data-lang="make"><span class="line"><span class="cl"><span class="nv">COMMENT-no_x11</span> <span class="o">=</span> roguelike game by Brian Walker </span></span></code></pre></div><p>Чтобы выполнять операции сборки и пакетирования не над основным пакетом, а над вариацией без иксов, нам нужно будет выставить переменную окружения. Например, вот так <code>env FLAVOR=no_x11 make build</code>. Обратите внимание, что флейвор собирается в отдельной директории и <code>WRKSRC</code> у неё будет другой.</p> <h2 id="добавляем-свои-файлы-в-порт">Добавляем свои файлы в порт</h2> <p>Если собрать игру сейчас и немного в неё поиграть, вы заметите что она любит создавать самые разные файлы (сохранения, записи и т.п.) прямо в текущей рабочей директории. Нет никакой возможности указать нужную директорию при сборке, такое поведение зашито в код сразу в нескольких местах. Это крайне неудобно и поначалу я пытался пропатчить все эти места так, чтобы они использовали новый флаг сборки, введенный мною. Но большое количество патчей это сложное и хрупкое решение. К счастью, в рассылке мне подсказали более элегантный способ, спасибо <a href="https://www.omarpolo.com/">Omar Polo</a> за это и многие другие улучшения моего порта.</p> <p>Мы переместим исполняемый файл игры в <code>/usr/local/libexec</code>, где лежат все дополнительные исполняемые файлы портов, а в <code>/usr/local/bin</code> мы положим небольшой скрипт, который определит директорию с сохранениями для текущего пользователя, сделает её текущей и уже там запустит игру.</p> <p>Дополнительные файлы для порта должны лежать в директории <code>files</code>. Создадим там файл <code>brogue</code> со следующим содержимым.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="cp">#!/bin/sh </span></span></span><span class="line"><span class="cl"><span class="cp"></span> </span></span><span class="line"><span class="cl"><span class="nb">set</span> -e </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nv">userdir</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">XDG_DATA_HOME</span><span class="k">:-</span><span class="nv">$HOME</span><span class="p">/.local/share</span><span class="si">}</span><span class="s2">/Brogue&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">mkdir -p <span class="s2">&#34;</span><span class="nv">$userdir</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl"><span class="nb">cd</span> <span class="s2">&#34;</span><span class="nv">$userdir</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl"><span class="nb">exec</span> <span class="si">${</span><span class="nv">TRUEPREFIX</span><span class="si">}</span>/libexec/brogue <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span> </span></span></code></pre></div><h2 id="установка-и-plist">Установка и PLIST</h2> <p>У brogue нет нормального установочного скрипта в <code>Makefile</code>, так что нам придется взять эту обязанность на себя.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-make" data-lang="make"><span class="line"><span class="cl"><span class="nf">do-install</span><span class="o">:</span> </span></span><span class="line"><span class="cl"> <span class="si">${</span><span class="nv">INSTALL_PROGRAM</span><span class="si">}</span> <span class="si">${</span><span class="nv">WRKBUILD</span><span class="si">}</span>/bin/brogue <span class="si">${</span><span class="nv">PREFIX</span><span class="si">}</span>/libexec/ </span></span><span class="line"><span class="cl"> <span class="si">${</span><span class="nv">SUBST_PROGRAM</span><span class="si">}</span> <span class="si">${</span><span class="nv">FILESDIR</span><span class="si">}</span>/brogue <span class="si">${</span><span class="nv">PREFIX</span><span class="si">}</span>/bin/brogue </span></span><span class="line"><span class="cl"> <span class="si">${</span><span class="nv">INSTALL_DATA_DIR</span><span class="si">}</span> <span class="si">${</span><span class="nv">PREFIX</span><span class="si">}</span>/share/brogue/assets </span></span><span class="line"><span class="cl"> <span class="si">${</span><span class="nv">INSTALL_DATA</span><span class="si">}</span> <span class="si">${</span><span class="nv">WRKDIST</span><span class="si">}</span>/bin/assets/tiles.png <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> <span class="si">${</span><span class="nv">PREFIX</span><span class="si">}</span>/share/brogue/assets/tiles.png </span></span><span class="line"><span class="cl"> <span class="si">${</span><span class="nv">INSTALL_DATA</span><span class="si">}</span> <span class="si">${</span><span class="nv">WRKDIST</span><span class="si">}</span>/bin/assets/tiles.bin <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> <span class="si">${</span><span class="nv">PREFIX</span><span class="si">}</span>/share/brogue/assets/tiles.bin </span></span><span class="line"><span class="cl"> <span class="si">${</span><span class="nv">INSTALL_DATA</span><span class="si">}</span> <span class="si">${</span><span class="nv">WRKDIST</span><span class="si">}</span>/bin/assets/icon.png <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> <span class="si">${</span><span class="nv">PREFIX</span><span class="si">}</span>/share/brogue/assets/icon.png </span></span></code></pre></div><p>Мы добавили новую цель, которая будет использоваться во время сборки пакета, для проведение &ldquo;фейковой&rdquo; установки. Наберите в директории порта <code>make fake</code>, а затем <code> cd $(make show=WRKINST)</code>. Вы окажетесь в директории <code>fake-amd64</code> (если вы на amd64, а не на Байкале, конечно), в которую был установлен ваш пакет. Можете убедиться, что файлы порта лежат именно там, где вы ожидаете, на следующем шаге именно фейковая установка послужит основой для описания того, что где и с какими правами должно лежать после установки на реальной системе. Больше про фейковую установку можно <a href="https://man.openbsd.org/bsd.port.mk#THE_FAKE_FRAMEWORK">почитать тут</a>.</p> <p>Вернитесь в директорию порта и наберите <code>make update-plist</code>. Когда скрипт отработает, он сообщит вам что создал новый файл - <code>pkg/PLIST</code>.</p> <pre tabindex="0"><code>bin/brogue @bin libexec/brogue share/brogue/ share/brogue/assets/ share/brogue/assets/icon.png share/brogue/assets/tiles.bin share/brogue/assets/tiles.png </code></pre><p>В нашем случае он выглядит достаточно просто - это перечисление файлов и директорий, с одним только <code>@bin</code> напротив исполняемого файла. Но PLIST может больше, указывать права на файлы, группы пользователей и даже запускать какие-то программы при установке пакета. О формате и его возможностей <a href="https://man.openbsd.org/pkg_create.1#PACKING-LIST_DETAILS">написано здесь</a>.</p> <p>Дело осталось за малым - добавьте описание пакета в файл <code>pkg/DESCR</code> и проверьте, не забыли ли вы чего, запустив <code>/usr/ports/infrastructure/bin/portcheck</code> и <code>make port-lib-depends-check</code>. Если всё хорошо, то смело набирайте <code>make package</code>, пора поставить пакет в вашу систему и хорошо всё потестировать.</p> <p>При установке пакета <code>pkg_add</code> проверяет его подпись, но наш пакет самособранный и подписи не имеет. К тому же, он находится не в репозитории с остальными пакетами, а в вашем разделе с портами. Решить обе проблемы легко.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PKG_PATH</span><span class="o">=</span>/usr/ports/packages/amd64/all/ </span></span><span class="line"><span class="cl">doas pkg_add -D unsigned brogue </span></span></code></pre></div><p>Если все работает хорошо и стабильно - пакет ставится и удаляется без проблем, сохранения работают и лишние файлы нигде не появляются, поздравляю - у вас есть ваш собственный пакет. Если вы хотите осчастливить им мир - обратитесь в рассылку <code>ports@openbsd.org</code>. Если на ваш порт не обращают внимания не расстраивайтесь, их поддержкой занимаются волонтеры и им может банально не хватать времени, чтобы уследить за всем. Я пинговал рассылку два раза, с промежутком в неделю между каждым пингом, прежде чем смог набрать два ok, требуемых для принятия нового пакета.</p> <p>Последнее о чем хочется упомянуть - используйте <code>make clean</code>, если исправленная вами ошибка не уходит или происходит что-то странное. Эту команду можно использовать с аргументами, мне очень помогал <code>make clean=package</code> пока я разбирался со сборкой пакета. Ну а если ничего не помогает, делайте <code>make clean=all</code>.</p> <p>Из полезных ресурсов можно упомянуть:</p> <ul> <li>подробный и практически пошаговый <a href="https://www.openbsd.org/faq/ports/guide.html">гайд на офф. сайте</a></li> <li>man для <a href="https://man.openbsd.org/bsd.port.mk">bsd.port.mk</a></li> <li>и почтовую рассылку <code>ports@openbsd.org</code>.</li> </ul> Фильтрация почты в формате MailDir https://konyahin.xyz/blog/mail-filtering/ Sun, 09 Apr 2023 20:24:25 +0300 https://konyahin.xyz/blog/mail-filtering/ <p>Я использую почту в формате MailDir. Мне кажется удобным держать весь свой архив сообщений локально, в виде директории с файлами, мне удобно его бэкапить и переносить с одного компьютера на другой. У меня всегда был не очень большой поток сообщений, рабочая почта жила отдельно, в своем огороженном корпоративном мирке, так что я получал письма либо от рекрутеров (которые хотели меня купить), либо от сайтов (которые хотели мне что-то продать).</p> <p>Но, со временем, я подписался на кое-какое количество почтовых рассылок. Писем стало больше и появилось настойчивое желание их хоть как-то разделять, раскладывая по разным ящикам.</p> <p>Я слышал про <code>notmuch</code>, однако теги и их фильтрация мне показались излишним усложнением, хотелось просто разложить яйца по корзинам. Слышал я и про <code>procmail</code>, однако он работает как фильтр с входящими сообщениями и не очень ложится ни на мой архив сообщений, ни на мою привычку получать почту через <code>isync</code>. В итоге, в одном из чатов, мне посоветовали посмотреть на <a href="https://git.vuxu.org/mblaze/">mblaze</a>, набор утилит для работы с MailDir. Хоть там и не было того, что прямо бы решало мою проблему, но утилиты были вполне себе удобными и превращали написание скрипта для фильтрации почты в тривиальное задание.</p> <h2 id="формируем-тз">Формируем ТЗ</h2> <p>Перед тем, как начинать писать код давайте определим пару моментов. Само собой, хранить список правил в тексте программы мы не будем. Класть их куда-то ещё, в несвязанные с почтой места тоже не очень хочется. Я не придумал ничего лучше, чем хранить их прямо в корне почтовой директории, в файле <code>.filter</code>. Так как правила у меня будут максимально простые, то и формат файла будет прост. Пишем почту, сообщения от которой (или с которой) должны под правило попадать, и директорию, в которую нам эти письма нужно будет переложить.</p> <p>Начало моего <code>.filter</code> выглядит вот так:</p> <pre tabindex="0"><code>ports@openbsd.org ports-obsd misc@openbsd.org misc-obsd announce@openbsd.org announce-obsd dev@suckless.org dev-suckless </code></pre><p>Блоки отделяются пустой строкой для визуального удобства, никакого влияния на работу скрипта это не оказывает.</p> <p>Теперь можно подумать об аргументах нашей программы. Хочется чтобы по умолчанию она обрабатывала только новые письма, однако при добавлении правила я хочу иметь возможность прогнать фильтрацию и по старым сообщениям. Также хочется уметь просто распечатать правила фильтрации, не выполняя никаких действий, помимо этого. Ну и добавим возможность вывести небольшую справку, чтобы пользователь (я) мог ожидаемым способом напомнить себе, как же тут все работает.</p> <p>Я люблю набрасывать черновик справки ещё до написания кода, иногда это подсвечивает какие-то непродуманные моменты и позволяет лучше оценить удобство использования программы. Для сегодняшнего скрипта у меня вышло что-то такое:</p> <pre tabindex="0"><code>mfilter [-hcp] [maildir] -h show this help -c filter old mails (by default we look at new) -p print all rules without filtering You should specify maildir with -c and -p options. You can use only one flag at time. </code></pre><h2 id="пишем-код-приготовления-флаги-и-проверки">Пишем код: приготовления, флаги и проверки</h2> <p>Теперь, <em>после</em> решения основных архитектурных вопросов, можно и код писать.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">set</span> -eu </span></span></code></pre></div><p>Shell скрипты вещь хрупкая, и подверженная ошибкам. Чтобы хоть немного снизить вероятность их появления (и вероятность загаживания моей почтовой директории), давай включим пару опций. Первая, <code>e</code>, говорит, что если хоть одна команда в скрипте завершится с ошибкой, то мы с ошибкой завершим весь скрипт, продолжать его выполнение будет бессмысленно и опасно в любом случае. Это не касается команд, для которых мы явно проверяем код выполнения. Вторая, <code>u</code>, приводит к тому, что использование не инициализированной переменной приводит к ошибке, а учитывая <code>e</code> - к остановке выполнения всего скрипта. У нас будет пара моментов, где мы используем <em>возможно</em> не инициализированные переменные, и там нам будет нужно явно указать, что мы о такой возможности знаем.</p> <p>По ходу написания скрипта я понял, что выводить помочь мне нужно в двух местах, при указании флага и при запуске без аргументов. Чтобы избежать дублирования я вынес вывод помощи и выход из скрипта в отдельную функцию.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">help</span> <span class="o">()</span> <span class="o">{</span> </span></span><span class="line"><span class="cl"> cat <span class="s">&lt;&lt;EOF </span></span></span><span class="line"><span class="cl"><span class="s">$0 [-hcp] [maildir] </span></span></span><span class="line"><span class="cl"><span class="s"> -h show this help </span></span></span><span class="line"><span class="cl"><span class="s"> -c filter old mails (by default we look at new) </span></span></span><span class="line"><span class="cl"><span class="s"> -p print all rules without filtering </span></span></span><span class="line"><span class="cl"><span class="s">You should specify maildir with -c and -p options. You can use only one flag at time. </span></span></span><span class="line"><span class="cl"><span class="s">EOF</span> </span></span><span class="line"><span class="cl"> <span class="nb">exit</span> <span class="m">0</span> </span></span><span class="line"><span class="cl"><span class="o">}</span> </span></span></code></pre></div><p>Для того чтобы можно было удобно хранить и править текст справки я воспользуюсь <code>heredoc</code>. Это конструкция, которая позволяет встраивать данные прямо в шелл скрипт. Всё, от <code>EOF</code> до <code>EOF</code>, считается обычным текстом, который мы перенаправляем на ввод <code>cat</code>.</p> <p>Теперь займемся аргументами нашей программы. Первым делом давайте обработаем тот случай, когда скрипт вызывается вовсе без аргументов. Обратите внимание, так как мы указали флаг <code>u</code>, то обращение просто к <code>$1</code> приведет к ошибке, в случае если пользователь не указал аргумент при запуске. Чтобы этого избежать мы явно указываем, что если переменная пуста, то мы её заменим на пустую строку. <code>-z</code> здесь проверяет, что переданная ему в качестве аргумента строка имеет нулевую длину. Я очень плохо помню такие ключи проверки на память, к счастью, их всегда можно напомнить себе, если набрать <code>man test</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> -z <span class="s2">&#34;</span><span class="si">${</span><span class="nv">1</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span> </span></span><span class="line"><span class="cl"> <span class="nb">help</span> </span></span><span class="line"><span class="cl"><span class="k">fi</span> </span></span></code></pre></div><p>Далее мы обрабатываем флаги, полученные на вход и, на их основе, выставляем значения наших внутренних переменных.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="k">case</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> in </span></span><span class="line"><span class="cl"> <span class="s2">&#34;-h&#34;</span> <span class="o">)</span> </span></span><span class="line"><span class="cl"> <span class="nb">help</span> </span></span><span class="line"><span class="cl"> <span class="p">;;</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;-p&#34;</span> <span class="o">)</span> </span></span><span class="line"><span class="cl"> <span class="c1"># если пользователь не хочет ничего фильтровать, то мы запомним это</span> </span></span><span class="line"><span class="cl"> <span class="nv">PRINT_ONLY</span><span class="o">=</span>yes </span></span><span class="line"><span class="cl"> <span class="nv">MAIL_DIR</span><span class="o">=</span><span class="si">${</span><span class="nv">2</span><span class="k">:-</span><span class="si">}</span> </span></span><span class="line"><span class="cl"> <span class="p">;;</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;-c&#34;</span> <span class="o">)</span> </span></span><span class="line"><span class="cl"> <span class="c1"># чтобы обработать старые почтовые сообщения, мы меняем флаг</span> </span></span><span class="line"><span class="cl"> <span class="c1"># фильтрации для программы mlist (о ней позднее) и поддиректорию,</span> </span></span><span class="line"><span class="cl"> <span class="c1"># в которую мы будем перекладывать файлы</span> </span></span><span class="line"><span class="cl"> <span class="nv">MAIL_DIR</span><span class="o">=</span><span class="nv">$2</span> </span></span><span class="line"><span class="cl"> <span class="nv">MLIST_FILTER</span><span class="o">=</span>-C </span></span><span class="line"><span class="cl"> <span class="nv">MAIL_DEST</span><span class="o">=</span>cur </span></span><span class="line"><span class="cl"> <span class="p">;;</span> </span></span><span class="line"><span class="cl"> * <span class="o">)</span> </span></span><span class="line"><span class="cl"> <span class="c1"># если в первом аргументе не было ничего знакомого - будем считать</span> </span></span><span class="line"><span class="cl"> <span class="c1"># что это путь к maildir и мы хотим обработать только новые сообщения</span> </span></span><span class="line"><span class="cl"> <span class="nv">MAIL_DIR</span><span class="o">=</span><span class="nv">$1</span> </span></span><span class="line"><span class="cl"> <span class="nv">MLIST_FILTER</span><span class="o">=</span>-N </span></span><span class="line"><span class="cl"> <span class="nv">MAIL_DEST</span><span class="o">=</span>new </span></span><span class="line"><span class="cl"> <span class="p">;;</span> </span></span><span class="line"><span class="cl"><span class="k">esac</span> </span></span></code></pre></div><p>Теперь мы сделаем две последние проверки: что пользователь указал maildir и что в этом самом maildir лежит файл с правилами переноса почты.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> -z <span class="s2">&#34;</span><span class="nv">$MAIL_DIR</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span> </span></span><span class="line"><span class="cl"> <span class="nb">echo</span> <span class="s2">&#34;You should specify maildir path&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nb">exit</span> <span class="m">1</span> </span></span><span class="line"><span class="cl"><span class="k">fi</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nv">FILTER_FILE</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$MAIL_DIR</span><span class="s2">/.filter&#34;</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> ! -e <span class="s2">&#34;</span><span class="nv">$FILTER_FILE</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span> </span></span><span class="line"><span class="cl"> <span class="nb">echo</span> <span class="s2">&#34;You should have .filter file in maildir&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nb">exit</span> <span class="m">1</span> </span></span><span class="line"><span class="cl"><span class="k">fi</span> </span></span></code></pre></div><h2 id="пишем-код-основная-рабочая-логика">Пишем код: основная рабочая логика</h2> <p>Вся работа с почтовыми сообщениями будет сосредоточена в функции <code>move</code>. Для начала, обработаем самый простой случай - когда пользователь хочет только распечатку правил фильтрации.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">PRINT_ONLY</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span> </span></span><span class="line"><span class="cl"> <span class="nb">printf</span> <span class="s2">&#34;%s\t-&gt; %s\n&#34;</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl"><span class="k">else</span> </span></span></code></pre></div><p>Здесь первый аргумент - почтовый адрес, а второй - целевая директория, все точно так же, как в файле <code>.filter</code>. Я использую символ табуляции, так как большинство терминалов столкнувшись с ним поймет, что имеет дело с таблицей, и попробует хоть немного выровнять её строки.</p> <p>Далее мы, в простом конвейере, вызываем три утилиты из <code>mblaze</code> и первая из них - <code>mlist</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">mlist <span class="s2">&#34;</span><span class="nv">$MLIST_FILTER</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$MAIL_DIR</span><span class="s2">/INBOX&#34;</span> </span></span></code></pre></div><p>Данная утилита выводит список сообщений из указанного почтового ящика. Первый аргумент здесь этот тот самый флаг фильтрации, который мы меняем входным параметром <code>-c</code>. В <code>man</code> по утилите можно посмотреть список всех вариантов выбора сообщений, они достаточно просты и ориентированы на флаги и местоположение почты, без анализа её содержимого.</p> <p>Теперь, когда мы получили список новых (или старых, если указан <code>-c</code>) почтовых сообщений, мы передаем его утилите <code>mpick</code>. Она позволяет нам осуществить более глубокую фильтрацию, на основе содержимого сообщения. Лично меня интересовали только отправитель или получатель, но в <code>man mpick</code> можно увидеть много других возможных правил.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">mpick -t <span class="s2">&#34;\&#34;To\&#34; =~~ \&#34;</span><span class="nv">$1</span><span class="s2">\&#34; || </span></span></span><span class="line"><span class="cl"><span class="s2"> \&#34;Cc\&#34; =~~ \&#34;</span><span class="nv">$1</span><span class="s2">\&#34; || </span></span></span><span class="line"><span class="cl"><span class="s2"> \&#34;From\&#34; =~~ \&#34;</span><span class="nv">$1</span><span class="s2">\&#34;&#34;</span> </span></span></code></pre></div><p>Осталось просто переложить сообщения из одной директории в другую. Однако, даже для такого простого действия, я воспользуюсь отдельной утилитой, а именно <code>mrefile</code>. Дело в том, что у сообщений в maildir есть идентификатор, уникальный в пределах ящика, и если перекладывать почту без учета этого момента (как я, поначалу, и делал), можно напороться на <code>Maildir error: duplicate UID</code> при попытке запустить <code>isync</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">mrefile -v <span class="s2">&#34;</span><span class="nv">$MAIL_DIR</span><span class="s2">/</span><span class="nv">$2</span><span class="s2">&#34;</span> </span></span></code></pre></div><p>Ключ <code>-v</code> мы указываем чтобы утилита напечатала новый путь для каждого сообщения. Эти пути мы передадим финальной программе в нашем конвейере - <code>mv</code>.</p> <p><code>mrefile</code> кладет все почтовые сообщения в директорию <code>cur</code>, даже если изначально они лежали в <code>new</code>. Это логично, ведь <code>new</code> указывает на то, что почтовое сообщение только пришло и не было никем обработано, а как раз его обработкой мы с помощью <code>refile</code> и занимались. Но для меня это было не самым удобным поведением. Я использую <code>mutt</code> для работы с почтой и в нем новые сообщения, из папки <code>new</code>, подсвечиваются как в списке самих сообщений, так и в боковой панели, где есть список всех моих почтовых ящиков. Сообщения, которые ещё не открывались, но уже лежат в <code>cur</code>, в терминологии <code>mutt</code>, считаются не новыми, а &ldquo;старыми&rdquo;. Они помечаются флагом <code>O</code>. Я смог настроить подсветку старых сообщений внутри ящика, но подсветить ящик со старыми сообщениями в боковой панели мне не удалось. Поэтому я просто двину все обработанные сообщения обратно в <code>new</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">xargs -r -n1 -I <span class="o">{}</span> mv <span class="o">{}</span> <span class="s2">&#34;</span><span class="nv">$MAIL_DIR</span><span class="s2">/</span><span class="nv">$2</span><span class="s2">/</span><span class="nv">$MAIL_DEST</span><span class="s2">&#34;</span> </span></span></code></pre></div><p><code>mv</code> не читает аргументы из стандартного ввода, поэтому нам потребуется использовать <code>xargs</code>. <code>-r</code> используется чтобы ничего не делать, если ничего не поступило на вход, <code>-n1</code> говорит брать аргументы по одному, а <code>-I {}</code> задает паттерн (<code>{}</code>), который будет заменен на поступающие на стандартный вход аргументы. По сути, мы для каждого сообщения, которое <code>mrefile</code> переложила в <code>cur</code>, выполним команду</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">mv mail_file <span class="s2">&#34;</span><span class="nv">$MAIL_DIR</span><span class="s2">/</span><span class="nv">$2</span><span class="s2">/</span><span class="nv">$MAIL_DEST</span><span class="s2">&#34;</span> </span></span></code></pre></div><p>Чтобы в новые сообщения не попали старые, попавшие под фильтр из-за флага <code>-c</code>, мы используем <code>$MAIL_DEST</code>. При указанном флаге мы переместим файл из <code>cur</code> в <code>cur</code>, в самого себя. Это не вызовет ошибок, так что я решил не писать здесь условие и не усложнять скрипт зря.</p> <p>Наша функция готова и все что нам остается, это скормить ей набор правил из <code>.filter</code>.</p> <pre tabindex="0"><code>while read -r line || [ -n &#34;$line&#34; ]; do if [ -n &#34;$line&#34; ]; then # shellcheck disable=SC2086 move $line fi done &lt; &#34;$FILTER_FILE&#34; </code></pre><p>Из-за особенностей shell мы делаем здесь пару неочевидных вещей. Например мы указываем флаг <code>-r</code> для <code>read</code>. Он нужен чтобы отключить фичу, позволяющую разбивать строку на две, с использованием <code>\</code>. Если <code>read</code> встретит такую строку, он сжует и слеш, и идущий следом символ переноса строки. Обычно это не то, что ожидает автор скрипта. Несмотря на то, что в нашем конкретном случае, появление слеша в конце строки не очень вероятно, лучше приучать себя ставить <code>-r</code> везде, где поедание слеша не является ожидаемой фичей.</p> <p>Вторым удивительным приемом является двойное условие при чтении файла. Мы не просто проверяем какой код вернул <code>read</code>, мы ещё и смотрим, не осталось ли чего в <code>$line</code>. Связано это с тем, что в Unix любой файл <strong>должен</strong> оканчиваться переводом строки. Однако, ничего не запрещает не поставить его при ручном редактировании файла правил. <code>read</code> считает последнюю строку в переменную <code>line</code>, но не найдя переноса строки сделает вид, что файл кончился. Двойная проверка позволит нам этого избежать.</p> <p>Последняя примечательная особенность скрипта это отключение проверки от <code>shellcheck</code>. Я стараюсь всегда проверять свои скрипты этим линтером, он указывает на многие неочевидные вещи, которые могут привести к проблемам при исполнении. В этом случае он жалуется на то, что <code>$line</code> не заключен в двойные кавычки. Обычно это хороший совет, все остальные случай использования переменных в скрипте используют кавычки, но если мы укажем их здесь, то на вход <code>move</code> поступит только один аргумент, зато с пробелом. Я же хочу чтобы пара &ldquo;почта ящик&rdquo; побилась пробелом на два отдельных аргумента. По хорошему, в данном случае стоит использовать массив или функцию, смотрите примеры в <a href="https://github.com/koalaman/shellcheck/wiki/Sc2086">вики shellcheck</a>, но в данном случае я нахожу это излишним и запутывающим код.</p> <p>Вот и весь скрипт. Он небольшой и вызывается мной после каждой синхронизации почты. Читать рассылки стало куда удобнее, а предназначенная именно мне почта остается лежать в <code>INBOX</code>. Возможно, в дальнейшем мои сценарии использования почты станут сложнее и тогда больше смысла будет в использовании чего-то вроде <code>notmuch</code>, но сейчас мне достаточно и этого. Если вам интересна тема фильтрации и обработки почты из maildir - посмотрите на <code>mblaze</code>, там ещё много интересных утилит.</p> <p>Исходный код целиком можно посмотреть в <a href="https://git.konyahin.xyz/dotfiles/file/scripts/dot-bin/mfilter.html">моем git</a>.</p> Поднимаем Git сервер на OpenBSD https://konyahin.xyz/blog/git-server/ Wed, 15 Feb 2023 19:38:25 +0300 https://konyahin.xyz/blog/git-server/ <p>Если вам хочется иметь запасное (или даже основное) хранилище для ваших программных проектов, и вы не очень доверяете Github и его собратьям, то ваша дорога лежит в сторону selfhosted git сервера. Давайте поднимем его, используя машину на OpenBSD, и сверху накрутим простой web ui, для просмотра репозиториев через браузер.</p> <p>Тему базовой настройки сервера и сертификата я оставлю за скобками, скажу только что и httpd, и acme-client содержатся в базовой системе, работают просто прекрасно и обладают хорошей документацией.</p> <h2 id="gitdaemon">Gitdaemon</h2> <p>Помимо базовой системы нам потребуются ещё два пакета, сам git и простой генератор статического сайта из git репозиториев - stagit. Я выбрал именно его потому что мне нравилось пользоваться решениями на его основе, например <a href="https://git.suckless.org/">вот этим сайтом</a>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">pkg_add git stagit </span></span></code></pre></div><p>Git сервер будет работать из под своего собственного пользователя, так что давайте его заведем.</p> <p>В OpenBSD принято добавлять к сервисным учеткам префикс <code>_</code>, то есть наш пользователь должен называться <code>_git</code>. Однако, по эстетическим причинам, я префикс не использую. Я, конечно же, не прав.</p> <p>Не делайте так.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># -m приведет к созданию домашней директории для пользователя</span> </span></span><span class="line"><span class="cl">user add -m git </span></span></code></pre></div><p>Чтобы мы могли лить изменения со своего локального компьютера на сервер по ssh, давайте добавим на него наш публичный ключ.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">cd</span> /home/git </span></span><span class="line"><span class="cl">mkdir -p .ssh <span class="o">&amp;&amp;</span> chmod <span class="m">700</span> .ssh </span></span><span class="line"><span class="cl">touch .ssh/authorized_keys <span class="o">&amp;&amp;</span> chmod <span class="m">600</span> .ssh/authorized_keys </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">cat &gt; .ssh/authorized_keys &lt;&lt; <span class="s2">&#34;END&#34;</span> </span></span><span class="line"><span class="cl">no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 *** you@example.com </span></span><span class="line"><span class="cl">END </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">chown -R git:git .ssh </span></span></code></pre></div><p>Перед ключом мы указали ряд настроек, которые ограничивают возможности ssh соединения с этим ключом. Чтобы добавить ещё безопасности, стоит урезать возможности нашего служебного пользователя. Для этого мы поменяем ему shell на git-shell, позволяющий выполнять только операции над git репозиториями.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">chsh -s /usr/local/bin/git-shell git </span></span></code></pre></div><p>Всё что нам осталось, это создать директорию для хранения наших репозиториев и натравить на неё gitdaemon.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">mkdir -p /git/ </span></span><span class="line"><span class="cl">chown -R git:git /git </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">rcctl <span class="nb">enable</span> gitdaemon </span></span><span class="line"><span class="cl">rcctl <span class="nb">set</span> gitdaemon flags --export-all --base-path<span class="o">=</span><span class="s2">&#34;/git&#34;</span> </span></span><span class="line"><span class="cl">rcctl <span class="nb">set</span> gitdaemon user git </span></span><span class="line"><span class="cl">rcctl start gitdaemon </span></span></code></pre></div><p><code>rcctl</code> это системная утилита, которая используется для конфигурирования и настройки сервисов и демонов в OpenBSD. В данном случае мы указываем с какими аргументами командной строки сервис должен запускаться и под каким пользователем работать.</p> <p>Если вы хотите увидеть, с какими аргументами демон будет работать прямо сейчас, наберите <code>rcctl get gitdaemon</code>.</p> <p>Для добавления нового репозитория я использую небольшой скрипт, который приведу полностью.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env sh </span></span></span><span class="line"><span class="cl"><span class="cp"></span> </span></span><span class="line"><span class="cl"><span class="nb">set</span> -e </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nv">REPO</span><span class="o">=</span><span class="nv">$1</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> -z <span class="nv">$REPO</span> <span class="o">]</span> </span></span><span class="line"><span class="cl"><span class="k">then</span> </span></span><span class="line"><span class="cl"> <span class="nb">echo</span> <span class="s2">&#34;You should specify repository name&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nb">exit</span> <span class="m">1</span> </span></span><span class="line"><span class="cl"><span class="k">fi</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">mkdir -p /git/<span class="nv">$REPO</span> </span></span><span class="line"><span class="cl"><span class="nb">cd</span> /git/<span class="nv">$REPO</span> </span></span><span class="line"><span class="cl">git init --bare </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Owner name&#34;</span> &gt; owner </span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;git://git.example.com/</span><span class="nv">$REPO</span><span class="s2">&#34;</span> &gt; url </span></span><span class="line"><span class="cl"><span class="si">${</span><span class="nv">EDITOR</span><span class="k">:-</span><span class="nv">vi</span><span class="si">}</span> description </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">chown -R git:git /git/<span class="nv">$REPO</span> </span></span></code></pre></div><p>В качестве аргумента скрипт принимает название репозитория, которое будет так же использоваться в качестве названия директории для него. Помимо директории мы заполняем владельца и общедоступный url для скачивания на просмотр. Для заполнения описания мы открываем текстовый редактор. Все эти вещи будут использоваться stagit для генерации сайта.</p> <p>На этом моменте стоит попробовать создать новый репозиторий и закачать в него какие-либо изменения, чтобы убедиться что всё работает правильно. Для скачивания репозитория по ssh воспользуйтесь адресом вида <code>ssh://git@example.com:/git/repo-name</code>.</p> <h2 id="stagit">Stagit</h2> <p>Теперь, когда git daemon исправно работает, самое время прикрутить хоть какой-то интерфейс, чтобы любопытствующие могли покопаться в ваших проектах не скачивая их себе.</p> <p>Для начала давайте отредактируем <code>/etc/httpd.conf</code> и добавим туда новый сервер для нашей веб морды.</p> <pre tabindex="0"><code>server &#34;git.example.com&#34; { listen on * tls port 443 root &#34;/htdocs/git&#34; tls { certificate &#34;/etc/ssl/example.com.fullchain.pem&#34; key &#34;/etc/ssl/private/example.com.key&#34; } location &#34;./well-known/acme-challenge/*&#34; { root &#34;/acme&#34; request strip 2 } location &#34;*/style.css&#34; { request rewrite &#34;/style.css&#34; } location &#34;*/logo.png&#34; { request rewrite &#34;/logo.png&#34; } location &#34;*/favicon.png&#34; { request rewrite &#34;/favicon.png&#34; } } </code></pre><p>Я исхожу из того, что вы используете https на своем сайте, если это не так - конфиг станет ещё проще.</p> <p>Последние три блока нужны исключительно по той причине, что stagit генерирует относительные ссылки на логотип, стили и favicon сайта. Чтобы не подкладывать их в каждый репозиторий, я просто делаю редирект с любого урла, оканчивающегося запросом одного из этих файлов, на корень, где они, собственно и лежат.</p> <p>Данный конфиг будет раздавать пользователям статику из директории <code>/var/www/htdocs/git</code>, которую нам надо создать и передать во владение пользователю git, так как контент в ней будет генерироваться из него (но об этом чуть позже).</p> <pre tabindex="0"><code>mkdir -p /var/www/htdocs/git chown -R git:git /var/www/htdocs/git </code></pre><p>Чтобы проверить что мы правильно написали конфиг, стоит воспользоваться командой <code>httpd -n</code>, которая заставляет сервер распарсить и проверить конфиги, но ничего не запускать. Если всё ок, то давайте перезапустим httpd.</p> <pre tabindex="0"><code>rcctl restart httpd </code></pre><p>Делать генерацию сайта руками после каждого обновления репозиториев &ndash; занятие недостойное человека, поэтому давайте поручим его компьютеру.</p> <p>Для этого нам нужно создать скрипт, выполняющийся каждый раз после того, как сервер получил какие-либо изменения. Нам нужно создать файл по адресу <code>/usr/local/share/git-core/templates/hooks/post-receive</code> и записать туда приведенный ниже скрипт.</p> <pre tabindex="0"><code>#!/usr/bin/env sh set -e # удаляем индекс, если он уже есть rm -rf /var/www/htdocs/git/index.html # обходим все имеющиеся у нас репозитории cd /git/ for repo in */ ; do mkdir -p /var/www/htdocs/git/$repo cd /var/www/htdocs/git/$repo stagit /git/$repo done # создаем новый индекс файл cd /var/www/htdocs/git/ stagit-index /git/* &gt;&gt; index.html </code></pre><p>Не забудьте сделать этот хук исполняемым.</p> <pre tabindex="0"><code>chmod +x /usr/local/share/git-core/templates/hooks/post-receive </code></pre><p>Теперь всё должно работать. Попробуйте запушить что-нибудь в свой репозиторий и зайти на главную страницу веб интерфейса.</p> <p>Интерфейс очень простой и полностью статичный, речь не идет ни о каких пулл-реквестах или поиске по коду, но если вам это и не нужно, то stagit должен вам понравиться.</p> <p>Внешний вид можно настроить с помощью css файла. Погружаться в эту тему я не буду, если хотите, можете посмотреть на мой собственный <a href="https://git.konyahin.xyz/style.css">css файл</a>.</p> О сайте https://konyahin.xyz/about/ Sat, 01 Oct 2022 17:51:16 +0300 https://konyahin.xyz/about/ <p>Этот сайт сделан с использованием следующих технологий и сервисов:</p> <ul> <li><a href="http://openbsd.org/">OpenBSD</a> <a href="https://www.youtube.com/watch?v=xvFZjo5PgG0">(реферальная ссылка)</a></li> <li><a href="https://epik.com">Epik</a></li> <li><a href="https://www.vultr.com/">Vultr</a> <a href="https://www.vultr.com/?ref=8362553">(реферальная ссылка)</a></li> <li><a href="https://gohugo.io/">Hugo</a></li> <li><a href="https://github.com/LukeSmithxyz/lugo">Luke&rsquo;s Hugo Theme</a></li> </ul> Помодоро-таймер на Attiny45 https://konyahin.xyz/blog/pomodoro-attiny45/ Mon, 01 Jan 0001 00:00:00 +0000 https://konyahin.xyz/blog/pomodoro-attiny45/ <p>Коробка с ардуино и кучкой электронных компонентов стояла у меня в шкафу уже несколько лет. Я купил её в районе 2017-го года, побаловался с Arduino IDE, потом с программатором, приехавших ко мне с aliexpress, помигал диодами и успокоился. Интерес иссяк, придумать проект, который вдохновил бы меня на действия, мне так и не удалось. Я пережил несколько переездов, сменил город, переехал ещё раз, каждый раз с упорством таская за собой оранжевую коробку, с надписью Amperka на боку, и, наконец-то, спустя пять лет, понял что таскал её не зря. Что мне снова хочется что-то делать. И у меня даже была идея - помидоро таймер.</p> <p>Мой список требований к прибору выглядел так:</p> <ul> <li>включать рабочий режим по нажатию кнопки (красный диод)</li> <li>через 25 минут сообщать о том, что работа кончилась</li> <li>переключаться в режим отдыха (зеленый диод)</li> <li>через пять минут сообщать, что отдых тоже пора сворачивать</li> <li>засыпать.</li> </ul> <figure ><img src="https://konyahin.xyz/img/pomodoro-model.png"><figcaption>Примеряем компоненты на площадке</figcaption></figure> <h1 id="bill-of-materials">Bill of materials</h1> <figure ><img src="https://konyahin.xyz/img/pomodoro-kicad.png"><figcaption>Вот так выглядела получившаяся в KiCad схема</figcaption></figure> <p>Так как проект достаточно простой, то и компонентов для него было нужно не так много. Большая их часть встречается в любом базовом наборе для начинающего, докупать пришлось только гнездо для батарейки. Я использовал:</p> <ul> <li>два диода, красный и зеленый, они будут показывать, что идет работа/отдых</li> <li>кнопка без фиксации, единственный инструмент ввода нашего устройства</li> <li>пьезо-динамик, нужен чтобы издавать звуки извещая о смене режима</li> <li>батарейный отсек под 3V батарейку (у меня это CR2032)</li> <li>attiny45, самый простой микроконтроллер, который был у меня дома</li> <li>конденсатор на 100 нФ, блокировочный по питанию, чтобы ток был стабильнее и его не затрагивали переключения режимов</li> <li>два резистора на 10 кОм, использовались как подтягивающие для кнопки и ножки сброса</li> <li>два резистора на 220 Ом, чтобы ограничить ток, идущий через диоды</li> <li>две <a href="https://amperka.ru/product/troyka-perfboard">макетные платы</a>, к которым я всё это припаял</li> <li>ну и немного проводов, чтобы всё припаянное соединить.</li> </ul> <h1 id="пройдемся-по-коду">Пройдемся по коду</h1> <p>В <code>main.c</code> файле у нас содержится общий код проекта, осуществляющий оркестрацию работы устройства. Например, там хранится текущий режим работы, в виде <code>enum</code> и переменной для его хранения:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">enum</span> <span class="n">status_e</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">ON</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">WORK</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">REST</span> </span></span><span class="line"><span class="cl"><span class="p">};</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// int8_t чтобы сэкономить пару спичек на хранении значения </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">int8_t</span> <span class="n">status</span> <span class="o">=</span> <span class="n">ON</span><span class="p">;</span> </span></span></code></pre></div><p>Для перехода из статуса в статус написано три функции, выполняющие всю нужную работу. Так как они более-менее однотипные, то я покажу только одну.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">void</span> </span></span><span class="line"><span class="cl"><span class="nf">status_to_rest</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">status</span> <span class="o">=</span> <span class="n">REST</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="c1">// подаем 1 на ножку с зеленым диодом, и 0 на ножку с красным </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">PORTB</span> <span class="o">&amp;=</span> <span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">LED_RED</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="n">PORTB</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">LED_GREEN</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="c1">// играем мелодию (об этом ниже) </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nf">play_melody</span><span class="p">(</span><span class="o">&amp;</span><span class="n">rest_melody</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="c1">// вызываем переключение в другой статус через REST_MIN минут (опять же, подробности ниже) </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nf">set_timer_callback</span><span class="p">(</span><span class="n">REST_MIN</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">status_to_on</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p><code>PORTB</code> это специальный регистр, отвечающий за ножки, относящиеся к порту B. Так как Attiny45 это простой микроконтроллер и ножек у него мало, то все они относятся к порту B. Задавая значение того или иного бита в этом регистре, мы можем подавать или убирать напряжение с ножек микроконтроллера.</p> <p>Сама конструкция может выглядеть загадочно, но там работает обычная битовая логика. <code>(1 &lt;&lt; LED_GREEN)</code> дает нам число вида 00001000, с единичкой на том месте, которое соответствует пину, связанному с зеленым диодом. Соответственно выражение <code>PORTB |= (1 &lt;&lt; LED_GREEN)</code> устанавливает в единичку этот же самый бит (так как мы выполняем побитовое ИЛИ), а <code>PORTB &amp;= ~(1 &lt;&lt; LED_GREEN)</code> наоборот, зануляет его (так как мы выполняем побитовое И с инвертированным значением, таким в котором на нужном месте, и только на нем, стоит нолик).</p> <p>В самой функции <code>main</code> мы сначала настраиваем контроллер под наши нужды:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// указываем что обе эти ножки работают на вывод </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">DDRB</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">LED_GREEN</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="n">DDRB</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">LED_RED</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// указываем какой именно режим сна нам нужен и разрешаем его использование </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">set_sleep_mode</span><span class="p">(</span><span class="n">SLEEP_MODE_PWR_DOWN</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="nf">sleep_enable</span><span class="p">();</span> </span></span><span class="line"><span class="cl"><span class="c1">// включаем обработку прерываний </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">sei</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// настраиваем наши прерывания, инициализируем код связанный с музыкой и таймером </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">init_interrupt</span><span class="p">(</span><span class="n">switch_status</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="nf">init_music</span><span class="p">();</span> </span></span><span class="line"><span class="cl"><span class="nf">init_timer</span><span class="p">();</span> </span></span></code></pre></div><p><code>DDRB</code> это ещё один специальный регистр. Он также относится к порту B, но регулирует не состояние, а режим работы ножки. Ножка может работать как выход, то есть мы можем сами подавать на неё ток или убирать его, а может как вход. В последнем случае мы хотим проверять есть ли напряжение на ножке и, если есть, то сколько. Эти две ножки должны управлять диодами, так что мы включаем для них режим вывода.</p> <p>Теперь, когда все настроено, мы устраиваем маленький стартовый концерт:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// зажигаем все диоды </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">PORTB</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">LED_GREEN</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="n">PORTB</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">LED_RED</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="c1">// играем стартовую мелодию </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">play_melody</span><span class="p">(</span><span class="o">&amp;</span><span class="n">start_melody</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="c1">// по её окончании все диоды гасим </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">PORTB</span> <span class="o">&amp;=</span> <span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">LED_GREEN</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="n">PORTB</span> <span class="o">&amp;=</span> <span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">LED_RED</span><span class="p">);</span> </span></span></code></pre></div><p>Всё что нам осталось, это зациклить работу нашей программы, заодно проверяя, не пора ли увести микроконтроллер в режим сна:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">while</span> <span class="p">(</span><span class="nb">true</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">sleep</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">sleep</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="nf">sleep_cpu</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h2 id="что-будет-если-нажать-на-кнопку">Что будет, если нажать на кнопку</h2> <p>Чтобы упростить код нашей программы, мы не будем проверять нажата ли кнопка на каждом цикле. Микроконтроллер может сам сообщать нам, если её состояние меняется. Происходит это с помощью механизма прерываний. Чуть выше мы использовали функцию <code>init_interrupt</code>, чтобы повесить на нажатие кнопки смену режим работы устройства. Давайте посмотрим на её код.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">void</span> </span></span><span class="line"><span class="cl"><span class="nf">init_interrupt</span><span class="p">(</span><span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">on_press</span><span class="p">)</span> <span class="p">(</span><span class="kt">void</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// сохраняем коллбэк функцию, чтобы вызвать её позже </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">callback</span> <span class="o">=</span> <span class="n">on_press</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// ножку, к которой привязана кнопка, переключаем в режим ввода </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">DDRB</span> <span class="o">&amp;=</span> <span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">PB2</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="c1">// включаем внешние прерывания </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">GIMSK</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">INT0</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Режим работы микроконтроллера можно настраивать меняя состояние тех или иных регистров. Мы уже работали с ножками, меняя DDRB и PORTB, теперь же мы воспользуемся новым регистром - GIMSK. В документации на Attiny45 он гордо именуется <code>General Interrupt Mask Register</code> и выставляя на нем INT0 в единичку мы включаем внешние прерывания. Теперь изменение напряжения на ножке PB2 будет приводить к срабатыванию прерывания, которое остановит нормальное выполнение программы и передаст управление сециальному куску кода. Вот этому:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="nf">ISR</span> <span class="p">(</span><span class="n">INT0_vect</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">PINB</span> <span class="o">&amp;</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">PB2</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="nf">_delay_ms</span><span class="p">(</span><span class="mi">30</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">PINB</span> <span class="o">&amp;</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">PB2</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="o">*</span><span class="n">callback</span><span class="p">)();</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p><code>ISR</code> это специальный макрос, который позволяет нам определять функции для обработки прерываний. В скобках после него указывается название прерывания, в данном случае <code>INT0_vect</code>. Далее мы два раза, с перерывом в 30мс, проверяем состояние кнопки. Делается это чтобы избежать ложных срабатываний при дребезге контактов. Между контактами кнопки могут проскакивать отдельные электрические сигналы, особенно если мы начинаем её нажимать или отпускать, приближая контакты друг к другу. Два замера через 30мс дают нам большую уверенность, в том, что прерывание сработало на полноценное нажатие кнопки. После этого мы вызываем коллбэк функцию, которая меняет режим работы устройства и проигрывает какую-нибудь мелодию.</p> <h2 id="издаем-звуки">Издаем звуки</h2> <p>Чтобы проиграть мелодию мы будем использоваь пьезодинамик. Внутри него находится специальный кристал, который меняет свой размер при изменении приложенного к нему напряжения. Если менять напряжение достаточно часто, то кристалл будет своим движением производить звук. Не особо музыкальный, но для привлечения внимания к прибору нам хватит и такого.</p> <p>Звук это волна, но так как наш микроконтроллер работает по цифровым, а не по аналоговым принципам, то просто так подать волну на пьезодинамик мы не можем. Мы будем имитировать её с помощью PWM, широтно-импульсной модуляции. Суть её состоит в том, что вместо того, чтобы подавать на выход 0.5 мы будем 50% времени держать на выходе 1 и 50% времени 0. Если делать это достаточно быстро, то для внешнего наблюдателя это будет выглядеть как работа в половину мощности.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">void</span> </span></span><span class="line"><span class="cl"><span class="nf">init_music</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ножка с пьезодинамиком работает на выход и, по началу, сигнал на неё мы не даем </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">DDRB</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">SOUND</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="n">PORTB</span> <span class="o">&amp;=</span> <span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">SOUND</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">TCCR0A</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">WGM01</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="n">TCCR0A</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">COM0A0</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p><code>TCCR0A</code> это ещё один регистр нашего микроконтроллера, отвечающий за настройки его внутреннего таймера. Устанавливая в единичку бит <code>WGM01</code> мы говорим, что работать он должен в режиме CTC (Clear Timer on Compare Match). При каждом срабатывании таймера его значение будет увеличиваться на единицу и сравниваться со значением, лежащим в регистре <code>OCR0A</code>. Когда они окажутся равны, то состояние ножки с динамиком переключится, а когда значение переполнится и отсчет начнется с 0, то состояние ножки поменяется ещё раз. Таким образом мы можем управлять тем, какой процент времени на ножку подается напряжение. Это значение называется Duty Cycle или коэффициент заполнения.</p> <p>Мелодия для проигрывания у нас хранится в простенькой структуре, содержащей число нот и сами эти ноты.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="n">melody_s</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kt">uint8_t</span> <span class="n">length</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kt">uint8_t</span> <span class="n">tones</span><span class="p">[];</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kt">melody_t</span><span class="p">;</span> </span></span></code></pre></div><p>Ноты для наших мелодий зашиты в код программы, вместе с временем в мс, в продолжении которого будет звучать каждая нота.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#define TONE_A 42 </span></span></span><span class="line"><span class="cl"><span class="cp">#define TONE_AS 39 </span></span></span><span class="line"><span class="cl"><span class="cp">#define TONE_B 37 </span></span></span><span class="line"><span class="cl"><span class="cp">#define TONE_C 71 </span></span></span><span class="line"><span class="cl"><span class="cp">#define TONE_CS 67 </span></span></span><span class="line"><span class="cl"><span class="cp">#define TONE_D 63 </span></span></span><span class="line"><span class="cl"><span class="cp">#define TONE_DS 59 </span></span></span><span class="line"><span class="cl"><span class="cp">#define TONE_E 56 </span></span></span><span class="line"><span class="cl"><span class="cp">#define TONE_F 53 </span></span></span><span class="line"><span class="cl"><span class="cp">#define TONE_FS 50 </span></span></span><span class="line"><span class="cl"><span class="cp">#define TONE_G 47 </span></span></span><span class="line"><span class="cl"><span class="cp">#define TONE_GS 44 </span></span></span><span class="line"><span class="cl"><span class="cp"></span> </span></span><span class="line"><span class="cl"><span class="cp">#define DELAY 250 </span></span></span></code></pre></div><p>Значения для нот высчитывались исходя из частоты микроконтроллера, но статью с формулами для расчета я потерял. Так что просто поверьте в эти значения. Сами мелодии объявлены в <code>main.c</code>, вот пример одной из них:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">melody_t</span> <span class="n">start_melody</span> <span class="o">=</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">length</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">tones</span> <span class="o">=</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">TONE_A</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">TONE_D</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">TONE_G</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">TONE_E</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">TONE_B</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">};</span> </span></span></code></pre></div><p>Логика функции, которая играет музыку, крайне проста:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">void</span> </span></span><span class="line"><span class="cl"><span class="nf">play_melody</span><span class="p">(</span><span class="k">const</span> <span class="kt">melody_t</span> <span class="o">*</span><span class="n">melody</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// запускаем таймер, выставляя ему частоту срабатывания </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">TCCR0B</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">CS01</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kt">int8_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">melody</span><span class="o">-&gt;</span><span class="n">length</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// выставляем duty cycle, равный значению текущей ноты </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">OCR0A</span> <span class="o">=</span> <span class="n">melody</span><span class="o">-&gt;</span><span class="n">tones</span><span class="p">[</span><span class="n">i</span><span class="p">];</span> </span></span><span class="line"><span class="cl"> <span class="c1">// пока мы здесь ждем 250мс, нота продолжает звучать </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nf">_delay_ms</span><span class="p">(</span><span class="n">DELAY</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// выключаем таймер </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">TCCR0B</span> <span class="o">&amp;=</span> <span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">CS01</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Вот и всё, что нужно, чтобы издавать звуки. Осталось только определить, когда именно их надо издавать.</p> <h2 id="ещё-один-таймер">Ещё один таймер</h2> <p>Отсчитывать время работы (или отдыха) у нас будет другой таймер. Настроен он таким образом, чтобы срабатывать приблизительно раз в секунду и, при каждом своем срабатывании, вызывать прерывание.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">void</span> </span></span><span class="line"><span class="cl"><span class="nf">init_timer</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// отключаем прерывания, чтобы они не помешали нам полностью настроить таймер </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nf">cli</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> <span class="c1">// на каждом шаге таймер будет сранивать свое значени вот с этим, как было с `OCR0A` у таймера звука </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">OCR1A</span> <span class="o">=</span> <span class="mi">244</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="c1">// указываем что этот таймер тоже работает в режиме CTC </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">TIMSK</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">OCIE1A</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="c1">// выставляем ещё несколько бит для настройки таймера </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// CTC1 запускает таймер </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// CS13 CS12 CS10 - настраиваем то, с какой периодичностью, </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// относительного частоты процессора, он будет срабатывать, </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// в данном случае это CK/4096 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">TCCR1</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">CTC1</span><span class="p">)</span> <span class="o">|</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">CS13</span><span class="p">)</span> <span class="o">|</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">CS12</span><span class="p">)</span> <span class="o">|</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">CS10</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="c1">// снова включаем прерывания </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nf">sei</span><span class="p">();</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Если мы умножим 4096 на 244, то получим 999424. Так как наш микроконтроллер должен работать с частотой 1000000 операций в секунду, то мы можем ожидать, что таймер будет вызывать прерывание где-то раз в эту самую секунду. Но Attiny не очень точны в плане частоты. У микроконтроллеров из разных партий частота может немного отличаться, к тому же на неё может влиять температура и поданое напряжение. Чтобы приблизить ожидаемое время срабатывания к реальному, я прогонял тесты и выяснил, что за одну минуту таймер срабатывает в среднем 57 раз. Это значение записано у меня в <code>SEC</code> и если вы решите использовать мой код со своим микроконтроллером, то скорее всего вам нужно будет его поменять. Для устройств, которые требуют более точного измерения времени, стоит воспользоваться внешними часами реального времени, например <code>DS1307</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">uint8_t</span> <span class="n">minutes</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">uint8_t</span> <span class="n">seconds</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="nf">void</span> <span class="p">(</span><span class="o">*</span><span class="n">callback</span><span class="p">)</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nf">ISR</span> <span class="p">(</span><span class="n">TIMER1_COMPA_vect</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">seconds</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="o">--</span><span class="n">seconds</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">else</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// тот самый SEC равный 57, а не 60 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="n">seconds</span> <span class="o">=</span> <span class="n">SEC</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="o">--</span><span class="n">minutes</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">minutes</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">callback</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">callback</span><span class="p">();</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Код прерывания ещё проще. Отсчитываем секунды и минуты, заданные кодом смены режим работы, и, когда дошли до нуля, дергаем указанный при старте коллбэк.</p> <p>Когда очередной отрезок отдыха закончится, мы сыграем завершающую мелодию и уведем микроконтроллер в сон. Так он будет меньше есть батарейку между сеансами работы. Нажатие на кнопку выведет его из сна и весь процесс повторится снова.</p> <p>Вот такой вот получился проект. С исходным кодом целиком можно <a href="https://git.konyahin.xyz/pomodoro-avr/log.html">познакомиться здесь</a>. Несмотря на его простоту, в процессе я успел познакомиться с многими функциями Attiny, такими как таймеры, прерывания и режимы сна. Ну а главное, я получил нечто физическое, как результат своей работы. И это очень приятное чувство.</p> <video controls src="https://konyahin.xyz/video/IMG_6273.mp4" height="400px" />