---
title: "📟 Как создать приложение для Flipper Zero"
date: 2023-03-27T21:42:08+03:00
draft: false
tags: [tutorial, development, c, flipper zero]
---

**Это руководство полная копия
[статьи](https://amperka.ru/blogs/projects/flipper-zero-app-programming-tutorial)
с сайта amperka.ru**.  
Автор: **Максим Данилин**. Написано: 25.01.2023 года.

# Как создать приложение для Flipper Zero

Привет!

В этой статье мы разберёмся, как написать собственную
программу-приложение для Flipper Zero!

Сердцем гаджета Flipper Zero является 32-битный микроконтроллер STM32.
Программирование Дельфина сильно отличается
от программирования привычных нам Arduino.
Помимо самого микроконтроллера во Флиппере есть радиомодуль,
NFC-модуль, кардридер, модуль Bluetooth, дисплей,
микросхема управления подсветкой и так далее.
Эффективно управлять всеми этими устройствами
в одном цикле `loop`, как это обычно выглядит
в среде разработки Arduino, уже нельзя.

На помощь приходит
[операционная система реального времени](https://ru.wikipedia.org/wiki/%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D0%B0%D1%8F_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D1%80%D0%B5%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B3%D0%BE_%D0%B2%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%B8)
или RTOS (real-time operating system). RTOS разграничивает
логические части всей программы в разные потоки
и сама осуществляет переключение между ними,
а также выделяет необходимые для работы потоков ресурсы.

Так как Флиппер построен на чипе STM32, в нём используется, наверное, самая популярная операционка для этого типа микроконтроллеров —
[FreeRTOS](https://ru.wikipedia.org/wiki/FreeRTOS).

Мы не будем подробно разбирать, как реализована операционная система
на Flipper Zero, а лучше сосредоточимся на написании приложений.

**Это важно!** Проект Flipper Zero активно развивается,
и на момент выхода данной статьи официальной документации
по созданию собственных приложений нет, а сам API полностью не описан.
Однако принцип построения приложений вполне понятен
в процессе изучения исходного кода встроенных в Флиппер приложений.
Вполне возможно, что со временем API изменится,
и код из этой статьи станет неактуальным.
Разработчики Flipper Zero обещают, что документация появится,
когда стабилизируется API. Статья актуальна для прошивки Флиппера **0.75.0**.

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

* Windows 10+ с PowerShell и Git (архитектура x86_64).
* macOS 12+ с Command Line tools (архитектура x86_64 и arm64).
* Ubuntu 20.04+ с `build-essential` и Git (архитектура x86_64).

У пользователей macOS не должно быть проблем
со сборкой прошивки благодаря [Homebrew](https://docs.brew.sh/Homebrew-on-Linux).
Ну а если вы собираете прошивку на Windows и столкнулись с трудностями,
попробуйте воспользоваться [WSL](https://ubuntu.com/wsl).

Писать код для собственных приложений мы будем на языке программирования **C**
на настольном компьютере под управлением Linux,
а именно Ubuntu 22.04.1 LTS (Jammy Jellyfish).

## Установка необходимого софта

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

Сперва скачаем и установим официальную программу
[qFlipper](https://docs.flipperzero.one/basics/firmware-update#yK3zg)
для работы с Флиппером через графический интерфейс.
Мы будем использовать qFlipper для удобной загрузки
наших готовых приложений на SD-карту во Флиппере.

Скачиваем версию программы для OS Linux куда-нибудь,
например в домашнюю директорию.
На момент выхода статьи программа qFlipper имеет версию 1.2.2,
а сам файл называется `qFlipper-x86_64-1.2.2.AppImage`.
Также установите необходимую для работы программы библиотеку
`libfuse2`, если её нет в вашей системе.

```sh
wget https://update.flipperzero.one/builds/qFlipper/1.2.2/qFlipper-x86_64-1.2.2.AppImage
sudo apt install libfuse2
```

Установите разрешение на запуск qFlipper
и добавьте в систему `udev` правила доступа
к USB Serial-порту для обычного пользователя.
Иначе понадобится вести разработку от лица суперпользователя,
что небезопасно для всей системы в случае неосторожности.

```sh
sudo chmod +x qFlipper-x86_64-1.2.2.AppImage
./qFlipper-x86_64-1.2.2.AppImage rules install
```

Запустите qFlipper и подключите ваш Flipper Zero к компьютеру по USB.

```sh
./qFlipper-x86_64-1.2.2.AppImage
```

Убедитесь, что ваш Флиппер появился в программе qFlipper,
всё работает как положено и установлена свежая прошивка.

![](/content/images/2023/flipper-app/1.png)

Для быстрой и удобной установки необходимого софта
понадобится диспетчер пакетов
[Homebrew](https://docs.brew.sh/Homebrew-on-Linux).

Установите Homebrew:

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

После установки добавьте переменные окружения `PATH`,
`MANPATH` для Homebrew.  
Это можно сделать, добавив следующую строку в файле `.profile` для вашего юзера:

```sh
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /home/$USER/.profile
```

Прошивка для Flipper Zero хранится в репозитории
[flipperzero-firmware](https://github.com/flipperdevices/flipperzero-firmware)
на GitHub.

Склонируйте к себе репозиторий прошивки Flipper Zero со всеми модулями. Репозиторий займет чуть больше 2 ГБ пространства.

```sh
git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git
```

Установите перечисленный в описании репозитория софт:

```sh
sudo apt update
sudo apt install openocd clang-format-13 dfu-util protobuf-compiler
```

Перейдите в директорию репозитория и установите
все необходимые пакеты с помощью Homebrew:

```sh
cd flipperzero-firmware
brew bundle --verbose
```

Готово! Прошивка для Флиппера, как и пользовательские приложения,
собираются с помощью утилиты `fbt` (Flipper Build Tool).
Для создания собственных приложений нет необходимости
каждый раз собирать всю прошивку целиком,
однако при первом запуске утилиты `fbt`
будут скачаны необходимые `gcc-arm` тулчейны.

Соберите прошивку:

```sh
./fbt
```

Все результаты сборки и бинарные файлы будут помещены в директорию `./dist`.

## Пример 1. Простейшее приложение

Создадим простейшее компилируемое приложение.

Мы будем создавать приложение типа FAP (Flipper Application Package).
Готовое приложение этого типа представляет собой файл формата `.fap`.
По сути, `.fap` приложение — это исполняемый файл `.elf`
с дополнительными встроенными данными.

**Кстати!** Весь исходный код приложений
из этой стати мы выложили на GitHub
в репозитории [flipperzero-examples](https://github.com/amperka/flipperzero-examples).

При создании пользовательские приложения помещаются
в отдельные папки в специально организованную директорию `applications_user`:

![](/content/images/2023/flipper-app/2.png)

Придумайте имя для приложения. Мы назвали наше первое приложение `example_1`,
этим же именем назвали и папку. В неё помещаются все файлы,
которые относятся к вашему приложению: исходный код, изображения и прочее.

В папке `example_1` создадим файл исходного кода на языке С — `example_1_app.c`.
Вот как выглядит код простейшего приложения.

```c
#include <furi.h>

int32_t example_1_app(void* p) {
    UNUSED(p);
    return 0;
}
```

Конвенционально в приложении точкой входа является функция,
которая имеет имя приложения и суффикс `app`.
Точка входа в наше приложение — функция `example_1_app`.
Главная функция традиционно возвращает код ошибки числом типа `int32_t`.
Возвращаемый ноль сообщает об отсутствии ошибок.

При компиляции кода для Флиппера любой `warning` воспринимается как ошибка.
Да, ваш код должен быть чистеньким.
Неиспользованные в функции аргументы вызывают `warning`,
поэтому для обозначения неиспользуемого указателя `p`
мы используем макрос `UNUSED`.
Реализация данного макроса описана в заголовке `furi.h`.
FURI расшифровывается как «Flipper Universal Registry Implementation».
Этим заголовочным файлом мы, по сути, подключаем вce core API Флиппера.

Нашему приложению понадобится иконка. Для пользовательских приложений,
которые находятся на самом Флиппере в разделе **Applications**,
в качестве иконок используются изображения PNG
с глубиной цвета 1 бит (чёрно-белые) и размером 10×10 пикселей.

Раздел **Apllications** с пользовательскими приложениями:

![](/content/images/2023/flipper-app/3.png)

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

![](/content/images/2023/flipper-app/emoji_smile_icon_10x10px.png)

Нарисовать свою иконку можно и в Paint.
Файл изображения помещаем в папку нашего приложения `example_1`.

Помимо исходного кода и иконки нам нужен
файл манифеста приложения — `application.fam`.
Этот файл является обязательным.
Наш манифест приложения имеет следующий вид:

```c
App(
    appid="example_1",
    name="Example 1 application",
    apptype=FlipperAppType.EXTERNAL,
    entry_point="example_1_app",
    cdefines=["APP_EXAMPLE_1"],
    stack_size=1 * 1024,
    order=90,
    fap_icon="emoji_smile_icon_10x10px.png",
    fap_category="Misc",
)
```

Разберёмся, за что отвечают данные параметры.
Параметры для всех видов приложений:

* `appid` — строка, которая используется как **ID**
приложения при конфигурации сборки `fbt`,
а также для разрешения зависимостей и конфликтов.
Есть смысл использовать здесь непосредственно имя вашего приложения.
* `name` — читабельное имя приложения,
которое будет отображаться в меню приложений на Флиппере.
* `apptype` — тип приложения.
Существуют разные типы для тестовых, системных, сервисных,
архивных приложений и для приложений,
которые должны быть в главном меню Флиппера.
В конце сборки наше приложение будет типа FAP.
Для приложений подобного рода используется
тип `EXTERNAL` (`FlipperAppType.EXTERNAL`).
* `entry_point` — точка входа приложения.
Имя главной функции, с выполнения которой начнётся работа вашего приложения.
Если в качестве точки входа вы хотите использовать функцию **C++**,
то она должна быть обёрнута в `extern "C"`.
* `cdefines` — препроцессорное глобальное объявление для других приложений,
когда текущее приложение включено в активную конфигурацию сборки.
* `stack_size` — размер стека в байтах, выделяемый для приложения при его запуске.
Обратите внимание, что выделение слишком маленького стека приведёт
к сбою системы из-за переполнения стека,
а выделение слишком большого уменьшит полезный объём
`heap`-памяти для обработки данных приложениями.
* `order` — порядок приложения внутри своей группы
при сортировке записей. Чем ниже значение,
тем выше по списку окажется ваше приложение.

Параметры для внешних приложений типа `EXTERNAL`:

* `fap_icon` — путь и имя PNG-изображения размером 10×10 пикселей,
которое используется как иконка. Здесь пишем путь и имя нашей PNG-иконки.
* `fap_category` — подкатегория приложения.
Определяет путь `.fap`-файла в папке приложений в файловой системе.
Может быть пустым. Мы поместили наше приложение в категорию **Misc** на Флиппере.

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

![](/content/images/2023/flipper-app/4.png)

В терминале переходим в корневую директорию прошивки `flipperzero-firmware`.
Сборка осуществляется командой `./fbt fap_{APPID}`,
где `{APPID}` — это **ID**, указанный в `.fam`-файле манифеста приложения.

```sh
./fbt fap_example_1
```

Сбилдить все имеющиеся в прошивке приложения FAP можно командой `./fbt faps`.

Готовое FAP-приложение находится в директории `build`
в скрытой директории `.extapps`.
Наш файл приложения называется `example_1.fap`.

![](/content/images/2023/flipper-app/5.png)

Используя программу qFlipper, перенесём файл приложения
на SD-карту в директорию `/apps/Misc`.
Файл можно перенести мышкой прямо в окно программы.

![](/content/images/2023/flipper-app/6.png)

После последнего обновления все приложения
FAP можно сбилдить и перенести на Флиппер одной командой из консоли:

```sh
./fbt fap_deploy
```

Готово! Теперь наше приложение появилось на Флиппере в разделе `Misc`:

![](/content/images/2023/flipper-app/7.png)

Главная функция `example_1_app()` сейчас пуста,
поэтому при запуске приложения программа просто
завершит свою работу и мы не увидим никаких изменений на экране.

Смотреть видео: https://youtu.be/qfpTYmIwJbA.

Как видно, ваше приложение может вовсе не иметь графического интерфейса
и отработать как бы «за кулисами».
Но у нашего Флиппера есть экранчик,
поэтому нельзя не сделать графический интерфейс.

## Пример 2. Графический интерфейс

Добавим нашему приложению графический интерфейс.

Чтобы не путаться между пунктами статьи,
мы сделаем новое приложение с именем `example_2`,
но по сути будем продолжать предыдущее приложение.

Новое приложение поместим в директорию
`applications_user/example_2`.
Соответственно, файл с исходным кодом приложения имеет имя `example_2_app.c`.

Для создания графического интерфейса вам придётся значительно расширить
исходный код приложения. Создадим в директории приложения
заголовочный файл `example_2_app.h` для описания типов данных,
структур и прототипов функций.

Включим уже знакомый нам заголовочный файл ядра `furi.h`.
Для графического интерфейса понадобится заголовок `gui/gui.h`.

```c
#pragma once

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

struct Example2App {
    Gui* gui;
    ViewPort* view_port;
};

typedef struct Example2App Example2App;
```

В заголовочном файле мы создали структуру `Example2App`,
которая будет хранить указатели на все важные компоненты нашего приложения,
и ввели новый тип для этой структуры.
В структуре нашего приложения есть указатели
на графический интерфейс `Gui` и на `ViewPort`.
`ViewPort` — это структура, которая используется
для отрисовки единичного полного экрана.
К ней привязываются указатели на `callback`-функции
отрисовки графических объектов на экране
и функции обработки различных событий (`Events`), например нажатие клавиш.

Исходный код приложения в файле `example_2_app.c` теперь выглядит так:

```c
#include "example_2_app.h"

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

Example2App* example_2_app_alloc() {
    Example2App* app = malloc(sizeof(Example2App));

    app->view_port = view_port_alloc();

    app->gui = furi_record_open(RECORD_GUI);
    gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);

    return app;
}

void example_2_app_free(Example2App* app) {
    furi_assert(app);

    view_port_enabled_set(app->view_port, false);
    gui_remove_view_port(app->gui, app->view_port);
    view_port_free(app->view_port);

    furi_record_close(RECORD_GUI);
}

int32_t example_2_app(void *p) {
    UNUSED(p);
    Example2App* app = example_2_app_alloc();

    furi_delay_ms(10000);

    example_2_app_free(app);
    return 0;
}
```

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

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

```c
Example2App* example_2_app_alloc()
```

И функцию, которая освобождает занятую приложением память:

```c
void example_2_app_free(Example2App* app)
```

В функции выделения памяти мы сначала выделяем память
под структуру `app` типа `Example2App` для нашего приложения.
Затем выделяем память для `view_port`:

```c
app->view_port = view_port_alloc();
```

Получаем указатель на текущий `Gui` Флиппера — `gui`.
Перехватываем управление `Gui` и говорим операционной системе,
что у нашего приложения есть некий интерфейс
c отрисовкой экрана `view_port` и мы хотим его отобразить.

```c
app->gui = furi_record_open(RECORD_GUI);
gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
```

В функции освобождения памяти мы, соответственно,
работаем в обратном направлении. Выключаем рендер нашего `view_port`:

```c
view_port_enabled_set(app->view_port, false);
```

Отключаем `view_port` от `Gui` и освобождаем память, занятую `view_port`:

```c
gui_remove_view_port(app->gui, app->view_port);
view_port_free(app->view_port);
```

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

```c
furi_record_close(RECORD_GUI);
```

Так как мы пишем на С и регулярно используем указатели,
в ядре FURI существует крайне полезная для нас функция `furi_assert()`,
которая экстренно остановит работу приложения и выдаст сообщение,
если мы вдруг передадим указатель на пустой участок памяти.

Точкой входа приложения на этот раз будет функция с именем `example_2_app`:

```c
int32_t example_2_app(void *p) {
    UNUSED(p);
    Example2App* app = example_2_app_alloc();

    furi_delay_ms(10000);

    example_2_app_free(app);
    return 0;
}
```

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

Кроме того, у нас получился хороший шаблон
с выделением/освобождением памяти для будущих приложений.

Иконку для приложения оставляем прежней.

Вносим правки в файл манифеста приложения `application.fam`.
Здесь всё остаётся прежним, за исключением нового параметра `requires`.
В этом параметре мы указываем,
что для работы нашему приложению нужен сервис,
отвечающий за графические интерфейсы `gui`.

```c
App(
    appid="example_2",
    name="Example 2 application",
    apptype=FlipperAppType.EXTERNAL,
    entry_point="example_2_app",
    cdefines=["APP_EXAMPLE_2"],
    requires=[
        "gui",
    ],
    stack_size=1 * 1024,
    order=90,
    fap_icon="emoji_smile_icon_10x10px.png",
    fap_category="Misc",
)
```

Собираем новое приложение:

```sh
./fbt fap_example_2
```

Используя программу qFlipper, перенесём новое
FAP-приложение из папки `build` на SD-карту в папку `/apps/Misc`.
Или используем терминал и команду `./fbt fap_deploy`.

Находим новое приложение в списке и запускаем его:

![](/content/images/2023/flipper-app/8.png)

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

Смотреть видео: https://youtu.be/7LlnIKkdM8s.

## Пример 3. Текст и изображения

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

Снова создадим копию предыдущего приложения,
но теперь под именем `example_3`.

Для рендера графических объектов создадим
в исходном файле `example_3_app.c` `callback`-функцию
отрисовки `example_3_app_draw_callback`:

```c
static void example_3_app_draw_callback(Canvas* canvas, void* ctx) {
    UNUSED(ctx);
    canvas_clear(canvas);
}
```

Callback-функция имеет определённую сигнатуру
и два аргумента `canvas`, то есть «холст»,
на котором мы будем рисовать, и контекст `ctx`.
Контекстом могут быть другие данные,
в зависимости от которых рендерится `canvas`.
Пока что мы оставим контекст неиспользованным, то есть `UNUSED`.

Первым делом перед рендером очистим наш экран:

```c
canvas_clear(canvas);
```

Графические объекты размещаются на экране согласно системе координат.
Экран Флиппера имеет разрешение **128×64**,
а начало координат находится в левом верхнем углу:

![](/content/images/2023/flipper-app/9.png)

Добавим текст в интерфейс нашего приложения.

Подключим заголовочный файл с графическими текстовыми элементами
`gui/elements.h`. Текст отрисовывается функцией
`canvas_draw_str()` с указанием координаты (`x` и `y`) исходной точки текста
и, собственно, самой строки. Возможен выбор шрифта для текста.
Шрифт устанавливается функцией `canvas_set_font()`.
Шрифт может быть главным — `FontPrimary` (высота 8 пикселей),
второстепенным — `FontSecondary` (высота 7 пикселей),
`FontKeyboard` или `FontBigNumbers`.
При желании вы сможете создать и собственный шрифт.

Например, напишем главным шрифтом строку
«**This is an example app!**» вверху экрана
и примерно по центру, по координатам (`4` и `8`).
По умолчанию начало координат текста находится в левом нижнем углу.

```c
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 4, 8, "This is an example app!");
```

В заголовочном файле `gui/elements.h` можно найти различные имплементации
для отрисовки простых элементов, скроллбаров, кнопок или выравнивания текста.

Например, снизу от нашей первой надписи разместим длинный двухстрочный текст
«**Some long long long long aligned multiline text**»,
написанный второстепенным шрифтом и автоматически выровненный
по верхней и правой границам.
Для этого воспользуемся функцией `elements_multiline_text_aligned()`:

```c
canvas_set_font(canvas, FontSecondary);
elements_multiline_text_aligned(canvas, 127, 15, AlignRight, AlignTop, "Some long long long long \n aligned multiline text");
```

Теперь разберёмся, как вывести на экран картинку.

Для эксперимента мы нарисовали чёрно-белый логотип Амперки
в PNG разрешением в **128×35** пикселей:

![](/content/images/2023/flipper-app/amperka_ru_logo_128x35px.png)

В папке с вашим приложением создайте специальную директорию
для хранения изображений. Например, мы назвали свою папку `images`.
В данную папку поместите все изображения,
которые планируете выводить на экран.
Мы назвали нашу картинку `amperka_ru_logo_128x35px.png`
и поместили её в созданную папку `images`:

![](/content/images/2023/flipper-app/10.png)

В манифесте приложения `application.fam`
добавляем новый параметр `fap_icon_assets`
с путём до директории с изображениями:

```c
App(
    appid="example_3",
    name="Example 3 application",
    apptype=FlipperAppType.EXTERNAL,
    entry_point="example_3_app",
    cdefines=["APP_EXAMPLE_3"],
    requires=[
        "gui",
    ],
    stack_size=1 * 1024,
    order=90,
    fap_icon="emoji_smile_icon_10x10px.png",
    fap_category="Misc",
    fap_icon_assets="images",
)
```

Теперь при сборке приложения все изображения из папки `images`
будут переведены в код, а сам код будет сгенерирован
в специальном заголовочном файле с именем `{APPID}_icons.h`,
где `{APPID}` — это **ID**, указанный в `.fam`-файле манифеста приложения.

Наше приложение имеет **ID** `example_3`,
значит заголовочный файл получит имя `example_3_icons.h`.
Добавим данный файл в заголовок приложения:

```c
#include "example_3_icons.h"
```

Теперь мы можем получить указатель на область памяти,
где хранится наше изображение в виде массива байтов.
Имя указателя будет соответствовать имени самого файла изображения,
но с приставкой `I_ИМЯ_ВАШЕГО_ФАЙЛА`.
Для нашей картинки с именем `amperka_ru_logo_128x35px.png` имя указателя будет
`I_amperka_ru_logo_128x35px`.

Теперь, имея адрес изображения, мы можем отрендерить его
на экране функцией `canvas_draw_icon()`.
Выведем изображение внизу экрана по координатам (`0` и `29`):

```c
canvas_draw_icon(canvas, 0, 29, &I_amperka_ru_logo_128x35px);
```

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

![](/content/images/2023/flipper-app/11.png)

Наша `callback`-функция отрисовки готова,
и её необходимо привязать к структуре `ViewPort` нашего приложения.
Это нужно сделать в функции инициализации нашего приложения
сразу после выделения памяти под `ViewPort`.
В качестве контекста мы ничего не отправляем (`NULL`).
После привязки первый раз `callback`-функция будет выполнена автоматически.

```c
view_port_draw_callback_set(app->view_port, example_3_app_draw_callback, NULL);
```

Заголовочный файл нашего третьего приложения не изменился,
за исключением имени приложения и подключаемого заголовка с изображениями.

Код `example_3_app.h`:

```c
#pragma once

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

#include "example_3_icons.h"

struct Example3App {
    Gui* gui;
    ViewPort* view_port;
};

typedef struct Example3App Example3App;
```

Код `example_3_app.с`:

```c
#include "example_3_app.h"

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

#include <gui/elements.h>

static void example_3_app_draw_callback(Canvas* canvas, void* ctx) {
    UNUSED(ctx);

    canvas_clear(canvas);

    canvas_draw_icon(canvas, 0, 29, &I_amperka_ru_logo_128x35px);

    canvas_set_font(canvas, FontPrimary);
    canvas_draw_str(canvas, 4, 8, "This is an example app!");

    canvas_set_font(canvas, FontSecondary);
    elements_multiline_text_aligned(canvas, 127, 15, AlignRight, AlignTop, "Some long long long long \n aligned multiline text");
}

Example3App* example_3_app_alloc() {
    Example3App* app = malloc(sizeof(Example3App));

    app->view_port = view_port_alloc();

    view_port_draw_callback_set(app->view_port, example_3_app_draw_callback, NULL);

    app->gui = furi_record_open(RECORD_GUI);
    gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);

    return app;
}

void example_3_app_free(Example3App* app) {
    furi_assert(app);

    view_port_enabled_set(app->view_port, false);
    gui_remove_view_port(app->gui, app->view_port);
    view_port_free(app->view_port);

    furi_record_close(RECORD_GUI);
}

int32_t example_3_app(void *p) {
    UNUSED(p);
    Example3App* app = example_3_app_alloc();

    furi_delay_ms(10000);

    example_3_app_free(app);
    return 0;
}
```

Главную функцию приложения `example_3_app` оставляем как есть.
Приложение снова отработает 10 секунд и завершит свою работу,
но на этот раз у нас будет графика.

Собираем новое приложение:

```sh
./fbt fap_example_3
```

Используя программу qFlipper, перенесём новое FAP-приложение
из папки `build` на SD-карту в папку `/apps/Misc`.
Или используем терминал и команду `./fbt fap_deploy`.

Находим новое приложение в списке и запускаем его:

![](/content/images/2023/flipper-app/12.png)

Смотреть видео: https://youtu.be/RNyTfBK4074.

## Пример 4. Кнопки

Разберёмся, как обрабатывать нажатия кнопок на Флиппере.

Продолжаем предыдущее приложение под новым именем `example_4`.

Для использования кнопок нам понадобится очередь сообщений
(`MessageQueue`) и ещё одна `callback`-функция для обработки этой очереди.

**Зачем нужна очередь сообщений?**
Поскольку за нас работает операционная система,
то `callback`-функция ввода с кнопок, как и `callback`-функция
рендера графики выполняется в контексте других потоков ОС Flipper Zero,
а не в потоке нашего приложения. Поток, отвечающий за нажатие кнопок,
не может вызвать какую-либо функцию напрямую из нашего приложения.
Но потоки могут отправлять друг другу сообщения
в любой момент выполнения, этим мы и воспользуемся.

Добавляем в структуру нашего приложения `Example4App`
очередь `event_queue` типа `FuriMessageQueue`:

```c
struct Example4App {
    Gui* gui;
    ViewPort* view_port;
    FuriMessageQueue* event_queue;
};
```

В функции инициализации приложения `example_4_app_alloc()`
выделяем память под новую очередь.

```c
app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
```

Наша очередь будет на 8 сообщений типа `InputEvent`,
который отвечает за нажатие клавиш.
Структуры данных и функции для работы с сообщениями от кнопок
находятся в заголовочном файле `input/input.h`.

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

```c
furi_message_queue_free(app->event_queue);
```

Теперь создадим `callback`-функцию для обработки этой очереди.

```c
static void example_4_app_input_callback(InputEvent* input_event, void* ctx) {
    furi_assert(ctx);

    FuriMessageQueue* event_queue = ctx;
    furi_message_queue_put(event_queue, input_event, FuriWaitForever);
}
```

Как и в случае с `callback`-функцией отрисовки, эта имеет два аргумента:
`input_event` и контекст `ctx`. Событие `input_event` сигнализирует
о каком-либо взаимодействии с кнопками. В отличие от функции отрисовки,
в этот раз контекст не пустой. Мы положили в контекст очередь сообщений
нашего приложения. Таким образом актуальные события ввода
с кнопок окажутся в потоке нашего приложения.

Привязываем новую `callback`-функцию к графическому интерфейсу `ViewPort`
нашего приложения. В качестве контекста указываем очередь сообщений
приложения `event_queue`:

```c
view_port_input_callback_set(app->view_port, example_4_app_input_callback, app->event_queue);
```

Готово! Теперь информация о состоянии кнопок находится
в нашем распоряжении, и её можно обработать.

Сейчас наше приложение работает 10 секунд,
а затем завершает работу. Давайте сделаем так, чтобы приложение закрывалось
не автоматически, а при нажатии на клавишу «**Назад**» на Флиппере.

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

```c
while (1) {
    if (furi_message_queue_get(app->event_queue, &event, 100) == FuriStatusOk) {
        if (event.type == InputTypePress) {
            if (event.key == InputKeyBack)
                break;
        }
    }
}
```

Пока очередь пуста, крутимся в бесконечном цикле.
Если в нашей очереди есть событие (`FuriStatusOk`),
нажалась кнопка (`InputTypePress`),
и это была кнопка «Назад» (`InputKeyBack`),
то выходим из цикла и, как следствие, движемся к завершению работы приложения.

Код `example_4_app.h`:

```c
#pragma once

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

#include "example_4_icons.h"

struct Example4App {
    Gui* gui;
    ViewPort* view_port;
    FuriMessageQueue* event_queue;
};

typedef struct Example4App Example4App;
```

Код `example_4_app.с`:

```c
#include "example_4_app.h"

#include <furi.h>
#include <gui/gui.h>
#include <gui/elements.h>

#include <input/input.h>

static void example_4_app_draw_callback(Canvas* canvas, void* ctx) {
    UNUSED(ctx);

    canvas_clear(canvas);

    canvas_draw_icon(canvas, 0, 29, &I_amperka_ru_logo_128x35px);

    canvas_set_font(canvas, FontPrimary);
    canvas_draw_str(canvas, 4, 8, "This is an example app!");

    canvas_set_font(canvas, FontSecondary);
    elements_multiline_text_aligned(canvas, 127, 15, AlignRight, AlignTop, "Some long long long long \n aligned multiline text");
}

static void example_4_app_input_callback(InputEvent* input_event, void* ctx) {
    furi_assert(ctx);

    FuriMessageQueue* event_queue = ctx;
    furi_message_queue_put(event_queue, input_event, FuriWaitForever);
}

Example4App* example_4_app_alloc() {
    Example4App* app = malloc(sizeof(Example4App));

    app->view_port = view_port_alloc();
    app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));

    view_port_draw_callback_set(app->view_port, example_4_app_draw_callback, NULL);
    view_port_input_callback_set(app->view_port, example_4_app_input_callback, app->event_queue);

    app->gui = furi_record_open(RECORD_GUI);
    gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);

    return app;
}

void example_4_app_free(Example4App* app) {
    furi_assert(app);

    view_port_enabled_set(app->view_port, false);
    gui_remove_view_port(app->gui, app->view_port);
    view_port_free(app->view_port);

    furi_message_queue_free(app->event_queue);

    furi_record_close(RECORD_GUI);
}

int32_t example_4_app(void *p) {
    UNUSED(p);
    Example4App* app = example_4_app_alloc();

    InputEvent event;

    while (1) {
        if (furi_message_queue_get(app->event_queue, &event, 100) == FuriStatusOk) {
            if (event.type == InputTypePress) {
                if (event.key == InputKeyBack)
                    break;
            }
        }
    }

    example_4_app_free(app);
    return 0;
}
```

Собираем новое приложение и переносим его на Flipper Zero.

```sh
./fbt fap_example_4
./fbt fap_deploy
```

![](/content/images/2023/flipper-app/13.png)

Смотреть видео: https://youtu.be/PlClOH5_yB0.

Изменим наше приложение и добавим ещё пару кнопок.

Например, будем по-разному рендерить изображение на экране
в зависимости от нажатой кнопки.

Пусть у нас будет три состояния интерфейса:
рендерится только текст, рендерится только изображение,
либо рендерится и то, и другое.
Переключаться между этими режимами будем при долгом нажатии
на кнопки «Влево» или «Вправо».

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

```c
typedef enum {
    DRAW_ALL,
    DRAW_ONLY_TEXT,
    DRAW_ONLY_PICTURES,
    TOTAL_DRAW_MODES = 3,
} DrawMode;

struct Example4App {
    Gui* gui;
    ViewPort* view_port;
    FuriMessageQueue* event_queue;

    DrawMode draw_mode;
};
```

Чтобы `draw_mode` был доступен для нашей `callback`-функции рендера экрана,
передадим в неё указатель на всю структуру приложения `app` в качестве контекста:

```c
view_port_draw_callback_set(app->view_port, example_4_app_draw_callback, app);
```

Теперь изменим саму `callback`-функцию рендера.
Пусть разные графические объекты рендерятся
в зависимости от текущего значения `draw_mode`:

```c
static void example_4_app_draw_callback(Canvas* canvas, void* ctx) {
    furi_assert(ctx);
    Example4App* app = ctx;

    canvas_clear(canvas);

    DrawMode mode = app->draw_mode;
    if (mode == DRAW_ONLY_PICTURES || mode == DRAW_ALL)
        canvas_draw_icon(canvas, 0, 29, &I_amperka_ru_logo_128x35px);
    if (mode == DRAW_ONLY_TEXT|| mode == DRAW_ALL) {
        canvas_set_font(canvas, FontPrimary);
        canvas_draw_str(canvas, 4, 8, "This is an example app!");
        canvas_set_font(canvas, FontSecondary);
        elements_multiline_text_aligned(canvas, 127, 15, AlignRight, AlignTop, "Some long long long long \n aligned multiline text");
    }
}
```

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

```c
while (1) {
    if (furi_message_queue_get(app->event_queue, &event, 100) == FuriStatusOk) {
        if (event.type == InputTypePress) {
            if (event.key == InputKeyBack)
                break;
        } else if (event.type == InputTypeLong) {
            DrawMode mode = app->draw_mode;
            if (event.key == InputKeyLeft)
                app->draw_mode = (mode - 1 + TOTAL_DRAW_MODES) % TOTAL_DRAW_MODES;
            else if (event.key == InputKeyRight)
                app->draw_mode = (mode + 1) % TOTAL_DRAW_MODES;

            view_port_update(app->view_port);
        }
    }
}
```

Теперь если будет зарегистрировано событие длительного нажатия
(`InputTypeLong`) кнопки «Влево» (`InputKeyLeft`)
или «Вправо» (`InputKeyRight`), наш режим отрисовки
`app->draw_mode` будет меняться от `0` до `TOTAL_DRAW_MODES`.

Функция `view_port_update()` запускает ререндер нашего интерфейса `view_port`.
Функция не обязательная, операционная система сама производит ререндер
раз в несколько миллисекунд, но мы можем форсировать это функцией.

Соберём обновлённое приложение, загрузим его на Флиппер,
запустим и посмотрим результат:

Смотреть видео: https://youtu.be/7EBGrZNekXM.

## Пример 5. Оповещения

Помимо дисплея Flipper Zero имеет и другой способ сообщать
нам о происходящих в программе событиях — оповещения
(`Notifications`). Нам доступно управление следующими встроенными девайсами:

* RGB-cветодиод.
* Вибромотор.
* Пьезопищалка.

Продолжаем предыдущее приложение под новым именем `example_5`.

За оповещения в операционной системе Flipper Zero отвечает отдельный поток,
и мы не можем вызывать его функции из нашего приложения напрямую.
Но мы можем отсылать в этот поток сообщения — `NotificationMessage`.
Из этих сообщений формируются последовательности `NotificationSequence`,
которые уже непосредственно отправляются в поток.

Описание структур сообщений и их последовательностей
находится в заголовочном файле `notification/notification_messages.h`,
добавляем его в наше приложение.

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

```c
struct Example5App {
    Gui* gui;
    ViewPort* view_port;
    FuriMessageQueue* event_queue;
    NotificationApp* notifications;

    DrawMode draw_mode;
};
```

В функции инициализации приложения `example_5_app_alloc()`
перехватываем управление оповещениями:

```c
app->notifications = furi_record_open(RECORD_NOTIFICATION);
```

А в функции освобождения памяти приложения
`example_5_app_free` отдаём управление обратно:

```c
furi_record_close(RECORD_NOTIFICATION);
```

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

Создадим нашу последовательность сообщений для управления RGB-светодиодом.
Назовем её `example_led_sequence` и разместим в заголовке нашего приложения.

Пусть светодиод мигнёт фиолетовым цветом **RGB(255, 0, 255)** три раза
с интервалом **500 мс**, а затем погаснет.
Сообщение будет выглядеть следующим образом:

```c
const NotificationSequence example_led_sequence = {
    &message_red_255,
    &message_blue_255,
    &message_delay_500,
    &message_red_0,
    &message_blue_0,
    &message_delay_500,
    &message_red_255,
    &message_blue_255,
    &message_delay_500,
    &message_red_0,
    &message_blue_0,
    &message_delay_500,
    &message_red_255,
    &message_blue_255,
    &message_delay_500,
    &message_red_0,
    &message_blue_0,
    NULL,
};
```

Последовательности сообщений для управления вибромотором
составляются схожим образом.
Создадим последовательность для вибромотора
с именем `example_vibro_sequence` и разместим её в заголовке.

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

```c
const NotificationSequence example_vibro_sequence = {
    &message_vibro_on,
    &message_do_not_reset,
    &message_delay_1000,
    &message_delay_1000,
    &message_delay_1000,
    &message_vibro_off,
    NULL,
};
```

По умолчанию максимально долгая описанная задержка составляет 1 секунду,
поэтому мы три раза использовали сообщение `message_delay_1000`.

Теперь создадим последовательность сообщений для пьезодинамика.
Назовем её `example_sound_sequence`.

Здесь нам уже доступна полная MIDI-клавиатура прямо из коробки!
Описание всех нот и их частот можно посмотреть
в заголовочном файле `notification_messages_notes.h`.

Добавим в наш Флиппер классическую мелодию звонка телефонов **Nokia**:

![](/content/images/2023/flipper-app/sound.png)

Последовательность сообщений с данной мелодией выглядит так:

```c
const NotificationSequence example_sound_sequence = {
    &message_note_e5,
    &message_delay_100,
    &message_note_d5,
    &message_delay_100,
    &message_note_fs4,
    &message_delay_250,
    &message_note_gs4,
    &message_delay_250,
    &message_note_cs5,
    &message_delay_100,
    &message_note_b4,
    &message_delay_100,
    &message_note_d4,
    &message_delay_250,
    &message_note_e4,
    &message_delay_250,
    &message_note_b4,
    &message_delay_100,
    &message_note_a4,
    &message_delay_100,
    &message_note_cs4,
    &message_delay_250,
    &message_note_e4,
    &message_delay_250,
    &message_note_a4,
    &message_delay_500,
    NULL,
};
```

Отлично! Теперь нужно решить, когда запускать данные оповещения.
Пусть при нажатии на кнопку «Вверх» (`InputKeyUp`) включится светодиод,
при нажатии на кнопку «Вниз» (`InputKeyDown`) включится вибромотор,
а при нажатии на кнопку «Ок» (`InputKeyOk`) заиграет мелодия.

Добавляем обработку для новых кнопок в бесконечный цикл
в главной функции нашего приложения `example_5_app()`:

```c
while (1) {
    if (furi_message_queue_get(app->event_queue, &event, 100) == FuriStatusOk) {
        if (event.type == InputTypePress) {
            if (event.key == InputKeyBack)
                break;
            else if (event.key == InputKeyUp)
                notification_message(app->notifications, &example_led_sequence);
            else if (event.key == InputKeyDown)
                notification_message(app->notifications, &example_vibro_sequence);
            else if (event.key == InputKeyOk)
                notification_message(app->notifications, &example_sound_sequence);

        } else if (event.type == InputTypeLong) {
            DrawMode mode = app->draw_mode;
            if (event.key == InputKeyLeft)
                app->draw_mode = (mode - 1 + TOTAL_DRAW_MODES) % TOTAL_DRAW_MODES;
            else if (event.key == InputKeyRight)
                app->draw_mode = (mode + 1) % TOTAL_DRAW_MODES;

            view_port_update(app->view_port);
        }
    }
}
```

Отправка сообщений осуществляется функцией `notification_message()`
с указанием соответствующей последовательности.

Собираем новое приложение:

```sh
./fbt fap_example_5
```

Используя программу qFlipper, переносим новый FAP-файл из папки `build`
на SD-карту в папку `/apps/Misc`.
Или загружаем приложение командой `./fbt fap_deploy`.

Запускаем приложение:

![](/content/images/2023/flipper-app/14.png)

Смотреть видео: https://youtu.be/238dmsw5DA4.

## Пример 6. GPIO

На Flipper Zero 18 контактов **GPIO**,
среди которых есть как пины питания, так и пины ввода-вывода.
Логическое напряжение питания — **3,3В**, и пины нетолерантны к **5В**
(за исключением пина `iButton`).
По сути, пины Флиппера соответствуют пинам установленного
в нём микроконтроллера `STM32WB55`
и обладают теми же настраиваемыми
альтернативными функциями (`ADC`, `USART`, `SPI` и др.).

Распиновка Флиппера:

![](/content/images/2023/flipper-app/pinout.png)

Подробное назначение пинов можно посмотреть
в заголовочном файле `furi_hal_resources.h`.
**FURI HAL** — специальный HAL Flipper Zero,
который призван упростить для нас взаимодействие с железом.

В FURI HAL GPIO-структура имеет имя `GpioPin`.
Без разборки Флиппера на куски нам доступны:

* `const GpioPin gpio_ext_pc0` — порт GPIOC, пин 0 (номер 16 на Флиппере).
* `const GpioPin gpio_ext_pc1` — порт GPIOC, пин 1 (номер 15 на Флиппере).
* `const GpioPin gpio_ext_pc3` — порт GPIOC, пин 3 (номер 7 на Флиппере).
* `const GpioPin gpio_ext_pb2` — порт GPIOB, пин 2 (номер 6 на Флиппере).
* `const GpioPin gpio_ext_pb3` — порт GPIOB, пин 3 (номер 5 на Флиппере).
* `const GpioPin gpio_ext_pa4` — порт GPIOA, пин 4 (номер 4 на Флиппере).
* `const GpioPin gpio_ext_pa6` — порт GPIOA, пин 6 (номер 3 на Флиппере).
* `const GpioPin gpio_ext_pa7` — порт GPIOA, пин 7 (номер 2 на Флиппере).
* `const GpioPin ibutton_gpio` — порт GPIOB, пин 14 (номер 17 на Флиппере).

Также доступны пины с функцией USART по умолчанию:

* `const GpioPin gpio_usart_tx` — порт GPIOB, пин 6 (номер 13 на Флиппере).
* `const GpioPin gpio_usart_rx` — порт GPIOB, пин 7 (номер 14 на Флиппере).

Ещё есть пины интерфейса SWD (Serial Wire Debug) для отладки,
маркированные на корпусе как SIO, SWC.
Все остальные пины микроконтроллера используются
для управления начинкой Flipper Zero:
дисплеем, кнопками, USB, NFC, I²C, SPI и т.д.

Сделаем приложение, через которое мы сможем управлять GPIO.
Сперва сделаем простой `DigitalWrite`, `DigitalRead`.
Читать значение будем с пина `А6`, а писать значение в пин `А7`.

Подключим к Флипперу простую кнопку к пину `А6` и светодиод к пину `А7`.
Максимальный ток на пине — **20 мА**, для светодиода хватит.
Питание берём с шины **3,3В**.
Установим светодиод и кнопку на макетную плату:

![](/content/images/2023/flipper-app/breadboard-1.jpg)

Назовём новое приложение `example_6` и сделаем его
на основе нашего предыдущего примера номер 4.
Предварительно уберём из приложения всё,
что касается оповещений, рендера графических элементов и обработки кнопок,
чтобы остался пустой интерфейс.

Для управления GPIO нам нужен HAL Флиппера.
Подключаем файл `furi_hal.h` в заголовок нашего приложения.

В главной структуре `Example6App` нашего приложения создадим
два пина для входа и выхода: `input_pin`, `output_pin`
и две булевы переменные для хранения текущих значений на этих пинах:
`input_value`, `output_value`.

Наш заголовочный файл принимает следующий вид:

```c
#pragma once

#include <furi.h>
#include <furi_hal.h>
#include <gui/gui.h>

struct Example6App {
    Gui* gui;
    ViewPort* view_port;
    FuriMessageQueue* event_queue;

    const GpioPin* input_pin;
    const GpioPin* output_pin;

    bool input_value;
    bool output_value;
};

typedef struct Example6App Example6App;
```

В функции инициализации приложения `example_6_app_alloc()`
задаём номера пинов. Функцией `furi_hal_gpio_init()` инициалзируем пины.
Для ввода устанавливаем режим `GpioModeInput` и включаем подтяжку `GpioPullUp`,
а для вывода режим `GpioModeOutputPushPull`
и отключаем подтяжку `GpioPullNo`.
Оба пина опрашиваются на максимальной скорости `GpioSpeedVeryHigh`:

```c
app->input_pin = &gpio_ext_pa6;
app->output_pin = &gpio_ext_pa7;

furi_hal_gpio_init(app->input_pin, GpioModeInput, GpioPullUp, GpioSpeedVeryHigh);
furi_hal_gpio_init(app->output_pin, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
```

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

```c
furi_hal_gpio_write(app->output_pin, app->output_value);
app->input_value = furi_hal_gpio_read(app->input_pin);
```

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

```c
if (furi_message_queue_get(app->event_queue, &event, 100) == FuriStatusOk) {
    if (event.key == InputKeyBack) {
        if (event.type == InputTypePress)
            break;

    } else if (event.key == InputKeyOk) {
        if (event.type == InputTypePress)
            app->output_value = true;
        else if (event.type == InputTypeRelease)
            app->output_value = false;
    }
```

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

```c
static void example_6_app_draw_callback(Canvas* canvas, void* ctx) {
    furi_assert(ctx);
    Example6App* app = ctx;

    canvas_clear(canvas);
    canvas_set_font(canvas, FontSecondary);
    elements_multiline_text_aligned(canvas, 32, 17, AlignCenter, AlignTop, "Output PA7:");
    elements_multiline_text_aligned(canvas, 96, 17, AlignCenter, AlignTop, "Input PA6:");

    canvas_set_font(canvas, FontBigNumbers);
    elements_multiline_text_aligned(canvas, 32, 32, AlignCenter, AlignTop, app->output_value ? "1" : "0");
    elements_multiline_text_aligned(canvas, 96, 32, AlignCenter, AlignTop, app->input_value ? "1" : "0");
}
```

Интерфейс будет выглядеть так:

![](/content/images/2023/flipper-app/15.png)

Собираем новое приложение и загружаем его на Флиппер:

```sh
./fbt fap_example_6
./fbt fap_deploy
```

Смотреть видео: https://youtu.be/6k5D1TGZm0Q.

**PWM и ADC**
С генерацией ШИМ-сигналов всё обстоит намного сложнее.
Здесь уже не обойтись одной-двумя функциями из FURI HAL,
а сам код сильно разрастается.

В прошивке Flipper Zero уже есть приложение **Signal Generator**
для генерации ШИМ-сигнала на пинах `PA7` и `PA4`.
Вы можете самостоятельно изучить исходный код для генерации ШИМ
в директории
[applications/plugins/signal_generator/](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/plugins/signal_generator)
и реплицировать его в ваше приложение.

А вот официальной документации на чтение аналоговых сигналов пока нет.
Кроме этого альтернативные функции аналого-цифрового преобразователя на пинах
ещё не имплементированы в FURI HAL.
Однако сам ADC на микроконтроллере STM32
и его возможности никуда от нас не делись.

На просторах интернета мы нашли пример использования **ADC**.
Мы разместили в репозитории с примерами
[flipperzero-examples](https://github.com/amperka/flipperzero-examples)
приложение [`adc_example`](https://github.com/amperka/flipperzero-examples/tree/main/applications/adc_example),
которое cчитывает аналоговое значение с пина `PC3`
и выводит его на экран.
Код ещё нуждается в доработке, и вы можете использовать его в своих приложениях,
однако мы советуем дождаться официальной документации и примеров.

Опорным напряжением является выборочно или **2.5В** или **2.048В**.
Подключив к флипперу потенциометр и взяв питание с пина **3.3В**
понадобится простой делитель напряжения в пределах тысячи **Ом**.
Разрешение АЦП - **12 бит**.

Читаем напряжение от **0** до **2.5В**
на пине `PC3` и меняем его потенциометром:

Смотреть видео: https://youtu.be/k2DK9xAi9QQ.

## Заключение

На этом мы заканчиваем базовое знакомство
с пользовательскими приложениями для Flipper Zero.

Покопавшись в документации, нам удалось зайти подальше банального
«Hello, world!» и написать несколько приложений для примера работы с GUI,
кнопками и встроенной периферией Флиппера —
RGB-светодиодом, вибромотором и баззером.

Ждём обновления официальной документации гаджета и надеемся,
что наши примеры помогут вам в создании своих приложений для Flipper Zero!
