За окном дождь, на календаре декабрь. Близится пора праздников, а значит и пора подарков. Коллега Павел желает себе новый ноутбук, а подруга Маша хочет домик у моря. И несмотря на достойный оклад профессии “тыжпрограммист”, мы не живём в мире бесконечных возможностей, а значит я не могу подарить этим людям именно то, чего они хотят больше всего (даже несмотря на то, что Паша может быть мой самый лучший Павел, которого я знаю, а Маша просто молодец).
Тут и появляется проблема “дешёвых подарков”. Подарить дорогой подарок всем не получается, а дарить очередную коробку конфет, свечку или невнятную статуэтку бог_пойми_чего просто не хочется. Значит нужно объединить приятное с полезным — я люблю делать вещи своими руками, а полученный результат отлично подходит в качестве подарка. Это рассказ о том, как я в очередной раз прикидывался инженером и рукожопил несколько подарков.
Небольшое отступление от основной темы
Я не могу сразу перейти к основной теме, поэтому начну издалека. Несколько лет назад вышла игра «The Witcher 3». Моя подруга очень советовала и мне поиграть, да и вообще очень восхищалась игрой. Тогда я и решил, что нужно сделать и подарить ей что-то тематическое, связанное с игрой… Не стану уходить слишком глубоко в описание игры (гугл поможет всем желающим) или говорить о том, что не так уж и много вещей приходило мне на ум по ключевым словам “Ведьмак, инженер, подарок”.
У главного героя игры есть лошадь по имени “Плотва” (в русской озвучке), герой время от времени погоняет её фразой “Шевелись, Плотва”. Я решил сделать механическую игрушку на тему “шевеления плотвой”.
Естественно, подарок задумывался, как совершенно бесполезный, бестолковый, но забавный предмет, поэтому план был таков:
- Есть коробка, которая прячет в себе механизм.
- Механизм представляет собой вал, вращая который, лошадь приходит в движение.
- Максимальная “упоротость” движения лошади приветствуется. Изначально даже задумывалось, что лошадь должна двигаться, как рыба, а не как лошадь (плотва ведь), но в результате получилось “что-то между”.
- Делаться всё это будет из фанеры, которую по чертежам можно будет точно нарезать на заказ лазером.
Главным минусом выбора материала и того, что резка будет делаться на заказ стало то, что я потерял возможность создать прототип, а значит и все проблемы кривых рук и несоблюдения правила “семь раз отмерь” довольно быстро дали о себе знать:
Хотя в итоге, тот факт, что многие детали механизма люфтили из-за слишком больших зазоров, помогло компенсировать то, что детали и вал я соединил довольно криво. Тут, как говорится “минус на минус даёт плюс” и механизм в целом работал, как и было задумано:
После была только небольшая покраска и результатом стала вот такая коробка:
Тут кто-то скажет, что нужно было делать коробку прозрачной, чтобы можно было наблюдать и за работой внутренностей, но на самом деле я изначально догадывался, что не смогу собрать весь механизм и коробку так чисто и аккуратно изнутри, чтобы не было больно на это смотреть через прозрачные стенки коробки. Поэтому, непрозрачная коробка — это вынужденная мера.
Осталось только упаковать это её, повязать на коробку бант и подарок готов:
Но чтобы не расстраивать внутреннего ребёнка, который хотел чувствовать себя инженером в ситуации, когда на деле я склеил пару кусочков фанеры, я решил сделать и “чертёж”. Простите меня все те, кто что-то понимают в технических чертежах, т.к. я понимаю, что это скорее пародия на чертёж, чем реальный чертёж, но как мне показалось, он добавлял ценности несложной игрушке:
Сегодня я...
Иногда судьба играет с нами и как-бы намекает на то, что скоро что-то должно случиться. История одного из моих устройств начинается точно так же. Я заказывал какую-то мелкую электронику на сумму около 5$, которая, как обычно провела месяц в дороге, после чего я получил смс, что посылка ждёт меня в местном почтовом отделении. Коробка оказалось куда больше, чем я ожидал и сразу вызвала сомнения по поводу её содержимого. Сомнения подтвердились — внутри, помимо заказанных мной мелочей, был и трехцветный e-ink дисплей с диагональю 4.2 дюйма. Я связался с продавцом, тот подтвердил, что что-то напутал при упаковке, но сказал, что я могу считать это подарком и пользоваться, как сочту нужным.
Судьба намекала мне, что пришло время заняться новым проектом и проект этот будет чем-то с e-ink дисплеем. После краткого ознакомления со спецификацией устройства и подключения дисплея к контроллеру, я убедился, что спецификация не врала мне про время обновления. Экран обновлялся более 10 секунд, моргая и пытаясь вызвать у меня эпилептический припадок:
(У меня нет ранних фото подключения дисплея, поэтому тут уже устройство имеет более осмысленный вид)
Стало ясно, что нужно простое устройство, которое не будет требовать сложных анимаций (да и анимаций вообще). И тут мне на глаза попался календарь настроений:
Календарь настроений — это такой набор простеньких картинок и ироничных подписей к ним. Я и решил, что буду делать календарь (который на самом деле совсем и не календарь) с названием “Сегодня я”… Это будет простое устройство, которое каждый день показывает какое-то изображение, характеризующее “состояние души” сегодня и описание к нему. Дополнительно, календарь мог бы иметь и ещё какие-то ф-ции, но основной ф-цией был бы именно “календарь” — своя картиночка на каждый из дней.
Модуль электронной бумаги позволял выводить три цвета (чёрный, красный и белый), которые следовало передать модулю, как две картинки — чёрно-белую и красно-белую. На самом деле, правильнее было бы сказать, что экран просто ожидает монохромный битмап, где каждый бит определяет цвет одного пикселя (не уверен, что для электронной бумаги этот термин применим, но надеюсь, что идея понятна). Отдельный битмап можно задать для чёрного или красного канала. Таким образом, есть набор байтов, где каждый бит, который выставлен на 1, будет соответствовать цветному пикселю, а все биты, которые останутся 0, будут белыми пикселями на экране. Разрешение экрана составляет 400 x 300 пикселей, т.е. для чёрно-белой картинки потребуется 15000 байтов (одним байтом кодируются 8 пикселей). Для трёх же цветов, потребуется 30000 байтов, если не пытаться упаковать картинку более компактно. С точки зрения реализации, создатели дисплея могли были бы и просто выделить два бита на пиксель и хранить реальное положение вещей (где красный цвет, где чёрный, а где белый), но это потребовало бы всё тех же 30000 байтов, но даже для монохромных изображений требовало бы передачи 30000 байтов.
Как и в любой другой своей “поделке”, я стал выдумывать для себя задачи, чтобы всё не превратилось в “подключи проводки и скачай библиотеку”. Даже если брать во внимание возможность реализовать на стороне контроллера какой-нибудь алгоритм сжатия изображения, стало ясно, что без внешнего хранилища для файлов изображения, контроллеру будет очень уныло. Было решено взять SD карту и написать простенькую кодовую обёртку для работы с FAT32. Безусловно, можно было бы и обойтись без файловой системы и писать данные на карту памяти напрямую, но мне показалось, что это менее удобно для добавления новых файлов, да и мне было интересно реализовать основные операции для работы с FAT32 своими руками.
Важно уточнить, что я имею привычку писать вещи сам (не следует говорить мне, что есть море библиотек, я в курсе и с гуглом знаком), когда делаю подобные штуки, а т.к. я до этого не имел удовольствия поработать с FAT32 на низком уровне, то это было довольно занятный процесс.
Не буду вдаваться в подробности работы с FAT32 и SD картой в целом, т.к. в интернете полно статей на эту тему (а я не хочу превращать свой рассказ в пересказ спецификаций), но расскажу про некоторые вещи, которые нужно было реализовать. Необходимо реализовывать не только ф-ции чтения из файла и записи в файл (я решил хранить некоторые настройки и состояния на той же самой SD карте, а не в EEPROM), но и ф-ции поиска файла в файловой системе если есть необходимость работать с привычными путями.
Все данные на карте памяти поделены на кластеры (основная адресуемая единица в файловой системе FAT32), а кластеры уже поделены на секторы. Для того, чтобы прочитать файл “habr.txt” с SD карты в FAT32, нужно:
- Инициализировать карту памяти.
- Прочитать сектор по нулевому адресу и проверить, что файловая система FAT32 по наличию определённого заголовка (этот шаг можно и пропустить, если мы верим в удачу).
- Прочитать сектор по нулевому адресу и получить “Logical Block Addressing” (LBA).
- Прочитать сектор по полученному адресу (LBA) и получить из него необходимые данные (какой кластер является корнем — root directory, сколько в кластере секторов, и.т.д).
- Прочитать “Root Directory” кластер (на самом деле чтение осуществляется секторами, просто нужно читать столько секторов, сколько секторов в кластере) по его номеру. Есть своя формула конвертации порядковых номеров кластеров в физические адреса. Здесь хранится информация о файлах, поэтому читаем данные в поисках нужного файла. Есть ещё и одна мелочь про имена файлов: имя может быть коротким или длинным, в зависимости от этого, данные хранятся либо в одном месте (в той же структуре), либо в дополнительной записи, которую нужно также прочитать отдельно. Когда файл с нужным именем найден, получаем кластер, в котором расположено его начало (а может и весь файл, если он помещается в размер кластера), а также размер файла. Если же файл не найден в этом кластере, то ищем следующий кластер и повторяем процесс поиска файла.
- Прочитать кластер по найденному адресу. И продолжать читать кластеры дальше и искать следующий кластер, пока все данные не прочитаны.
Мне кажется, что проще рассказать принцип работы файловой системы тем, кто хоть один раз реализовывал связанные списки, т.к. по сути файловая система FAT32 это набор блоков данных и некая таблица, которая говорит, где искать следующий блок данных. Чтение любых данных из FAT32 — это процесс, где читаем данные, ищем адрес следующего блока, читаем данные, ищем адрес следующего блока, и.т.д.
Я не стал реализовывать все возможные сценарии и упростил некоторые вещи:
- Все файлы лежат в корне карты памяти и переход по каталогам не используется.
- Все файлы имеют короткие имена.
- Контроллер при старте считывает список файлов и хранит их в памяти, чтобы не искать каждый файл заново.
- Максимальное количество файлов ограничено константой (да и на деле я нарисовал лишь пару десятков изображений).
Коротко говоря, всё вышеописанное является убедительным основанием для того, чтобы брать готовую библиотеку и не тратить уйму времени на чтение документации, разработку и отладку. Реализовывать подобные вещи самому я бы посоветовал только в образовательных целях или “для развлечения”.
Дисплей также накладывал некоторые ограничения, т.к. любое изображение на него выводится таким образом:
- Будим и инициализируем дисплей (для того, чтобы продлить его жизнь и экономить энергию, дисплей в основном спит).
- Выставляем необходимый канал (например, чёрный).
- Пересылаем 15000 байт изображения.
- Выставляем необходимый канал (например, красный).
- Пересылаем 15000 байт изображения.
- Говорим дисплею отрисовать данные из буфера.
- Ждём, пока дисплей закончит прорисовку.
- Говорим дисплею спать.
Для монохромных изображений можно пропустить шаги 4 и 5. Для трёхцветного изображения порядок передачи чёрно-белого и красно-белого изображений не важен, т.к. красный цвет всегда рисуется поверх чёрного, чёрный лишь поверх белого, а белыми остаются лишь пиксели, которые белые и чёрно-белом и красно-белом изображении.
Для простоты (чтобы не увеличивать общее число файлов), я решил хранить чёрно-белые и красно-белые изображения, как один файл, где за чёрно-белым изображением сразу следует красно-белое:
Результат сопоставления этих изображений был примерно такой (мне кажется, что каждый из нас хоть раз в жизни чувствовал себя как олень у которого рога — нога):
На самом деле, за двумя изображениями следовало ещё одно изображение, которое содержало описание состояния (по нажатию на кнопку, можно получить описание, которое соответсвует картинке, что отображается сегодня).
Ввиду ограниченности памяти контроллера, данные с карты памяти никогда не читаются целиком в память контроллера. Изображение с карты памяти читается фрагментами, которые обусловлены размером сектора (для простоты, будем считать, что этот размер всегда статичен и не может меняться от карты к карте) и попадают в ф-цию обработки данных с карты. В моей реализации, ф-ция чтения файла принимает в качестве входных данных номер кластера, размер файла и callback, который нужно “дёргать” на каждый считанный сектор. Такой подход позволил использовать несколько различных ф-ций обработки файлов для чтения и отображать либо само изображение, либо его описание.
Итак, на callback ф-цию чтения изображений с карты памяти ложилась следующая логика:
- Выставить необходимый канал (канал определяется случайным образом) перед отправкой данных дисплею.
- Вести счёт переданных данных, чтобы переключить канал, когда завершена отправка данных для предыдущего канала.
- Отправлять данные, полученные с карты.
- Определить, что переданы необходимые данные и что остальные данные (изображение с описанием) можно игнорировать.
Всё усложнилось и тем, что я решил ввести изображения, которые содержали больше данных, чем следовало выводить на экран. Т.е. есть некоторая картинка, которая является фоном и всегда статична и соответствует размеру экрана, а есть и картинка намного большего размера из которой нужно взять случайную область (размером с экран) и отобразить.
Такое изображение накладывало дополнительные требования к обработчику — необходимо посчитать, сколько байт из текущей строки экрана переданы, сколько ещё нужно добавить при следующем вызове (если строка попадает на границу размера сектора) и сколько нужно пропустить.
Визуально эту ситуацию можно показать вот так:
Есть некий буфер, в котором есть “окно” нужного размера (зелёный участок) и нужно прочитать лишь данные этого окна, читая данные из буфера фрагментами по N байт. Визуализировать такой процесс чтения можно было бы вот так:
В целом, сложить логику работы с экраном в ф-цию, которая всегда получает буфер длиной в 512 байт и которая должна определить, что из этих 512 байт нужно (если вообще что-то нужно), оказалось не такой уж и простой задачей для решения “в лоб”. Точнее задача оказалась не столько сложной, сколько нестандартной в сравнении с повседневными решениями, поэтому, на удивление, решать её было очень интересно. Для меня это была как “олимпиадная задача”, где требовалось подсчитать количество электроэнергии необходимой для сокращения мышц для того, чтобы двумя ложками (по одной ложке в каждой из рук) перелить воду из одного ведра в другое.
Чтобы было лучше понятно, в чём же тут сложность, я добавлю тут ещё одну анимацию, только “с точки зрения” содержимого кластера, а не всего файла (всю остальную информацию нужно хранить отдельно):
По задумке, календарь должен показывать новое изображение каждый день, т.е. он должен уметь следить за временем (даже тогда, когда выключен), а значит, что нужен и модуль часов реального времени (RTC). И несмотря на то, что у моего контроллера есть встроенные часы реального времени, батарейку к нему подцепить почти невозможно, т.к. контакты не разведены на плату. Я решил не насиловать ноги контроллера в попытке подпаяться к ним и взял внешний китайский RTC. Работа с RTC должна была быть совершенно простой:
- При включении, получить время от китайского RTC.
- Выставить время во внутреннем RTC.
- Использовать внутренний RTC для подсчёта времени и забыть про китайца до следующего выключения.
Но и тут всё оказалось не так просто, ведь внутренний RTC контроллера работает с временем в привычном мне формате (UNIX timestamp), а его китайский друг использует Binary Coded Decimal (BCD). До этого я не сталкивался с этим форматом и был очень удивлён, что даже современные модули для Arduino используют его. Суть формата довольно проста — есть один байт, который, например, хранит количество секунд. Четыре старших бита хранят десятки, а четыре младших бита хранят единицы. Получается, что шестнадцатеричное представление этого самого байта содержат удобочитаемое время — байту 0x49 соответствует значение 49 секунд (хотя в десятичной системе исчисления этому байту соответствует значение 72).
Насколько я понял, так просто исторически сложилось, чтобы было проще использовать RTC напрямую с энкодерами экранов для создания часов, а мне же пришлось конвертировать BDC в UNIX timestamp ручками.
Помимо дисплея, модуля часов реального времени, карты памяти и файловой системы для неё, нужно было завернуть все эти железки в корпус, который тоже было необходимо изготовить, т.е. “нарисовать”. Более подробно про всю боль, с которой я столкнулся можете почитать в моей прошлой публикации, тут всё было точно так же:
(Я не задумал массового производства, все детали вокруг — это результат неверных измерений или плохо продуманных решений)
В результате был нарисован корпус и необходимые детали (на изображении нет передней крышки):
Кнопки были вынесены в отдельную деталь для простоты сборки (похоже, что я всё-таки чему-то научился со времён прошлого проекта):
В сборе (за исключением передней и задней крышек) устройство выглядит вот так:
Дисплей крепится к корпусу стяжками. Это оказался куда более удобный метод, чем прикручивать его болтами.
Ну и немного ада для перфекционистов (борода из проводов и компонентов):
В сборе получилось вот такое устройство, к которому я сделал небольшую книжечку — инструкцию:
Задняя крышка крепится болтами, что позволяет разобрать устройство, если потребуется дополнительное насилие над его внутренностями:
Да проще же купить домик у моря!
И тут кто-то, кто дочитал до этого момента скажет: “И это альтернатива простым подаркам?! Да тут же нужно вложить 100500 часов своего времени, что куда дороже денег!”. Я не стану спорить на эту тему, лишь скажу, что для меня это процесс медитативный и я убиваю двух зайцев одним выстрелом — с одной стороны я делаю вещи которые по большому счёту никому не нужны (мало кто стал бы платить мне за разработку чего-то такого), но которые мне интересно делать, а человек получает в подарок уникальный предмет, который хоть и не несёт явной полезной нагрузки, но дарит улыбку.
Однако, чтобы не оставалось послевкусия от решений, которые требуют вложения уймы времени, я поделюсь ещё несколькими историями покороче.
Сейчас вообще стало очень модно печатать вещи на 3д принтере. Устройства для печати стали намного более доступны, да и ПО для создания моделей достаточно простое, чтобы в нём могли разобраться и дети.
Крик о помощи
На работе у нас появилась шутка про то, что если нужна помощь, нужно лишь покричать чайкой и кто-нибудь обязательно тебе поможет. По правде говоря, я и не знаю, с чем это связано и откуда пошла эта шутка, но во внутренней переписке часто стали появляться картиночки чаек, намекающие, что кому-то нужна помощь.
Однако, офис открытого типа, да и внутренняя скромность некоторых коллег не позволяла им в полной мере использовать возможность вот так призвать к себе помощь. Тут на помощь решил прийти “человек-не-инженер”, который берёт завалявшийся китайский “MP3 Music Player Module for Arduino”, китайский клон Arduino Nano, несколько проводов, да пару mp3 с записью звуков чаек.
Вообще, тут следует отметить, что имею привычку либо заказывать по скидке различные модули, которые, как мне кажется, однажды могут пригодиться, либо получаю подобную электронику в дар от друзей / знакомых, кому эта самая электроника не пригодилась. Поэтому слово “завалявшийся” не случайно, а очень точно описывает положение вещей.
Поскольку я человек далёкий от 3д моделирования, то я просто взял готовую модель готовы чайки и просто сделал в ней необходимые изменения. А именно из изменений было нужно:
- Отделить клюв от головы, чтобы распечатать клюв другим цветом.
- Сделать “полость” в которую ляжет вся электроника (динамик, плеер, ардуино, и провода).
Результат получился примерно такой (клюв разбит на две части для лучшего качества печати):
Я не думаю, что есть смысл говорить о коде, т.к. помимо инициализации плеера, там был только обработчик нажатий на кнопку и проигрывание одной из записей голоса чайки (в общем: playSound(rand() & 3);).
После этого просто оставляем чайку печататься и собираем всё вместе. В результате имеем полноценный “чайкоимитатор” (цвет чайки обусловлен пластиком, который был в наличии), который подключается к USB и при нажатии на кнопку на голове чайки, воспроизводит “зов о помощи”:
Ещё проще?
Коллега жаловалась на то, что её постоянно отвлекают, пока она пишет тесты. Находим в интернете 3д модель очков и вносим небольшие изменения:
В итоге имеем очки — шоры. Не отвлекаемся сами и окружающим понятно, что не следует отвлекать вас:
Совсем просто!
Итак, для создания нужно:
- Способность найти или нарисовать в векторном редакторе костыль.
- Способность нарисовать в 3д редакторе 2 цилиндра, один больше другого на ~0.4мм.
- Способность сделать простенькую бумажку — этикетку.
- 3д принтер (свой или сервис печати на заказ).
- Принтер обыкновенный.
А теперь по порядку… Рисуем костыль:
Рисуем два цилиндра:
Печатаем детали и этикетку и получаем отличный и простой подарок для “тыжпрограммистов”:
К чему всё это было?
“Иногда судьба играет с нами и как-бы намекает на то, что скоро что-то должно случиться.” Несколько раз в жизни у меня были идеи, которые я хотел реализовать, но ждал чего-то. Я бы не стал делать электронный “календарь”, если бы из китая не пришёл бесплатный экран, это значит, что я бы не стал и разбираться с FAT32, не узнал бы и о BCD формате, который используется в RTC.
Возможно, кто-то прочитает этот рассказ (или хоть посмотрит картиночки) и подумает: “Чёрт побери, если он смог сделать это, то я смогу ещё круче!” и в результате принесёт радость кому-то рядом.
За окном дождь, на календаре декабрь. Близится пора праздников, а значит и пора подарков. Делайте и получайте интересные подарки, а не очередную коробку конфет, свечку или статуэтку бог_пойми_чего!
Автор: simbiod