Blog/content/posts/2023/flipper-zero/make-app.md

1553 lines
70 KiB
Markdown
Raw Normal View History

2023-03-27 22:47:27 +03:00
---
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!