├── .gitignore ├── LICENSE.txt ├── README.md ├── application.fam ├── build.sh ├── deploy.sh ├── images ├── .gitkeep ├── cvc_36x36.png ├── one.png └── two.png ├── test_app.c └── test_app.png /.gitignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | .vscode 3 | .clang-format 4 | .editorconfig 5 | .DS_Store -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lewis Westbury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flipper-zero-tutorial-app 2 | 3 | This is a simple app for the Flipper Zero, intended to accompany the Flipper Zero developer tutorials at: [instantiator.dev](https://instantiator.dev) 4 | 5 | ## Known issues 6 | 7 | - [x] ~~This code runs on the `release` firmware, but hangs on the `dev` firmware. I'm working on it.~~ 8 | - Issue was twofold: 9 | 1. I was calling `view_set_context` on each GUI module's views. This is unnecessary, and the various module functions rely on view contexts pointing to the modules. Callback contexts for our callback methods are set using the various module functions (`popup_set_context`, and `menu_add_item`). 10 | 2. I was providing static icons (a single frame, no framerate) to the menu - which _always_ animates its icons. Having a framerate of zero results in a division by zero error. 11 | 12 | ## Supporting references 13 | 14 | A lot of the code here is derived from the work I did building the [resistance calculator](https://github.com/instantiator/flipper-zero-experimental-apps/tree/main/resistors) app, and you're welcome to sift through that code and plunder what you need for your own projects. 15 | 16 | In turn, that app relies heavily on the patterns provided in Derek Jamison's [basic scenes](https://github.com/jamisonderek/flipper-zero-tutorials/tree/main/plugins/basic_scenes) tutorial. Derek has been kindly supporting and debugging my work in the background, and has produced a vast library of valuable learning resources. 17 | 18 | ### With thanks 19 | 20 | This tutorial series would not have been possible without the help and support of various people in the Flipper Zero community. Many thanks to: 21 | 22 | * [Derek Jamison](https://github.com/jamisonderek) for the [Flipper Zero tutorials](https://github.com/jamisonderek/flipper-zero-tutorials) repository (in particular, [plugins](https://github.com/jamisonderek/flipper-zero-tutorials/tree/main/plugins)) 23 | * [DroomOne](https://github.com/DroomOne) for the [Flipper Plugin tutorial](https://github.com/DroomOne/Flipper-Plugin-Tutorial). 24 | * [Chris Hranj](https://brodan.biz/) (Brodan) for the [guide to Flipper Zero components](https://brodan.biz/blog/a-visual-guide-to-flipper-zero-gui-components/). 25 | 26 | If you're interested in developing apps for the Flipper, you should really check out Derek's YouTube channel: [@MrDerekJamison](https://www.youtube.com/@MrDerekJamison) 27 | -------------------------------------------------------------------------------- /application.fam: -------------------------------------------------------------------------------- 1 | # For details & more options, see documentation/AppManifests.md in firmware repo 2 | 3 | App( 4 | appid="test_app", # Must be unique 5 | name="App test_app", # Displayed in menus 6 | apptype=FlipperAppType.EXTERNAL, 7 | entry_point="test_app_app", 8 | stack_size=2 * 1024, 9 | fap_category="Examples", 10 | # Optional values 11 | # fap_version=(0, 1), # (major, minor) 12 | fap_icon="test_app.png", # 10x10 1-bit PNG 13 | # fap_description="A simple app", 14 | # fap_author="J. Doe", 15 | # fap_weburl="https://github.com/user/test_app", 16 | fap_icon_assets="images", # Image assets to compile for this application 17 | ) 18 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ufbt 4 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ufbt launch 4 | -------------------------------------------------------------------------------- /images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instantiator/flipper-zero-tutorial-app/f5182f6384c7c7479a5db001d1efe2e5625e8203/images/.gitkeep -------------------------------------------------------------------------------- /images/cvc_36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instantiator/flipper-zero-tutorial-app/f5182f6384c7c7479a5db001d1efe2e5625e8203/images/cvc_36x36.png -------------------------------------------------------------------------------- /images/one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instantiator/flipper-zero-tutorial-app/f5182f6384c7c7479a5db001d1efe2e5625e8203/images/one.png -------------------------------------------------------------------------------- /images/two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instantiator/flipper-zero-tutorial-app/f5182f6384c7c7479a5db001d1efe2e5625e8203/images/two.png -------------------------------------------------------------------------------- /test_app.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define TAG "test-app" 10 | 11 | /* generated by fbt from .png files in images folder */ 12 | #include 13 | 14 | /** ids for all scenes used by the app */ 15 | typedef enum { 16 | TestAppScene_MainMenu, 17 | TestAppScene_FirstPopup, 18 | TestAppScene_SecondPopup, 19 | TestAppScene_count 20 | } TestAppScene; 21 | 22 | /** ids for the 2 types of view used by the app */ 23 | typedef enum { TestAppView_Menu, TestAppView_Popup } TestAppView; 24 | 25 | /** the app context struct */ 26 | typedef struct { 27 | SceneManager* scene_manager; 28 | ViewDispatcher* view_dispatcher; 29 | Menu* menu; 30 | Popup* popup; 31 | } TestApp; 32 | 33 | /** all custom events */ 34 | typedef enum { TestAppEvent_ShowPopupOne, TestAppEvent_ShowPopupTwo } TestAppEvent; 35 | 36 | /* main menu scene */ 37 | 38 | /** indices for menu items */ 39 | typedef enum { TestAppMenuSelection_One, TestAppMenuSelection_Two } TestAppMenuSelection; 40 | 41 | /** main menu callback - sends a custom event to the scene manager based on the menu selection */ 42 | void test_app_menu_callback_main_menu(void* context, uint32_t index) { 43 | FURI_LOG_T(TAG, "test_app_menu_callback_main_menu"); 44 | TestApp* app = context; 45 | switch(index) { 46 | case TestAppMenuSelection_One: 47 | scene_manager_handle_custom_event(app->scene_manager, TestAppEvent_ShowPopupOne); 48 | break; 49 | case TestAppMenuSelection_Two: 50 | scene_manager_handle_custom_event(app->scene_manager, TestAppEvent_ShowPopupTwo); 51 | break; 52 | } 53 | } 54 | 55 | /** resets the menu, gives it content, callbacks and selection enums */ 56 | void test_app_scene_on_enter_main_menu(void* context) { 57 | FURI_LOG_T(TAG, "test_app_scene_on_enter_main_menu"); 58 | TestApp* app = context; 59 | menu_reset(app->menu); 60 | 61 | // NB. icons are specified as NULL below, because: 62 | // * icons are _always_ animated by the menu 63 | // * the icons provided (&I_one, &I_two) are generated by the build process 64 | // * these icons do not have a framerate (resulting in a division by zero) 65 | menu_add_item( 66 | app->menu, 67 | "First popup", 68 | NULL, 69 | TestAppMenuSelection_One, 70 | test_app_menu_callback_main_menu, 71 | app); 72 | menu_add_item( 73 | app->menu, 74 | "Second popup", 75 | NULL, 76 | TestAppMenuSelection_Two, 77 | test_app_menu_callback_main_menu, 78 | app); 79 | view_dispatcher_switch_to_view(app->view_dispatcher, TestAppView_Menu); 80 | } 81 | 82 | /** main menu event handler - switches scene based on the event */ 83 | bool test_app_scene_on_event_main_menu(void* context, SceneManagerEvent event) { 84 | FURI_LOG_T(TAG, "test_app_scene_on_event_main_menu"); 85 | TestApp* app = context; 86 | bool consumed = false; 87 | switch(event.type) { 88 | case SceneManagerEventTypeCustom: 89 | switch(event.event) { 90 | case TestAppEvent_ShowPopupOne: 91 | scene_manager_next_scene(app->scene_manager, TestAppScene_FirstPopup); 92 | consumed = true; 93 | break; 94 | case TestAppEvent_ShowPopupTwo: 95 | scene_manager_next_scene(app->scene_manager, TestAppScene_SecondPopup); 96 | consumed = true; 97 | break; 98 | } 99 | break; 100 | default: // eg. SceneManagerEventTypeBack, SceneManagerEventTypeTick 101 | consumed = false; 102 | break; 103 | } 104 | return consumed; 105 | } 106 | 107 | void test_app_scene_on_exit_main_menu(void* context) { 108 | FURI_LOG_T(TAG, "test_app_scene_on_exit_main_menu"); 109 | TestApp* app = context; 110 | menu_reset(app->menu); 111 | } 112 | 113 | /* popup 1 scene */ 114 | 115 | void test_app_scene_on_enter_popup_one(void* context) { 116 | FURI_LOG_T(TAG, "test_app_scene_on_enter_popup_one"); 117 | TestApp* app = context; 118 | popup_reset(app->popup); 119 | popup_set_context(app->popup, app); 120 | popup_set_header(app->popup, "Popup One", 64, 10, AlignCenter, AlignTop); 121 | popup_set_icon(app->popup, 10, 10, &I_cvc_36x36); 122 | popup_set_text(app->popup, "One! One popup. Ah ah ah...", 64, 20, AlignLeft, AlignTop); 123 | view_dispatcher_switch_to_view(app->view_dispatcher, TestAppView_Popup); 124 | } 125 | 126 | bool test_app_scene_on_event_popup_one(void* context, SceneManagerEvent event) { 127 | FURI_LOG_T(TAG, "test_app_scene_on_event_popup_one"); 128 | UNUSED(context); 129 | UNUSED(event); 130 | return false; // don't handle any events 131 | } 132 | 133 | void test_app_scene_on_exit_popup_one(void* context) { 134 | FURI_LOG_T(TAG, "test_app_scene_on_exit_popup_one"); 135 | TestApp* app = context; 136 | popup_reset(app->popup); 137 | } 138 | 139 | /* popup 2 scene */ 140 | 141 | void test_app_scene_on_enter_popup_two(void* context) { 142 | FURI_LOG_T(TAG, "test_app_scene_on_enter_popup_two"); 143 | TestApp* app = context; 144 | popup_reset(app->popup); 145 | popup_set_context(app->popup, app); 146 | popup_set_header(app->popup, "Popup Two", 64, 10, AlignCenter, AlignTop); 147 | popup_set_icon(app->popup, 10, 10, &I_cvc_36x36); 148 | popup_set_text(app->popup, "Two! Two popups. (press back)", 64, 20, AlignLeft, AlignTop); 149 | view_dispatcher_switch_to_view(app->view_dispatcher, TestAppView_Popup); 150 | } 151 | 152 | bool test_app_scene_on_event_popup_two(void* context, SceneManagerEvent event) { 153 | FURI_LOG_T(TAG, "test_app_scene_on_event_popup_two"); 154 | UNUSED(context); 155 | UNUSED(event); 156 | return false; // don't handle any events 157 | } 158 | 159 | void test_app_scene_on_exit_popup_two(void* context) { 160 | FURI_LOG_T(TAG, "test_app_scene_on_exit_popup_two"); 161 | TestApp* app = context; 162 | popup_reset(app->popup); 163 | } 164 | 165 | /** collection of all scene on_enter handlers - in the same order as their enum */ 166 | void (*const test_app_scene_on_enter_handlers[])(void*) = { 167 | test_app_scene_on_enter_main_menu, 168 | test_app_scene_on_enter_popup_one, 169 | test_app_scene_on_enter_popup_two}; 170 | 171 | /** collection of all scene on event handlers - in the same order as their enum */ 172 | bool (*const test_app_scene_on_event_handlers[])(void*, SceneManagerEvent) = { 173 | test_app_scene_on_event_main_menu, 174 | test_app_scene_on_event_popup_one, 175 | test_app_scene_on_event_popup_two}; 176 | 177 | /** collection of all scene on exit handlers - in the same order as their enum */ 178 | void (*const test_app_scene_on_exit_handlers[])(void*) = { 179 | test_app_scene_on_exit_main_menu, 180 | test_app_scene_on_exit_popup_one, 181 | test_app_scene_on_exit_popup_two}; 182 | 183 | /** collection of all on_enter, on_event, on_exit handlers */ 184 | const SceneManagerHandlers test_app_scene_event_handlers = { 185 | .on_enter_handlers = test_app_scene_on_enter_handlers, 186 | .on_event_handlers = test_app_scene_on_event_handlers, 187 | .on_exit_handlers = test_app_scene_on_exit_handlers, 188 | .scene_num = TestAppScene_count}; 189 | 190 | /** custom event handler - passes the event to the scene manager */ 191 | bool test_app_scene_manager_custom_event_callback(void* context, uint32_t custom_event) { 192 | FURI_LOG_T(TAG, "test_app_scene_manager_custom_event_callback"); 193 | furi_assert(context); 194 | TestApp* app = context; 195 | return scene_manager_handle_custom_event(app->scene_manager, custom_event); 196 | } 197 | 198 | /** navigation event handler - passes the event to the scene manager */ 199 | bool test_app_scene_manager_navigation_event_callback(void* context) { 200 | FURI_LOG_T(TAG, "test_app_scene_manager_navigation_event_callback"); 201 | furi_assert(context); 202 | TestApp* app = context; 203 | return scene_manager_handle_back_event(app->scene_manager); 204 | } 205 | 206 | /** initialise the scene manager with all handlers */ 207 | void test_app_scene_manager_init(TestApp* app) { 208 | FURI_LOG_T(TAG, "test_app_scene_manager_init"); 209 | app->scene_manager = scene_manager_alloc(&test_app_scene_event_handlers, app); 210 | } 211 | 212 | /** initialise the views, and initialise the view dispatcher with all views */ 213 | void test_app_view_dispatcher_init(TestApp* app) { 214 | FURI_LOG_T(TAG, "test_app_view_dispatcher_init"); 215 | app->view_dispatcher = view_dispatcher_alloc(); 216 | view_dispatcher_enable_queue(app->view_dispatcher); 217 | 218 | // allocate each view 219 | FURI_LOG_D(TAG, "test_app_view_dispatcher_init allocating views"); 220 | app->menu = menu_alloc(); 221 | app->popup = popup_alloc(); 222 | 223 | // assign callback that pass events from views to the scene manager 224 | FURI_LOG_D(TAG, "test_app_view_dispatcher_init setting callbacks"); 225 | view_dispatcher_set_event_callback_context(app->view_dispatcher, app); 226 | view_dispatcher_set_custom_event_callback( 227 | app->view_dispatcher, test_app_scene_manager_custom_event_callback); 228 | view_dispatcher_set_navigation_event_callback( 229 | app->view_dispatcher, test_app_scene_manager_navigation_event_callback); 230 | 231 | // add views to the dispatcher, indexed by their enum value 232 | FURI_LOG_D(TAG, "test_app_view_dispatcher_init adding view menu"); 233 | view_dispatcher_add_view(app->view_dispatcher, TestAppView_Menu, menu_get_view(app->menu)); 234 | 235 | FURI_LOG_D(TAG, "test_app_view_dispatcher_init adding view popup"); 236 | view_dispatcher_add_view(app->view_dispatcher, TestAppView_Popup, popup_get_view(app->popup)); 237 | } 238 | 239 | /** initialise app data, scene manager, and view dispatcher */ 240 | TestApp* test_app_init() { 241 | FURI_LOG_T(TAG, "test_app_init"); 242 | TestApp* app = malloc(sizeof(TestApp)); 243 | test_app_scene_manager_init(app); 244 | test_app_view_dispatcher_init(app); 245 | return app; 246 | } 247 | 248 | /** free all app data, scene manager, and view dispatcher */ 249 | void test_app_free(TestApp* app) { 250 | FURI_LOG_T(TAG, "test_app_free"); 251 | scene_manager_free(app->scene_manager); 252 | view_dispatcher_remove_view(app->view_dispatcher, TestAppView_Menu); 253 | view_dispatcher_remove_view(app->view_dispatcher, TestAppView_Popup); 254 | view_dispatcher_free(app->view_dispatcher); 255 | menu_free(app->menu); 256 | popup_free(app->popup); 257 | free(app); 258 | } 259 | 260 | /** go to trace log level in the dev environment */ 261 | void test_app_set_log_level() { 262 | #ifdef FURI_DEBUG 263 | furi_log_set_level(FuriLogLevelTrace); 264 | #else 265 | furi_log_set_level(FuriLogLevelInfo); 266 | #endif 267 | } 268 | 269 | /** entrypoint */ 270 | int32_t test_app_app(void* p) { 271 | UNUSED(p); 272 | test_app_set_log_level(); 273 | 274 | // create the app context struct, scene manager, and view dispatcher 275 | FURI_LOG_I(TAG, "Test app starting..."); 276 | TestApp* app = test_app_init(); 277 | 278 | // set the scene and launch the main loop 279 | Gui* gui = furi_record_open(RECORD_GUI); 280 | view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen); 281 | scene_manager_next_scene(app->scene_manager, TestAppScene_MainMenu); 282 | FURI_LOG_D(TAG, "Starting dispatcher..."); 283 | view_dispatcher_run(app->view_dispatcher); 284 | 285 | // free all memory 286 | FURI_LOG_I(TAG, "Test app finishing..."); 287 | furi_record_close(RECORD_GUI); 288 | test_app_free(app); 289 | return 0; 290 | } 291 | -------------------------------------------------------------------------------- /test_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instantiator/flipper-zero-tutorial-app/f5182f6384c7c7479a5db001d1efe2e5625e8203/test_app.png --------------------------------------------------------------------------------