Пишем Hex Viewer для Flipper Zero

46e28ad973d144b123a4ce513c895d18.png

Примерно месяц назад основная поставка Flipper’ов таки доехала до России. Вопреки моим ожиданиям, это не вызвало волну публикаций про создание приложений под него. Хорошие публикации есть (например, эта и вот эта), но массовости нет. Слишком долго ждали и перегорели? Пишут долго и обстоятельно? Технологический стек устройства не подходит для быстрого и легкого старта? Как бы то ни было, такой расклад ничуть не убавил мотивации поиграться с устройством! С удовольствием уделил несколько вечеров созданию своего первого приложения под Flipper Zero: Hex Viewer, шестнадцатеричного просмотрщика. О своем опыте и интересных находках расскажу в теле статьи.

Небольшое лирическое отступление

Более 12 лет опыта работы в индустрии программирования научили меня ценить возможность «зрить в корень» файлов, видеть их шестнадцатеричное отображение. Большинство программ показывают нам свою интерпретацию файла. Например, MS Word и Libre Office могут по-разному показывать один и тот же DOCX-файл. А современный текстовый редактор может по своему усмотрению преобразовать переносы строк, выравнивание (tab vs space), иногда даже кодировку. В основном это сделано для нашего блага, так как убирает сложность. Однако платой за это является потеря контроля над настоящим содержимым файла: байтами на диске. Шестнадцатеричный просмотрщик помогает вернуть контроль назад. Обычно я использую Lister в Total Commander. Считаю, что его хоткей — F3 — это лучшая кнопка в TC. С помощью Lister можно в мгновение ока открыть файл любого размера (за счет memory-mapped files), поискать что-то внутри (в том числе по регулярным выражениям), быстро открыть файл из диалога результатов поиска (его хоткей: Alt+F7).

Итак, 3 ноября я получил свой Flipper Zero. Полазил по менюшкам, отсканировал парочку домофонных ключей, послушал marble machine в 8 битах (лайк за выбор мелодии). На флэшке начали появляться разные файлы, и специфичные приложения могли их открывать. Это, конечно, здорово, но захотелось разобраться, как они устроены, что представляют из себя на самом деле. Сделать это было невозможно — в официальной прошивке нет даже файлового менеджера. Так родилась идея создать шестнадцатеричный просмотрщик. Не мудрствуя лукаво, решил назвать его просто Hex Viewer.

Создание приложения

На момент написания статьи раздел про разработку firmware на официальном сайте Flipper’а пустует, поэтому было решено начать с страницы исходников в GitHub’е. Репозиторий проекта уже был на моей машине, так как летом отправлял небольшой PR для змейки (в учебных целях порефачили его с командой в онлайне). Забегая вперед скажу, что это сослужило мне плохую службу: с того момента API системных вызовов изменилось, пришлось делать лишнюю работу по замене системных вызовов. Поэтому перед началом разработки приложения рекомендую забирать самую свежую версию исходников прошивки.

Перед началом разработки приложения под Flipper Zero перво-наперво нужно добиться работоспособности FBT (Flipper Build Tool) на вашей машине. Эта утилита умеет собирать прошивку целиком, собирать приложения, собирать и запускать конкретное приложение на устройстве, включать режим отладки, настраивать VS Code, etc. В общем, швейцарский нож. Настроили и забыли, очень удобно пользоваться.

В качестве основы для приложения был взят музыкальный плеер, music_player. Он подкупал тем, что показывал на старте нужный мне диалог выбора файлов, и, собственно, работал с этими файлами. По большому счёту я просто скопировал кодовую базу в папку applications_user, переименовал файлы, удалил лишние файлы воркера проигрывателя, переименовал структуры данных и оставил только нужные поля в структурах. Помимо правки исходников на C, заполнил FAM-файл манифеста поиском/заменой. Он достаточно интуитивный, удалось заполнить с первого раза без чтения документации.

Наблюдение

Большинство приложений под Flipper Zero, которые мне удалось изучить, имеют общую структуру: основную точку входа в приложение, процедуры аллокации и деаллокации, структуру под хранение контекста и открытых системных ресурсов, etc. Напрашивается шаблон по принципу Cookiecutter, когда можно просто задать несколько параметров, а на выходе готовая основа для приложения. Возможно такой шаблон появится в будущем

Подход с копированием плеера позволил быстро запустить условный «Hello, world!». Из функции рисования (render_callback) были удалены хитрости рисования клавиш фортепиано и проигрываемых нот. Оставлены утилитарные захват/освобождение мьютекса, подготовка канваса и шрифта, отрисовка названия приложения:

static void render_callback(Canvas* canvas, void* ctx) {
    HexViewer* hex_viewer = ctx;
    furi_check(furi_mutex_acquire(hex_viewer->model_mutex, FuriWaitForever) == FuriStatusOk);

    canvas_clear(canvas);
    canvas_set_color(canvas, ColorBlack);
    canvas_set_font(canvas, FontPrimary);
    canvas_draw_str(canvas, 0, 12, "HexViewer");

    furi_mutex_release(hex_viewer->model_mutex);
}

После нескольких итераций правок опечаток и очевидных ошибок, удалось запустить приложение на устройстве. На экране флиппера гордо загорелась надпись «Hex Viewer». Ура!

Кстати, запуск приложения в основном осуществлялся посредством всемогущего fbt:

sudo ./fbt launch_app APPSRC=applications_user/hex_viewer

Дальше мне было необходимо продумать:

  • Лэйаут приложения, понять какая информация будет показываться на экране

  • Как пользователь будет взаимодействовать с приложением

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

Дизайн приложения, нарисованный от руки. Практикую с 2014 годаДизайн приложения, нарисованный от руки. Практикую с 2014 года

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

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

После окончания проектирования, можно было приступать непосредственно к программированию логики просмотрщика. В модели был заведен буфер на 16 байт, а код рисования был дополнен отображением этого буфера. Строка формируется побайтно, а выводится единым вызовом canvas_draw_str. Учитывая еще один вызов для отрисовки адреса, получается по 2 вызова на строку, всего 8 на экран. Неплохо.

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

Фактически proof of concept того, что просмотрщик на флиппере использовать можноФактически proof of concept того, что просмотрщик на флиппере использовать можно

Кстати, шестнадцатеричное отображение байтового буфера и счётчика байт было создано с помощью старой доброй функций языка C под названием sprintf. Конечно, пришлось немного погуглить, вспомнить спецификаторы форматов функции printf, но в целом без особенной боли удалось достичь желаемого отображения.

Рисование элементов на экране в большинстве изученных приложений попиксельное. Т.е. нужно точно задать координаты на экране, никаких автоматических инструментов размещения а-ля Layout не обнаружил. Для отладки пиксельной точности размещения компонентов использовал макросъемку телефона. Можно было сделать проще: официальное desktop-приложение qFlipper умеет стримить экран устройства и делать скриншоты (наподобие того, что размещен в самом начале статьи). Но у меня не получилось одновременно держать запущенным qFlipper и запускать приложения через fbt. Видимо кто-то держит канал в блокирующем режиме. Поэтому быстрая итерация через фото на телефон оказалась удобнее.

Скриншот более поздней версии: честно прочитанные из файла байты и кнопка переключения режимаСкриншот более поздней версии: честно прочитанные из файла байты и кнопка переключения режима

Следующая задача: прочитать реальный файл с флэш-карты устройства. По простоте душевной решил поискать в проекте вызовы fopen, и к большому удивлению ничего не обнаружил. Далее решил обратиться к уже знакомому музыкальному плееру, и там напал на ложный след в функции music_player_worker_load_fmf_from_file. Обнаруженная группа функций flipper_format_file_* предназначается для работы с файлами в специальном внутреннем формате файлов Flipper’а (подсмотреть в него можно будет на одном из скриншотов в статье). Изучив заголовочный файл с описанием этих функций, я даже немного расстроился, потому что они плохо подходили под чтение файлов общего назначения (в произвольном формате). Из-за этого Hex Viewer мог получиться очень неэффективным. Благо буквально в соседней функции плеера music_player_worker_load_rtttl_from_file нашлось другое API для работы с файлами, группа функций storage_file _*. Они позволяли работать в привычном для языка C режиме: open/read/size/seek/close.

Первая реализация чтения с карты была максимально простой:

  • Открытие файла

  • Сдвиг на нужное количество байт (вычисленное по формуле: номер строки в просмотрщике * 4 байта)

  • Чтение буфера (16 байт)

  • Закрытие файла

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

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

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

Два символа уместились буквально пиксель в пиксельДва символа уместились буквально пиксель в пиксель

Кстати, с сохранением иконки были определенные трудности. Прямолинейное сохранение в PNG дало на выходе файл размером ~1Кб, тогда как оригинальная иконка весила ~140 байт. Пришлось доставать Lister и разбираться, что же такого Photoshop вставляет в файл :) Ответ был на поверхности: кучу метаданных. Немного помучившись с диалогом «Сохранить для Web», удалось ужать размер файла до ~150 байт (хоть и все метаданные выкусить не удалось). Картинка успешно открылась на устройстве, дальнейшая борьба за десятки байт показалась бессмысленной.

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

2433262d6c6025e260793c0f179939d1.png

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

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

Приложение на тот момент уже обрело целевую функциональность, пришло время заняться его производительностью. С самого начала меня беспокоил вопрос эффективности чтения байт файловой системы. В начальной версии на каждую прокрутку выполнялось открытие файла, чтение шестнадцати байт и закрытие файла. Это не очень эффективный способ работать с байтами хотя потому, что прокрутка содержимого выполняется на 4 байта, а вычитывается весь буфер из 16 байт. Поэтому я решил поискать в системе какие-нибудь API для буферизированного чтения, и как ни странно такой API нашелся. Рассказать о нем придется в два этапа. Во-первых, есть набор функций file_stream_*, которые предоставляют привычные методы для работы с файлом, но оперируют понятие «стрима», представленного структурой Stream. Фактически это более высокоуровневая это обертка, но не просто над file-like сущностью, а так же операциями над ним. Внутри структуры хранится реализованная вручную таблица виртуальных функций, выполняющих операции open/seek/read/close/etc. Во-вторых, есть группа функций buffered_file_stream_*, подменяющая виртуальные функции в структуре Stream на собственные, выполняющие магию буферизации. Основные функции для работы со стримами не догадываются о том, что используют буферизированные версии, переход абсолютно бесшовный. Моё уважение разработчикам стандартной библиотеки за удачное применение такого известного приема, как реализация в C самописной таблицы виртуальных функций (как в высокоуровневых языках).

struct StreamVTable {
    const StreamFreeFn free;
    const StreamEOFFn eof;
    const StreamCleanFn clean;
    const StreamSeekFn seek;
    const StreamTellFn tell;
    const StreamSizeFn size;
    const StreamWriteFn write;
    const StreamReadFn read;
    const StreamDeleteAndInsertFn delete_and_insert;
};

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

Несколько слов про рабочие окружение и работу в Visual Studio Code. Интеграция есть из коробки, выполняется вызовом одной единственной командной строки.

./fbt vscode_dist

Однако полноценно заставить VS Code видеть моё приложение и подсвечивать ошибки у меня почему-то не получилось. Возможно кто-то из читателей статьи подскажет, что для этого нужно сделать. Это не сильно мешало, ибо с помощью запуска приложения на устройстве посредством командной строки удавалось довольно быстро итерироваться. Компиляция и запуск занимали порядка 5 секунд что, собственно, никак не ограничивало мою производительность. Помимо небольшой сложности с настройкой Visual Studio Code непосредственно для моего приложения, в общем-то всё работало достаточно хорошо, среда позволяла «ходить по функциям», умела прыгать по определениям и так далее. То есть пользоваться было довольно удобно. Также при разработке помогает старый добрый Ctrl+Shift+F (глобальный поиск по проекту) в случаях когда нужно сделать что-то по аналогии или понять как пользоваться функциями.

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

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

Итог

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

Оказывается, родной формат файлов флиппера текстовый!Оказывается, родной формат файлов флиппера текстовый!

Хотелось бы выразить огромную благодарность @vvzvlad, автору статьи про разработку приложения-счетчика для флиппера. Я почерпнул оттуда полезную информацию, уже до начала разработки примерно знал к чему готовиться, реализовал многое по аналогии.

Ссылки проекта на GitHub

Ссылка на на собранное приложение

© Habrahabr.ru