03.10.2018


В этом году мы писали на Си, баловались ассемблером, вручную программировали таймеры микроконтроллера и игрались с ШИМом без использования Ардуиновских библиотек, паяли схему с полевым транзистором и пытались создать небольшой домашний дата-центр. Сегодня мы вновь окунёмся в далёкую и нетипичную для высокоуровневого разработчика область, вспомним основы дискретной математики и упрощения логических выражений, поработаем с промышленным контроллером Omron CP1L и обучающей моделью шлагбаума!

Статья составлена из материалов лабораторной работы по дисциплине «Программирование микроконтроллеров и микропроцессоров».


Видео с демонстрацией работы (как файл)

Дана модель шлагбаума с кнопками открытия/закрытия, датчиками максимального уровня открытия и минимального уровня закрытия, а также датчиком нахождения чего-либо в створе шлагбаума. Требуется разработать программу, которая будет запускать процесс открытия шлагбаума при нажатии кнопки «Открыть» и закрывать при нажатии кнопки «Закрыть». В случае появления в створе шлагбаума какого-либо объекта или возникновения иной внештатной ситуации, необходимо останавливать работу, чтобы позволить оператору устранить факторы, препятствующие дальнейшей работе, и возобновить работу шлагбаума.

Примечание

Некоторые могут сделать резонное замечание, что нет нужды останавливать шлагбаум при появлении в створе человека на подъёме. Так было сделано умышленно: в первую очередь для упрощения логики, ну а во-вторых почему бы и нет? Не лезь под работающий механизм! Мало ли что может случиться?!

Программируется контроллер Omron в специально предназначенной для этого среде разработки CX Programmer на языке контактно-релейных схем. Да-да, никакого кода — только графическая нотация! Нонсенс для обычного прикладного программиста, но как нам сказали на лекции, большинство программ для промышленных контроллеров пишется (рисуется?) именно на графическом языке контактно-релейных схем (она же LD — Ladder Diagram).

Поехали!

Начинается всё с теории конечных автоматов, которой я не хочу вас грузить (в принципе, про state machines и так должен знать любой программист), так что сразу рассмотрим всё на нашем конкретном примере. Итак, нам нужно представить шлагбаум в виде набора состояний и переходов между ними. Каждое состояние представляет собой определённое уникальное сочетание значений булевых переменных (поскольку мы работаем на максимально низком уровне и оперируем электрическими сигналами, которые могут находиться только в двух состояниях: ток или есть, или его нет). Например, состояние движения шлагбаума описывается тремя вариантами: он или открывается, или закрывается, или стоит на месте. Для хранения этого состояния требуется два бита (две переменные) — школьная формула по информатике 2i=N, где N — количество вариантов, а i — количество бит, которое необходимо для их представления.

Давайте перечислим все переменные, которые нужны для выполнения поставленной задачи (в скобочках приведены сокращения, которые будут использоваться дальше):

  1. шлагбаум открывается (открыв., ОТКР или О);
  2. шлагбаум закрывается (закрыв., ЗАКР или З);
  3. достигнуто минимальное значение хода (мин. или MIN);
  4. достигнуто максимальное значение хода (макс. или MAX);
  5. объект (человек) находится в створе (чел.);
  6. нажата кнопка открытия (кн. откр. или КО);
  7. нажата кнопка закрытия (кн. закр. или КЗ).

Первые две переменные одновременно являются и результирующими значениями (нам же как раз надо описать состояние движения шлагбаума). Остальные представляют собой провода, идущие от определённого датчика.

Теперь нам нужно построить таблицу состояний. Самый простой способ — это перебрать все возможные состояния. Но составлять таблицу из 27=128 строк, большая часть из которых либо вообще не будет иметь смысла, либо не будет представлять для нас какого-либо интереса — удовольствие не из приятных. К счастью, при наличии хорошего воображения можно представить весь процесс работы шлагбаума в голове и выписать только нужные состояния — у меня получилось 19 основных и 3 внештатных. Но надо быть очень внимательным! Потому что изначально я всё-таки умудрился упустить два состояния.

Таблица состояний
Вопросительный знак означает, что данная переменная не играет роли для этого состояния и может принимать оба значения (и 0, и 1).

Теперь нам остаётся только вывести две функции (О и З) из полученных сочетаний. Для этого преподаватель предложил нам воспользоваться картами Карно. Вряд ли я смогу нормально объяснить, как ими пользоваться (правильно разбить развёртку на переменные нам всё равно помогли), так что лучше просто посмотрите видео:

Для каждого истинного значения обеих функций, проставляем все единички и вопросики из соответствующей строчки в карту. В результате у меня получились следующие карты Карно:

Карты Карно
Пустые поля подразумевают логические нули, а вместо вопросительных знаков можно подставить любое значение

Теперь группируем и упрощаем получившиеся единицы, заменив на них вопросительные знаки. Но прежде чем перейти к записи получившихся функций, вынужден сделать важное отступление.

Эмулятор шлагбаума

Поскольку большýю часть работы я выполнял дома, нужно было как-то проверять правильность получаемых функций и логических выражений. Для решения этой проблемы был разработан простенький эмулятор, написанный на Котлине и использующий возможности этого прекрасного языка по созданию няшных DSLей. Опробовать его можно прямо из браузера.

Всё самое важное находится в файле Main.kt: перечисление состояний с переходами между ними из таблицы (переменная cases), а также функции открытия (openingFunc) и закрытия (closingFunc). Внутри данных функций определены следующие переменные: opening (открыв.), closing (закрыв.), min (мин.), max (макс.), openButtonPressed (кн. откр.), closeButtonPressed (кн. закр.), somethingUnderBarrier (чел.). Их можно объединять либо традиционными операторами, либо перегруженными операторами сложения и умножения:

!opening && openButtonPressed || opening && !closing
// или
!opening * openButtonPressed + opening * !closing

Но в графическом изображении контактно-релейных схем элементы рисуются последовательно в линию (конъюнкция, логическое И), и несколько таких строк располагаются параллельно (дизъюнкция, логическое ИЛИ). Поэтому можно воспользоваться вспомогательной функцией line, которая применяет конъюнкцию к переданным ей параметрам. Сами же линии можно дизъюнктивно объединять либо теми же плюсами, либо через точки, как в ООПшных фабриках:

line(!opening, closing, min, !max, openButtonPressed, !closeButtonPressed, !somethingUnderBarrier)
.line(!opening, !closing, !min, !max, openButtonPressed, !closeButtonPressed, !somethingUnderBarrier)

Поддерживается и компромиссный синтаксис, который может кому-нибудь показаться более красивым и наглядным:

line { !max * !closeButtonPressed * !somethingUnderBarrier } +
line { !min * !openButtonPressed * !somethingUnderBarrier }

Обратно к работе!

В эмуляторе получившиеся функции выглядят следующим образом:

val openingFunc: LD = {
    line(!opening, closing, min, !max, openButtonPressed, !closeButtonPressed, !somethingUnderBarrier) +
    line(!opening, closing, !min, !max, openButtonPressed, !closeButtonPressed, !somethingUnderBarrier) +
    line(!opening, !closing, min, !max, openButtonPressed, !closeButtonPressed, !somethingUnderBarrier) +
    line(opening, !closing, !min, !max, !closeButtonPressed, !somethingUnderBarrier) +
    line(opening, !closing, min, !max, !closeButtonPressed, !somethingUnderBarrier) +
    line(!opening, !closing, !min, !max, openButtonPressed, !closeButtonPressed, !somethingUnderBarrier)
}

val closingFunc: LD = {
    line(opening, !closing, !min, !max, !openButtonPressed, closeButtonPressed, !somethingUnderBarrier) +
    line(opening, !closing, !min, max, !openButtonPressed, closeButtonPressed, !somethingUnderBarrier) +
    line(!opening, !closing, !min, max, !openButtonPressed, closeButtonPressed, !somethingUnderBarrier) +
    line(!opening, closing, !min, max, !openButtonPressed, !somethingUnderBarrier) +
    line(!opening, closing, !min, !max, !openButtonPressed, !somethingUnderBarrier) +
    line(!opening, !closing, !min, !max, !openButtonPressed, closeButtonPressed, !somethingUnderBarrier)
}

Функции оказались достаточно сложными. При этом если присмотреться к ним и вспомнить базовые правила преобразования логических выражений, можно заметить здесь обширное поле для упрощений. А учитывая, что с готовым эмулятором мы сразу можем узнать, нарушилась ли логика выражения, грех не воспользоваться этой возможностью! К тому же не стоит забывать, что никто не запрещает выносить некоторые вычисления в отдельные переменные. Это особенно важно при бóльшем количестве переменных, так как в таком случае мы бы упёрлись в ограничение контроллера (или среды разработки?) на максимальное количество элементов в строке — не более 7 штук.

После танцев с гуглением уже успешно забытых формул у меня получилось сократить функции до следующего вида:

val openingFunc: LD = {
    val x = !max * !closeButtonPressed * !somethingUnderBarrier
    
    line(!opening, openButtonPressed, x) +
    line(opening, !closing, x) +
    line(!opening, !closing, !min, openButtonPressed, x)
}

val closingFunc: LD = {
    val x = !min * !openButtonPressed * !somethingUnderBarrier
    
    line(!closing, closeButtonPressed, x) +
    line(!opening, closing, x) +
    line(!opening, !closing, !max, closeButtonPressed, x)
}

В таком виде они и были загружены в контроллер, чтобы получить зачёт по лабораторной работе и снять видео из начала статьи:

Контактно-релейная схема в CX Programmer

Впоследствии эксперименты на эмуляторе показали, что можно спокойно выкинуть ещё по одной строке из каждой функции, но на практике корректность этого предположения не проверялась:

val openingFunc: LD = {
    val x = !max * !closeButtonPressed * !somethingUnderBarrier
    
    line(!opening, openButtonPressed, x) +
    line(opening, !closing, x)
}

val closingFunc: LD = {
    val x = !min * !openButtonPressed * !somethingUnderBarrier
    
    line(!closing, closeButtonPressed, x) +
    line(!opening, closing, x)
}

Заключение

Вот и подошёл к концу краткий экскурс в мир программирования промышленного железа. Надеюсь, вы узнали для себя хоть что-то новое — тогда мой труд не был напрасен. А если хотите больше подобного необычного контента в блоге, ставьте лайки и делитесь ссылками на пост с друзьями! =)

05.07.2018


Апельсин, распечатанный на 3D-принтере

Сегодня мы рассмотрим вопрос экономии денег и размещения веб-сайтов и прочих сетевых сервисов (например, ботов для мессенджера Telegram) на собственном «железе» в обычной городской квартире и постараемся выяснить, есть ли в этом смысл или всё-таки лучше поручить поддержку инфраструктуры профессионалам. Изначально этот пост планировался познавательным туториалом, но в итоге всё скатилось к разбору полётов…

Как-то давным-давно я кинул клич в одном из чатов и посетовал на отсутствие денежных средств для оплаты хостинга на следующий год. Откликнулся хорошо известный в очень узких кругах товарищ Кив и предложил безумную идею с созданием собственного домашнего сервера! Сам он хостит свои сайты на Cubieboard 2, а мне согласился бесплатно отправить «Апельсинку» — китайский одноплатный компьютер Orange Pi PC с 4-ядерным процессором на архитектуре ARM, 1 Гб оперативной памяти и постоянной памятью в виде любой воткнутой в него SD-карты.


Необходимое оборудование и первоначальная настройка

Orange Pi PC

Поскольку Кив — уважаемый и опытный человек, занимающийся созданием роботов и написанием операционных систем, но, тем не менее, не гнушающийся время от времени замарать руки вебом, фронтендом и мобильной разработкой, то он быстро смог побороть мой изначальный скептицизм, ответив на ряд вопросов. Одной из основных проблем является, разумеется, мой динамический IP-адрес. Но разговор об этом мы пока ненадолго отложим.

Итак, к одноплатнику я докупил на AliExpress блок питания с корпусом и охлаждением за $10.50 (при том что сам компьютер можно найти за $15.56).

Одноплатник с блоком питания и корпусом

Поскольку у меня нет монитора с HDMI, пришлось прикупить USB-адаптер COM-порта, чтобы выводить сообщения с консоли в PuTTY по интерфейсу UART.

USB to TTL HW-597

Трёхпиновый коннектор есть на плате Апельсина. Общий провод (землю, GND) нужно соединить, TX и RX подключить крест-накрест (передачу к приёмнику), питание (VCC) замкнуть перемычкой на 3.3 вольта. Скорость выставить при подключении — 115200 бод.

Для первоначальной настройки COM-порта вполне хватает. В дальнейшем работать, конечно, гораздо удобнее настроив SSH-сервер (благо это легко и в Сети полно туториалов: нужно всего лишь сгенерировать пару ключей и закинуть открытый ключ в файл ~/.ssh/authorized_keys).

В качестве прошивки я выбрал Armbian — дистрибутив, производный от Debian и оптимизированный для работы на одноплатных ARM-компьютерах с флеш-памятью. Там исправлены косяки инженеров Xunlong с завышенными и плохо адаптирующимися под условия частотами процессора, ведущими к его перегреву, а также оптимизирована запись служебных данных (логов, например) для продления жизни SD-карты. Впрочем, другие варианты прошивок (а их много) я не пробовал, так что ничего плохого или хорошего о них сказать не могу. Сама установка весьма проста и прекрасно описана как в официальном руководстве, так и в посте по предыдущей ссылке на сборник дистрибутивов: нужно просто записать готовую систему на флешку.


Охлаждение

Купленная мной система охлаждения состоит из радиатора и небольшого пятивольтового вентилятора, у которого есть один недостаток — слишком уж он шумный. При этом Армбиан заявляется достаточно оптимизированной системой, чтобы можно было использовать одноплатник лишь с пассивным охлаждением. Но раз уж вентилятор есть, то хотелось его как-то использовать. Выход один: считывать датчик температуры и включать/выключать вентилятор в зависимости от его значения.

Подходящий скрипт легко находится на форуме Армбиана. Рекомендую, кстати, посмотреть на содержимое каталогов /sys/class/thermal и /sys/class/gpio. В первом находится файл, из которого можно получить информацию о температуре процессора (точный путь прописан в скрипте), а на втором я остановлюсь подробнее, потому что это жутко интересно и мне хочется поделиться.

Забавно, как Линукс позволяет считывать состояние датчиков из файлов или менять состояния пинов GPIO, просто записывая новые значения в файлы специальной файловой системы, транслирующей эти обращения в системные вызовы. Напомню, что GPIO — это просто набор контактов общего назначения на плате, сгруппированных в один порт, в котором, кроме них, есть пин на «землю» и питание на 3.3 или 5 вольт (точная распиновка есть в руководстве). С каждым из портов ассоциировано какое-то число, чтобы к нему можно было обращаться из системы (вот тут точную распиновку дать не могу: так до конца и не разобрался). В скрипте это число 12, которое присваивается переменной GPIO.

В папке /sys/class/gpio/ имеется два файла: export и unexport. Если записать в первый файл номер порта, то рядом появится папка gpio$GPIO (подстановка в стиле Bash'а). Убрать её можно, очевидно, записав то же число во второй файл. Внутри неё находятся файлы direction и value. Пишем в первый out, если хотим записывать значения, или in, если считывать, и работаем дальше со вторым. Всё гениально и просто!

Примечание

Записать значение в файл можно двумя способами. Если работа выполняется от имени суперпользователя, то всё просто:

echo 12 > /sys/class/gpio/export

Иначе придётся применить небольшой трюк с программой tee:

echo 12 | sudo tee /sys/class/gpio/export > /dev/null

Впрочем, вывод можно и не перенаправлять вникуда, если он не мешает.

Дополнено 11.07.18: ещё можно запускать отдельную оболочку от имени суперпользователя:

sudo bash -c "echo 12 > /sys/class/gpio/export"

Ладно, что-то я отвлёкся от темы. Вернёмся к охлаждению, ведь проблема тут возникла вовсе не программная. А всё дело в том, что пины GPIO выдают всего 3.3 В. Причём даже их категорически нельзя использовать для питания устройств. Совсем. Они подключены без какой-то специальной защитной обвязки и такая эксплуатация может в любой момент привести к выходу всей платы из строя. Соответственно, питать вентилятор можно только со специальных контактов питания: либо на 5, либо на 3.3 вольта. Поэтому придётся использовать полевой транзистор, на который будем подавать управляющий сигнал с GPIO.

Проблема с транзисторами в том, что для неподготовленного человека это уже довольно сложная электроника, которую нужно подбирать тщательно и аккуратно, чтобы не получить транзистор который не будет открываться от управляющего сигнала. Причём с покупкой тоже могут возникнуть проблемы: в провинциальных магазинах выбор скуп, заказывать из российских интернет-магазинов дорого, а китайцы могут обмануть. И на последнем я хочу заострить особое внимание, потому что пошёл именно по этому пути, заказав набор мосфетов IRL3705N, которые мне посоветовали. Догадайтесь, что произошло дальше?

MOSFET-транзисторы

Я редко что-то делаю руками, потому что обычно всё получается через одно место. В этот раз, после не особо удачного опыта припаивания ножек к Ардуине, я решил основательно подготовиться к процессу: обзавёлся новым паяльником, оловоотсосом, флюсом. Разумеется, опять помогая экономике Китая (ох уж этот путь нищеброда). В итоге всё шло по почте настолько долго и неравномерно (+ внезапно возникли дела по учёбе), что паять схему я начал, только когда прошли уже все сроки для открытия диспута по транзисторам. Заранее я их тоже не догадался проверить, потому что не умел. Лишь потом пришлось во всё это вникать, когда после пайки и сборки схемы оказалось, что ничего не работает: транзисторы поддельные и открываются только от 5 вольт, но не от 3.3, как должны по спецификации.

Собранная схема Разобранная схема соединения вентилятора с транзистором

В итоге я плюнул, расковырял всё обратно, отпаял, разделил парный коннектор на два отдельных провода, позаимствовав коннекторы у одного из проводов, которые покупал для Ардуины, и воткнул вентилятор в пин на 3.3 В, чтобы замедлить обороты и уменьшить шум (благо такой опыт у меня уже был с десктопным ПК, когда пришлось перепаивать коннектор нерегулирующегося вентилятора на мулекс по схеме с пониженным напряжением). В принципе, результатом доволен. Работает очень тихо и его совершенно не слышно.

Окончательный вид сервера

Результаты тестовых измерений температуры

На пассивном охлаждении в простое Апельсин даёт 36-37 °C. С небольшим обдувом — 27-29. До 59 градусов под 100% нагрузкой нагрелся, пока считал простые числа до 2000. Если время теста увеличить, выросло бы ещё, судя по динамике. С охлаждением под нагрузкой даёт 47-48. И, вроде бы, застыл на этих значениях.

В общем, выигрыш10 градусов спокойно даёт небольшой вентилятор на 3.3 V. С пятью выигрыш был бы ещё больше. Однако без постоянных и длительных нагрузок, в принципе, спокойно вывозит и обычный радиатор.


Динамический IP

Вот мы и вернулись к тому, о чём я упоминал ещё в самом начале статьи — проблеме динамического IP-адреса. Поскольку адрес постоянно меняется, нельзя просто прописать его в настройках домена. К счастью, есть решения, отчасти нивелирующие данную проблему. К сожалению, она довольно нишевая, так что все решения предельно костыльные.

Простейший набор бесплатного (а как мы помним, весь сыр-бор затевался именно ради экономии) решения проблемы отсутствия статического IP состоит из двух инструментов: Cloudflare и ddclient. Первый широко извествен как сервис, предоставляющий защиту от DDoS-атак, а также ускоряющий работу многих сайтов путём минификации и кэширования страниц в обширной сети своих CDN-серверов. Указываешь в настройках домена DNS-сервера Cloudflare, он чудесным образом автоматически определяет все записи и переносит их к себе. Дальше для каждой записи можно выбрать, будет ли доступ к указанному в адресе серверу осуществляться напрямую (то есть адрес сервера виден пользователям и доступен для атаки) или он будет спрятан за HTTP-прокси от Cloudflare, попутно подвергаясь магическим оптимизирующим преобразованиям. У них есть публичный API, чтобы можно было в зависимости от ситуации гибко управлять настройками сайта с помощью скриптов. И поскольку всё работает через DNS, то для него «ручки», разумеется, тоже имеются.

Тут на сцену выходит второй герой нашего набора нищеброда — ddclient — скрипт на языке Perl для отслеживания смены IP-адреса сервера и автоматического обновления DNS-записей. Он запоминает последний установленный через него IP-адрес и обращается к API только при получении нового адреса. Для получения фактического IP-адреса существует несколько вариантов, но для большинства домашних пользователей это будет обращение к внешнему сайту, который скажет IP, с которого к нему пришёл запрос.

Пример конфигурационного файла

/etc/ddclient.conf или /etc/ddclient/ddclient.conf:

daemon=60
protocol=cloudflare
use=web
server=www.cloudflare.com
ssl=yes
login=<адрес электронной почты>
password=<API-токен>
zone=<основной домен>
<домен>
<поддомен 1>
<поддомен 2>
…

Проблема с этим скриптом состоит в его нелёгкой судьбе. Оригинальная версия на SourceForge не обновлялась уже несколько лет. И вот похоже, что буквально на этой неделе Cloudflare отключил старые версии API, из-за чего всё сломалось. На GitHub есть форк, в котором реализована поддержка последней версии протокола. Я попытался его поставить, но он тоже сыплет ошибками и ничего не обновляет. В общем, пока всё очень плохо. Ждём фиксов.

И ладно бы это была первая проблема с ddclient'ом. Но нет! Различные неприятности с ним случались и до этого. Как будто на нём лежит какое-то древнее проклятье…


Архитектурные проблемы — ARM

Не то чтобы все домашние сервера были одноплатниками на ARMе, но благодаря пассивному охлаждению такой вариант выглядит куда привлекательнее, чем шумный и прожорливый ПК на x86. Но тут можно столкнуться с проблемой, что не всё ПО поддерживает данную архитектуру.

На самом деле всё не так плохо и большинство программ спокойно устанавливаются из репозиториев и нормально работают. Пока у меня возникла проблема только со сборкой Java-программ в EXEшники с помощью программы launch4j, которая не работает на ARMе. В принципе, можно попытаться обойти с помощью QEMU (опять же идея Кива), но для этого нужно нетривиально пердолить процесс сборки (вероятно, писать свой плагин для интеграции launch4j с Gradle/Maven), а также, судя по обсуждениям на форумах, и так небыстрый процесс сборки на слабеньких мобильных процессорах может растянуться ещё в несколько раз, так как эмуляция инструкций «полноценных» компьютеров даётся нелегко. В итоге я решил отказаться от данной затеи.


Обеспечение надёжности

А теперь серьёзная часть, касающаяся всех домашних серверов — обеспечение надёжности и бесперебойной работы сервера. Даже если не брать в расчёт возможность физического выхода из строя самого компьютера (что, конечно, является серьёзным допущением), существуют две куда более серьёзные и очевидные проблемы: бесперебойное обеспечение сервера электроэнергией и недопущение разрывов интернет-соединения. Согласитесь, отключение электричества или «пропажа интернета» (пусть даже и на относительно короткий срок) происходит гораздо чаще, чем поломка оборудования. За последний год могу припомнить несколько таких случаев, которые разрешались как за несколько минут, так и за несколько часов.

Что же с этим делать? Ну, во-первых, обзавестись источником бесперебойного питания. Причём желательно с аккумулятором побольше, чтобы сервер как можно дольше мог обходиться без внешнего источника питания (благо сам по себе он потребляет крайне мало электроэнергии, но нужно питать ещё и роутеры, которых может быть несколько, как в моём случае). Во-вторых, можно вставить в сервер модем с симкой какого-либо мобильного оператора и обмазаться скриптами для автоматического переключения подключений при пропаже соединения. Впрочем, тут появляется вопрос о том, сколько трафика мы можем себе позволить пропустить через такое соединение и не разориться?

Да, решение возникших проблем выйдет в копеечку и добавит много дополнительных хлопот и работы. А ведь мы ещё даже не рассмотрели вариант с поломкой самого одноплатника. Хотя никто и не говорил, что всё будет просто!.. Стоп, мне же как раз так и говорили. Хм…

Выводом из этого раздела является осознание факта, что хостеры кушают свой хлеб не даром: построение и обеспечение бесперебойной работы датацентра — это сложная и дорогостоящая задача. Только когда пытаешься построить собственную инфраструктуру, начинаешь понимать всю трудоёмкость этого процесса и ценить услуги хостинга. Были бы ещё деньги на его оплату…


Блокировки

Последней проблемой, о которой я даже не сразу вспомнил в силу её относительной новизны, являются блокировки российскими властями разных электронных ресурсов. Например, мессенджера Telegram, для которого я зачастил пилить ботов. Разумеется, теперь держать их на сервере в обычной российской квартире, подключённой к обычному российскому провайдеру, становится проблематично: адрес, на который отправляются запросы, заблокирован. Приходится использовать различного рода обходы блокировок, которые негативно сказываются как на скорости ответа, так и на надёжности в общем. Подробнее о них расскажу как-нибудь в другой раз и в не таком явном виде (а то мало ли).

Напомню также, что во время активной фазы войны Роскомнадзора с «Телеграммом» под удар попало и множество вполне легальных ресурсов. Периодически подобные эксцессы случались и ранее. Неприятно, когда что-то может «упасть», просто потому что чей-то IP-адрес случайно (или не совсем) попал в выгрузку. Не правда ли?


Выводы

На данный момент проект самохостинга свёрнут на неопределённое время, пока не решится проблема с обновлением DNS-записей. Но даже без этого надо понимать, что у самохостинга есть ряд серьёзных проблем с надёжностью. И если держать там небольшой бложик, на который практически никто никогда не заходит, ещё нормально, то вот для тех же ботов это может быть куда критичнее (как же бесит огромное множество неработающих ботов в Telegram!). Я собирался перенести ряд некритичных и редко посещаемых ресурсов с платного VPS на домашний сервер, но теперь, видимо, их придётся закрыть совсем.

Нет свободной оперативной памяти

Поскольку я до сих пор нищий безработный и оплачиваю хостинг с доменами на подарочные деньги за Новый год и день рождения, то пользуюсь самым дешёвым тарифом на VPS, который нашёл — 12 долларов в год (со скидкой бывает 9). И в последнее время я стал упираться в установленные лимиты по оперативной памяти — 128 Мб (на скрине 256, потому что, видимо, ещё 128 Мб выделены под систему и не учитываются). Пока всё более-менее работает, однако Линукс уже начал чистить страницы из памяти и убивать процессы-воркеры веб-сервера (чем освободил память почти на четверть), так что дальнейшее добавление сервисов и расширение существующих представляется затруднительным. Выхода два: либо платить больше, либо ужиматься в то, что имеем.

Следующий тариф обойдётся в $2.5-3 в месяц (я не понимаю, почему KVM-виртуализация у них дешевле, чем OpenVZ). Если пойти этим путём, сумма оплаты за хостинг превысит 2 000 рублей в год. Что на данный момент для меня является неподъёмной суммой (так как вместе с доменами это уже будет около 3 000 рублей, а подарки за день рождения в этом году уже ушли в расход). Так что придётся как-то сбавлять аппетиты и урезать количество сервисов, закрывая самые неприоритетные и непопулярные. Благо простора для оптимизации много! Ну либо ждать и надеяться, что кто-нибудь придёт и решит задонатить ;)

На этой попрошайнической ноте, пожалуй, я и закончу данный пост. До скорых встреч, друзья!

23.02.2018


В далёком 3-ем семестре (то есть ещё в начале второго курса) у нас в институте организовали вечерние занятия по программированию микроконтроллеров на базе платформ LEGO Mindstorms и Arduino. Нам было слишком лень на них ходить (такая уж у нас группа ленивых раздолбаев ¯\_(ツ)_/¯ ), но на нескольких занятиях я всё же побывал и немного поиграться с Ардуинкой успел (хотя всё равно большую часть времени провёл в эмуляторе). С тех времён у меня осталась пара проектов на Circuits.io (которые потом переехали на Tinkercad).


Светофор

Первым из них был простенький, но полноценный светофор с соблюдением всех фаз.

Диаграмма состояний светофора

В то время я почему-то радовался получившемуся коду, но сейчас он выглядит очень примитивно:

// Константы с номерами портов
const int redLight = 8;
const int yellowLight = 9;
const int greenLight = 10;

// Счётчик секунд.
int counter = 0;

// Эта функция вызывается при запуске микроконтроллера...
void setup() {
  // Настраиваем порты на вывод сигнала.
  pinMode(redLight, OUTPUT);
  pinMode(yellowLight, OUTPUT);
  pinMode(greenLight, OUTPUT);
}

// ...а эта постоянно вызывается из вечного цикла во время его работы.
void loop() {
  // Красный свет
  if (counter >= 0 && counter < 15) {
    digitalWrite(redLight, HIGH);     // включаем светодиод
    digitalWrite(yellowLight, LOW);   // выключаем светодиод
    digitalWrite(greenLight, LOW);
  }
  // Красный и жёлтый сигналы светофора
  else if (counter >= 15 && counter < 20) {
    digitalWrite(yellowLight, HIGH);
  }
  // Зелёный свет
  else if (counter >= 20 && counter < 31) {
    digitalWrite(redLight, LOW);
    digitalWrite(yellowLight, LOW);
    digitalWrite(greenLight, HIGH);
  }
  // Мигающий зелёный
  else if (counter >= 31 && counter < 35) {
    if (counter % 2 == 0) {
      digitalWrite(greenLight, HIGH);
    } else {
      digitalWrite(greenLight, LOW);
    }
  }
  // Жёлтый свет
  else if (counter >= 35 && counter < 40) {
    digitalWrite(yellowLight, HIGH);
    digitalWrite(greenLight, LOW);
    
    // Сбрасываем счётчик
    if (counter == 39) {
        counter = -1;
    }
  }
  
  counter++;
  delay(1000);    // секундная задержка
}

Ссылка в заголовке ведёт на страницу проекта на Tinkercad'е. Этот сервис позволяет создавать несложные схемы, писать программы для них и запускать эмуляцию! К сожалению, чтоб запустить на выполнение чужой проект, нужно сначала зарегистрироваться и скопировать его на свой аккаунт. Печально, что всё так сложно, но ничего не поделаешь :(


Переливающийся цветами светодиод

После создания светофора мне захотелось поиграться с цветным светодиодом. У него всё-таки целых 4 ноги, управлять которыми надо через широтно-импульсную модуляцию. Однако у меня не хватило фантазии придумать что-то лучше светодиода, переливающегося из красного в зелёный, из зелёного в синий, а затем обратно в красный и так далее по кругу. Посмотреть, как это выглядит, можно опять же на Tinkercad'е (если есть аккаунт), а код я снова продублирую здесь с русскими комментариями:

// Константы с номерами портов
const int redPin = 5;
const int greenPin = 6;
const int bluePin = 10;

// Компоненты цвета
int red = 0;
int green = 0;
int blue = 0;

// Эта функция вызывается при запуске микроконтроллера...
void setup() {
  pinMode(redPin, OUTPUT);
  pinMode(greenPin, OUTPUT);
  pinMode(bluePin, OUTPUT);
}

// ...а эта постоянно вызывается из вечного цикла во время его работы.
void loop() {
  // Увеличиваем красную компоненту
  if (red < 255 && green == 0) {
    if (blue > 0) {
      analogWrite(bluePin, --blue);
    }
    analogWrite(redPin, ++red);
  }
  // Увеличиваем зелёную и уменьшаем красную
  else if (green < 255 && blue == 0) {
    analogWrite(redPin, --red);
    analogWrite(greenPin, ++green);
  }
  // Увеличиваем синию и уменьшаем зелёную
  else if (blue < 255) {
    analogWrite(greenPin, --green);
    analogWrite(bluePin, ++blue);
  }
  
  // небольшая задержка
  delay(10);
}

Collecting all things together...

А теперь мы резко переносимся на два года вперёд и попадаем на четвёртый курс. Я уже полгода-год состою во флудилке DC, а среди предметов числится «Микропроцессорная техника». Самое время тряхнуть стариной! Тем более, следуя советам знающих людей, ещё летом на Aliexpress я прикупил пару китайских Arduino Nano, ЖК-дисплей с I2C-адаптером, макетную плату и целую кучу всякой электронной мелочовки (светодиоды, «резюки» на 1 кОм, проводочки и т. п.).

Распакованные покупки прямо с почты

Дополнительным усложняющим условием стал запрет на использование Arduino IDE. Ну ладно, на самом деле это оказалось не совсем так, как я понял изначально. Ей рекомендовали не пользоваться, потому что она неудобная и не умеет даже в автодополнение. Но совет всё равно не становится менее странным для новичка и «виндузятника», потому что с нуля настроить окружение для компиляции и сборки кода на C/C++ с последующей прошивкой микроконтроллера — ни разу не тривиальная задача. Тем более что надо ещё и как-то вручную библиотеки подключать. А без них нужно учиться не только писать на чистой «сишечке», но ещё и на довольно низком уровне, работая напрямую с регистрами микроконтроллера по его спецификации (даташиту).

Проект я реализовывал постепенно: сначала всё настроил и заставил работать светофор, затем уже прикрутил цветной светодиод. Поскольку мне было жалко разбирать схему первого, то когда они оба оказались на макетной плате, я решил заставить их работать вместе одновременно. Тем более, что к этому времени я уже много знал про таймеры и прерывания (осиливать даташит было непросто, но вполне реально, опираясь на множество туториалов и примеров готовых программ в интернете).

В итоге у меня получилась следующая схема:

Схема подключения компонентов
Собранная схема
inb4: ножки припаяны просто ужасно

Кодировать будем всё на чистом Си, как завещал батя. Весь проект оформлен в виде полноценного git-репозитория, ссылкой на который является заголовок раздела. Я работал над ним в основном под Windows, но собирать под Linux тоже довелось, так что рассмотрим сначала, какие инструменты нам потребуются:

ОСИнструкции по настройке инструментария для сборки и прошивки
Windows

Писать код и собирать прошивку я предлагаю в официальной IDE от разработчиков микроконтроллера — AtmelStudio, основанной на Visual Studio. Там есть все библиотеки, компилятор и линковщик. Как прошивать в ней саму Ардуину по USB я так и не понял, поэтому использовал внешнюю программу SinaProg, представляющую собой графическую оболочку для AVRDude.

AVRDude
Linux (Debian 8)

Для начала нужно поставить все необходимые пакеты: gcc-avr (компилятор), avr-libc (стандартная библиотека языка Си), binutils-avr (набор утилит, в который входят, например, линковщик и avr-objcopy, позволяющий сконвертировать elf-файл в hex-файл), avrdude (утилита для загрузки программы в микроконтроллер (прошивки)).

Скомпилировать проект можно следующей командой:

avr-gcc -Os -std=c99 -mmcu=atmega328p -o firmware.elf main.c initialization.c trafficlight.c

На выходе получится elf-файл, который для прошивки необходимо сконвертировать в hex-файл:

avr-objcopy -j .text -j .data -O ihex firmware.elf firmware.hex

После чего наконец-то можно загрузить получившуюся программу в микроконтроллер:

# При наличии нескольких подключенных устройств может понадобиться смена ttyUSB0 на другое устройство.
avrdude -pm328p -carduino -P/dev/ttyUSB0 -b57600 -Uflash:w:firmware.hex:i

На самом деле, если будете клонировать проект из репозитория, то там уже есть два готовых shell-скрипта: build.sh собирает проект, а upload.sh загружает прошивку в микроконтроллер (если загружать нечего, автоматически вызывает сборку). Так что для сборки и прошивки микроконтроллера просто нужно выполнить следующую команду:

./upload.sh USB0    # или другой порт, к которому подключена Ардуина
Сборка и прошивка на Debian 8

Дополнено 24.02.2018: На всякий случай решил приложить файл с уже собранной прошивкой, который остаётся только загрузить в Arduino. Сборка производилась на 32-разрядной версии Debian 8.

Дополнено 26.09.2018: Обновлённая версия прошивки, где используется уход в спящий режим вместо холостого цикла в основном цикле. Сборка производилась на 64-разрядной версии Windows 10.

Дальше будет большая выжимка кусков кода с пояснениями из отчёта о лабораторном практикуме. Вполне допускаю, что начинающим многие вещи будут непонятны. Но поскольку я объяснять не умею, то могу только направить к другим веб-сайтам, которые подскажет поисковая система. Для отправной точки посоветую эту статью про порты и регистры.

В микроконтроллере ATmega328/P (согласно даташиту) 3 таймера и 6 пинов с аппаратным ШИМом (по 2 на каждый таймер). Использовать будем 8-битные таймеры под номерами 0 и 2. К первому относится пин D6, а ко второму – пины D11 и D3. При этом надо помнить, что пины D6 и D3 на уровне микроконтроллера относятся к порту D (PD6 и PD3 соответственно), а D11 – к порту B (PB3). Светофор же подключен к произвольно выбранным пинам A0-A2, привязанным к порту C (PC0-PC2). Чтоб использовать более говорящие названия пинов в коде, был написан следующий заголовочный файл pins.h:

#ifndef PINS_H_
#define PINS_H_

// ** Цветной светодиод **

#define RED_PIN         PD6     // D6
#define RED_PIN_PORT    PORTD
#define RED_PIN_DDR     DDRD

#define GREEN_PIN       PD3     // D3
#define GREEN_PIN_PORT  PORTD
#define GREEN_PIN_DDR   DDRD

#define BLUE_PIN        PB3     // D11
#define BLUE_PIN_PORT   PORTB
#define BLUE_PIN_DDR    DDRB


#define RED_COMPONENT    OCR0A   // D6
#define GREEN_COMPONENT  OCR2B   // D3
#define BLUE_COMPONENT   OCR2A   // D11


// ** Светофор **

#define TRFFIC_LIGHT_DDR    DDRC
#define TRAFFIC_LIGHT_PORT  PORTC
#define RED_LIGHT           PC2     // A2
#define YELLOW_LIGHT        PC1     // A1
#define GREEN_LIGHT         PC0     // A0

#endif

Регистр DDR* задаёт режим ввода/вывода. Если бит, соответствующий какому-то пину установлен в единицу, он работает на вывод. Иначе на ввод.

Регистр OCR** задаёт число, с которым сравнивается счётчик таймера на каждой итерации приращения. Цифра в имени означает таймер, а буква – пин. Его значение может быть использовано для создания прерывания, но здесь оно использовано для генерации широтно-импульсной модуляции, так как в режиме «Fast PWM» на выводе ставится высокий уровень сигнала, когда таймер обнуляется, и сбрасывается в ноль, когда таймер достигает значения в OCR**.

Текущее состояние светофора хранится в структуре TrafficLightState, в которую входит структура TrafficLightColor, которая определяет состояние включённых цветов. Определены они в заголовочном файле trafficlight.h:

#ifndef TRAFFICLIGHT_H_
#define TRAFFICLIGHT_H_

#include <stdbool.h>


struct TrafficLightColor {
    bool red;
    bool yellow;
    bool green;
};

struct TrafficLightState {
    struct TrafficLightColor color;
    bool isBlinking;
    unsigned int blinksToChange;
} trafficLightState;


void turnRedOn(void);
void turnRedOff(void);
void turnYellowOn(void);
void turnYellowOff(void);
void turnGreenOn(void);
void turnGreenOff(void);

void setFastTimer(void);
void setSlowTimer(void);

#endif

Также здесь определён ряд функций для управления состоянием светофора. Две последние используются для изменения режима таймера, чтобы мигание было быстрее, чем обычная смена цветов. Реализация данных функций находится в файле trafficlight.c:

#include <avr/io.h>
#include <stdbool.h>

#include "pins.h"
#include "trafficlight.h"


void turnRedOn(void) {
    TRAFFIC_LIGHT_PORT |= _BV(RED_LIGHT);
    trafficLightState.color.red = true;
}

void turnRedOff(void) {
    TRAFFIC_LIGHT_PORT &= ~_BV(RED_LIGHT);
    trafficLightState.color.red = false;
}

void turnYellowOn(void) {
    TRAFFIC_LIGHT_PORT |= _BV(YELLOW_LIGHT);
    trafficLightState.color.yellow = true;
}

void turnYellowOff(void) {
    TRAFFIC_LIGHT_PORT &= ~_BV(YELLOW_LIGHT);
    trafficLightState.color.yellow = false;
}

void turnGreenOn(void) {
    TRAFFIC_LIGHT_PORT |= _BV(GREEN_LIGHT);
    trafficLightState.color.green = true;
}

void turnGreenOff(void) {
    TRAFFIC_LIGHT_PORT &= ~_BV(GREEN_LIGHT);
    trafficLightState.color.green = false;
}

void setFastTimer(void) {
    TCCR1B |= _BV(CS11) | _BV(CS10);
    TCCR1B &= ~_BV(CS12);
}

void setSlowTimer(void) {
    TCCR1B |= _BV(CS12) | _BV(CS10);
    TCCR1B &= ~_BV(CS11);
}

_BV – макрос, превращающийся сдвиг на указанное количество битов влево. А под названиями портов как раз скрывается это смещение.

TCCR1B – регистр управления таймером 1. Это 16-битный таймер, который используется для медленной смены сигналов светофора, когда указан максимальный делитель тактовой частоты (1024), и более быстрой, когда он снижается (до 64).

В файле initialization.h определены две отдельные функции для настройки портов, таймеров, обнуления всех значений и установки прерываний:

#ifndef INITIALIZATION_H_
#define INITIALIZATION_H_

void initializeRgbLed(void);
void initializeTraficLight(void);

#endif

Они определены в initialization.c:

#include <avr/io.h>

#include "pins.h"
#include "trafficlight.h"
#include "initialization.h"


void initializeRgbLed(void) {
    // Устанавливаем пины на вывод.
    RED_PIN_DDR |= _BV(RED_PIN);
    GREEN_PIN_DDR |= _BV(GREEN_PIN);
    BLUE_PIN_DDR |= _BV(BLUE_PIN);
    
    // Обнуляем значения на портах.
    RED_PIN_PORT &= ~_BV(RED_PIN);
    GREEN_PIN_PORT &= ~_BV(GREEN_PIN);
    BLUE_PIN_PORT &= ~_BV(BLUE_PIN);

    // Обнуляем Timer/Counter0 (пины D6 и D5) и Timer/Counter2 (D11, D3).
    TCNT0 = 0;
    TCNT2 = 0;

    // Начинаем с полностью погашенного светодиода.
    RED_COMPONENT = 0;
    GREEN_COMPONENT = 0;
    BLUE_COMPONENT = 0;

    // Устанавливаем, что хотим получить на пине D6 ШИМ.
    TCCR0A |= _BV(COM0A1) | _BV(WGM01) | _BV(WGM00);
    // Устанавливаем делитель частоты равным 1024.
    TCCR0B |= _BV(CS02) | _BV(CS00);

    // То же самое, но для пинов D11 и D3.
    TCCR2A |= _BV(COM2A1) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);  
    TCCR2B |= _BV(CS22) | _BV(CS20);

    // Включаем создание прерываний при переполнении счётчика.
    TIMSK0 |= _BV(TOIE0);   
}

void initializeTraficLight(void) {
    // Устанавливаем пины на выход
    TRFFIC_LIGHT_DDR |= _BV(RED_LIGHT) | _BV(YELLOW_LIGHT) | _BV(GREEN_LIGHT);

    // Начинать будем с красного цвета.
    turnRedOn();
    turnYellowOff();
    turnGreenOff();

    // Сбрасываем Timer/Counter1.
    TCNT1 = 0x00;
    // Устанавливаем делитель частоты равным 1024.
    setSlowTimer();

    // Включаем создание прерываний при переполнении счётчика.
    TIMSK1 |= _BV(TOIE1);
}

В процессе инициализации включаются прерывания переполнения для таймеров 0 и 1. Обработчик первого используется для изменения цвета через смену значения в соответствующем регистре OCR**. Обработчик второго выполняется гораздо реже и используется для смены состояния светофора. Все эти действия прописаны в основном файле main.c:

#include <avr/interrupt.h>

#include "pins.h"
#include "trafficlight.h"
#include "initialization.h"
#include 


// Количество миганий зелёного цвета перед тем, как его сменит жёлтый.
// Должно быть нечётным числом!
#define BLINKS 7

#if BLINKS % 2 == 0
    #error "BLINKS должно быть нечётным числом!"
#endif


/* Определяем переменные для хранения компонентов цвета многоцветного светодиода.
Причём просим компилятор всегда брать данные из памяти, а не использовать регистры
для оптимизации, чтобы обработчики прерываний всегда видели актуальные значения. */
volatile uint8_t red = 0;
volatile uint8_t green = 0;
volatile uint8_t blue = 0;


// Точка входа в программу.
int main(void)
{   
    initializeRgbLed();
    initializeTraficLight();
    // Устанавливаем регистр глобальных прерываний
    // (то есть включаем их в принципе).
    sei();
    // Программа должна крутиться в цикле, даже если он ничего не делает!
    // Иначе выполняться начнут данные и мусор в памяти.
    // Но вместо того, чтобы просто гонять процессор зазря, воспользуемся самым
    // "лёгким" режимом сна, который будет прерываться только по таймерам.
    // Спасибо товарищу KivApple за подсказку (обновлено 26.09.2018)
    set_sleep_mode(SLEEP_MODE_IDLE);
    while (1) {
      sleep_mode();
    }
}


// Обработчик смены цвета многоцветного светодиода.
ISR(TIMER0_OVF_vect) {
    // 1) Увеличиваем красную компоненту
    // (и уменьшаем синюю, если это не первая итерация).
    if (red < 0xFF && green == 0)
    {
        if (blue > 0) {
            BLUE_COMPONENT = --blue;
        }
        RED_COMPONENT = ++red;
    }
    // 2) Увеличиваем зелёную и уменьшаем красную.
    else if (green < 0xFF && blue == 0)
    {
        RED_COMPONENT = --red;
        GREEN_COMPONENT = ++green;
    }
    // 3) Увеличиваем синюю и уменьшаем зелёную.
    else if (blue < 0xFF)
    {
        GREEN_COMPONENT = --green;
        BLUE_COMPONENT = ++blue;
    }
}

// Обработчик изменения состояния светофора.
ISR(TIMER1_OVF_vect) {
    // Если горит красный...
    if (trafficLightState.color.red) {
        // ...но не жёлтый, тогда они оба должны гореть.
        if (!trafficLightState.color.yellow) {
            turnYellowOn();
        }
        // Если оба, то должен загореться зелёный.
        else {
            turnRedOff();
            turnYellowOff();
            turnGreenOn();
        }
    }
    // Если горит жёлтый без красного, то до этого был зелёный и
    // следующим будет красный.
    else if (trafficLightState.color.yellow) {
        turnYellowOff();
        turnRedOn();
    }
    // Так как все остальные состояния мы уже проверили, то здесь
    // может либо гореть зелёный, либо мигать.
    else {
        // И если он не мигает, то сделаем, чтоб мигал!
        if (!trafficLightState.isBlinking) {
            turnGreenOff();
            trafficLightState.blinksToChange = BLINKS;
            trafficLightState.isBlinking = true;
            // Мигания должны быть быстрее, чем смена цветов.
            setFastTimer();
        }
        // Реализация миганий.
        else if (trafficLightState.blinksToChange > 0) {
            if (trafficLightState.blinksToChange % 2 == 0) {
                turnGreenOn();
            } else {
                turnGreenOff();
            }
            trafficLightState.blinksToChange--;
        }
        // После миганий зажигаем жёлтый сигнал светофора.
        else {
            turnGreenOff();
            turnYellowOn();
            trafficLightState.isBlinking = false;
            // И не забываем вернуть медленную скорость таймера.
            setSlowTimer();
        }
    }
    
}

Результат работы программы выглядит следующим образом:

Результат выполнения программы

Видео с демонстрацией работы (как файл)
Для светодиодов разных цветов нужно разное сопротивление. Поскольку у меня резисторы одинаковые, то и горят они по-разному.

Hello World

Напоследок поработаем с жидкокристаллическим монохромным двустрочным дисплеем на 16 символов. Мне посоветовали сразу взять с припаянным адаптером PCF8574, чтобы работать с ним по шине I2C и сэкономить выходы микроконтроллера (и не запутаться в проводах!).

Схема подключения I2C-адаптера

Изначально я рассматривал вариант и тут пойти по сложному пути и написать всё самому самом низком уровне, но, почитав даташит, почувствовал себя немного overwhelmed. Да и сроки уже поджимали. Поэтому я всё-таки сдался, пожалел себя и решил срезать, установив Arduino IDE, для которой есть готовая библиотека LiquidCrystal-I2C.

Arduino IDE с набранной программой

Важно устанавливать библиотеку из архива и подключать к проекту через саму среду разработки (если её можно таковой назвать), чтобы она распаковала файлы в нужное место и правильно расставила пути для сборки.

В результате получаем следующее:

Моё имя, выведенное на дисплей

Если текста не видно, то нужно покрутить отвёрткой потенциометр на адаптере и настоить контрастность дисплея.

В качестве домашнего задания можете вывести классическое программистское приветствие из заголовка.

Немного усложним задачу. Выводимые символы жёстко заданы прямо в контроллере дисплея. Покупая экран у китайцев, глупо ожидать увидеть там русские буквы. Вместо них там находятся какие-то иероглифы. Чтобы убедиться в этом, напишем небольшую программу, которая будет перебирать все 255 символов и выводить их поочерёдно на экран:

#include <LiquidCrystal_I2C.h>

// Задаём параметры дисплея: адрес на шине I2C (0x3F), количество символов (16) и строк (2).
LiquidCrystal_I2C lcd(0x3F, 16, 2);

// Счётчик, который будет обнуляться при инкрементировании после 255.
byte i = 0;

void setup() {
  // Инициализируем экран.
  lcd.begin();
  // Неизменяющийся текст выводим один раз при старте программы.
  lcd.print("Number:");
  lcd.setCursor(0, 1);
  lcd.print("Character:");
}

void loop() {
  // Выводим значение счётчика.
  // При этом надо не забывать про обновление всех трёх разрядов числа.
  lcd.setCursor(9, 0);
  lcd.print("  ");
  lcd.setCursor(8, 0);
  lcd.print(i);

  // Выводим соответствующий ему символ, зашитый в дисплее.
  lcd.setCursor(11, 1);
  lcd.print((char) i);
  
  i++;
  // Установим задержку в полсекунды, чтоб успевать следить за символами.
  delay(500);
}
Перебор символов с выводом их кодов

Вообще, дисплей позволяет задать 8 пользовательских глифов. Существуют библиотеки, которые пользуются этим небольшим запасом и одинаковостью/похожестью кириллических и латинских символов, чтобы эмулировать хотя бы часть русских букв (не помню: то ли только заглавные, то ли только строчные). Но они сделаны либо жёстко под LiquidCrystal без I2C, либо не завелись у меня.


Бонус

Пост получился и так очень большим, но всё-таки не могу удержаться и не вставить ещё одну лабораторную работу, написанную под эмулятор процессора i8086 на чистом ассемблере.

Задание: разработать подпрограмму определения количества вхождений заданного числа в массив чисел, заданный начальным и конечным адресом памяти. Вызов подпрограммы сделать из основной программы.

mvi a, 02     ; Искомое число
lxi b, 0900   ; Начальный адрес
push b
lxi b, 090a   ; Конечный адрес
push b

call count    ; Вызов подпрограммы
hlt

count:
    pop b     ; Адрес возврата -> BC
    pop d     ; Адрес последнего значения  -> DE
    pop h     ; Адрес первого значения -> HL
    push b    ; Возвращаем адрес возврата в стек
    mov b, a
    mvi c, 00
    mov a, d
    cmp h
    jm swap   ; Если передан сначала больший адрес, то меняем их местами
    mov a, e
    cmp l
    jm swap   ; Проверяем и для старшей, и для младшей части
    dcx h
check:
    inx h
    mov a, h
    cmp d
    jnz do_work     ; Для обеих частей проверяем, не последний ли это элемент
    mov a, e
    cmp l
    jnz do_work     ; Если нет, то выполняем сравнение
    mov a, c        ; Иначе размещаем результат в аккумуляторе
    ret             ; И выходим из подпрограммы
do_work:
    mov a, b
    cmp m
    jnz check
    inr c   
    jmp check
swap:
    push d
    mov d, h
    mov e, l
    pop h
    jmp check
27.01.2018


В последние дни, поддавшись «эффекту Википедии», я прочитал много страниц документации Пайтона, посвящённых различным модулям из стандартной библиотеки. И, скажу вам, что их немало. На каждый чих и любой типичный use case есть свой готовый модуль. И в этом посте я хочу привести подборку некоторых не самых известных (на самом деле может быть просто я такой слоупок, что узнал о них только сейчас), но очень полезных модулей, которые помогут экономить время и силы вместо того, чтобы изобретать колесо заново. Надеюсь, кому-то эта информация окажется полезной, а то, если честно, я даже устал столько всего читать :(

Предполагается, что это пролонгированный пост, который будет периодически дополняться, пополняясь новыми ссылками и названиями. Следите за обновлениями в соцсетях (в частности, в ВК, Щебетаче и Телеграмме)!


re — регулярные выражения

Начнём с известнейшего модуля для работы с регулярными выражениями. Думаю, все о нём знают, поэтому я включил его сюда, только чтобы напомнить о необходимости чтения документации. По размеру она у него приличная, но реально полезная. Кто, например, знал, что можно отключить сохранение группы, если скобочки нужны исключительно для группировки выражений, с помощью вопросительного знака и двоеточия? Простой пример: тян(?:ка)?. Или что группы можно именовать? А про режим многословных (verbose) выражений, когда выражение можно дробить на строки и вписывать комментарии прямо в нём же? То-то же! Думаю, многие про них знают, но не могу не упомянуть и про опережающие и ретроспективные проверки, которые порой ну очень полезны.

Также отдельно отмечу, что питоновкий модуль из коробки умеет кэшировать скомпилированные выражения, а все функции, расположенные на уровне модуля, вызывают точно ту же компиляцию, что и явная функция compile. Так что если в программе используется всего несколько выражений (на данный момент _MAXCACHE = 512), то нет ничего страшного, если не сохранять результат их компиляции в переменную явно. В Java, например, так нельзя. Там компиляция выражения каждый раз выполняется заново, так что обязательно нужно куда-то сохранять её результат.


configparser — парсинг конфигурационных файлов

В состав KozConfigurator'а входят два скрипта, предназначенных для облегчения работы с Samba: ksharman.py и kremount.py, которые предназначены для изменения конфигурационных файлов Samba, чтоб «шарить» локальные папки и монтировать сетевые. Конфиги в них парсятся вручную регулярными выражениями. А в ksharman.py так ещё и написан целый класс для составления правильной структуры файла при перезаписи. Но, оказывается, всё это было зря, потому что уже есть готовый модуль для работы с конфигами в стиле *.ini-файлов! Он берёт на себя отображение секций, ключей и значений в обычные dict'оподобные объекты и предоставляет вспомогательные функции для преобразования типов. Модуль поддаётся настройке и пригоден для использования с многими похожими форматами файлов.


csv — текстовые таблицы

Модуль для работы с файлами в так называемом comma-separated values формате. Причём значения могут быть разделены не только запятыми, но и табуляциями. Модуль позволяет выбирать из нескольких форматов и отдельный акцент делается на работе с экспортированными из Microsoft Excel таблицами. Не припомню, чтоб мне нужно было работать с такими файлами, но вообще это очень распространённый формат, так что определённо рано или поздно модуль пригодится.


cmd — создаём свой командный интерпретатор

Другой пример модуля, помогающего избежать велосипедостроения. Возьмём мой недавний скрипт для подсчёта квадратных уравнений. Там я вручную зацикливал программу для организации ввода произвольного количества выражений. Оказывается, для этого тоже уже предусмотрен отдельный модуль! И пусть в моём случае переизобретённое колесо работает вполне неплохо, но в более сложных ситуациях, когда есть набор чётко определённых команд и необходимость вывода справочного сообщения по работе с программой, готовый модуль поможет не только спасти немного времени, но и позволит написать красивый код. Ну вот например:

import cmd


class Main(cmd.Cmd):
    intro = "Just a counter :3"
    prompt = "SadBot > "

    def __init__(self):
        super().__init__()
        self.counter = 0

    def do_inc(self, arg):
        """Increase the value of the counter."""
        
        self.counter += 1
        print(self.counter)

    def do_dec(self, arg):
        """Decrease the value of the counter."""

        self.counter -= 1
        print(self.counter)

    def do_set(self, arg):
        """Set the counter to a specified value."""

        try:
            self.counter = int(arg)
        except ValueError:
            print("Invalid value!")
        else:
            print(self.counter)

    def do_print(self, arg):
        """Print the value of the counter."""

        print(self.counter)

    def do_bye(self, arg):
        print("Bye!")
        exit()

if __name__ == "__main__":
    Main().cmdloop()

Скриншот с результатом выполнения программы


shutil — операции с файлами

Ещё одно моё недавнее открытие. Поскольку Python часто используется в *NIX'ах в качестве скриптового языка общего назначения, то там этот модуль может быть особенно полезен. Копирование, перемещение, удаление, архивирование и разархивирование файлов, изменение влацельца файла или прав доступа — это всё к нему.


pathlib — пути для людей

В последних версиях Python наконец-то завезли человеческие пути в виде полноценных объектов, прям как в Java 8. Так что можно больше не дёргать строки туда-сюда через модуль os.path! К тому же объекты путей поддерживают некоторые файловые операции вроде удаления, перемещения или переименования.

Небольшой пример. Как было:

import os
import shutil

path = os.path.join("dir1", "dir2", "file.txt")
if not os.path.exists(path):
    absolute_path = os.path.abspath(path)
    print('"%s" does not exist!' % path)
else:
    with open(path) as f:
        print(f.readline())
    new_path = os.path.join(os.path.dirname(path), "file2.txt")
    shutil.move(path, new_path)

Как стало (примерно):

from pathlib import Path

path = Path.cwd() / "dir1" / "dir2" / "file.txt"
if not path.exists():
    print('"%s" does not exist!' % path)
else:
    with path.open() as f:
        print(f.readline())
    path.rename("file2.txt")

К сожалению, последний пример будет работать лишь в последней на данный момент версии змеиного языка — Python 3.6. Несмотря на то, что модуль добавлен ещё в 3.4, полноценная поддержка во встроенных функциях появилась совсем недавно.


io.StringIO — StringBuilder

Как в Java, так и в Python строки — это неизменяемые объекты. Поэтому при каждой конкатенации создаётся новый объект, что не особо эффективно при динамическом создании строк, когда присоединений выполняется очень много. Для решения этой проблемы в Java есть отдельный класс StringBuilder. В Питоне его роль играет класс StringIO из модуля io. На самом деле прямое сравнение правильнее было бы проводить с классами stream'ов, но не суть.

Очень простой и тупой пример:

s = io.StringIO()
for c in "abcdef":
    s.write(c)
s.getvalue()

В модуле есть аналогичный класс для работы с байтами и ещё много других классов, используемых высокоуровневыми API. Например, при открытии файла в текстовом режиме функция open вернёт io.TextIOWrapper, а при открытии в режиме чтения байтов — io.BufferedReader.


array — массивы для упоротых

Пока не ушли далеко от низкоуровневых штук, упомяну, что в Пайтоне есть возможность создавать не только списки, но и обычные типизированные массивы. Вызывающее название пункта обусловлено тем, что я не знаю, зачем это может кому-то пригодиться. Наверное, для каких-то оптимизаций. Хотя я едва ли могу представить человека, который будет писать что-то, требующее хорошей производительности, на Питоне.


warnings — предупреждения

Кроме исключений, приводящих к завершению программы, если их никто не перехватывает, есть ещё такой зверь, как предупреждения. Функции модуля позволяют настроить, как предупреждения будут обрабатываться: игнорироваться, выводиться в stderr или же генерировать соответствующие исключения, завершая выполнение программы. Как и сообщения из модуля логирования (его, кстати, можно настроить на перехват всех предупреждений и превращение их в записи лога), предупреждения делятся на уровни по своей важности.

Думаю, иногда этот модуль может быть полезен.


pdb — отладчик

Последним на сегодня рассмотрим модуль отладчика. Вряд ли кто-то будет им пользоваться во время разработки (ведь отлаживать в IDE гораздо удобнее), но для общего развития и на случай, если кому-то придётся отлаживать код на продакшене (упаси господи, конечно!), упомянуть его стоит. Отладчик построен на основе другого модуля bdb, представляющего собой фреймворк для создания отладчиков. Но внутренняя кухня нам не особо интересна. Главное — что через отладчик можно запустить любой скрипт (python -m pdb my_script.py) и затем управлять выполнением программы, используя специальные команды, перечисленные в документации.

24.09.2017


Примечание

Данная статья была написана мной более года назад в качестве отчёта о практике для универа. В кои-то веки я решил её опубликовать. Надеюсь, ещё не совсем потеряла актуальности :/

В связи со спецификой появления, думаю, во избежание лишних вопросов мне стоит пояснить, что САФУ/NArFU — это название университета, а ИСМАРТ — конкретный институт в его составе, где я учусь. Стиль изложения также может несколько отличаться от моих обычных публикаций, так как подразумевалось, что читать текст будут люди совсем далёкие от игр и современной сетевой культуры.

Также для основной версии сайта я прикрутил подсветку синтаксиса (на lite- и pda-версиях не работает), но Ren'Py — довольно специфичный язык, поэтому используется подсветка от Python'а. В итоге подсвечены будут, по сути, только строки и некоторые совпадающие ключевые слова.

Если при чтении возникнут какие-то проблемы или вопросы по коду, то можно скачать архив с примерами. Он упакован в формат 7z, потому что, в отличие от zip'а, тот умеет упаковывать идентичные файлы так, чтобы они занимали место, как один.


Гайд по созданию визуальной новеллы

Здравствуй, дорогой читатель! Возможно, ты уже слышал о такой широко известной в узких кругах видеоигре от отечественных разработчиков, как «Бесконечное лето» (Everlasting Summer)? Впрочем, если нет, то ничего страшного: я расскажу о ней вкратце.

Как гласит официальный сайт, «„Бесконечное лето“ — это визуальная новелла от российских разработчиков, дарящая самые искренние и светлые переживания об ушедших днях и надеждах, которым ещё предстоит сбыться». Поясняю:

  • Визуальный роман (но чаще встречается вариант с англицизмом: визуальная новелла) — это жанр компьютерных игр, в котором упор делается на текстовый контент, сдобренный порциями аудиовизуального сопровождения. По сути, это скорее книги, чем игры, но обильно снабжённые иллюстрациями, анимацией, музыкой и звуками окружающего мира. Также, в отличие от книг, представители данного жанра обычно имеют нелинейный сюжет с несколькими концовками, и исход зависит от выборов игрока.
  • «Бесконечное лето» повествует историю об одном асоциальном человеке, волею судеб оказавшемся в загадочном пионерлагере. Многим игра нравится именно атмосферой, позволяющей почувствовать себя пионером либо поностальгировать о временах былой молодости.

Однако мы здесь собрались вовсе не для того, чтобы слушать рекламу и хвалебные оды, не так ли? Так что перейдём ближе к сути.

«Бесконечное лето» написано на бесплатном движке Ren'Py. Это специальный движок, предназначенный для создания визуальных романов сценаристами, далёкими от программирования. Он имеет предельно простой синтаксис и множество встроенных средств для реализации практически всех функций, которые могут понадобиться в подобных играх. Тем не менее, сам движок свободен (open source) и расширяем, предоставляя возможность пользоваться всей мощью языка Python, на котором он написан.

В этом небольшом гайде мы рассмотрим базовые возможности Ren'Py, а также слегка коснёмся темы создания модификаций к игре «Бесконечное лето».


SDK

Для того, чтобы начать создавать игры на Ren'Py, первым делом скачай и установи/распакуй набор для разработчиков с официального сайта. После запуска покажется такое окно:

Окно Ren'Py Launcher

Это Ren'Py Launcher — программа, из которой можно запустить любой из разрабатываемых проектов, открыть папку с ним или сразу перейти к редактированию скриптов. Все команды написаны на простом английском языке, не выходящем за рамки даже слабой школьной российской программы, так что не вижу смысла на них останавливаться. К тому же в настройках можно включить кривой перевод на русский язык.

Создай новый проект, укажи папку (желательно, без кириллицы, но это не обязательно), имя проекта (вот тут только латиница!) и выбери любую тему (это непринципиально).


Основы основ

Ren'Py уже при создании производит базовую настройку проекта, предоставляя интерфейс для меню, стили по умолчанию и т. д. Можешь посмотреть файлы скриптов, которые он создал:

  • В options.rpy находится большинство настроек: размеры окна, стили, переходы по умолчанию и т. д.
  • В screens.rpy описываются меню, диалоговое окно и прочие «экраны».
  • script.rpy предназначен, собственно, для самого скрипта-сценария романа.

Вообще, это всё условно. Все файлы проекта находятся в папке game и Ren'Py читает там все файлы с расширением *.rpy, так что можно разбивать скрипты на любое количество файлов.

Я бы мог рассказать ещё про основы языка Python, как сделано в документации, но всё, что нам понадобится для базового знакомства, интуитивно-понятно или будет пояснено в месте применения. Так что предлагаю просто нажать по ссылке на scripts.rpy в «лончере» (launcher), выбрать редактор (любой, я вообще пользуюсь Sublime Text 3 с плагином) и приступим к созданию твоего первого творения на Ren'Py!


Текст, картинки, переходы

Сотри всё, что уже есть в файле: мы начнём с нуля.

Для упорядочивания кода Ren'Py пользуется системой меток (привет, ассемблер и GOTO!). При выборе кнопки «Начать игру» в главном меню игры, которое отображается сразу после запуска, управление передаётся на метку start:

label start:
    "Здесь начинается наш рассказ. Со слов рассказчика."
    "Аня" "Кто здесь?"
    "Ответом была лишь тишина."
    "Аня" "Эй! Я точно знаю, что ты там!"

Что мы видим? После объявления метки start начинается принадлежащий ей блок кода, строки которого обладают отступом в 4 пробела. Внутри блока расположены речевые конструкции (say statements), с помощью которых выводится текст.

В первой и третьей строчках (блока) располагаются слова автора или рассказчика. Фраза просто заключается в двойные кавычки (если внутри фразы нужно вывести кавычки, то они предваряются обратным слешем: \").

Во второй и четвёртой строках находится фраза персонажа под именем Аня. Видно, что имя персонажа, так же заключённое в двойные кавычки, и его фраза отделяются пробелом.

Возникает первая проблема: постоянно приходится повторять имя персонажа (а в больших сценариях с более длинными именами это реально представляет проблему). Ну и сразу обращаю внимание на вторую: все персонажи отображаются абсолютно одинаково, без какой-либо толики индивидуальности. Решим обе эти проблемы:

define a = Character('Аня', color='#ffaaaa')   # могут быть и двойные кавычки
# Ах да, после знака решётки идут комментарии, которые просто игнорируются.

label start:
    "Здесь начинается наш рассказ. Со слов рассказчика."
    a "Кто здесь?"
    "Ответом была лишь тишина."
    a "Эй! Я точно знаю, что ты там!"

В самое начало я добавил новую строку, которая определяет (define) нового персонажа (character) и задаёт ему имя и красноватый цвет имени (color). Новый персонаж присваивается короткой переменной a, которая теперь используется в речевых конструкциях вместо имени. Обратите внимание, что определения (что персонажей, что изображений, как мы увидим позже) пишутся вне любых меток, так как выполняются во время инициализации (немного про это будет сказано позже).

Что же у нас получилось?

Первый запуск нашего проекта

Хм, надо бы избавиться от этих шашечек и добавить собственный фон. Я использую, например, фотографию крыльца своего института, найденную в Интернете. Закинь любое изображение достаточного размера в папку images внутри папки game. Внутри этой папки Ren'Py сканирует все изображения и по определённым правилам создаёт для них имена, но для наглядности мы определим фон вручную. Самый простой способ:

image bg ismart = 'images/ismart.jpg'

Просто путь к файлу! Но если мы сделаем так, то получим странную картину:

Фон слишком большой

Виной всему слишком большой размер изображения. Поэтому придётся немного усложнить определение. Вот весь код:

image bg ismart = im.Scale('images/ismart.jpg', config.screen_width, config.screen_height)
define a = Character('Аня', color='#ffaaaa')

label start:
    scene bg ismart

    "Здесь начинается наш рассказ. Со слов рассказчика."
    a "Кто здесь?"
    "Ответом была лишь тишина."
    a "Эй! Я точно знаю, что ты там!"

Во! Теперь всё в порядке:

Теперь фон вписывается идеально

Теперь объясню, что делает добавленный код и как вообще происходит отображение. Всего появились две новые строчки: конструкция с image и конструкция с scene:

  • image, собственно, определяет изображение. Между ключевым словом и знаком равенства помещается имя отображаемого объекта. С ним не всё так просто. Как мы увидим дальше, в играх такого жанра, часто используются так называемые спрайты, то есть изображения персонажей с разными эмоциями и одеждой. Соответственно, частой операцией является смена спрайтов одного персонажа. По этой причине название изображения для Ren'Py состоит из, собственно, имени и атрибутов, разделённых пробелами. Всё это станет понятнее, когда будем рассматривать спрайты. А сейчас достаточно понять, что bg — название изображения, ismart — атрибут.
  • scene, как понятно из названия, предназначен для смены сцены. Выражение с этим словом очищает экран от любых других изображений и выводит указанное (обычно фон, хотя может и не выводить ничего).
  • im.Scale(d, width, height) — функция масштабирования, которая принимает отображаемый объект (displayable), в частности изображения, и изменяет его размеры к указанной ширине (width) и высоте (height). Сразу замечу, что существует аналогичная функция без префикса im.* (вне объекта im), которая отличается тем, что производит расчёты при каждом отображении и не сохраняет результат вычислений при определении.
  • config.screen_width и config.screen_height — две переменные в объекте конфигурации, хранящие заданные в настройках ширину и высоту экрана игры (их легко можно найти в options.rpy).

Надеюсь, я объяснил достаточно понятно. Давай теперь поиграем со стилями? Открой screens.rpy, найди в самом начале описание экрана диалогов (screen say) и удалим всё «лишнее», добавив немного своего (выделено полужирным):

screen say:

    default side_image = None
    default two_window = True

    vbox:
        style "say_two_window_vbox"

        if who:
            window:
                style "say_who_window"
                background "#555522"

                text who:
                    id "who"

        window:
            id "window"
            background "#555599"

            has vbox:
                style "say_vbox"

            text what id "what" color "#000000" italic True

Рассмотрение стилей, к сожалению, выходит за рамки данного гайда, так что я не стану ничего пояснять. Будем считать, что разобраться в работе стилей будет твоим домашним заданием ;)

Ну а своим вандализмом мы добились такого цветастого безобразия без меню и возможности переключения режима отображения имени персонажа (в том же окне или в отдельном) из сценария:

Зря мы трогали стили

«Бесконечное лето»

Мы обязательно должны рассмотреть отображение спрайтов, изменение места их расположения и переходы, но искать спрайты в Интернете мне лень, так что рассмотрим этот материал внутри обещанного блока про создание модификаций для «Бесконечного лета».

Прежде всего скачай саму игру: либо через сервис цифровой дистрибуции Steam, либо через торрент-трекер Rutracker. Актуальные ссылки можно всегда найти на официальном сайте. Далее зайди в папку с игрой (для версии из Steam это <папка библиотеки>\SteamApps\common\Everlasting Summer). Там обычная структура файлов игры на движке Ren'Py. Заходим в game, как и прежде, а оттуда в папку mods. Скопируй туда scripts.rpy нашего проекта. Можешь даже его как-нибудь переименовать — не суть.

Теоретически можно создать наш файл со скриптом в любом месте внутри папки game, но давай будем придерживаться соглашений. К тому же, нам надо перекинуть и картинку. Создай папку narfu_images рядом с файлом скрипта и скопируй туда наш фон.

В последних сборках игры добавлен загрузчик модификаций, так что нам нужно лишь дать ему знать, что мы есть за скрипт. Открой файл в любимом текстовом редакторе. Пришло время познакомиться с блоками инициализации и запуском кода на Python!

Определение изображений и персонажей, как было сказано в примечании ранее, автоматически размещается в блоке инициализации, который выполняется до загрузки игры. Соответственно, это то самое место, где наш скрипт будет заявлять о себе. Добавь этот код в самое начало скрипта:

init python:
    mods["narfu_practice"] = "Учебный мод"

Блок инициализации объявляется ключевым словом init. Если дальше идут выражения на языке Python, а не на Ren'Py, то добавляется слово python. Вообще, в любом месте блоки кода на Python объявляются с помощью этого слова. Другой путь написать одиночную команду на Python — использовать знак доллара ($) в начале строки.

init 0:
    $ mods["narfu_practice"] = "Учебный мод"

В примере выше я вместо слова python явно задал приоритет блока. Блоки выполняются от меньшего приоритета к большему. По умолчанию подразумевается блок с нулевым приоритетом. Порядок выполнения блоков с одинаковым приоритетом не гарантирован и может быть случайным.

Теперь объясняю саму команду. Переменная mods — это специальный список «ключ-значение», где перечисляются все модификации. Ключ должен быть уникальным значением на латинице без пробелов и в кавычках. Значение — название мода для отображения в загрузчике модификаций. Ключ играет роль названия метки при запуске мода, так что исправь label start на label narfu_practice. Также давай уберём определение персонажа a. Он нам больше не понадобится.

Теперь сделаем магию! Вставляй в редактор оставшийся код и запускай игру через меню модификаций в настройках!

label narfu_practice:
    scene bg ismart with fade

    window show
    "Здесь начинается наш рассказ. Со слов рассказчика." 

    show dv scared pioneer with dissolve
    dv "Кто здесь?"
    "Ответом была лишь тишина."

    show dv rage pioneer
    dv "Эй! Я точно знаю, что ты там!" with dissolve

    window auto
Наш мод в загрузчике модификаций

С чего бы начать? «Бесконечное лето» предоставляет интерфейс, персонажей, спрайты к ним, фоны и музыку. Здесь мы пока использовали только первые три пункта.

dv — это персонаж и тег изображений для Алисы Двачевской. Соответственно, scared, raged и pioneer — это атрибуты. Вообще, названия всех спрайтов в игре строятся по следующему плану: короткое_имя_персонажа эмоция одежда. Полный список спрайтов можно найти в знаменитых уроках LolBot'а (один из программистов игры, создатель системы модификаций). Официальной ссылки, насколько мне известно, не существует, а прочие быстро станут не актуальны, так что Google в помощь!

При показе второго изображения с таким же тегом, первое исчезает. Это нужно как раз для удобной смены эмоций, как сделано в нашем примере. Чтобы отобразить два одинаковых изображения, можно воспользоваться конструкцией show..as <новая метка> (см. следующий параграф).

Для отображения спрайтов используется ключевое слово show, похожее на scene, но не очищающее экран. Для обеих конструкций могут быть указаны переходы с помощью конструкции with. Строго говоря, изображение вообще отображается только после этой конструкции. Таким образом можно планировать показ нескольких изображений, для которых потом задавать один переход. Если with не встречается до следующего диалога, то Ren'Py использует with None, просто выводящий изображения мгновенно. А ещё его полезно использовать, когда нужно одно изображение отобразить мгновенно, а лишь второе — с переходом.

Ren'Py имеет ряд встроенных переходов (мы использовали dissolve и fade), а также позволяет создавать свои с помощью специальных функций (например, with Dissolve(0.5) для «растворения» одного изображения в другом за полсекунды). Всё это очень интересно, но выходит за рамки данного гайда. Смотри документацию, если хочешь знать больше!

Обрати внимание, как отображаются диалоги с изображениями в обоих случаях речи Алисы: в первом сначала рисуется персонаж, а затем печатается текст, а во втором — и текст, и персонаж переключатся с «растворением» одновременно.

Для того, чтобы диалоговое окно не мерцало при показе изображений, я использовал window show, которое держит окно всегда на экране. По умолчанию же оно показывается при отображении диалогов и скрывается при показе изображений, что я обратно и включаю в конце скрипта (надо помнить, что мод заканчивается, но игра продолжает выполняться, и мы обязаны всё вернуть в исходное состояние!).

Разумеется, существуют конструкции hide для скрытия изображении и window hide для скрытия диалогового окна.

А теперь давайте добавим второго героя, музыки и украшательств!

label narfu_practice:
    # Специальная функция и переменная «Бесконечного лета», которые задают
    # правильное освещение спрайтов и вид интерфейса для меню и диалогового окна.
    $ day_time()
    $ persistent.sprite_time = "day"

    # Да будет музыка!
    play music music_list["so_good_to_be_careless"]

    scene bg ismart with fade

    window show
    "Здесь начинается наш рассказ. Со слов рассказчика." 

    show dv scared pioneer with dissolve
    dv "Кто здесь?"
    "Ответом была лишь тишина."

    show dv rage pioneer
    dv "Эй! Я точно знаю, что ты там!" with dissolve

    # Отображаем спрайт за пределами экрана,
    # чтобы потом его переместить оттуда
    show sl normal pioneer:
        xanchor 0.0 xpos 1.0
    with None

    # Двигаем спрайты
    show dv rage pioneer:
        xanchor 0.5 xpos 0.25
    show sl normal pioneer:
        xanchor 0.5 xpos 0.75
    with move

    sl "О, привет, {i}Алиса{/i}!"
    sl "Как дела?"

    window auto

Прочитал комментарии? Я не буду пояснять про особенности игры, так как статья не об этом. Здесь мы обратим внимание позиционирование изображений, стилизацию текста и музыку.

Для проигрывания музыки и звуков существуют play music и play sound соответственно. Они принимают либо путь к файлу, либо список путей к файлам (в данном случае все пути к музыке из игры расположены в специальном списке, из которого мы берём конкретный). Для остановки используются stop music и stop sound, соответственно.

Музыка и звуки воспроизводятся на разных каналах, громкость которых игрок может регулировать в меню по отдельности. Ну а ещё звуки выполняются один раз, а музыка зацикливается. Разработчик же может регулировать не только громкость, но и скорость воспроизведения, баланс в колонках и многое другое. В том числе, можно создавать свои собственные каналы и играть несколько звуков одновременно.

Расположение спрайтов задаётся при показе либо с помощью at, либо в виде отдельного блока. В первом случае указывается предопределённое расположение (left или right, например), а во втором используется специальный язык ATL. Подробнее о нём можно прочитать в документации, а я лишь опишу базовое позиционирование, о котором также можно посмотреть наглядно в туториале, прилагаемом к Ren'Py SDK.

Положение изображений по обеим координатам задаётся с помощью двух точек: одной на изображении, называемой якорем (anchor), и второй на экране (pos). Задаётся либо с помощью целого числа, обозначающего координаты относительно верхнего левого угла экрана, либо с помощью дробного числа от нуля до единицы, обозначающего долю экрана, как сделано в примере.

Стилизация текста. Текст может содержать специальные теги, показывающие, как он должен отображаться. Для знакомых с языком HTML тут будет совсем просто: чаще всего используются точно такие же теги: {b}, {i}, {u}, {a} (для ссылок). Хотя есть и различия: {size=+5}. Просто. Также в тексте в квадратных скобках могут использоваться подстановки из переменных, определённых в коде. Если же есть необходимость использовать в тексте сами символы фигурных и квадратных скобках, но их нужно продублировать: {{, }}, [[, ]].

Внизу на картинке приведён результат. Неплохо получилось, не так ли?

Вот что у нас получилось

За сим я, пожалуй, откланиваюсь. В Ren'Py есть ещё очень много всего интересного, но место в этой статье не резиновое. Дальше, дорогой читатель, можешь либо почитать документацию (если знаешь английский язык, конечно), либо искать обучающие видео и руководства от других людей.

Удачи в творческих начинаниях!

13.12.2011


Когда я знакомился с различными языками программирования, то писал краткие конспекты, чтобы всегда можно было вспомнить основную информацию. Многие функции я лично не использовал, поэтому не могу гарантировать, что всё там написано правильно. Если найдёте какие-нибудь ошибки или опечатки, напишите, пожалуйста, о них в комментариях - исправлю.

Конспекты представлены текстовыми документами, чтобы вам было удобнее скачать и читать их в оффлайне. Все эти текстовые документы я запаковал в один ZIP-архив, так как не вижу смысла загружать такие маленькие файлы отдельными архивами.

Скачать ZIP-архив с конспектами.

11.12.2011


Вступление

О взломе через СЕ я уже писал, так что не вижу смысла повторяться. Скажу лишь пару слов об особенностях именно этой игры.

Количество "Солнца" и денег ищются как обычно через 4 байта, так что ничего сложного здесь нет. Единственное, так как в игре все монеты/алмазы дают "круглое" значение денег, то последний нуль в значении отбрасывается и приписывается самим кодом программы, то есть искать нужно без последнего нуля.
Максимальное значение денег - $ 999 990.
Максимальное количество "Солнца" - 9 999.
Если поставить больше, то игра их откатит именно до тих значений.

Тема этой статьи куда интереснее. Файл-сохранение игры весит меньше килобайта, так что я решил его исследовать. Честно говоря, впервые исследовал такой файл, но я быстро учусь и смог найти много интересного.

Тем, что мне известно о СЕ, невозможно или очень сложно достичь многого того, что доступно через файл-сохранения. В нём описано количество денег (единственное, что можно легко сделать через СЕ), пройденных миссий, количество прохождений кампании (при втором прохождении открывается Зомби-Йети, а также количество прохождений выдаёт при наведении золотой подсолнух), прохождении мини-игр, головоломок и выживаний, купленные предметы, цветы в Дзен-Саду.

Но всё-таки есть и ограничения. Пройти кампанию за минуту изменением байт не получится. Если нужно получить Серебряный Подсолнух, то придётся пройти последнюю миссию (5-10) против Доктора Зомбоса. Нельзя посадить растения в Дзен-Сад (подробнее написано в соответствующей главе статьи).

Что нам понадобится?

  • Сама игра Plants vs Zombies.
  • Шестнадцатиричный редактор WinHex.

Основы работы в WinHex'е

Я не буду расписывать, как пользоваться этой программой, потому что сам почти ничего не знаю. Расскажу лишь про те основы, которые надо знать, чтобы понять описываемый в этой статье материал. Если Вы захотите подробнее изучить данную прорамму, то сможете найти нужную информацию в Интернете.

С чем мы будем работать? Вот скрины моего файла. Используйте их для справки:
Скрин 1, скрин 2, скрин 3.

Откройте в WinHex'е файл "userX.dat", где Х - номер игрока (скорее всего 1), который находится в папке "PlantsVsZombies\userdata", где "PlantsVsZombies" - папка с игрой (по умолчанию "C:\Program Files\PlantsVsZombies".

Ориентироваться будем по слолбику слева (offset'у) и порядку байтов в ряду. Изменять байты можно, выбрав мышкой и введя на клавиатуре нужное значение в шестнадцатиричной системе исчисления.

Деньги, кампания, мини-игры, головоломки и выживания

В первой же строчке файла находим три интересные вещи: количество пройденных миссий, денег и прохождений кампании. Ваши параметры, разумеется, будут отличаться от моих. Привожу скрин (screenshot - снимок с экрана) для максимальных параметров (тут это не критично, если поставите больше, то значения либо будут равны нулю, либо откатятся до максимальных).

Красным цветом выделен байт, отвечающий за количество пройденных миссий. "32" - последняя миссия, если поставить больше, кампания начнётся сначала, но приз Вы не получите, так что эту миссию проходить обязательно.
Синим - байты, отвечающие за количество денег. Значение на скрине в игре равно $ 999 990.
Чёрным - байты, отвечающие за количество прохождений кампании. На скрине - 16777215.

Дальше идут значения выживаний. Так как они идут по раундам, значения у них равны раундам. На скрине прописаны значения, необходимые для прохождения всех выживаний (последнее одиннадцатое выживание бесконечное и почему-то число десять, поставленное для него, в игре не отображается, то есть его можно упустить при редактировании, так как кубок за него не дадут).

Дальше идут мини-игры и головоломки. Делается всё аналогично, как с выживанием, только раунд один.

Мини-игры:

Головоломки:

Взлом магазина Дейва

Следующий скрин. У себя нужный участок ищете по оффсету.

Первые два ряда, выделенные чёрным цветом - байты улучшений растений. По порядку: Горохострел, Двойной Подсолнух, Мракогриб, Кошкамышь, Колючий Камень, Золотой Магнит, Морозный Арбуз, Кукурузная Пушка.
Красным цветом выделен байт, отвечающий за количество оставшихся раундов, где будут Грабли.
Зелёным - байты Чистильщика Бассейна и Чистильщика Крыши.
Синим - слоты под растения. В начале игры 6 отделений. Расширение можно купить 4 раза. Итого 10 отделений и байт "04". Ставить больше четырёх - значит вылететь в начале миссии!
Тёмно синим - Золотая Лейка.
Далее описываются параметры, имеющие на скрине значение "00". Тут я немного со скрином ошибся. Для покупки предмета значение нужно поменять на "01".
Голубым - Имитатор.
Светло-зелёным/жёлтым (кому как) - Аптечка.
Тёмно-зелёным - Фонограф.
Оранжевым - Садовые Перчатки.
Розовым - Грибной сад.
Зелёным цветом (может, салатовым) - Садовый Аквариум.

Байты Дерева Мудрости находится в другом месте, а точнее около байтов мини-игр. На скрине ниже он выделен красным цветом. Значение может быть "FF FF FF":

Дзен-Сад

Здесь я нашёл не такие уж и интересные возможности. Параметры Дзен-Сада находятся в самом низу файла. В зависисости от количества растений длина этого блока различается. В моём случае, там всего два растения.

Первый байт, выделенный чёрным цветом, обозначает количество растений в Дзен-Саду. Но при его изменении игра выдаст ошибку и не запустится! При получении нового растения в файле дописывется новый блок байтов! Вы могли заметить, что байты раздвоились по цветам, это произошло именно из-за того, что у меня два растения.

Красным цветом выделены байты, отвечающие за ID (идентификатор) растения. То есть, если у Вас Золотая Ромашка, то, поменяв его, можно сделать, например, Скорострел. Скачать список ID (txt).
Зелёным - расположение растений. Тёмно-зелёным по оси Х, а светло-зелёным по оси Y. Отсчёт ведётся с нуля.
Синим - степень роста. "03" - степень взрослого растения, который при следующем ухаживании засветится и начнёт приносить прибыль. Ограничение: нельзя заставить растение светиться.

Размножаем растения

Выделяем байты растения (лучше счастливого), копируем их, щёлкаем по последнему байту, вставляем байты (на вопрос соглашаемся) и не забываем изменить количество растений.
Это наиболее простой способ.
Скрин.

Ещё можно щёлкнуть по последнему байту правой кнопкой мыши и выбрать "Edit" -> "Paste Zero Bytes" -> Подтверждаем -> Вводим "88" (без кавычек) и заменяем нули нужными параметрами* (не забудьте увеличить общее количество растений).
* Тут начинаются сложности, так как ещё не все параметры известны. Предлагаю кому-нибудь разобраться и написать в комментариях.
Скрин.

11.12.2011


Вступление

Здравствуйте! Я решил написать подробнейший гайд по взлому одиночных игр и созданию трейнеров для них. В этом гайде должны разобраться даже новички.
Конечно, одиночные игры начинают проигрывать многопользовательским, но их век ещё долго не закончится. Пока все онлайн игры до ужаса однообразны!
Готовые трейнеры можно скачать с этого сайта. Трейнер для игры, рассматриваемой здесь, делает бесконечное супер-оружие, которое позволяет очень легко выиграть игру.

Что понадобится?

  • Игра "АвиаНалёт". Для примера я решил взять её.
  • Программа Cheat Engine версии 5.5 (5.6 или 5.6.1). Самая удобная, быстрая и функциональная программа такого рода. Не пугайтесь сначала, на самом деле всё очень просто!
  • Программа Restorator 2009 Portable.

Взлом на бесконечное здоровье

Заходим в игру, и что мы видим?

  1. Сворачиваем игру. При этом она будет на паузе, но можете перед сворачиванием выйти в меню.
  2. Запускаем СЕ.
  3. Чтобы выбрать процесс игры нажимаем по изображению монитора (), потом ищем название процесса (смотрите на скрине ниже) и нажимаем "Open"

Выбираем процесс.

Так как в этой игре здоровье обозначается полоской (шкалой), и мы не знаем значение здоровья, придётся искать неизвестное значение.
Тип сканирования выбираем "Unknown initial value". Тип значения оставляем "4 Bytes".

Скрин.

Он найдёт 70774784 значения. Теперь нам надо уменьшить количество значений, то есть отсеять лишние.

Уменьшаем количество здоровья в игре:

Тип сканирования: уменьшенные значения (Decreased value) и нажимаем "Next".

Значений стало меньше 195 322.

Повторяем ещё несколько раз. Скрин 2.

Осталось 13 значений.

По логике, скорее всего, нужное значение 17207 или 16031. Методом тыка выясняем, что нужное значение 17207. Поменяем его на 17400.

Здоровье стало полным:

Теперь поставим 20000.

Более лёгкий способ поиска здоровья

Теперь, когда мы узнали количество здоровья, можно искать сначала точное значение в 4 байта, а потом уменьшенное. Находит 3 значения. Зелёное из них только одно, оно-то нам и нужно.

Зелёным цветом выделяются адреса, которые по мнению программы, не изменяют своё расположение. То есть при каждом запуске игры, они одинаковые и подходят для создания трейнеров.

Здоровье можно "заморозить", то есть программа сама будет восстанавливать указанное значение. Для этого нужно поставить слева галочку. Скрин будет ниже.
Если Вы не поняли, как ставить значение, скрин тоже будет ниже.

Взлом на 1 000 000 очков

По вышесказанному материалу попробуйте по скринам сами догадаться, как надо делать. Если не получится, пишите в комментариях, поможем.

Взлом 1-ого вида ракет
(со всеми остальными и с супер-оружием всё аналогично)

Опять попытайтесь разобраться по скринам.

Создание трейнера

Когда таблица создана, можно сделать трейнер. В меню "File" выбираем "Save". Тип файла: "Cheat Engine Trainer (*.EXE)".

Скрин 1. Скрин 2.

Нам предлагают создать новый трейнер, изменяющий данные в запущенной игре. Нажимаем "ОК".

Скрин.

Появилось два пугающих окна. Справа находится макет будующего трейнера. Мы можем за края изменить его размеры. Левое окно - настройки трейнера.
"Change Image" - изображение в левую часть трейнера.
"Change Icon" - иконка трейнера, на моём скрине значок СЕ.
"Title" - заголовок окна (скрин ниже).
"Launch file" - запускаемый файл игры, так как он бывает в разных местах, смотря куда устанавливают, лучше тут ничего не указывать, тогда пользователю будет предложено самому указать путь к файлу.
"Process" - нужный процесс.
Следующие два параметра никогда не менял, так что не знаю, что это.
"About text" - текст справки.
"Design own userinterface" - дизайн окна трейнера. Никогда не менял. Если разберётесь - напишите в комментариях.
Что за галочка тоже не знаю.
"Cancel" - отмена.
"Generate trainer" - создать трейнер, понадобится нам позже.

Скрин.

Слева список изменяемых параметров.
"Add Entry" и "Delete Entry" - добавить и удалить значение из списка соответственно.

Скрин.

Нажимаем "Add". У адресов можно было изменить описание, чтобы тут выбрать его и вписать значение. Переключатель стоит в положение заморозки. Жмём "Add".

Скрин.

Адрес появляется в списке. В поле "Description" можно вписать описание действия, я в поле "Hotkey" горячую клавишу, активирующую действие.

Скрин.

Если замораживать не надо, то ставим второй пункт переключателя.

Скрин.

Всё добавляем и генерируем трейнер.

Полный перевод трейнера

Трейнер, созданный в СЕ, содержит английские надписи, как на скрине:

Скрин.

Чтобы его перевести понадобится программа Restorator.
Запускаем её -> нажимаем "Открыть" -> находим трейнер и открываем его -> переходим в папку "RCData" -> в ресурс "TFRMMEMORYTRAINER".

Скрин 1. Скрин 2. Скрин 3. Скрин 4. Скрин 5. Скрин 6.

По скринам видно, где заменяются слова "About" и "Close" на "Справка" и "Закрыть" соответственно, "Hotkey" и "Effect" на "Клавиши" и "Эффект" соответственно.
Нажимаем "Применить" () и "Сохранить" ().

В итоге получился переведённый вариант.

Пояснения по типам сканирования переменных

Тип сканирования (Scan Type) - тип поиска.
Точное значение (Exact Value) используется, если мы знаем число, которое надо искать (например, когда здоровье в игре равно 100).
Больше чем... (Bigger than...), меньше чем... (Smaller than...) используется, если известно, что нужное значение меньше или больше какого-то числа (редко используются).
Значение между... (Value between...) используется, если значение находится между какими-то значениями (используется тоже рёдко).
Неизвестное значение (Unknown initial value) используется, если значение вообще неизвестно, далее следует долгое отсеевание.
Появляющиеся после первого сканирования:
Decreased value - уменьшившиеся значения.
Increased value - увеличившиеся значения.
Changed value - изменившиеся значения.
Unchanged value - неизменившиеся значения.
Same a first scan - такие же значения, как при первом сканировании.

Тип значения (Value type) - тип переменной. Под переменные (вспоминаем из курса математики что это такое) выделяется определённое количество памяти, зависящее от того, для чего она будет использоваться. Под текст выделяется больше памяти, чем на число.
Двоичные (Binary) - числа в двоичной системе счисления, то есть состоящие из последовательности нулей и единиц.
Байты (Byte, 2 Bytes, 4 Bytes, 8 Bytes) - размеры целочисленных чисел. Для большинства случаев подходит "4 Bytes".
Переменные с плавающей точкой (Float, Double) - числа, с цифрами после запятой. Double больше Float, то есть количество чисел после запятой у Double больше, но из-за этого он и используется реже.

Я постарался объяснить как можно лучше. Если что-то непонятно или есть дополнения, пишите в комментариях. Для лучшего понимания размеров переменных, попробуйте изучить какой-нибудь простенький язык программирования (Basic, Pascal) по какой-нибудь книжке для "чайников". Но, если Вы этого не поняли, то ничего страшного. В начале читерского пути это не будет мешать.

<<   / 1   >>