Как стать автором
Обновить

Разработка под Flipper Zero: быстрый старт и первое приложение

Время на прочтение19 мин
Количество просмотров29K

Итак, это случилось. Я добыл Flipper Zero в бою (ну, на самом деле мне его выдали на хакатоне), и естественно, что интереснее всего не играть в него, используя уже сделанные кем-то функции, а писать что-то новое. Примерно так же я развлекался с Pebble. Тут, кстати, все очень похоже — МК, RTOS, небольшой монохромный экранчик, С (ну, С++ тоже есть, но зачем).

Когда-то надпись была другой, по-русски и не очень цензурная (https://twitter.com/vvzvlad/status/1467813142885548038, подписывайтесь, кстати), но разработчики послушали восхищенные возгласы комьюнити и заменили ее :(
Когда-то надпись была другой, по-русски и не очень цензурная (https://twitter.com/vvzvlad/status/1467813142885548038, подписывайтесь, кстати), но разработчики послушали восхищенные возгласы комьюнити и заменили ее :(

Поэтому давайте попробуем что-нибудь под него написать. Для начала, что-то совсем простое, чтобы освоиться с SDK, не закапываясь в отладку и сложности RTOS, но функциональное. Есть кнопки, есть экранчик, давайте напишем счетчик-кликер. Нажимаете кнопку — число увеличивается. Подойдет считать посетителей, круги на стадионе, взломанные домофоны, молитвы (без шуток, я видел ребят, которые делали электронные четки) или сообщения в чате русскоязычного комьюнити флиппера (по утрам, в особенно активное время там их бывает до 2к, так что я бы на вашем месте подумал, прежде чем нажимать кнопку Join).

Шаг первый: установка нужного софта, проверка сборки и загрузка прошивки

Web-ide, как у Pebble, у флиппера нет, но это вопрос времени. Но пока прошивку надо собирать на своем компьютере.

Обратите внимание, у меня на ноутбуке MacOS, поэтому первые 4 команды я привожу для нее. Для Linux будет отличаться. Там в чем-то проще — можно одной командой запустить докер, у которого внутри все уже будет работать, но могут быть проблемы с пробросом USB-устройств туда. Установить в систему чуть сложнее, но тоже сводится к десятку команд.

Версия оригинальной прошивки (запомните ее)
Версия оригинальной прошивки (запомните ее)

1)Устанавливаем brew и git (если еще не. Но тогда что вы тут делаете?):

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install git

2)Клонируем репозиторий с прошивкой:

git clone https://github.com/flipperdevices/flipperzero-firmware.git
cd flipperzero-firmware

3) Устанавливаем необходимые утилиты (список берется из файла Brewfile в корне репозитория, поэтому не стоит пытаться выполнить эту команду в другом каталоге):

brew bundle --verbose

Если brew после установки bundle скажет "Homebrew Bundle complete! 6 Brewfile dependencies now installed", то все хорошо, ничего делать больше не надо.

Если у вас уже был установлены пакеты, связанные со сборкой под нативный arm (arm-none-eabi-*) какой-то старой версии, то вы получите ошибку при установке. В этом случае стоит посмотреть, что у вас стоит командой brew list | grep arm (в моем случае это был gcc-arm-none-eabi-62, который неведомо как прожил в системе с 2015 года) и удалить его командой brew remove gcc-arm-none-eabi-62 (у вас название пакета может быть другое).

4) Собираем прошивку:

make 

Дожидаемся, пока make скажет нам что-то типа:

	LD	 .obj/f7/bootloader.elf
   text	   data	    bss	    dec	    hex	filename
  30680	    640	   2656	  33976	   84b8	.obj/f7/bootloader.elf
	BIN	 .obj/f7/bootloader.bin
	HEX	 .obj/f7/bootloader.hex
	DFU	 .obj/f7/bootloader.dfu
	JSON	 .obj/f7/bootloader.json
CFLAGS ok

	HEX	 .obj/f7/firmware.hex
	DFU	 .obj/f7/firmware.dfu
	JSON	 .obj/f7/firmware.json
Firmware binaries can be found at:
	/Users/vvzvlad/Documents/Projects/flipper-hack/flipperzero-firmware/dist/f7
Use this file to flash your Flipper:
	flipper-z-f7-full-local.dfu

Готово, вы великолепны! Осталось только загрузить эту прошивку в устройство.

5) Программатор для загрузки прошивки не требуется, только провод со штекером USB Type-C.

Флиппер, подключенный к компьютеру
Флиппер, подключенный к компьютеру

USB-A/USB-C есть в комплекте с флиппером, тем, кто на новых маках, придется озаботиться поиском USB-С/USB-C провода самостоятельно. На крайний случай подойдет тот, через который ноут заряжается.

Флиппер в режиме DFU
Флиппер в режиме DFU


Для прошивки надо перейти в режим DFU — зажать одновременно кнопку "назад" и "влево"(<+↩), после перезагрузки отпустить "назад"(↩), оставив зажатой "влево"(<). Если отпустить сразу обе, то флиппер просто перезагрузится, а если при перезагрузке оставить <, то войдет в DFU.

Загрузка прошивки делается вот такой командой:

make -C firmware upload

Если make говорит, что "Nothing to be done for upload.", то это странное поведение makefile: два раза прошить одну и ту же прошивку не получится, make считает, что все таргеты выполнены. Обходной путь, если вам надо прошить тоже самое второй раз — сделать что-то вроде touch ./applications/about/about.c, заставив make думать, что вы что-то изменили в одном файле. Ну или make clean && make, но в этом случае он пересоберет всю прошивку заново, что займет ощутимо больше времени.

После этого на экране вы увидите что-то вроде такого:

> make -C firmware upload
CFLAGS ok

dfu-util -D .obj/f7/firmware.bin -a 0 -s 0x08008000 
dfu-util 0.11

dfu-util: Warning: Invalid DFU suffix signature
dfu-util: A valid DFU suffix will be required in a future dfu-util release
Opening DFU capable USB device...
Device ID 0483:df11
Device DFU version 011a
Claiming USB DFU Interface...
Setting Alternate Interface #0 ...
Determining device status...
DFU state(2) = dfuIDLE, status(0) = No error condition is present
DFU mode device DFU version 011a
Device returned transfer size 1024
DfuSe interface name: "Internal Flash   "
Downloading element to address = 0x08008000, size = 728896
Erase   	[=========================] 100%       728896 bytes
Erase    done.
Download	[====================     ]  81%       590848 bytes

Стирание и прошивка занимает где-то минуту. Если в это время отключить флиппер, то прошивка прервется, флиппер работать не будет, но он останется в режиме DFU и повторным запуском той же команды можно довести дело до конца. Если же вы вдруг перезагрузите его после неудачной прошивки (сочетанием <+↩), то вас встретит пустой экран и в DFU с картинкой он больше не зайдет. Но это не значит, что вы его окончательно окирпичили, и вам поможет только программатор.
Нет, достаточно чуть другой комбинации для входа в DFU в этом случае: зажимаем сочетание <+↩+⦿, потом отпускаем < и ↩, оставляя нажатым ⦿(центральную кнопку в джойстике), через секунду отпускаем и ее. Все, можно прошивать.

После прошивки девайс нужно перезагрузить сочетанием <+↩, и попасть уже в свежезалитую прошивку.

Версия прошивки после сборки и загрузки. Последняя строка — это ветка гитхаба, я собирал из своего репозитория, поэтому она "vvzvlad-dev"
Версия прошивки после сборки и загрузки. Последняя строка — это ветка гитхаба, я собирал из своего репозитория, поэтому она "vvzvlad-dev"

Лайфхак: если вам не хочется нажимать каждый раз на сочетание кнопок для перезагрузки в DFU, то вы можете зайти в него, отправив в cli флиппера команду "dfu". Автоматизировать это можно следующим способом:

stty -f /dev/cu.usbmodemflip_* 115200
echo -en "dfu\r\n" > /dev/cu.usbmodemflip_*

Выход из DFU чуть сложнее:

dfu-util -a 0 -s :leave ; sleep 0.5
dfu-util -s 0x08008000 -a 0 -R

Соответственно, собрав это в одну команду, чтобы получить что-то вроде stty -f /dev/cu.usbmodemflip_* 115200 && echo -en "dfu\r\n" > /dev/cu.usbmodemflip_* && make -C firmware upload && dfu-util -a 0 -s :leave ; sleep 0.5 && dfu-util -s 0x08008000 -a 0 -R, у вас получится команда для запуска DFU, прошивки, и перезагрузки в прошивку после прошивки (гм). Естественно, что она не сработает, если вы настолько сломаете прошивку, что она не сможет запуститься или будет уходить в бесконечный цикл с блокированием прерываний и переключений тасков или просто не будет принимать команды в cli. Придется запускать вручную.
Но в принципе, лайфхак вовсе не обязателен, если вам надо собрать одну прошивку и загрузить ее в устройство, хватит qflipper (приложения для компьютера, скачать можно с https://update.flipperzero.one/) или ручного перехода в DFU по сочетанию кнопок. Но в том случае, если во время отладки вы будете прошивать десяток-другой сборок и хотите, чтобы это требовало от вас как можно меньше действий, и у вас нет программатора вроде ST-Link/J-Link, то время это сэкономит.

Теперь, когда мы умеем собирать прошивку, самое время сделать какое-нибудь осмысленное маленькое приложение.

Шаг второй: пишем приложение

Для начала, поймем, а под что мы вообще собрались писать.
Центральный контроллер — STM32WB55, два arm-ядра (одно Cortex-M4, второе Cortex-M0+, заведует всяким радио), 1 мегабайт флеша, 256 кб оперативки, 2.4ггц трансивер (Zigbee, Thread, BLE), SPI, I2C, USB, куча таймеров, криптография (включая хранилище неизвлекаемых ключей). В общем, Linux не поставить, но по меркам микроконтроллеров довольно жирный камень.
Кроме самого микроконтроллера есть еще и субгигагерцовый трансивер — СС1101 от TI, который может работать на основных частотах, 315/433/868/915 МГц. Для работы с NFC используется отдельный ридер, ST25R3916, работа с 125 кГц метками осуществляется силами самого процессора, как и с iButton и IR.

Однако, в отличии от "обычной" разработки под МК, когда на микроконтроллере выполняется исключительно ваша логика, тут не весь камень отдан в ваше распоряжение. Помимо самого контроллера, тут есть куча железок — и трансивер, и NFC, и экран, и всякое-разное на I2C шине, типа микросхемы управления светодиодом и подсветкой. Нормально управлять всем этим в суперцикле не получится (точнее, вы в процессе этого напишите свою RTOS), поэтому тут есть операционная система — FreeRTOS, в которой, во-первых, удобно разграничивать разные логические приложения в таски, а во-вторых, которая сама занимается переключением тасков и их контекста, сама определяет, что именно надо запустить сейчас и так далее.

Отладчик показывает список тасков
Отладчик показывает список тасков

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

Зачем нужна такая сложность? Ну, например, для удобства разработки. Вот хочется, например, вам в приложении сделать "дрр" вибромотором. Для этого надо не только его включить, но и выключить в нужное время. Если вы будете делать это внутри своего приложения, которое запускается на контроллере монопольно, то вам придется делать какую-то логику для ожидания перед отключением. А если ваше приложением упадет или зависнет, то вибромотор останется включенным. А это самый простой пример, гораздо сложнее разрулить такого рода штуки с другим железом, требующим жестких таймингов. А если в это время вы еще и хотите что-то на экран выводить.. Поэтому и RTOS. Тут можно просто отправить сообщение процессу, ответственному за работу с вибромотором, в котором сказать "включи его на 300мс", и забыть про это — ОС сама разбудит этот таск, сама позаботится о доставке в него сообщения, и вновь разбудит его через 300мс, чтобы оно могло вибромотор отключить. А в это время другие процессы будут продолжать работать.

Многозадачность тут вытесняющая, так что можно не думать над тем, когда отдавать управление планировщику, если считаете что-то сложное, он сам отберет у вас управление. Но наломать дров все же можно — это не настольная ОС, где до железа пять слоев абстракций, тут можно работать напрямую, например с шиной I2C, и часто эта работа требует реалтайма, поэтому существуют способы сказать RTOS, чтобы во время выполнения этого куска кода вас ни за что не прерывали. Но вот если в процессе выполнения этого кода вы уйдете в бесконечный цикл, девайсу кроме перезагрузки уже ничего не сможет помочь.

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

Сейчас приложения — это часть прошивки, т.е. для добавления нового вам придется добавить его в код прошивки, собрать ее и залить в устройство. Работы над загрузкой приложений без пересборки прошивки и даже с SD-карты ведутся, но пока ситуация такая.

Самое короткое приложение

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

long counter_app(void* p) {
    return 0;
}

Эти три строчки надо положить в файл .с, и кинуть его, в целом, где угодно в папке flipperzero-firmware/applications. Я положил в /applications/counter, но в целом не принципиально — компилируется в ходе сборки все подряд, это потом линкер определяет своих. Следите за тем, чтобы название функций были уникальными, а то компилироваться оно будет, а вот собираться в бинарник нет.
Еще одна строчка — объявление имени главной функции приложения в applications/applications.c с ключевым словом extern:

extern int32_t counter_app(void* p);

И еще одна строчка — это создание пункта меню, который будет запускать приложение, там же, в applications.c:

{.app = counter_app, .name = "Counter", .stack_size = 1024, .icon = &A_BadUsb_14},

.app — это имя функции-точки входа в приложение, .name — имя в меню, .stack_size — размер стека для приложения (для нашего многовато как-то, не находите?), .icon — иконка для меню. В главном меню иконки обязательны, поэтому NULL-ом обойтись не получится, и я взял иконку у соседнего приложения. А сама запись — это просто один элемент массива:

const FlipperApplication FLIPPER_APPS[] = {
...
    {.app = counter_app, .name = "Counter", .stack_size = 1024, .icon = &A_BadUsb_14},
...
};

Еще хорошо бы его окружить ifdef/endif (как тут, например) для того, чтобы можно было в applications.mk выбирать (вот тут логика работы ifdef, вот тут сам выбор), с какими приложениями собирать прошивку, а с какими нет, но это уже за пределами минимального ТЗ "лишь бы запускалось". Итак, три строчки нам потребовалось, чтобы добавить новое приложение в прошивку.

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

Давайте добавим интерфейс, но прежде всего приведем в порядок уже написанные пять строк, превратив их в 15. Ничего необычного, в приложении заменили long на int32_t (пришлось потратить еще одну строчку на инклуд furi.h, все равно он понадобится дальше), и как описано выше, обвязали пункт меню ifdef для возможности выбрать, включать или нет это приложение ключами при компиляции.

Furi, инклуд которого мы добавили, он же фурри — это наполовину HAL, наполовину системная библиотека, реализующая всякие полезные штуки, которые обычно лежат в папке utils. Когда-то название расшифровывалось как Flipper Universal Registry Interface, но актуальность этого давно потерялась, но разработчики так и не смогли расстаться с приятной их сердцу аббревиатурой, поэтому она тут.

Идем дальше. Соорудим приложению хоть какой-нибудь интерфейс, чтобы при запуске происходило хоть что-то. До обработки кнопок мы еще не дошли, поэтому приложение будет открываться и закрываться само. Но если это сделать без задержки, то процесс будет настолько быстрым, что поведение будет неотличимо от предыдущего варианта, поэтому полезной нагрузкой приложения будет просто delay(2000).
Delay тут можно вставлять в неограниченных количествах, по целым двум причинам: во-первых, вытесняющая многозадачность, которая отберет управление даже у while-цикла (но не отдаст его таскам с более низким приоритетом и не даст процессору уйти в сон, так что лучше бесполезные долгие циклы не плодить), а во-вторых, delay тут вызывает vTaskDelay из FreeRTOS, который лишь сообщает планировщику, что в следующий раз надо вызвать этот таск через определенное количество времени, после чего планировщик передает управление другим таскам или уводит процессор в сон, если таковых нет.

Приложение приобретает следующий вид (коммит):

#include <furi.h>
#include <furi-hal.h>
#include <gui/gui.h>

int32_t counter_app(void* p) {
    ViewPort* view_port = view_port_alloc();
    Gui* gui = furi_record_open("gui");
    gui_add_view_port(gui, view_port, GuiLayerFullscreen);

    delay(2000);

    gui_remove_view_port(gui, view_port);
    furi_record_close("gui");
    view_port_free(view_port);
    return 0;
}

То, что в нем делается, довольно просто: сначала создаем ViewPort — это такая структура, в которую записываются указатели на коллбеки отрисовки экрана и получения эвентов от пользователя, потом вызываем furi_record_open, получая структуру Gui — говорим операционной системе, что у нас тут есть есть некоторый интерфейс и мы его хотим показывать, потом добавляем в полученный Gui созданный ViewPort, ждем две секунды, освобождаем все ресурсы и выходим. Негусто, да. Но работает:

Давайте разнесем выделение ресурсов и их освобождение по отдельным функциям, чтобы они не мешались в main, а сами ресурсы положим в структуру, так код станет читабельнее и переносимее. Создадим структуру с Gui и ViewPort (у нас больше ничего и нет пока):

typedef struct {
    Gui* gui;
    ViewPort* view_port;
} CounterApp;

И перепишем основную функцию вот так (коммит):

void counter_app_free(CounterApp* app) {
    gui_remove_view_port(app->gui, app->view_port);
    view_port_free(app->view_port);
    furi_record_close("gui");
    free(app);
}

CounterApp* counter_app_alloc() {
    CounterApp* app = furi_alloc(sizeof(CounterApp));
    app->view_port = view_port_alloc();
    app->gui = furi_record_open("gui");
    gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
    return app;
}

int32_t counter_app(void* p) {
    CounterApp* app = counter_app_alloc();

    delay(2000);

    counter_app_free(app);
    return 0;
}

Приложение, которое хоть что-то делает

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

void counter_draw_callback(Canvas* canvas, void* ctx) {
    canvas_set_font(canvas, FontPrimary);
    canvas_draw_str(canvas, 2, 10, "Counter application");
}

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

Что делаем дальше — понятно по названиям функций. Устанавливаем шрифт и по координатам 2:10 выводим этим шрифтом надпись. Осталось указать эту функцию в качестве коллбека для отрисовки. Для этого где-нибудь в конце counter_app_alloc вызываем функцию view_port_draw_callback_set вот так:

view_port_draw_callback_set(app->view_port, counter_draw_callback, app);

Первый аргумент — это обьект вьюпорта, второй — непосредственно коллбек, а третий — тот самый контекст, который появляется во втором аргументе коллбека. Я туда уже положил app, хоть он в коллбеке никак еще не используется. Еще для каждой отрисовки надо вызывать функцию view_port_update, которая сообщает треду о том, что надо запросить коллбек, когда у треда интерфейса будет время, но в данном случае ее вызывать не обязательно — после регистрации коллбека он будет в первый раз вызван автоматически.

Работает (коммит):

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

Для начала, надо создать очередь сообщений. Добавляем в структуру CounterApp очередь (event_queue с типом osMessageQueueId_t):

  typedef struct {
      Gui* gui;
      ViewPort* view_port;
+     osMessageQueueId_t event_queue;
  } CounterApp;

Создаем при инициализации очередь и удаляем ее при выходе:

CounterApp* counter_app_alloc() {
    ...
+    app->event_queue = osMessageQueueNew(8, sizeof(InputEvent), NULL);
    ...
}

void counter_app_free(CounterApp* app) {
    ...
+    osMessageQueueDelete(app->event_queue);
    ...
}

В osMessageQueueNew "8" — это количество сообщений в очереди, следующий аргумент это размер одного элемента — мы будем класть туда приходящий в коллбек события этого же типа, и последний — это какие-то атрибуты, сейчас нам ненужные.

Создаем коллбек:

void counter_input_callback(InputEvent* input_event, void* ctx) {
    furi_assert(ctx);
    osMessageQueuePut((osMessageQueueId_t)ctx, input_event, 0, 0);
}

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

Теперь в counter_app_alloc мы регистрируем этот коллбек:

CounterApp* counter_app_alloc() {
  ...
+    view_port_input_callback_set(app->view_port, counter_input_callback, app->event_queue);
	...
}

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

Теперь самое вкусное — разбираем сообщения в основной функции. Вместо sleep(2000) пишем такое:

while(1) {
    InputEvent input;
    osStatus_t result = osMessageQueueGet(app->event_queue, &input, NULL, osWaitForever);
    furi_check(result == osOK);
    if(input.type == InputTypeShort && input.key == InputKeyBack) {
        break;
    }
}

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

osMessageQueueGet возвращает только маркер статуса, который должен быть "OK", а если он не ОК, то случилось что-то нехорошее и надо упасть и дождаться пользователя с отладчиком, что и делает furi_check. furi_assert и furi_check имеют одинаковое поведение, но с одной разницей — furi_assert предназначен для дебага программы, чтобы не забыть что-то передать, например, и отловить это как можно раньше и при сборке релизной прошивки распадается на плесень и липовый мед, а furi_check никуда не девается, и используется там, где приходящие данные от разработчика не зависят: мы можем делать все правильно, но RTOS может по независящим от нас причинам передавать статус ошибки, поэтому даже в релизной версии имеет смысл упасть и дождаться дебаггера (о настройке железного дебаггера и отладке с ним тоже будет во второй статье).

Код далее — тривиален. input, переданный нам коллбеком и пересланный через очередь сообщений, имеет в себе type и key. Type — это тип нажатия: Press, Release (оба уже после устранения дребезга), Short, Long, Repeat. Тип уже за нас обработался в треде пользовательского ввода, поэтому нам не надо заниматься подсчетом длительности нажатия для определения, короткое оно или длинное и заботиться об устранении дребезга. Key — это идентификатор клавиши (Up, Down, Right, Left, Ok, Back). Нам будут приходить все сообщения от всех всех нажатых клавиш, в том числе и события Press и Release. Но мы их игнорируем и фильтруем только короткие нажатия на кнопку "назад". Как только что-то пройдет фильтр — выходим из цикла, освобождаем ресурсы и выходим из программы. Тут break, а не counter_app_free и return, потому что иметь в программе различное число вызовов аллокации и освобождения — не очень хорошая идея, можно запутаться и либо попытаться освободить что-то второй раз, либо забыть и не освободить.

Работает! (коммит)

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

furi_check(osMessageQueueGet(app->event_queue, &app->event, NULL, osWaitForever) == osOK);

Поехали дальше. Чего не хватает счетчику? Правильно, самого счетчика.

И наконец, счетчик

Добавляем счетчик. Для этого нам нужна, как минимум, переменная счетчика — положим ее туда же, в общую структуру. Думаю, uint16 хватит:

 typedef struct {
     Gui* gui;
     ViewPort* view_port;
     osMessageQueueId_t event_queue;
     InputEvent event;
+    uint16_t counter;
 } CounterApp;

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

 while(1) {
     furi_check(osMessageQueueGet(app->event_queue, &app->event, NULL, osWaitForever) == osOK);
-         if(app->event.type == InputTypeShort && app->event.key == InputKeyBack) {
+    if(app->event.type == InputTypeLong && app->event.key == InputKeyBack) {
         counter_app_free(app);
         return 0;
     }
+    if(app->event.type == InputTypeShort) {
+        app->counter++;
+    }
+    if(app->event.type == InputTypeLong && app->event.key == InputKeyOk) {
+        app->counter = 0;
+    }
+    view_port_update(app->view_port);
 }

Изменения понятны и говорят сами за себя. Отдельно надо отметить только вызов view_port_update в конце — это тот самый вызов, который сообщает процессу Gui, что у нас есть новые данные и хорошо бы их отрисовать. Раньше мы не использовали его, потому что данные на экране рисовались один раз, а теперь мы хотим обновлять экран каждый раз после нажатия кнопки и изменения счетчика. В целом, судя по данным отладчика, view_port_update вызывается сам по себе примерно каждую секунду и так, но гарантировать это вам система не может (как и то, что такое поведение сохранится в будущих версиях), поэтому лучше все-таки вызвать его специально.

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

 void counter_draw_callback(Canvas* canvas, void* ctx) {
+    CounterApp* app = (CounterApp*)ctx;
     canvas_set_font(canvas, FontPrimary);
     canvas_draw_str(canvas, 2, 10, "Counter");
 

+   char string[6];
+   sprintf(string, "%d", app->counter);
+
+   canvas_set_font(canvas, FontBigNumbers);
+   canvas_draw_str(canvas, 53, 38, string);
+
+   canvas_set_font(canvas, FontSecondary);
+   canvas_draw_str(canvas, 15, 60, "Long press back for exit");
}

Первой добавленной строчкой мы приводим переданный контекст к нужному типу. Остальными добавленными — создадим строку, с помощью sprintf запишем в нее текстовое представление переменной счетчика, достав ее из контекста, переключимся на шрифт с большими цифрами, нарисуем цифры, переключимся на обычный шрифт, нарисуем комментарий "Long press back for exit", чтобы не забыть, как выходить, все.

Собственно, счетчик мы сделали (вот коммит):

Минусы — поле фиксировано по левому краю, и при увеличении разрядности немного уползает вбок, некрасиво (но есть canvas_draw_str_aligned, которая делает это красиво). Еще эта реализация не потокобезопасна — операции инкремента не атомарные, и может случиться ситуация, когда основной тред увеличивает число, а в этот момент ОС решает отдать управление треду Gui и тот решает отрисовать экран, и на экране появляется не совсем то, что мы задумали. Могла быть ситуация еще хуже, если бы мы имели какие-то операции с изменением указателя. Стоит смотреть на все действия внутри программы с точки зрения "а что случится, если в произвольный момент вызовется коллбек отрисовки" или любой другой процесс, потенциально конкурирующий за данные. И если вам не нравится, что может произойти в этом случае, не надо оставлять так, стоит обмазать конструкцию мьютексами. Пример этого можно увидеть вот тут.

Заключение

Разработка под флиппер отличается от разработки под обычные МК: в первую очередь тем, что для правильной работы надо соблюдать соглашения о совместном использовании ресурсов, потому что ваш код не выполняется на контроллере единолично, и ему требуется сосуществовать с другой логикой, в том числе системной. 

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

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

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 122: ↑119 и ↓3+116
Комментарии74

Публикации

Истории

Работа

Программист С
46 вакансий

Ближайшие события