From 418c4191717e9359a5a4cb1af1ee7884d1583b69 Mon Sep 17 00:00:00 2001 From: Derek Jamison Date: Wed, 8 Mar 2023 08:57:38 -0500 Subject: [PATCH] Initial check-in for scenes demo. --- plugins/scenes/README.md | 41 +++ plugins/scenes/application.fam | 10 + plugins/scenes/scenes_demo.png | Bin 0 -> 1817 bytes plugins/scenes/scenes_demo_app.c | 469 +++++++++++++++++++++++++++++++ 4 files changed, 520 insertions(+) create mode 100644 plugins/scenes/README.md create mode 100644 plugins/scenes/application.fam create mode 100644 plugins/scenes/scenes_demo.png create mode 100644 plugins/scenes/scenes_demo_app.c diff --git a/plugins/scenes/README.md b/plugins/scenes/README.md new file mode 100644 index 0000000..50e17c8 --- /dev/null +++ b/plugins/scenes/README.md @@ -0,0 +1,41 @@ +# BASIC DEMO + +## Introduction + +This is a scenes and view application for the Flipper Zero, using both a custom +'knob' component and a widget. The goal of this project is to use demonstrate +how to use sceness and views. + +TODO: WRITE TUTORIAL ON SCENES AND VIEWS. + +## Installation Directions + +This project is intended to be overlayed on top of an existing firmware repo. + +- Clone, Build & Deploy an existing flipper zero firmware repo. See this [tutorial](/firmware/updating/README.md) for updating firmware. +- Copy the "scenes" [folder](..) to the \applications\plugins\scenes folder in your firmware. +- Build & deploy the firmware. See this [tutorial](/firmware/updating/README.md) for updating firmware. +- NOTE: You can also extract the scenes.FAP from resources.tar file and use qFlipper to copy the file to the SD Card/apps/Misc folder. + +## Running the updated firmware + +These directions assume you are starting at the flipper desktop. If not, please press the back button until you are at the desktop. + +- Press the OK button on the flipper to pull up the main menu. +- Choose "Applications" from the menu. +- Choose "Misc" from the sub-menu. +- Choose "Scenes Demo" + +- The flipper should say "VOLUME" and "Knob demo 50". +- Press UP/DOWN buttons to change value. +- Press OK button to switch to "FREQUENCY" and "Knob demo 440". +- Press UP/DOWN buttons to change value. +- Press OK button to go to Summary page. +- Press UP/DOWN buttons to scroll message. +- Press OK button to go to "VOLUME" page. +- Press BACK button for previous page. +- Keep pressing BACK button until exit. + +## How it works + +TODO: WRITE TUTORIAL ON SCENES AND VIEWS. diff --git a/plugins/scenes/application.fam b/plugins/scenes/application.fam new file mode 100644 index 0000000..e317c23 --- /dev/null +++ b/plugins/scenes/application.fam @@ -0,0 +1,10 @@ +App( + appid="Scenes_Demo", + name="Scenes Demo", + apptype=FlipperAppType.EXTERNAL, + entry_point="scenes_demo_app", + requires=["gui"], + stack_size=2 * 1024, + fap_icon="scenes_demo.png", + fap_category="Misc", +) diff --git a/plugins/scenes/scenes_demo.png b/plugins/scenes/scenes_demo.png new file mode 100644 index 0000000000000000000000000000000000000000..522b64971b2a44e7d665d4beaa1d5285b30ddb74 GIT binary patch literal 1817 zcma)7eM}Q)7(XO1`I_0rlo+;L4vji&?|M*>3$=r`LN^X8-|P#I-dlRo_HK7qT0TOI zu&EmgWHS>ao5;`&4a=A;a|(V;C8EMImL+40825+EVw?n~b75icwdJFZeKcwBeV^y| z{NCU5yw5$|Yst-uiCi6tAV`ck+n5KR$~tm+DE$5Mj=mW|LTWjK!D2QTP>aahI0rzG z^`i$`M-z?PVu$or`^7zERMgw=&183G(2jT88rzfiZ3{otmU{NvYuZ`k9GW9vVi ztUOQ-Uwz2t*TDki0z5ebYXMNu$=hW^?HEZHq#@ibYOt08KyItFLyRlmwm%a!6GtVA> z(nIQ#RD*O;VmNbovWvJ97g9H!wDG$vJaqcXe*LY1iOKwp<`d5qb?q6!I5PIn4)tZpE9g znef7s_g}TY((umITPr7ajk)co#>#8%yetm)ywH8~;qui-zOIXKPgRW$-;TeL$rcX& zvg~uu;rzR)>bj7Fs*0Jfl7^Lg3Af}Fm}X~Z;j-|;SUr412-&3)fDjN77W17 zW!H!xzb3c9T2pCFV=?{an8Bxw;gTq?4)YOHaIKhe4D~_eN*z{0TTlZp z0#vKkstF8+qiDLw+NeC^mOwb%=`g!23lxrfJRY?tNzIEDxF#(v4JSyPBvlZhlB!)Y z<5jt&1V2fD#t0;<$O$sXyHJIeDd*j?4ucAO(twv+Aa_ZD0>Dz>iU-%I2}P_AD%g3M zm+ZVSFD3{-?+yx*Iom(*E1n=Itl84cUPgEnNYnogbvox|OR}j7CLE|=kmNaHsi0Z_ zcpi{=w`c{XD&Ue6FjOf3W#ApWm=C)E=rB#dX5qZRQDWqrz$L-5LI<(fQ~HQ!zA%0= znr9(b5{53=;j9RZ3LPfpWo4pflOaWumXe|()$mZIQk2=rRDc}ds*vse0~f5t92;a* z5^=bd8!vQmFm*COQL^CBVW}iOM_u}NJG={uhMmeXGGk<9D3~N*pJ_-HNfZ!TDw(7- z+C(CaB4Fx1Fwb(f>i>fJx*LU`bQnX^R5mBcyjVSN4)kn3czuy6V03WGK`@B1Dyiu( zLF8Gt6|g=5&@S;d*~5q+qXOQa4$H9FIGBDlYEk+Y)u=UuI%N*)u>;5uoRcP*A~wzC zgfA(;*Go$X@Z0~N06%sq0YODwEFc|M-rU7iNynEepdYvBjrS$tx59A(cQ9^lHYvOL z`378YsEKfTE!)Y&!pZlH*_crfS8RPgtf{GqSQ* +#include + +#include +#include + +#include +#include + +#include +#include + +#include + +#define TAG "scenes_demo_app" + +///////////////////////////////////////////////////////////////// +// Routine for logging messages with a delay. +///////////////////////////////////////////////////////////////// + +void message(char* message) { + FURI_LOG_I(TAG, message); + furi_delay_ms(10); +} + +///////////////////////////////////////////////////////////////// +// Knob - Our component with custom input/draw routines +///////////////////////////////////////////////////////////////// + +typedef enum { + KnobEventDone, +} KnobCustomEvents; + +typedef void (*KnobCallback)(void* context, uint32_t index); + +typedef struct Knob { + View* view; +} Knob; + +typedef struct KnobModel { + FuriString* buffer; + uint16_t counter; + char* heading; + KnobCallback callback; + void* callback_context; +} KnobModel; + +// Set a callback to invoke when knob has an event. +// @knob is a pointer to our Knob instance. +// @callback is a function to invoke when we have custom events. +static void knob_set_callback(Knob* knob, KnobCallback callback, void* callback_context) { + with_view_model( + knob->view, + KnobModel * model, + { + model->callback_context = callback_context; + model->callback = callback; + }, + true); +} + +// Invoked when input (button press) is detected. +// @input_even is the event the occured. +// @ctx is a pointer to our Knob instance. +static bool knob_input_callback(InputEvent* input_event, void* ctx) { + message("knob_input_callback"); + Knob* knob = (Knob*)ctx; + + bool handled = false; + + if(input_event->type == InputTypePress && input_event->key == InputKeyUp) { + bool updated = false; + with_view_model( + knob->view, + KnobModel * model, + { + if(model->counter) { + model->counter--; + updated = true; + } + }, + updated); + handled = true; + } else if(input_event->type == InputTypePress && input_event->key == InputKeyDown) { + with_view_model( + knob->view, + KnobModel * model, + { model->counter++; }, + true); // Render new data. + handled = true; + } else if(input_event->type == InputTypePress && input_event->key == InputKeyOk) { + with_view_model( + knob->view, + KnobModel * model, + { + if(model->callback) { + message("invoking callback"); + model->callback(model->callback_context, KnobEventDone); + } else { + message("no callback set; use knob_set_callback first."); + } + }, + false); // No new data. + handled = true; + } + + return handled; +} + +// Invoked by the draw callback to render the knob. +// @canvas is the canvas to draw on. +// @ctx is our model. +static void knob_render_callback(Canvas* canvas, void* ctx) { + message("knob_render_callback"); + KnobModel* model = ctx; + + furi_string_printf(model->buffer, "Knob demo %d", model->counter); + + canvas_set_font(canvas, FontPrimary); + + if(model->heading) { + canvas_draw_str_aligned(canvas, 64, 5, AlignCenter, AlignTop, model->heading); + } + + canvas_draw_str_aligned( + canvas, 15, 30, AlignLeft, AlignTop, furi_string_get_cstr(model->buffer)); +} + +// Allocates a Knob instance. +Knob* knob_alloc() { + message("knob_alloc"); + Knob* knob = malloc(sizeof(Knob)); + knob->view = view_alloc(); + + // context passed to input_callback. + view_set_context(knob->view, knob); + + // context passed to render. + view_allocate_model(knob->view, ViewModelTypeLockFree, sizeof(KnobModel)); + with_view_model( + knob->view, + KnobModel * model, + { + model->buffer = furi_string_alloc(); + model->counter = 0; + model->heading = NULL; + }, + true); + + view_set_draw_callback(knob->view, knob_render_callback); + view_set_input_callback(knob->view, knob_input_callback); + return knob; +} + +// Free a Knob instance. +// @knob pointer to a Knob instance. +void knob_free(Knob* knob) { + message("knob_free"); + furi_assert(knob); + with_view_model( + knob->view, KnobModel * model, { furi_string_free(model->buffer); }, true); + view_free(knob->view); + free(knob); +} + +// Gets the view associated with our Knob. +// @knob pointer to a Knob instance. +View* knob_get_view(Knob* knob) { + message("knob_get_view"); + furi_assert(knob); + return knob->view; +} + +// Gets the current counter value for a given Knob instance. +// @knob pointer to a Knob instance. +uint32_t knob_get_counter(Knob* knob) { + message("knob_get_counter"); + furi_assert(knob); + + uint32_t value = 0; + with_view_model( + knob->view, KnobModel * model, { value = model->counter; }, false); + + return value; +} + +// Set the counter value for a given Knob instance. +// @knob pointer to a Knob instance. +void knob_set_counter(Knob* knob, uint32_t count) { + with_view_model( + knob->view, KnobModel * model, { model->counter = count; }, true); +} + +// Sets the heading for displaying our knob. +// @knob pointer to a Knob instance. +// @heading the kind of knob. +void knob_set_heading(Knob* knob, char* heading) { + with_view_model( + knob->view, KnobModel * model, { model->heading = heading; }, true); +} + +///////////////////////////////////////////////////////////////// +// This is our application. +///////////////////////////////////////////////////////////////// + +typedef struct App { + SceneManager* scene_manager; + ViewDispatcher* view_dispatcher; + Knob* knob; + Widget* widget; + float volume; + float frequency; +} App; + +typedef enum { + AppSceneSetVolume, + AppSceneSetFrequency, + AppSceneSummary, + AppSceneNum, +} appScene; + +typedef enum { + AppViewKnob, + AppViewWidget, +} appViews; + +typedef enum { + AppKnobEventDone, + AppWidgetEventDone, +} AppKnobCustomEvents; + +void app_knob_callback(void* context, uint32_t index) { + message("app_knob_callback"); + App* app = context; + if(index == KnobEventDone) { + view_dispatcher_send_custom_event(app->view_dispatcher, AppKnobEventDone); + } +} + +bool app_scene_custom_callback(void* context, uint32_t custom_event) { + message("app_scene_custom_callback"); + furi_assert(context); + App* app = context; + return scene_manager_handle_custom_event(app->scene_manager, custom_event); +} + +bool app_back_event_callback(void* context) { + furi_assert(context); + App* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +void app_scene_set_volume_on_enter(void* context) { + message("app_scene_set_volume_on_enter"); + App* app = context; + + Knob* knob = app->knob; + knob_set_callback(knob, app_knob_callback, app); + knob_set_heading(knob, "VOLUME"); + knob_set_counter(knob, (uint32_t)app->volume); + + view_dispatcher_switch_to_view(app->view_dispatcher, AppViewKnob); +} + +bool app_scene_set_volume_on_event(void* context, SceneManagerEvent event) { + message("app_scene_set_volume_on_event"); + App* app = context; + + bool consumed = false; + if(event.type == SceneManagerEventTypeBack) { + // Back button pressed + } else if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + if(event.event == AppKnobEventDone) { + FURI_LOG_I(TAG, "Custom Event!"); + uint32_t counter = knob_get_counter(app->knob); + FURI_LOG_I(TAG, "The counter is %ld.", counter); + app->volume = counter; + scene_manager_next_scene(app->scene_manager, AppSceneSetFrequency); + } + } + return consumed; +} + +void app_scene_set_volume_on_exit(void* context) { + message("app_scene_set_volume_on_exit"); + UNUSED(context); +} + +void app_scene_set_frequency_on_enter(void* context) { + message("app_scene_set_frequency_on_enter"); + App* app = context; + + Knob* knob = app->knob; + knob_set_callback(knob, app_knob_callback, app); + knob_set_heading(knob, "FREQUENCY"); + knob_set_counter(knob, (uint32_t)app->frequency); + + view_dispatcher_switch_to_view(app->view_dispatcher, AppViewKnob); +} + +bool app_scene_set_frequency_on_event(void* context, SceneManagerEvent event) { + message("app_scene_set_frequency_on_event"); + App* app = context; + + bool consumed = false; + if(event.type == SceneManagerEventTypeBack) { + // Back button pressed + } else if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + if(event.event == AppKnobEventDone) { + FURI_LOG_I(TAG, "Custom Event!"); + uint32_t counter = knob_get_counter(app->knob); + FURI_LOG_I(TAG, "The counter is %ld.", counter); + app->frequency = counter; + scene_manager_next_scene(app->scene_manager, AppSceneSummary); + } + } + return consumed; +} + +void app_scene_set_frequency_on_exit(void* context) { + message("app_scene_set_frequency_on_exit"); + UNUSED(context); +} + +void app_scene_summary_button_callback(GuiButtonType result, InputType type, void* ctx) { + message("app_scene_summary_button_callback"); + App* app = (App*)ctx; + UNUSED(type); + + if(result == GuiButtonTypeCenter) { + view_dispatcher_send_custom_event(app->view_dispatcher, AppWidgetEventDone); + } +} + +void app_scene_summary_on_enter(void* context) { + message("app_scene_summary_on_enter"); + App* app = context; + + Widget* widget = app->widget; + widget_reset(widget); + widget_add_text_scroll_element( + widget, + 20, + 10, + 80, + 30, + "THIS WILL BE THE SUMMARY PAGE WHERE WE SHOW THE DATA." + " FOR NOW THIS IS JUST A PLACEHOLDER."); + widget_add_button_element( + widget, GuiButtonTypeCenter, "OK", app_scene_summary_button_callback, app); + + view_dispatcher_switch_to_view(app->view_dispatcher, AppViewWidget); +} + +bool app_scene_summary_on_event(void* context, SceneManagerEvent event) { + message("app_scene_summary_on_event"); + App* app = context; + + bool consumed = false; + if(event.type == SceneManagerEventTypeBack) { + // Back button pressed + } else if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + if(event.event == AppWidgetEventDone) { + FURI_LOG_I(TAG, "Custom Widget Event!"); + scene_manager_next_scene(app->scene_manager, AppSceneSetVolume); + } + } + return consumed; +} + +void app_scene_summary_on_exit(void* context) { + message("app_scene_summary_on_exit"); + App* app = (App*)context; + Widget* widget = app->widget; + widget_reset(widget); +} + +void (*const app_on_enter_handlers[])(void*) = { + app_scene_set_volume_on_enter, + app_scene_set_frequency_on_enter, + app_scene_summary_on_enter, +}; + +bool (*const app_on_event_handlers[])(void* context, SceneManagerEvent event) = { + app_scene_set_volume_on_event, + app_scene_set_frequency_on_event, + app_scene_summary_on_event, +}; + +void (*const app_on_exit_handlers[])(void* context) = { + app_scene_set_volume_on_exit, + app_scene_set_frequency_on_exit, + app_scene_summary_on_exit, +}; + +const SceneManagerHandlers app_scene_handlers = { + .on_enter_handlers = app_on_enter_handlers, + .on_event_handlers = app_on_event_handlers, + .on_exit_handlers = app_on_exit_handlers, + .scene_num = AppSceneNum, +}; + +App* scenes_app_alloc() { + message("scenes_app_alloc"); + App* app = malloc(sizeof(App)); + app->frequency = 440; + app->volume = 50; + app->scene_manager = scene_manager_alloc(&app_scene_handlers, app); + app->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_enable_queue(app->view_dispatcher); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + view_dispatcher_set_custom_event_callback(app->view_dispatcher, app_scene_custom_callback); + view_dispatcher_set_navigation_event_callback(app->view_dispatcher, app_back_event_callback); + app->knob = knob_alloc(); + view_dispatcher_add_view(app->view_dispatcher, AppViewKnob, knob_get_view(app->knob)); + app->widget = widget_alloc(); + view_dispatcher_add_view(app->view_dispatcher, AppViewWidget, widget_get_view(app->widget)); + + return app; +} + +void scenes_app_free(App* app) { + message("scenes_app_free"); + view_dispatcher_remove_view(app->view_dispatcher, AppViewKnob); + knob_free(app->knob); + view_dispatcher_remove_view(app->view_dispatcher, AppViewWidget); + widget_free(app->widget); + view_dispatcher_free(app->view_dispatcher); + scene_manager_free(app->scene_manager); + free(app); +} + +int32_t scenes_demo_app(void* p) { + UNUSED(p); + message("scenes_demo_app"); + + App* app = scenes_app_alloc(); + message("scenes_app_alloc"); + + Gui* gui = furi_record_open(RECORD_GUI); + + view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen); + message("view_dispatcher_attach_to_gui"); + + scene_manager_next_scene(app->scene_manager, AppSceneSetVolume); + message("scene_manager_next_scene"); + + view_dispatcher_run(app->view_dispatcher); + message("view_dispatcher_run"); + + // Free resources + message("Freeing resources..."); + scenes_app_free(app); + furi_record_close(RECORD_GUI); + + return 0; +} \ No newline at end of file