1553 lines
70 KiB
Markdown
1553 lines
70 KiB
Markdown
---
|
||
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!
|