unleashed-firmware/applications/examples/example_event_loop/example_event_loop_multi.c
MX bf6c6c231f
[FL-3841] FuriEventLoop Pt.2 (#3703)
* Abstract primitive type from main logic in FuriEventLoop
* Remove message_queue_i.h
* Add stream buffer support for event loop
* Add semaphore support for event loop
* Add temporary unit test workaround
* Make the linter happy
* Add mutex support for event loop
* Implement event subscription and unsubscription while the event loop is running
* Implement edge events
* Fix leftover logical errors
* Add event loop timer example application
* Implement flag-based edge trigger and one-shot mode
* Add event loop mutex example application
* Only notify the event loop if stream buffer is at or above its trigger level
* Reformat comments
* Add event loop stream buffer example application
* Add event loop multiple elements example application
* Improve event loop flag names
* Remove redundant signal handler as it is already handled by the event loop
* Refactor Power service, improve ViewHolder
* Use ViewHolder instead of ViewDispatcher in About app
* Enable ViewDispatcher queue on construction, deprecate view_dispatcher_enable_queue()
* Remove all invocations of view_dispatcher_enable_queue()
* Remove app-scened-template
* Remove missing library from target.json
* Port Accessor app to ViewHolder
* Make the linter happy
* Add example_view_holder application, update ViewHolder docs
* Add example_view_dispatcher application, update ViewDispatcher docs
* Replace FuriSemaphore with FuriApiLock, remove workaround delay
* Fix logical error
* Fix another logical error
* Use the sources directive to speed up compilation
* Use constant define macro
* Improve FuriEventLoop documentation
* Improve FuriEventLoop documentation once more
* Bump API Version
* Gui: remove redundant checks from ViewDispatcher
* Gui: remove dead ifs from ViewDispatcher

Co-authored-by: Silent <CookiePLMonster@users.noreply.github.com>
Co-authored-by: hedger <hedger@users.noreply.github.com>
Co-authored-by: あく <alleteam@gmail.com>
2024-08-10 14:32:27 +03:00

343 lines
13 KiB
C

/**
* @file example_event_loop_multi.c
* @brief Example application that demonstrates multiple primitives used with two FuriEventLoop instances.
*
* This application simulates a complex use case of having two concurrent event loops (each one executing in
* its own thread) using a stream buffer for communication and additional timers and message passing to handle
* the keypad input. Additionally, it shows how to use thread signals to stop an event loop in another thread.
* The GUI functionality is there only for the purpose of exclusive access to the input events.
*
* The application's functionality consists of the following:
* - Print keypad key names and types when pressed,
* - If the Back key is long-pressed, a countdown starts upon completion of which the app exits,
* - The countdown can be cancelled by long-pressing the Ok button, it also resets the counter,
* - Blocks of random data are periodically generated in a separate thread,
* - When ready, the main application thread gets notified and prints the data.
*/
#include <furi.h>
#include <gui/gui.h>
#include <gui/view_port.h>
#include <furi_hal_random.h>
#define TAG "ExampleEventLoopMulti"
#define COUNTDOWN_START_VALUE (5UL)
#define COUNTDOWN_INTERVAL_MS (1000UL)
#define WORKER_DATA_INTERVAL_MS (1500UL)
#define INPUT_QUEUE_SIZE (8)
#define STREAM_BUFFER_SIZE (16)
typedef struct {
FuriEventLoop* event_loop;
FuriEventLoopTimer* timer;
FuriStreamBuffer* stream_buffer;
} EventLoopMultiAppWorker;
typedef struct {
Gui* gui;
ViewPort* view_port;
FuriThread* worker_thread;
FuriEventLoop* event_loop;
FuriMessageQueue* input_queue;
FuriEventLoopTimer* exit_timer;
FuriStreamBuffer* stream_buffer;
uint32_t exit_countdown_value;
} EventLoopMultiApp;
/*
* Worker functions
*/
// This function is executed each time the data is taken out of the stream buffer. It is used to restart the worker timer.
static bool
event_loop_multi_app_stream_buffer_worker_callback(FuriEventLoopObject* object, void* context) {
furi_assert(context);
EventLoopMultiAppWorker* worker = context;
furi_assert(object == worker->stream_buffer);
FURI_LOG_I(TAG, "Data was removed from buffer");
// Restart the timer to generate another block of random data.
furi_event_loop_timer_start(worker->timer, WORKER_DATA_INTERVAL_MS);
return true;
}
// This function is executed when the worker timer expires. The timer will NOT restart automatically
// since it is of one-shot type.
static void event_loop_multi_app_worker_timer_callback(void* context) {
furi_assert(context);
EventLoopMultiAppWorker* worker = context;
// Generate a block of random data.
uint8_t data[STREAM_BUFFER_SIZE];
furi_hal_random_fill_buf(data, sizeof(data));
// Put the generated data in the stream buffer.
// IMPORTANT: No waiting in the event handlers!
furi_check(
furi_stream_buffer_send(worker->stream_buffer, &data, sizeof(data), 0) == sizeof(data));
}
static EventLoopMultiAppWorker*
event_loop_multi_app_worker_alloc(FuriStreamBuffer* stream_buffer) {
EventLoopMultiAppWorker* worker = malloc(sizeof(EventLoopMultiAppWorker));
// Create the worker event loop.
worker->event_loop = furi_event_loop_alloc();
// Create the timer governing the data generation.
// It is of one-shot type, i.e. it will not restart automatically upon expiration.
worker->timer = furi_event_loop_timer_alloc(
worker->event_loop,
event_loop_multi_app_worker_timer_callback,
FuriEventLoopTimerTypeOnce,
worker);
// Using the same stream buffer as the main thread (it was already created beforehand).
worker->stream_buffer = stream_buffer;
// Notify the worker event loop about data being taken out of the stream buffer.
furi_event_loop_subscribe_stream_buffer(
worker->event_loop,
worker->stream_buffer,
FuriEventLoopEventOut | FuriEventLoopEventFlagEdge,
event_loop_multi_app_stream_buffer_worker_callback,
worker);
return worker;
}
static void event_loop_multi_app_worker_free(EventLoopMultiAppWorker* worker) {
// IMPORTANT: The user code MUST unsubscribe from all events before deleting the event loop.
// Failure to do so will result in a crash.
furi_event_loop_unsubscribe(worker->event_loop, worker->stream_buffer);
// IMPORTANT: All timers MUST be deleted before deleting the associated event loop.
// Failure to do so will result in a crash.
furi_event_loop_timer_free(worker->timer);
// Now it is okay to delete the event loop.
furi_event_loop_free(worker->event_loop);
free(worker);
}
static void event_loop_multi_app_worker_run(EventLoopMultiAppWorker* worker) {
furi_event_loop_timer_start(worker->timer, WORKER_DATA_INTERVAL_MS);
furi_event_loop_run(worker->event_loop);
}
// This function is the worker thread body and (obviously) is executed in the worker thread.
static int32_t event_loop_multi_app_worker_thread(void* context) {
furi_assert(context);
EventLoopMultiApp* app = context;
// Because an event loop is used, it MUST be created in the thread it will be run in.
// Therefore, the worker creation and deletion is handled in the worker thread.
EventLoopMultiAppWorker* worker = event_loop_multi_app_worker_alloc(app->stream_buffer);
event_loop_multi_app_worker_run(worker);
event_loop_multi_app_worker_free(worker);
return 0;
}
/*
* Main application functions
*/
// This function is executed in the GUI context each time an input event occurs (e.g. the user pressed a key)
static void event_loop_multi_app_input_callback(InputEvent* event, void* context) {
furi_assert(context);
EventLoopMultiApp* app = context;
// Pass the event to the the application's input queue
furi_check(furi_message_queue_put(app->input_queue, event, FuriWaitForever) == FuriStatusOk);
}
// This function is executed each time new data is available in the stream buffer.
static bool
event_loop_multi_app_stream_buffer_callback(FuriEventLoopObject* object, void* context) {
furi_assert(context);
EventLoopMultiApp* app = context;
furi_assert(object == app->stream_buffer);
// Get the data from the stream buffer
uint8_t data[STREAM_BUFFER_SIZE];
// IMPORTANT: No waiting in the event handlers!
furi_check(
furi_stream_buffer_receive(app->stream_buffer, &data, sizeof(data), 0) == sizeof(data));
// Format the data for printing and print it to the debug output.
FuriString* tmp_str = furi_string_alloc();
for(uint32_t i = 0; i < sizeof(data); ++i) {
furi_string_cat_printf(tmp_str, "%02X ", data[i]);
}
FURI_LOG_I(TAG, "Received data: %s", furi_string_get_cstr(tmp_str));
furi_string_free(tmp_str);
return true;
}
// This function is executed each time a new message is inserted in the input queue.
static bool event_loop_multi_app_input_queue_callback(FuriEventLoopObject* object, void* context) {
furi_assert(context);
EventLoopMultiApp* app = context;
furi_assert(object == app->input_queue);
InputEvent event;
// IMPORTANT: No waiting in the event handlers!
furi_check(furi_message_queue_get(app->input_queue, &event, 0) == FuriStatusOk);
if(event.type == InputTypeLong) {
// The user has long-pressed the Back key, try starting the countdown.
if(event.key == InputKeyBack) {
if(!furi_event_loop_timer_is_running(app->exit_timer)) {
// Actually start the countdown
FURI_LOG_I(TAG, "Starting exit countdown!");
furi_event_loop_timer_start(app->exit_timer, COUNTDOWN_INTERVAL_MS);
} else {
// The countdown is already in progress, print a warning message
FURI_LOG_W(TAG, "Countdown has already been started");
}
// The user has long-pressed the Ok key, try stopping the countdown.
} else if(event.key == InputKeyOk) {
if(furi_event_loop_timer_is_running(app->exit_timer)) {
// Actually cancel the countdown
FURI_LOG_I(TAG, "Exit countdown cancelled!");
app->exit_countdown_value = COUNTDOWN_START_VALUE;
furi_event_loop_timer_stop(app->exit_timer);
} else {
// The countdown is not running, print a warning message
FURI_LOG_W(TAG, "Countdown has not been started yet");
}
} else {
// Not a Back or Ok key, just print its name.
FURI_LOG_I(TAG, "Long press: %s", input_get_key_name(event.key));
}
} else if(event.type == InputTypeShort) {
// Not a long press, just print the key's name.
FURI_LOG_I(TAG, "Short press: %s", input_get_key_name(event.key));
}
return true;
}
// This function is executed each time the countdown timer expires.
static void event_loop_multi_app_exit_timer_callback(void* context) {
furi_assert(context);
EventLoopMultiApp* app = context;
FURI_LOG_I(TAG, "Exiting in %lu ...", app->exit_countdown_value);
// If the coundown value has reached 0, exit the application
if(app->exit_countdown_value == 0) {
FURI_LOG_I(TAG, "Exiting NOW!");
// Send a signal to the worker thread to exit.
// A signal handler that handles FuriSignalExit is already set by default.
furi_thread_signal(app->worker_thread, FuriSignalExit, NULL);
// Request the application event loop to stop.
furi_event_loop_stop(app->event_loop);
// Otherwise just decrement it and wait for the next time the timer expires.
} else {
app->exit_countdown_value -= 1;
}
}
static EventLoopMultiApp* event_loop_multi_app_alloc(void) {
EventLoopMultiApp* app = malloc(sizeof(EventLoopMultiApp));
// Create event loop instances.
app->event_loop = furi_event_loop_alloc();
// Create a worker thread instance. The worker event loop will execute inside it.
app->worker_thread = furi_thread_alloc_ex(
"EventLoopMultiWorker", 1024, event_loop_multi_app_worker_thread, app);
// Create a message queue to receive the input events.
app->input_queue = furi_message_queue_alloc(INPUT_QUEUE_SIZE, sizeof(InputEvent));
// Create a stream buffer to receive the generated data.
app->stream_buffer = furi_stream_buffer_alloc(STREAM_BUFFER_SIZE, STREAM_BUFFER_SIZE);
// Create a timer to run the countdown.
app->exit_timer = furi_event_loop_timer_alloc(
app->event_loop,
event_loop_multi_app_exit_timer_callback,
FuriEventLoopTimerTypePeriodic,
app);
app->gui = furi_record_open(RECORD_GUI);
app->view_port = view_port_alloc();
// Start the countdown from this value
app->exit_countdown_value = COUNTDOWN_START_VALUE;
// Gain exclusive access to the input events
view_port_input_callback_set(app->view_port, event_loop_multi_app_input_callback, app);
gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
// Notify the event loop about incoming messages in the queue
furi_event_loop_subscribe_message_queue(
app->event_loop,
app->input_queue,
FuriEventLoopEventIn,
event_loop_multi_app_input_queue_callback,
app);
// Notify the event loop about new data in the stream buffer
furi_event_loop_subscribe_stream_buffer(
app->event_loop,
app->stream_buffer,
FuriEventLoopEventIn | FuriEventLoopEventFlagEdge,
event_loop_multi_app_stream_buffer_callback,
app);
return app;
}
static void event_loop_multi_app_free(EventLoopMultiApp* app) {
gui_remove_view_port(app->gui, app->view_port);
furi_record_close(RECORD_GUI);
// IMPORTANT: The user code MUST unsubscribe from all events before deleting the event loop.
// Failure to do so will result in a crash.
furi_event_loop_unsubscribe(app->event_loop, app->input_queue);
furi_event_loop_unsubscribe(app->event_loop, app->stream_buffer);
// Delete all instances
view_port_free(app->view_port);
furi_message_queue_free(app->input_queue);
furi_stream_buffer_free(app->stream_buffer);
// IMPORTANT: All timers MUST be deleted before deleting the associated event loop.
// Failure to do so will result in a crash.
furi_event_loop_timer_free(app->exit_timer);
furi_thread_free(app->worker_thread);
furi_event_loop_free(app->event_loop);
free(app);
}
static void event_loop_multi_app_run(EventLoopMultiApp* app) {
FURI_LOG_I(TAG, "Press keys to see them printed here.");
FURI_LOG_I(TAG, "Long press \"Back\" to exit after %lu seconds.", COUNTDOWN_START_VALUE);
FURI_LOG_I(TAG, "Long press \"Ok\" to cancel the countdown.");
// Start the worker thread
furi_thread_start(app->worker_thread);
// Run the application event loop. This call will block until the application is about to exit.
furi_event_loop_run(app->event_loop);
// Wait for the worker thread to finish.
furi_thread_join(app->worker_thread);
}
/*******************************************************************
* vvv START HERE vvv
*
* The application's entry point - referenced in application.fam
*******************************************************************/
int32_t example_event_loop_multi_app(void* arg) {
UNUSED(arg);
EventLoopMultiApp* app = event_loop_multi_app_alloc();
event_loop_multi_app_run(app);
event_loop_multi_app_free(app);
return 0;
}