diff --git a/plugins/knob_component/README.md b/plugins/knob_component/README.md new file mode 100644 index 0000000..92a5fd9 --- /dev/null +++ b/plugins/knob_component/README.md @@ -0,0 +1,33 @@ +# BASIC DEMO +## Introduction +This project is to learn how to build a custom component (an up-down knob) for a scene for the Flipper Zero. + +TODO: WRITE TUTORIAL ON CREATING THE KNOB COMPONENT. + +## 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 "knob_component" [folder](..) to the \applications\plugins\knob_component folder in your firmware. +- Build & deploy the firmware. See this [tutorial](/firmware/updating/README.md) for updating firmware. +- NOTE: You can also extract the knob_component.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 "Knob 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 "VOLUME". +- Press BACK button for last page. +- Keep pressing BACK button until exit. + + +## How it works +TODO: WRITE TUTORIAL ON CREATING THE KNOB COMPONENT. \ No newline at end of file diff --git a/plugins/knob_component/application.fam b/plugins/knob_component/application.fam new file mode 100644 index 0000000..36daf31 --- /dev/null +++ b/plugins/knob_component/application.fam @@ -0,0 +1,10 @@ +App( + appid="Knob_Demo", + name="Knob Demo", + apptype=FlipperAppType.EXTERNAL, + entry_point="knob_demo_app", + requires=["gui"], + stack_size=2 * 1024, + fap_icon="knob_demo.png", + fap_category="Misc", +) diff --git a/plugins/knob_component/knob_demo.png b/plugins/knob_component/knob_demo.png new file mode 100644 index 0000000..522b649 Binary files /dev/null and b/plugins/knob_component/knob_demo.png differ diff --git a/plugins/knob_component/knob_demo_app.c b/plugins/knob_component/knob_demo_app.c new file mode 100644 index 0000000..7a1eeff --- /dev/null +++ b/plugins/knob_component/knob_demo_app.c @@ -0,0 +1,405 @@ +/* +@CodeAllNight +https://github.com/jamisonderek/flipper-zero-tutorials + +This project is to learn how to build a custom component (an up-down knob) +for a scene for the Flipper Zero. + +TODO: WRITE TUTORIAL ON CREATING THE KNOB COMPONENT. +*/ + +#include +#include + +#include +#include + +#include +#include + +#include +#include + +#define TAG "knob_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; + float volume; + float frequency; +} App; + +typedef enum { + AppSceneSetVolume, + AppSceneSetFrequency, + AppSceneNum, +} appScene; + +typedef enum { + AppViewSetCounter = 200, +} appViews; + +typedef enum { + AppKnobEventDone, +} 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_start_custom_callback(void* context, uint32_t custom_event) { + message("app_scene_start_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, AppViewSetCounter); +} + +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, AppViewSetCounter); +} + +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, AppSceneSetVolume); + } + } + return consumed; +} + +void app_scene_set_frequency_on_exit(void* context) { + message("app_scene_set_frequency_on_exit"); + UNUSED(context); +} + +void (*const app_on_enter_handlers[])(void*) = { + app_scene_set_volume_on_enter, + app_scene_set_frequency_on_enter, +}; + +bool (*const app_on_event_handlers[])(void* context, SceneManagerEvent event) = { + app_scene_set_volume_on_event, + app_scene_set_frequency_on_event, +}; + +void (*const app_on_exit_handlers[])(void* context) = { + app_scene_set_volume_on_exit, + app_scene_set_frequency_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* knob_app_alloc() { + message("knob_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_start_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, AppViewSetCounter, knob_get_view(app->knob)); + + return app; +} + +void knob_app_free(App* app) { + message("knob_app_free"); + view_dispatcher_remove_view(app->view_dispatcher, AppViewSetCounter); + knob_free(app->knob); + view_dispatcher_free(app->view_dispatcher); + scene_manager_free(app->scene_manager); + free(app); +} + +int32_t knob_demo_app(void* p) { + UNUSED(p); + message("knob_demo_app"); + + App* app = knob_app_alloc(); + message("knob_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..."); + knob_app_free(app); + furi_record_close(RECORD_GUI); + + return 0; +} \ No newline at end of file