├── assets ├── image-1.png ├── image-2.png ├── image-3.png ├── image.png ├── flipper_hello_world.mp4 ├── Screenshot-20221024-224504.png ├── Screenshot-20221024-231150.png ├── Screenshot-20221024-234456.png └── Screenshot-20221024-234910.png ├── 3_gui ├── hello_world.png ├── application.fam └── hello_world.c ├── 4_timer ├── hello_world.png ├── application.fam └── hello_world.c ├── 5_event ├── hello_world.png ├── application.fam └── hello_world.c ├── 1_basic_app ├── hello_world.png ├── hello_world.c └── application.fam ├── 2_msgqueue ├── hello_world.png ├── application.fam └── hello_world.c ├── 6_notifications ├── hello_world.png ├── application.fam └── hello_world.c ├── LICENSE └── README.md /assets/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/assets/image-1.png -------------------------------------------------------------------------------- /assets/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/assets/image-2.png -------------------------------------------------------------------------------- /assets/image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/assets/image-3.png -------------------------------------------------------------------------------- /assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/assets/image.png -------------------------------------------------------------------------------- /3_gui/hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/3_gui/hello_world.png -------------------------------------------------------------------------------- /4_timer/hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/4_timer/hello_world.png -------------------------------------------------------------------------------- /5_event/hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/5_event/hello_world.png -------------------------------------------------------------------------------- /1_basic_app/hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/1_basic_app/hello_world.png -------------------------------------------------------------------------------- /2_msgqueue/hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/2_msgqueue/hello_world.png -------------------------------------------------------------------------------- /assets/flipper_hello_world.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/assets/flipper_hello_world.mp4 -------------------------------------------------------------------------------- /6_notifications/hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/6_notifications/hello_world.png -------------------------------------------------------------------------------- /1_basic_app/hello_world.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int32_t hello_world_app(void* p) { 4 | (void)(p); 5 | 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /assets/Screenshot-20221024-224504.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/assets/Screenshot-20221024-224504.png -------------------------------------------------------------------------------- /assets/Screenshot-20221024-231150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/assets/Screenshot-20221024-231150.png -------------------------------------------------------------------------------- /assets/Screenshot-20221024-234456.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/assets/Screenshot-20221024-234456.png -------------------------------------------------------------------------------- /assets/Screenshot-20221024-234910.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmactep/flipperzero-hello-world/HEAD/assets/Screenshot-20221024-234910.png -------------------------------------------------------------------------------- /3_gui/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="hello_world", 3 | name="Hello World", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="hello_world_app", 6 | cdefines=["APP_HELLO_WORLD"], 7 | requires=[ 8 | "gui", 9 | ], 10 | stack_size=1 * 1024, 11 | order=90, 12 | fap_icon="hello_world.png", 13 | fap_category="Misc", 14 | ) -------------------------------------------------------------------------------- /2_msgqueue/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="hello_world", 3 | name="Hello World", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="hello_world_app", 6 | cdefines=["APP_HELLO_WORLD"], 7 | requires=[ 8 | "gui", 9 | ], 10 | stack_size=1 * 1024, 11 | order=90, 12 | fap_icon="hello_world.png", 13 | fap_category="Misc", 14 | ) -------------------------------------------------------------------------------- /4_timer/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="hello_world", 3 | name="Hello World", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="hello_world_app", 6 | cdefines=["APP_HELLO_WORLD"], 7 | requires=[ 8 | "gui", 9 | ], 10 | stack_size=1 * 1024, 11 | order=90, 12 | fap_icon="hello_world.png", 13 | fap_category="Misc", 14 | ) -------------------------------------------------------------------------------- /5_event/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="hello_world", 3 | name="Hello World", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="hello_world_app", 6 | cdefines=["APP_HELLO_WORLD"], 7 | requires=[ 8 | "gui", 9 | ], 10 | stack_size=1 * 1024, 11 | order=90, 12 | fap_icon="hello_world.png", 13 | fap_category="Misc", 14 | ) -------------------------------------------------------------------------------- /1_basic_app/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="hello_world", 3 | name="Hello World", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="hello_world_app", 6 | cdefines=["APP_HELLO_WORLD"], 7 | requires=[ 8 | "gui", 9 | ], 10 | stack_size=1 * 1024, 11 | order=90, 12 | fap_icon="hello_world.png", 13 | fap_category="Misc", 14 | ) -------------------------------------------------------------------------------- /6_notifications/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="hello_world", 3 | name="Hello World", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="hello_world_app", 6 | cdefines=["APP_HELLO_WORLD"], 7 | requires=[ 8 | "gui", 9 | ], 10 | stack_size=1 * 1024, 11 | order=90, 12 | fap_icon="hello_world.png", 13 | fap_category="Misc", 14 | ) -------------------------------------------------------------------------------- /2_msgqueue/hello_world.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | int32_t hello_world_app(void* p) { 8 | UNUSED(p); 9 | 10 | // Текущее событие типа InputEvent 11 | InputEvent event; 12 | // Очередь событий на 8 элементов размера InputEvent 13 | FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); 14 | 15 | // Бесконечный цикл обработки очереди событий 16 | while(1) { 17 | // Выбираем событие из очереди в переменную event (ждем бесконечно долго, если очередь пуста) 18 | // и проверяем, что у нас получилось это сделать 19 | furi_check(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk); 20 | 21 | // Если нажата кнопка "назад", то выходим из цикла, а следовательно и из приложения 22 | if(event.key == InputKeyBack) { 23 | break; 24 | } 25 | } 26 | 27 | // Специальная очистка памяти, занимаемой очередью 28 | furi_message_queue_free(event_queue); 29 | 30 | return 0; 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Pavel Yakovlev 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /3_gui/hello_world.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | static void draw_callback(Canvas* canvas, void* ctx) { 8 | UNUSED(ctx); 9 | 10 | canvas_clear(canvas); 11 | canvas_set_font(canvas, FontPrimary); 12 | canvas_draw_str(canvas, 0, 10, "Hello World!"); 13 | } 14 | 15 | static void input_callback(InputEvent* input_event, void* ctx) { 16 | // Проверяем, что контекст не нулевой 17 | furi_assert(ctx); 18 | FuriMessageQueue* event_queue = ctx; 19 | 20 | furi_message_queue_put(event_queue, input_event, FuriWaitForever); 21 | } 22 | 23 | int32_t hello_world_app(void* p) { 24 | UNUSED(p); 25 | 26 | // Текущее событие типа InputEvent 27 | InputEvent event; 28 | // Очередь событий на 8 элементов размера InputEvent 29 | FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); 30 | 31 | // Создаем новый view port 32 | ViewPort* view_port = view_port_alloc(); 33 | // Создаем callback отрисовки, без контекста 34 | view_port_draw_callback_set(view_port, draw_callback, NULL); 35 | // Создаем callback нажатий на клавиши, в качестве контекста передаем 36 | // нашу очередь сообщений, чтоб запихивать в неё эти события 37 | view_port_input_callback_set(view_port, input_callback, event_queue); 38 | 39 | // Создаем GUI приложения 40 | Gui* gui = furi_record_open(RECORD_GUI); 41 | // Подключаем view port к GUI в полноэкранном режиме 42 | gui_add_view_port(gui, view_port, GuiLayerFullscreen); 43 | 44 | // Бесконечный цикл обработки очереди событий 45 | while(1) { 46 | // Выбираем событие из очереди в переменную event (ждем бесконечно долго, если очередь пуста) 47 | // и проверяем, что у нас получилось это сделать 48 | furi_check(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk); 49 | 50 | // Если нажата кнопка "назад", то выходим из цикла, а следовательно и из приложения 51 | if(event.key == InputKeyBack) { 52 | break; 53 | } 54 | } 55 | 56 | // Специальная очистка памяти, занимаемой очередью 57 | furi_message_queue_free(event_queue); 58 | 59 | // Чистим созданные объекты, связанные с интерфейсом 60 | gui_remove_view_port(gui, view_port); 61 | view_port_free(view_port); 62 | furi_record_close(RECORD_GUI); 63 | 64 | return 0; 65 | } 66 | -------------------------------------------------------------------------------- /4_timer/hello_world.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | static void draw_callback(Canvas* canvas, void* ctx) { 8 | UNUSED(ctx); 9 | 10 | canvas_clear(canvas); 11 | canvas_set_font(canvas, FontPrimary); 12 | canvas_draw_str(canvas, 0, 10, "Hello World!"); 13 | } 14 | 15 | static void input_callback(InputEvent* input_event, void* ctx) { 16 | // Проверяем, что контекст не нулевой 17 | furi_assert(ctx); 18 | FuriMessageQueue* event_queue = ctx; 19 | 20 | furi_message_queue_put(event_queue, input_event, FuriWaitForever); 21 | } 22 | 23 | static void timer_callback(FuriMessageQueue* event_queue) { 24 | // Проверяем, что контекст не нулевой 25 | furi_assert(event_queue); 26 | 27 | // А что делать тут?! 28 | } 29 | 30 | int32_t hello_world_app(void* p) { 31 | UNUSED(p); 32 | 33 | // Текущее событие типа InputEvent 34 | InputEvent event; 35 | // Очередь событий на 8 элементов размера InputEvent 36 | FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); 37 | 38 | // Создаем новый view port 39 | ViewPort* view_port = view_port_alloc(); 40 | // Создаем callback отрисовки, без контекста 41 | view_port_draw_callback_set(view_port, draw_callback, NULL); 42 | // Создаем callback нажатий на клавиши, в качестве контекста передаем 43 | // нашу очередь сообщений, чтоб запихивать в неё эти события 44 | view_port_input_callback_set(view_port, input_callback, event_queue); 45 | 46 | // Создаем GUI приложения 47 | Gui* gui = furi_record_open(RECORD_GUI); 48 | // Подключаем view port к GUI в полноэкранном режиме 49 | gui_add_view_port(gui, view_port, GuiLayerFullscreen); 50 | 51 | // Создаем периодический таймер с коллбэком, куда в качестве 52 | // контекста будет передаваться наша очередь событий 53 | FuriTimer* timer = furi_timer_alloc(timer_callback, FuriTimerTypePeriodic, event_queue); 54 | // Запускаем таймер 55 | furi_timer_start(timer, 500); 56 | 57 | // Бесконечный цикл обработки очереди событий 58 | while(1) { 59 | // Выбираем событие из очереди в переменную event (ждем бесконечно долго, если очередь пуста) 60 | // и проверяем, что у нас получилось это сделать 61 | furi_check(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk); 62 | 63 | // Если нажата кнопка "назад", то выходим из цикла, а следовательно и из приложения 64 | if(event.key == InputKeyBack) { 65 | break; 66 | } 67 | } 68 | 69 | // Специальная очистка памяти, занимаемой очередью 70 | furi_message_queue_free(event_queue); 71 | 72 | // Чистим созданные объекты, связанные с интерфейсом 73 | gui_remove_view_port(gui, view_port); 74 | view_port_free(view_port); 75 | furi_record_close(RECORD_GUI); 76 | 77 | return 0; 78 | } 79 | -------------------------------------------------------------------------------- /5_event/hello_world.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | typedef enum { 8 | EventTypeTick, 9 | EventTypeInput, 10 | } EventType; 11 | 12 | typedef struct { 13 | EventType type; 14 | InputEvent input; 15 | } HelloWorldEvent; 16 | 17 | static void draw_callback(Canvas* canvas, void* ctx) { 18 | UNUSED(ctx); 19 | 20 | canvas_clear(canvas); 21 | canvas_set_font(canvas, FontPrimary); 22 | canvas_draw_str(canvas, 0, 10, "Hello World!"); 23 | } 24 | 25 | static void input_callback(InputEvent* input_event, void* ctx) { 26 | // Проверяем, что контекст не нулевой 27 | furi_assert(ctx); 28 | FuriMessageQueue* event_queue = ctx; 29 | 30 | HelloWorldEvent event = {.type = EventTypeInput, .input = *input_event}; 31 | furi_message_queue_put(event_queue, &event, FuriWaitForever); 32 | } 33 | 34 | static void timer_callback(FuriMessageQueue* event_queue) { 35 | // Проверяем, что контекст не нулевой 36 | furi_assert(event_queue); 37 | 38 | HelloWorldEvent event = {.type = EventTypeTick}; 39 | furi_message_queue_put(event_queue, &event, 0); 40 | } 41 | 42 | int32_t hello_world_app(void* p) { 43 | UNUSED(p); 44 | 45 | // Текущее событие типа кастомного типа HelloWorldEvent 46 | HelloWorldEvent event; 47 | // Очередь событий на 8 элементов размера HelloWorldEvent 48 | FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(HelloWorldEvent)); 49 | 50 | // Создаем новый view port 51 | ViewPort* view_port = view_port_alloc(); 52 | // Создаем callback отрисовки, без контекста 53 | view_port_draw_callback_set(view_port, draw_callback, NULL); 54 | // Создаем callback нажатий на клавиши, в качестве контекста передаем 55 | // нашу очередь сообщений, чтоб запихивать в неё эти события 56 | view_port_input_callback_set(view_port, input_callback, event_queue); 57 | 58 | // Создаем GUI приложения 59 | Gui* gui = furi_record_open(RECORD_GUI); 60 | // Подключаем view port к GUI в полноэкранном режиме 61 | gui_add_view_port(gui, view_port, GuiLayerFullscreen); 62 | 63 | // Создаем периодический таймер с коллбэком, куда в качестве 64 | // контекста будет передаваться наша очередь событий 65 | FuriTimer* timer = furi_timer_alloc(timer_callback, FuriTimerTypePeriodic, event_queue); 66 | // Запускаем таймер 67 | furi_timer_start(timer, 500); 68 | 69 | // Бесконечный цикл обработки очереди событий 70 | while(1) { 71 | // Выбираем событие из очереди в переменную event (ждем бесконечно долго, если очередь пуста) 72 | // и проверяем, что у нас получилось это сделать 73 | furi_check(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk); 74 | 75 | // Наше событие — это нажатие кнопки 76 | if(event.type == EventTypeInput) { 77 | // Если нажата кнопка "назад", то выходим из цикла, а следовательно и из приложения 78 | if(event.input.key == InputKeyBack) { 79 | break; 80 | } 81 | // Наше событие — это сработавший таймер 82 | } else if(event.type == EventTypeTick) { 83 | // Сделаем что-то по таймеру 84 | } 85 | } 86 | 87 | // Очищаем таймер 88 | furi_timer_free(timer); 89 | 90 | // Специальная очистка памяти, занимаемой очередью 91 | furi_message_queue_free(event_queue); 92 | 93 | // Чистим созданные объекты, связанные с интерфейсом 94 | gui_remove_view_port(gui, view_port); 95 | view_port_free(view_port); 96 | furi_record_close(RECORD_GUI); 97 | 98 | return 0; 99 | } 100 | -------------------------------------------------------------------------------- /6_notifications/hello_world.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | typedef enum { 8 | EventTypeTick, 9 | EventTypeInput, 10 | } EventType; 11 | 12 | typedef struct { 13 | EventType type; 14 | InputEvent input; 15 | } HelloWorldEvent; 16 | 17 | static void draw_callback(Canvas* canvas, void* ctx) { 18 | UNUSED(ctx); 19 | 20 | canvas_clear(canvas); 21 | canvas_set_font(canvas, FontPrimary); 22 | canvas_draw_str(canvas, 0, 10, "Hello World!"); 23 | } 24 | 25 | static void input_callback(InputEvent* input_event, void* ctx) { 26 | // Проверяем, что контекст не нулевой 27 | furi_assert(ctx); 28 | FuriMessageQueue* event_queue = ctx; 29 | 30 | HelloWorldEvent event = {.type = EventTypeInput, .input = *input_event}; 31 | furi_message_queue_put(event_queue, &event, FuriWaitForever); 32 | } 33 | 34 | static void timer_callback(FuriMessageQueue* event_queue) { 35 | // Проверяем, что контекст не нулевой 36 | furi_assert(event_queue); 37 | 38 | HelloWorldEvent event = {.type = EventTypeTick}; 39 | furi_message_queue_put(event_queue, &event, 0); 40 | } 41 | 42 | int32_t hello_world_app(void* p) { 43 | UNUSED(p); 44 | 45 | // Текущее событие типа кастомного типа HelloWorldEvent 46 | HelloWorldEvent event; 47 | // Очередь событий на 8 элементов размера HelloWorldEvent 48 | FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(HelloWorldEvent)); 49 | 50 | // Создаем новый view port 51 | ViewPort* view_port = view_port_alloc(); 52 | // Создаем callback отрисовки, без контекста 53 | view_port_draw_callback_set(view_port, draw_callback, NULL); 54 | // Создаем callback нажатий на клавиши, в качестве контекста передаем 55 | // нашу очередь сообщений, чтоб запихивать в неё эти события 56 | view_port_input_callback_set(view_port, input_callback, event_queue); 57 | 58 | // Создаем GUI приложения 59 | Gui* gui = furi_record_open(RECORD_GUI); 60 | // Подключаем view port к GUI в полноэкранном режиме 61 | gui_add_view_port(gui, view_port, GuiLayerFullscreen); 62 | 63 | // Создаем периодический таймер с коллбэком, куда в качестве 64 | // контекста будет передаваться наша очередь событий 65 | FuriTimer* timer = furi_timer_alloc(timer_callback, FuriTimerTypePeriodic, event_queue); 66 | // Запускаем таймер 67 | furi_timer_start(timer, 500); 68 | 69 | // Включаем нотификации 70 | NotificationApp* notifications = furi_record_open(RECORD_NOTIFICATION); 71 | 72 | // Бесконечный цикл обработки очереди событий 73 | while(1) { 74 | // Выбираем событие из очереди в переменную event (ждем бесконечно долго, если очередь пуста) 75 | // и проверяем, что у нас получилось это сделать 76 | furi_check(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk); 77 | 78 | // Наше событие — это нажатие кнопки 79 | if(event.type == EventTypeInput) { 80 | // Если нажата кнопка "назад", то выходим из цикла, а следовательно и из приложения 81 | if(event.input.key == InputKeyBack) { 82 | break; 83 | } 84 | // Наше событие — это сработавший таймер 85 | } else if(event.type == EventTypeTick) { 86 | // Отправляем нотификацию мигания синим светодиодом 87 | notification_message(notifications, &sequence_blink_blue_100); 88 | } 89 | } 90 | 91 | // Очищаем таймер 92 | furi_timer_free(timer); 93 | 94 | // Специальная очистка памяти, занимаемой очередью 95 | furi_message_queue_free(event_queue); 96 | 97 | // Чистим созданные объекты, связанные с интерфейсом 98 | gui_remove_view_port(gui, view_port); 99 | view_port_free(view_port); 100 | furi_record_close(RECORD_GUI); 101 | 102 | // Очищаем нотификации 103 | furi_record_close(RECORD_NOTIFICATION); 104 | 105 | return 0; 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Написание Hello World для Flipper Zero 2 | 3 | Это минимально модифицированная копия [статьи из моего блога](https://yakovlev.me/hello-flipper-zero/). 4 | 5 | ## Сборка прошивки 6 | 7 | Внезапно, но для написания своего приложения в первую очередь потребуется научиться собирать прошивку. Для официальной и модифицированной порядок действий не меняется, потому пока инструкция подходит для любого варианта. 8 | 9 | Изучая репозиторий прошивки я наткнулся на `Brewfile` с зависимостями для macOS. Воспользоваться им не удалось, поскольку, например, указанный `gdb` не поставить из Homebrew на M1. Так что я поставил несколько пакетов вручную: 10 | 11 | $ brew install protobuf protobuf-c dfu-util gcc-arm-embedded 12 | 13 | > **UPDATE**. Как выяснилось, и этого можно было не делать. Все, что нужно — это только git. А присутствующий в репозитории Flipper Build Tool (aka `fbt` — читай далее) сам разберется со всеми зависимостями. 14 | 15 | Следующим шагом нужно склонировать репозиторий со всеми сабмодулями. 16 | 17 | $ git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git 18 | 19 | > **UPDATE**. Когда я только разбирался с прошивкой и читал пользоватлеьские чаты, то почти на каждый вопрос "почему что-то не работает"  встречал ответ, что пропуск флага `--recursive` — главная ошибка новичка, и надо обязательно его указывать. Но, как оказалось, это не так и `fbt` (вот буквально через один абзац уже!) скачает все сам, как бы вы ни клонировали репозиторий. 20 | 21 | При клонировании с сабмодулями будет скачан целый гигабайт всякой всячины. 22 | 23 | $ du -sh flipperzero-firmware 24 | 1,0G flipperzero-firmware 25 | 26 | Наш главный друг среди всего этого — утилита `fbt` (Flipper Build Tool) через которую мы будем собирать прошивку и приложения. При её первом запуске без параметров будет скачан toolchain с `gcc-arm`, что намекает, что `gcc-arm-embedded` из Homebrew можно было и не ставить. Неприятно, что даже на мой M1 мак качается версия x86_64, хотя aarm64 сборка тоже существует. Но пока Apple не отключил Rosetta 2 это влияет только на эстетическую составляющую. 27 | 28 | $ ./fbt 29 | Checking tar..yes 30 | Checking downloaded toolchain tgz..no 31 | Checking curl..yes 32 | Downloading toolchain: 33 | ######################################################################### 100,0% 34 | done 35 | Removing old toolchain..done 36 | Unpacking toolchain: 37 | ####################################################### 100.0% 38 | done 39 | Clearing..done 40 | 41 | Дальше начнется сборка прошивки в debug-режиме — именно это происходит при запуске `fbt` без параметров. У меня она привела к ошибке: 42 | 43 | APPS build/f7-firmware-D/applications/applications.c 44 | ICONS build/f7-firmware-D/assets/compiled/assets_icons.c 45 | CC applications/main/archive/scenes/archive_scene.c 46 | CC applications/main/bad_usb/scenes/bad_usb_scene.c 47 | PREGEN build/f7-firmware-D/sdk_origin.i 48 | SDKSRC build/f7-firmware-D/sdk_origin.i 49 | CC applications/main/gpio/scenes/gpio_scene.c 50 | CC applications/main/gpio/gpio_item.c 51 | CC applications/main/gpio/usb_uart_bridge.c 52 | CC applications/main/ibutton/scenes/ibutton_scene.c 53 | CC applications/main/ibutton/ibutton_cli.c 54 | CC applications/main/infrared/scenes/infrared_scene.c 55 | In file included from applications/services/gui/canvas.h:9, 56 | from ./applications/services/dialogs/dialogs.h:3, 57 | from build/f7-firmware-D/sdk_origin.i.c:124: 58 | applications/services/gui/icon_animation.h:10:10: fatal error: assets_icons.h: No such file or directory 59 | 10 | #include 60 | | ^~~~~~~~~~~~~~~~ 61 | compilation terminated. 62 | scons: *** [build/f7-firmware-D/sdk_origin.i] Error 1 63 | ********** ERRORS ********** 64 | Failed building build/f7-firmware-D/sdk_origin.i: Error 1 65 | 66 | Написание [issue](https://github.com/flipperdevices/flipperzero-firmware/issues/1918) на github репозитории официальной прошивки привело к ответу через 12 минут: 67 | 68 | ![](assets/image.png) 69 | 70 | Действительно, перезапуск `fbt` решает проблему: 71 | 72 | $ ./fbt 73 | ... перечисление всех компилируемых файлов ... 74 | INFO 75 | Loaded 74 app definitions. 76 | Firmware modules configuration: 77 | Service: 78 | bt, cli, dialogs, dolphin, desktop, gui, input, loader, notification, power, storage 79 | System: 80 | updater_app, storage_move_to_sd 81 | App: 82 | subghz, lfrfid, nfc, infrared, gpio, ibutton, bad_usb, u2f, fap_loader 83 | Archive: 84 | archive 85 | Settings: 86 | bt_settings, notification_settings, storage_settings, power_settings, desktop_settings, passport, system_settings, about 87 | StartupHook: 88 | crypto_start, rpc_start, infrared_start, nfc_start, subghz_start, lfrfid_start, ibutton_start, bt_start, power_start, storage_start, updater_start, storage_move_to_sd_start 89 | Package: 90 | basic_services, main_apps, settings_apps, system_apps 91 | Firmware size 92 | .text 598400 (584.38 K) 93 | .rodata 157768 (154.07 K) 94 | .data 1444 ( 1.41 K) 95 | .bss 8432 ( 8.23 K) 96 | .free_flash 290628 (283.82 K) 97 | HEX build/f7-firmware-D/firmware.hex 98 | BIN build/f7-firmware-D/firmware.bin 99 | Setting build/f7-firmware-D as latest built dir (./build/latest/) 100 | firmware.bin: 186 flash pages (last page 4.59% full) 101 | DFU build/f7-firmware-D/firmware.dfu 102 | 2022-10-24 18:12:46,569 [INFO] Firmware binaries can be found at: 103 | dist/f7-D 104 | 105 | Заливать эту прошивку на Flipper мы не будем, вместо этого пойдем дальше к написанию своего приложения. 106 | 107 | Последний и весьма опциональный пункт здесь — создать проект для Visual Studio Code. Мне писать в ней удобнее, так чтоя нашел такую возможность весьма приятной. Тем более, что делается это в одну команду все того же `fbt`: 108 | 109 | $ ./fbt vscode_dist 110 | INSTALL .vscode/c_cpp_properties.json 111 | INSTALL .vscode/launch.json 112 | INSTALL .vscode/settings.json 113 | INSTALL .vscode/tasks.json 114 | 115 | ## Создание приложения 116 | 117 | > **NB** Полный код этого раздела доступен в [1_basic_app](1_basic_app/) 118 | 119 | Канонический способ создания новых приложений — это размещение их в директории `applications_user`. 120 | 121 | ![](assets/image-1.png) 122 | 123 | Сначала создадим простейшее приложение, которое будет компилироваться. Делать оно, правда, пока ничего не будет. Но и это мы полечим. 124 | 125 | Создадим директорию приложения, конечно же, `hello_world`, а в ней заведем три файла: 126 | 127 | - Си-исходник с кодом программы (`hello_world.c`). 128 | - Манифест приложения — Flipper Application Manifest (`application.fam`). 129 | - Иконка приложения — ч/б PNG размером 10x10 px (`hello_world.png`). 130 | 131 | Как всегда, самое сложное — это иконка, так что дарю. 132 | ![](1_basic_app/hello_world.png) 133 | Теперь разберемся с кодом. По соглашениям Flipper Zero точкой входа является функция, которая будет называться как наше приложение + суффикс *`app`. Традиционно для C функция будет возвращать код ошибки (0 — все круто), а принимать некоторые данные по указателю. Данные нам не потребуются, так что код с инклудом для *`*int32_*t` будет выглядеть так: 134 | 135 | #include 136 | 137 | int32_t hello_world_app(void *p) { 138 | return 0; 139 | } 140 | 141 | Этот гениальный код, к сожалению не скомпирируется, поскольку в проекте по умолчанию включены довольно жесткие правила, а потому любой warning воспринимается как ошибка. Решить эту проблему можно сделав вид, что данные мы все же используем: 142 | 143 | #include 144 | 145 | int32_t hello_world_app(void* p) { 146 | (void)(p); 147 | 148 | return 0; 149 | } 150 | 151 | Отлично, теперь осталось составить только манифест, с помощью которого волшебный `fbt` поймет, что мы делаем новое приложение. О том, как устроен этот файл даже есть [какая-никакая документация](https://github.com/flipperdevices/flipperzero-firmware/blob/dev/documentation/AppManifests.md), из которой мы можем почерпнуть следующее: 152 | 153 | - В поле `appid` нужно указать какое-то уникальное имя без пробелов, по которому `fbt` будет собирать наше приложение. 154 | - В поле `name` можно написать что угодно, так приложение будет называться на самом Flipper. 155 | - Поле `apptype` задает тип приложения, для всех наших поделий, пока мы не лезем в систему это будет `FlipperAppType.EXTERNAL`. 156 | - `entry_point` задает точку входа — ту самую функцию, что мы написали выше. 157 | - `fap_icon` указывает на иконку приложения. 158 | - `fap_category` говорит, к какой категории будет относиться приложение (`GPIO`, `Music`, `Misc`, `Tool` и т.д.). 159 | 160 | Вооружившись этими знаниями соорудим следующий манифест: 161 | 162 | App( 163 | appid="hello_world", 164 | name="Hello World", 165 | apptype=FlipperAppType.EXTERNAL, 166 | entry_point="hello_world_app", 167 | cdefines=["APP_HELLO_WORLD"], 168 | requires=[ 169 | "gui", 170 | ], 171 | stack_size=1 * 1024, 172 | order=90, 173 | fap_icon="hello_world.png", 174 | fap_category="Misc", 175 | ) 176 | 177 | Все, мы можем собирать наше приложение. Делается это командой: 178 | 179 | $ ./fbt fap_{APPID НАШЕГО ПРИЛОЖЕНИЯ} 180 | 181 | В нашем случае: 182 | 183 | $ ./fbt fap_hello_world 184 | CC applications_user/hello_world/hello_world.c 185 | SDKCHK firmware/targets/f7/api_symbols.csv 186 | LINK build/f7-firmware-D/.extapps/hello_world_d.elf 187 | API version 5.0 is up to date 188 | APPMETA build/f7-firmware-D/.extapps/hello_world.fap 189 | FAP build/f7-firmware-D/.extapps/hello_world.fap 190 | APPCHK build/f7-firmware-D/.extapps/hello_world.fap 191 | 192 | И все! В папке `build/f7-firmware-D/.extapps` теперь лежит наш FAP-файл, который можно любым удобным вам способом закинуть на Flipper. Я пользовался qFlipper: 193 | 194 | ![](assets/image-3.png) 195 | 196 | > **UPDATE**. Продвинутые ребята не дрыгают мышкой, а и такие операции делают через `fbt` из консоли или Visual Studio Code. Ловите команду: `./fbt launch_app APPSRC=hello_world`. 197 | 198 | После этого приложение появится в списке в искомой папке: 199 | 200 | ![](assets/Screenshot-20221024-224504.png) 201 | 202 | Запуск приложения не будет приводить ни к каким ошибкам, функция честно исполнится, и приложение тут же завершится. Но это явно не то, ради чего мы тут собрались, так что погрузимся в мелководье организации приложений, которые я уяснил за пару часов ковыряния. 203 | 204 | > **NB** Полный код этого раздела доступен в [1_basic_app](1_basic_app/) 205 | 206 | ## Очередь сообщений 207 | 208 | > **NB** Полный код этого раздела доступен в [2_msgqueue](2_msgqueue/) 209 | 210 | Как и очень многие графические фреймворки, Flipper организует работу через очередь сообщений. На практике это выглядит примерно так: 211 | 212 | ОЧЕРЕДЬ_СООБЩЕНИЙ = новая_очередь_сообщений() 213 | 214 | бесконечный_цикл { 215 | СООБЩЕНИЕ = возьми_сообщение_из(ОЧЕРЕДЬ_СООБЩЕНИЙ) 216 | 217 | если есть СООБЩЕНИЕ { 218 | хитрая_обработка(СООБЩЕНИЕ) 219 | } 220 | } 221 | 222 | очисти_очередь(ОЧЕРЕДЬ_СООБЩЕНИЙ) 223 | 224 | Для того, чтоб использовать такой функционал нам уже не обойтись одним только `stdio.h`, а потребуется начать использовать заголовки из SDK Flipper. Называется она `FURI`, где-то читал, что расшифровывается это как `FlipperUniversal Registry Implementation`. Кажется, суть уже ушла далеко от названия, но аббревеатура классная, так что её оставили. Помимо нее сразу подключим еще несколько заголовков: 225 | 226 | - `gui/gui.h` — отвечает за работу с интерфейсом. 227 | - `input/input.h` — содержит структуры данных и функции для работы с сообщениями от кнопок. 228 | - `notification/notification_messages.h` — содержит структуры данных и функции для работы с уведомлениями, например, светодиодом. 229 | 230 | Если что-то из этого вам не нужно, смело удаляйте. Мой же исходник теперь будет выглядеть так: 231 | 232 | #include 233 | #include 234 | #include 235 | #include 236 | #include 237 | 238 | int32_t hello_world_app(void* p) { 239 | UNUSED(p); 240 | 241 | return 0; 242 | } 243 | 244 | `UNUSED` — это макрос, определенный в `FURI`, его реализация ровно такая же, как мы использовали раньше, но выглядит в коде симпатичнее. 245 | 246 | Очереди сообщений, как я понял (могу жестоко ошибаться), на самом деле глубоко наплевать какие именно сообщения хранить. Это используется для того, чтоб вы могли использовать тот набор событий, который хотите именно вы. При создании вам нужно только указать размер очереди (сколько событий она может хранить до "исчерпания") и размер каждого из них. 247 | 248 | Мы начнем с того, что будем поддерживать только один тип событий — `InputEvent`, определенный в `input/input.h`, то есть события от кнопок. Для этого модифицируем нашу функцию следующим образом: 249 | 250 | #include 251 | #include 252 | #include 253 | #include 254 | #include 255 | 256 | int32_t hello_world_app(void* p) { 257 | UNUSED(p); 258 | 259 | // Текущее событие типа InputEvent 260 | InputEvent event; 261 | // Очередь событий на 8 элементов размера InputEvent 262 | FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); 263 | 264 | // Бесконечный цикл обработки очереди событий 265 | while(1) { 266 | // Выбираем событие из очереди в переменную event (ждем бесконечно долго, если очередь пуста) 267 | // и проверяем, что у нас получилось это сделать 268 | furi_check(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk); 269 | 270 | // Если нажата кнопка "назад", то выходим из цикла, а следовательно и из приложения 271 | if(event.key == InputKeyBack) { 272 | break; 273 | } 274 | } 275 | 276 | // Специальная очистка памяти, занимаемой очередью 277 | furi_message_queue_free(event_queue); 278 | 279 | return 0; 280 | } 281 | 282 | Если вы скомпилируете, скопируете на устройство запустите это приложение, то ваш Flipper будет вечно показывать вот такой экран, не реагируя ни на какие нажатия. Единственное, что вас спасет — это перезагрузка (влево + назад). 283 | 284 | ![](assets/Screenshot-20221024-231150.png) 285 | 286 | Почему так происходит? Мы, очевидно, висим в бесконечном цикле, но разве нажатие кнопки "назад" не должно его прерывать? 287 | 288 | На самом деле, нет. Потому что в нашей очереди сообщений пока ничего не лежит. Мы из нее читаем, но ничего в неё не пишем. А это в случае с Flipper мы тоже должны делать сами. Разберемся как это воплотить. Но для этого нам придется сначала разобраться с GUI. 289 | 290 | > **NB** Полный код этого раздела доступен в [2_msgqueue](2_msgqueue/) 291 | 292 | ## Графический интерфейс 293 | 294 | > **NB** Полный код этого раздела доступен в [3_gui](3_gui/) 295 | 296 | Скажу честно, я не особенно сильно тут разбирался, но глобальная идея выглядит очень понятно. Создаем некоторый GUI, который инициирует нашу систему. К этому GUI привязываем view port, который говорит, куда будет рендериться наш интерфейс (очевидно, полноэкранный для нашего простейшего приложения), а дальше привязываем колбэки отрисовки и всяких поддерживаемых событий. В частности, к ним будут относиться те самые нажатия кнопок. 297 | 298 | Нового кода будет много, так что сначала кусочки. Создаем новый view port. Тут все просто и без параметров. 299 | 300 | ViewPort* view_port = view_port_alloc(); 301 | 302 | Создаем коллбэки для отрисовки (`view_port_draw_callback_set`) и отлова нажатий на клавиши (`view_port_input_callback_set`). В обоих случаях помимо указания view port и функции-callback нам также дают возможность передать в callback произвольный контекст. Для отрисовки нам ничего пока не потребуется, а вот в callback нажатий на клавиши удобно передать указатель на нашу очередь сообщений, чтоб пихать в нее нажатые клавиши: 303 | 304 | view_port_draw_callback_set(view_port, draw_callback, NULL); 305 | view_port_input_callback_set(view_port, input_callback, event_queue); 306 | 307 | Теперь создаем общее GUI нашего приложения и подключаем к нему созданный view port в режиме полного экрана: 308 | 309 | Gui* gui = furi_record_open(RECORD_GUI); 310 | gui_add_view_port(gui, view_port, GuiLayerFullscreen); 311 | 312 | После выхода из нашего бесконечного цикла чистить теперь придется не только очередь сообщений, но и созданные объекты: 313 | 314 | gui_remove_view_port(gui, view_port); 315 | view_port_free(view_port); 316 | furi_record_close(RECORD_GUI); 317 | 318 | Собираем нашу функцию целиком: 319 | 320 | int32_t hello_world_app(void* p) { 321 | UNUSED(p); 322 | 323 | // Текущее событие типа InputEvent 324 | InputEvent event; 325 | // Очередь событий на 8 элементов размера InputEvent 326 | FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); 327 | 328 | // Создаем новый view port 329 | ViewPort* view_port = view_port_alloc(); 330 | // Создаем callback отрисовки, без контекста 331 | view_port_draw_callback_set(view_port, draw_callback, NULL); 332 | // Создаем callback нажатий на клавиши, в качестве контекста передаем 333 | // нашу очередь сообщений, чтоб запихивать в неё эти события 334 | view_port_input_callback_set(view_port, input_callback, event_queue); 335 | 336 | // Создаем GUI приложения 337 | Gui* gui = furi_record_open(RECORD_GUI); 338 | // Подключаем view port к GUI в полноэкранном режиме 339 | gui_add_view_port(gui, view_port, GuiLayerFullscreen); 340 | 341 | // Бесконечный цикл обработки очереди событий 342 | while(1) { 343 | // Выбираем событие из очереди в переменную event (ждем бесконечно долго, если очередь пуста) 344 | // и проверяем, что у нас получилось это сделать 345 | furi_check(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk); 346 | 347 | // Если нажата кнопка "назад", то выходим из цикла, а следовательно и из приложения 348 | if(event.key == InputKeyBack) { 349 | break; 350 | } 351 | } 352 | 353 | // Специальная очистка памяти, занимаемой очередью 354 | furi_message_queue_free(event_queue); 355 | 356 | // Чистим созданные объекты, связанные с интерфейсом 357 | gui_remove_view_port(gui, view_port); 358 | view_port_free(view_port); 359 | furi_record_close(RECORD_GUI); 360 | 361 | return 0; 362 | } 363 | 364 | Теперь нужно написать, собственно, коллбэки. Начнем с `draw_callback`, отвечающего за отрисовку. Он будет принимать на вход два аргумента — canvas, т.е. "холст" на котором мы будем рисовать, и контекст. В нашем случае контекст использоваться не будет, так что воспользуемся уже привычным макросом `UNUSED`. Холст же будем при каждом вызове функции очищать. 365 | 366 | static void draw_callback(Canvas* canvas, void* ctx) { 367 | UNUSED(ctx); 368 | 369 | canvas_clear(canvas); 370 | } 371 | 372 | В этой же функции мы сможем и рисовать-писать на экране. Чтоб приложение не получалось совсем грустным давайте сразу после очистки холста добавим заветные слова "Hello World!". Для этого нужно выбрать шрифт и разместить текст по каким-то координатам. Координаты указывают левый нижний угол объекта, а считаются от левого верхнего угла экрана. 373 | 374 | static void draw_callback(Canvas* canvas, void* ctx) { 375 | UNUSED(ctx); 376 | 377 | canvas_clear(canvas); 378 | canvas_set_font(canvas, FontPrimary); 379 | canvas_draw_str(canvas, 0, 10, "Hello World!"); 380 | } 381 | 382 | Аналогичным образом разберемся и с callback для клавиш. Он будет получать на вход `InputEvent` — событие нажатия, и наш передаваемый контекст. Функция будет очень простой: возьмем из контекста очередь и положим в нее пришедшее событие. 383 | 384 | static void input_callback(InputEvent* input_event, void* ctx) { 385 | // Проверяем, что контекст не нулевой 386 | furi_assert(ctx); 387 | FuriMessageQueue* event_queue = ctx; 388 | 389 | furi_message_queue_put(event_queue, input_event, FuriWaitForever); 390 | } 391 | 392 | Компилируем, заливаем и наслаждаемся! 393 | 394 | ![](assets/Screenshot-20221024-234910.png) 395 | ![](assets/Screenshot-20221024-234456.png) 396 | 397 | Мало того, что мы получили "Hello World!" на экране (кажется, стоило сделать отступ слева не нулевым), так еще и кнопка "назад" закрывает приложение! 398 | 399 | > **NB** Полный код этого раздела доступен в [3_gui](3_gui/) 400 | 401 | ## Таймер 402 | 403 | > **NB** Полный код этого раздела доступен в [4_time](4_timer/) 404 | 405 | На этом можно было бы закончить этот текст, но мне захотелось добавить еще два элемента: таймер и мигание светодиодом. Делаю я это не из-за любви к искусству, а чтоб показать кастомные события в очереди. Это проще всего показать именно на таймере. 406 | 407 | Добавляется он так же просто, как и предыдущие события, но при этом не требует инициализации GUI. При создании таймера мы указываем функцию-callback, тип таймера (периодический в нашем случае) и контекст callback`а. 408 | 409 | FuriTimer* timer = furi_timer_alloc(timer_callback, FuriTimerTypePeriodic, event_queue); 410 | 411 | Следующим действием мы таймер, который будет срабатывать каждые 500 миллисекунд: 412 | 413 | furi_timer_start(timer, 500); 414 | 415 | А в конце, конечно, очистить память: 416 | 417 | furi_timer_free(timer); 418 | 419 | Функция-callback тоже выглядит для нас уже похоже: 420 | 421 | static void timer_callback(FuriMessageQueue* event_queue) { 422 | // Проверяем, что контекст не нулевой 423 | furi_assert(event_queue); 424 | 425 | // А что делать тут?! 426 | } 427 | 428 | И здесь возникает вопрос, а что же нам делать в этом callback? Кажется, было бы здорово так же пихать какие-то события в нашу очередь `event_queue`, чтоб потом вытаскивать их в бесконечном цикле. Но вот незадача, наша очередь умеет хранить события только одного типа — `InputEvent`. Пришла пора это исправить. 429 | 430 | > **NB** Полный код этого раздела доступен в [4_time](4_timer/) 431 | 432 | ## Кастомные события 433 | 434 | > **NB** Полный код этого раздела доступен в [5_events](5_events/) 435 | 436 | Теперь наша замечательная программа требует сразу два типа событий — нажатие на клавиши и "тикание" таймера. Реализовать это в имеющемся коде довольно просто. Логика будет следующая: 437 | 438 | - Перечислим возможные события в виде `enum`. 439 | - Заведем кастомную структуру, которая будет содержать тип события и возможный payload (например, нажатая клавиша для событий нажатия на кнопки). 440 | - Модифицируем нашу очередь под прием таких событий. 441 | - Модифицируем все вызовы `furi_message_queue_put`, чтоб упаковывать события `FURI` в наши кастомные. 442 | - Модифицируем наш цикл обработки событий, чтоб обрабатывать события разного типа. 443 | 444 | По этому плану и поедем. Необходимые нам дополнительные структуры будут выглядеть так: 445 | 446 | typedef enum { 447 | EventTypeTick, 448 | EventTypeInput, 449 | } EventType; 450 | 451 | typedef struct { 452 | EventType type; 453 | InputEvent input; 454 | } HelloWorldEvent; 455 | 456 | В наше событие в качестве payload мы прямо пакуем `InputEvent`. Конечно, можно из него доставать только нужную информацию, но так проще, так что сэкономим думать. 457 | 458 | Теперь модифицируем нашу очередь: 459 | 460 | // Текущее событие типа кастомного типа HelloWorldEvent 461 | HelloWorldEvent event; 462 | // Очередь событий на 8 элементов размера HelloWorldEvent 463 | FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(HelloWorldEvent)); 464 | 465 | 466 | Запакуем событие в `input_callback`: 467 | 468 | static void input_callback(InputEvent* input_event, void* ctx) { 469 | // Проверяем, что контекст не нулевой 470 | furi_assert(ctx); 471 | FuriMessageQueue* event_queue = ctx; 472 | 473 | HelloWorldEvent event = {.type = EventTypeInput, .input = *input_event}; 474 | furi_message_queue_put(event_queue, &event, FuriWaitForever); 475 | } 476 | 477 | И запакуем событие в нашем таймере (`timer_callback`): 478 | 479 | static void timer_callback(FuriMessageQueue* event_queue) { 480 | // Проверяем, что контекст не нулевой 481 | furi_assert(event_queue); 482 | 483 | HelloWorldEvent event = {.type = EventTypeTick}; 484 | furi_message_queue_put(event_queue, &event, 0); 485 | } 486 | 487 | Остается теперь только правильно обрабатывать события в цикле: 488 | 489 | // Бесконечный цикл обработки очереди событий 490 | while(1) { 491 | // Выбираем событие из очереди в переменную event (ждем бесконечно долго, если очередь пуста) 492 | // и проверяем, что у нас получилось это сделать 493 | furi_check(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk); 494 | 495 | // Наше событие — это нажатие кнопки 496 | if (event.type == EventTypeInput) { 497 | // Если нажата кнопка "назад", то выходим из цикла, а следовательно и из приложения 498 | if (event.input.key == InputKeyBack) { 499 | break; 500 | } 501 | // Наше событие — это сработавший таймер 502 | } else if (event.type == EventTypeTick) { 503 | // Сделаем что-то по таймеру 504 | } 505 | } 506 | 507 | Можно проверить, что получившийся код вновь компилируется и работает. Вот только по таймеру пока ничего не происходит. Исправим и это! 508 | 509 | > **NB** Полный код этого раздела доступен в [5_events](5_events/) 510 | 511 | ## Мигание светодиодом 512 | 513 | > **NB** Полный код этого раздела доступен в [6_notifications](6_notifications/) 514 | 515 | Как я понял, мигание в Flipper — это один из видов нотификации. Подключение использования нотификаций происходит тоже довольно логично: 516 | 517 | - Инициализируем нотификатор, в который будем запихивать нотификации. 518 | - Когда нужно шлем нотификации. 519 | - В конце очищаем нотификатор. 520 | 521 | Собственно, как здесь описано, так и идем. В блоке инициализации нашей главной функции добавляем: 522 | 523 | NotificationApp* notifications = furi_record_open(RECORD_NOTIFICATION); 524 | 525 | В месте, где обрабатываем таймер: 526 | 527 | notification_message(notifications, &sequence_blink_blue_100); 528 | 529 | И в конце: 530 | 531 | furi_record_close(RECORD_NOTIFICATION); 532 | 533 | У нас возникла магическая константа `sequence_blink_blue_100`, определяющая код нотификации, соответствующей миганию синим светодиодом. На самом деле она определена в подключенном `notification/notification_messages.h`. Там еще много всякого интересного (например, мигание красным светодиодом). Мы же остановимся для примера только на этом. Теперь каждое срабатывание таймером мы будем мигать синим светодиодом! 534 | 535 | Ну, и финальная демонстрация! 536 | 537 | [![](https://img.youtube.com/vi/d8QXtE0Nidg/0.jpg)](https://youtu.be/d8QXtE0Nidg) 538 | 539 | > **NB** Полный код этого раздела доступен в [6_notifications](6_notifications/) 540 | 541 | --------------------------------------------------------------------------------