diff --git a/alloc/flip_social_alloc.c b/alloc/flip_social_alloc.c index b0c97bc3b3b..0af1f8ef8cf 100644 --- a/alloc/flip_social_alloc.c +++ b/alloc/flip_social_alloc.c @@ -176,11 +176,11 @@ FlipSocialApp *flip_social_app_alloc() } // Allocate Submenu(s) - if (!easy_flipper_set_submenu(&app->submenu_logged_out, FlipSocialViewLoggedOutSubmenu, "FlipSocial v0.7", flip_social_callback_exit_app, &app->view_dispatcher)) + if (!easy_flipper_set_submenu(&app->submenu_logged_out, FlipSocialViewLoggedOutSubmenu, "FlipSocial v0.8", flip_social_callback_exit_app, &app->view_dispatcher)) { return NULL; } - if (!easy_flipper_set_submenu(&app->submenu_logged_in, FlipSocialViewLoggedInSubmenu, "FlipSocial v0.7", flip_social_callback_exit_app, &app->view_dispatcher)) + if (!easy_flipper_set_submenu(&app->submenu_logged_in, FlipSocialViewLoggedInSubmenu, "FlipSocial v0.8", flip_social_callback_exit_app, &app->view_dispatcher)) { return NULL; } diff --git a/application.fam b/application.fam index 52f19b48cbe..d0a1258316e 100644 --- a/application.fam +++ b/application.fam @@ -9,6 +9,6 @@ App( fap_icon_assets="assets", fap_author="jblanked", fap_weburl="https://github.com/jblanked/FlipSocial", - fap_version="0.7", + fap_version="0.8", fap_description="Social media platform for the Flipper Zero.", ) diff --git a/assets/CHANGELOG.md b/assets/CHANGELOG.md index 01350f3d732..f3bc5bc84d0 100644 --- a/assets/CHANGELOG.md +++ b/assets/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.8 +- Add support for RPC_KEYBOARD + ## 0.7 - Improved memory allocation - Increased the max explore users from 50 to 100 diff --git a/text_input/rpc_keyboard.h b/text_input/rpc_keyboard.h new file mode 100644 index 00000000000..1db8fd570ca --- /dev/null +++ b/text_input/rpc_keyboard.h @@ -0,0 +1,162 @@ +#pragma once + +#include +#include +#include + +#define RECORD_RPC_KEYBOARD "rpckeyboard" + +#define RPC_KEYBOARD_KEY_RIGHT '\x13' +#define RPC_KEYBOARD_KEY_LEFT '\x14' +#define RPC_KEYBOARD_KEY_ENTER '\x0D' +#define RPC_KEYBOARD_KEY_BACKSPACE '\x08' + +typedef enum +{ + // Unknown error occurred + RpcKeyboardChatpadStatusError, + // The chatpad worker is stopped + RpcKeyboardChatpadStatusStopped, + // The chatpad worker is started, but not ready + RpcKeyboardChatpadStatusStarted, + // The chatpad worker is ready and got response from chatpad + RpcKeyboardChatpadStatusReady, +} RpcKeyboardChatpadStatus; + +typedef struct RpcKeyboard RpcKeyboard; + +typedef enum +{ + // Replacement text was provided by the user + RpcKeyboardEventTypeTextEntered, + // A single character was provided by the user + RpcKeyboardEventTypeCharEntered, + // A macro was entered by the user + RpcKeyboardEventTypeMacroEntered, +} RpcKeyboardEventType; + +typedef struct +{ + // The mutex to protect the data, call furi_mutex_acquire/furi_mutex_release. + FuriMutex *mutex; + // The text message, macro or character. + char message[256]; + // The length of the message. + uint16_t length; + // The newline enabled flag, allow newline to submit text. + bool newline_enabled; +} RpcKeyboardEventData; + +typedef struct +{ + RpcKeyboardEventType type; + RpcKeyboardEventData data; +} RpcKeyboardEvent; + +typedef FuriPubSub *(*RpcKeyboardGetPubsub)(RpcKeyboard *rpc_keyboard); +typedef void (*RpcKeyboardNewlineEnable)(RpcKeyboard *rpc_keyboard, bool enable); +typedef void (*RpcKeyboardPublishCharFn)(RpcKeyboard *keyboard, char character); +typedef void (*RpcKeyboardPublishMacroFn)(RpcKeyboard *rpc_keyboard, char macro); +typedef char *(*RpcKeyboardGetMacroFn)(RpcKeyboard *rpc_keyboard, char macro); +typedef void (*RpcKeyboardSetMacroFn)(RpcKeyboard *rpc_keyboard, char macro, char *value); +typedef void (*RpcKeyboardChatpadStartFn)(RpcKeyboard *rpc_keyboard); +typedef void (*RpcKeyboardChatpadStopFn)(RpcKeyboard *rpc_keyboard); +typedef RpcKeyboardChatpadStatus (*RpcKeyboardChatpadStatusFn)(RpcKeyboard *rpc_keyboard); + +typedef struct RpcKeyboardFunctions RpcKeyboardFunctions; +struct RpcKeyboardFunctions +{ + uint16_t major; + uint16_t minor; + RpcKeyboardGetPubsub fn_get_pubsub; + RpcKeyboardNewlineEnable fn_newline_enable; + RpcKeyboardPublishCharFn fn_publish_char; + RpcKeyboardPublishMacroFn fn_publish_macro; + RpcKeyboardGetMacroFn fn_get_macro; + RpcKeyboardSetMacroFn fn_set_macro; + RpcKeyboardChatpadStartFn fn_chatpad_start; + RpcKeyboardChatpadStopFn fn_chatpad_stop; + RpcKeyboardChatpadStatusFn fn_chatpad_status; +}; + +/** + * @brief STARTUP - Register the remote keyboard. + */ +void rpc_keyboard_register(void); + +/** + * @brief UNUSED - Unregister the remote keyboard. + */ +void rpc_keyboard_release(void); + +/** + * @brief Get the pubsub object for the remote keyboard. + * @details This function returns the pubsub object, use to subscribe to keyboard events. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @return FuriPubSub* pointer to the pubsub object. + */ +FuriPubSub *rpc_keyboard_get_pubsub(RpcKeyboard *rpc_keyboard); + +/** + * @brief Enable or disable newline character submitting the text. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @param[in] enable true to enable, false to disable. + */ +void rpc_keyboard_newline_enable(RpcKeyboard *rpc_keyboard, bool enable); + +/** + * @brief Publish the replacement text to the remote keyboard. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @param[in] bytes pointer to the text buffer. + * @param[in] buffer_size size of the text buffer. + */ +void rpc_keyboard_publish_text(RpcKeyboard *rpc_keyboard, uint8_t *bytes, uint32_t buffer_size); + +/** + * @brief Publish a single key pressed on the remote keyboard. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @param[in] character the character that was pressed. + */ +void rpc_keyboard_publish_char(RpcKeyboard *rpc_keyboard, char character); + +/** + * @brief Publish a macro key pressed on the remote keyboard. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @param[in] character the macro key that was pressed. + */ +void rpc_keyboard_publish_macro(RpcKeyboard *rpc_keyboard, char macro); + +/** + * @brief Get the macro text associated with a macro key. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @param[in] macro the macro key. + * @return char* pointer to the macro text. NULL if the macro key is not set. User must free the memory. + */ +char *rpc_keyboard_get_macro(RpcKeyboard *rpc_keyboard, char macro); + +/** + * @brief Set the macro text associated with a macro key. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @param[in] macro the macro key. + * @param[in] value the macro text. + */ +void rpc_keyboard_set_macro(RpcKeyboard *rpc_keyboard, char macro, char *value); + +/** + * @brief Initializes the chatpad and starts listening for keypresses. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + */ +void rpc_keyboard_chatpad_start(RpcKeyboard *rpc_keyboard); + +/** + * @brief Stops the chatpad & frees resources. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + */ +void rpc_keyboard_chatpad_stop(RpcKeyboard *rpc_keyboard); + +/** + * @brief Get the status of the chatpad. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @return RpcKeyboardChatpadStatus the status of the chatpad. + */ +RpcKeyboardChatpadStatus rpc_keyboard_chatpad_status(RpcKeyboard *rpc_keyboard); diff --git a/text_input/rpc_keyboard_stub.c b/text_input/rpc_keyboard_stub.c new file mode 100644 index 00000000000..eb7afb52bce --- /dev/null +++ b/text_input/rpc_keyboard_stub.c @@ -0,0 +1,150 @@ +#include "rpc_keyboard.h" + +#include + +static bool rpc_keyboard_functions_check_version(RpcKeyboardFunctions *stub) +{ + furi_check(stub); + if (stub->major == 1 && stub->minor > 2) + { + return true; + } + FURI_LOG_D("RpcKeyboard", "Unsupported version %d.%d", stub->major, stub->minor); + return false; +} + +/** + * @brief Get the pubsub object for the remote keyboard. + * @details This function returns the pubsub object, use to subscribe to keyboard events. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @return FuriPubSub* pointer to the pubsub object. + */ +FuriPubSub *rpc_keyboard_get_pubsub(RpcKeyboard *rpc_keyboard) +{ + RpcKeyboardFunctions *stub = (RpcKeyboardFunctions *)rpc_keyboard; + if (!rpc_keyboard_functions_check_version(stub)) + { + return NULL; + } + return stub->fn_get_pubsub((RpcKeyboard *)rpc_keyboard); +} + +/** + * @brief Enable or disable newline character submitting the text. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @param[in] enable true to enable, false to disable. + */ +void rpc_keyboard_newline_enable(RpcKeyboard *rpc_keyboard, bool enable) +{ + RpcKeyboardFunctions *stub = (RpcKeyboardFunctions *)rpc_keyboard; + if (!rpc_keyboard_functions_check_version(stub)) + { + return; + } + stub->fn_newline_enable((RpcKeyboard *)rpc_keyboard, enable); +} + +/** + * @brief Publish a single key pressed on the remote keyboard. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @param[in] character the character that was pressed. + */ +void rpc_keyboard_publish_char(RpcKeyboard *rpc_keyboard, char character) +{ + RpcKeyboardFunctions *stub = (RpcKeyboardFunctions *)rpc_keyboard; + if (!rpc_keyboard_functions_check_version(stub)) + { + return; + } + stub->fn_publish_char((RpcKeyboard *)rpc_keyboard, character); +} + +/** + * @brief Publish a macro key pressed on the remote keyboard. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @param[in] character the macro key that was pressed. + */ +void rpc_keyboard_publish_macro(RpcKeyboard *rpc_keyboard, char macro) +{ + RpcKeyboardFunctions *stub = (RpcKeyboardFunctions *)rpc_keyboard; + if (!rpc_keyboard_functions_check_version(stub)) + { + return; + } + stub->fn_publish_macro((RpcKeyboard *)rpc_keyboard, macro); +} + +/** + * @brief Get the macro text associated with a macro key. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @param[in] macro the macro key. + * @return char* pointer to the macro text. NULL if the macro key is not set. User must free the memory. + */ +char *rpc_keyboard_get_macro(RpcKeyboard *rpc_keyboard, char macro) +{ + RpcKeyboardFunctions *stub = (RpcKeyboardFunctions *)rpc_keyboard; + if (!rpc_keyboard_functions_check_version(stub)) + { + return NULL; + } + return stub->fn_get_macro((RpcKeyboard *)rpc_keyboard, macro); +} + +/** + * @brief Set the macro text associated with a macro key. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @param[in] macro the macro key. + * @param[in] value the macro text. + */ +void rpc_keyboard_set_macro(RpcKeyboard *rpc_keyboard, char macro, char *value) +{ + RpcKeyboardFunctions *stub = (RpcKeyboardFunctions *)rpc_keyboard; + if (!rpc_keyboard_functions_check_version(stub)) + { + return; + } + stub->fn_set_macro((RpcKeyboard *)rpc_keyboard, macro, value); +} + +/** + * @brief Initializes the chatpad and starts listening for keypresses. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + */ +void rpc_keyboard_chatpad_start(RpcKeyboard *rpc_keyboard) +{ + RpcKeyboardFunctions *stub = (RpcKeyboardFunctions *)rpc_keyboard; + if (!rpc_keyboard_functions_check_version(stub)) + { + return; + } + stub->fn_chatpad_start((RpcKeyboard *)rpc_keyboard); +} + +/** + * @brief Stops the chatpad & frees resources. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + */ +void rpc_keyboard_chatpad_stop(RpcKeyboard *rpc_keyboard) +{ + RpcKeyboardFunctions *stub = (RpcKeyboardFunctions *)rpc_keyboard; + if (!rpc_keyboard_functions_check_version(stub)) + { + return; + } + stub->fn_chatpad_stop((RpcKeyboard *)rpc_keyboard); +} + +/** + * @brief Get the status of the chatpad. + * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. + * @return RpcKeyboardChatpadStatus the status of the chatpad. + */ +RpcKeyboardChatpadStatus rpc_keyboard_chatpad_status(RpcKeyboard *rpc_keyboard) +{ + RpcKeyboardFunctions *stub = (RpcKeyboardFunctions *)rpc_keyboard; + if (!rpc_keyboard_functions_check_version(stub)) + { + return RpcKeyboardChatpadStatusError; + } + return stub->fn_chatpad_status((RpcKeyboard *)rpc_keyboard); +} diff --git a/text_input/uart_text_input.c b/text_input/uart_text_input.c index 572167cc43a..b8e8fc9f283 100644 --- a/text_input/uart_text_input.c +++ b/text_input/uart_text_input.c @@ -4,6 +4,7 @@ #include #include "flip_social_icons.h" #include +#include "rpc_keyboard.h" struct UART_TextInput { @@ -25,6 +26,9 @@ typedef struct size_t text_buffer_size; bool clear_default_text; + FuriPubSubSubscription *keyboard_subscription; + bool invoke_callback; + UART_TextInputCallback callback; void *callback_context; @@ -281,6 +285,21 @@ static void uart_text_input_view_draw_callback(Canvas *canvas, void *_model) uint8_t needed_string_width = canvas_width(canvas) - 8; uint8_t start_pos = 4; + if (model->invoke_callback) + { + model->invoke_callback = false; + if (model->validator_callback && (!model->validator_callback(model->text_buffer, model->validator_text, model->validator_callback_context))) + { + model->valadator_message_visible = true; + } + else if (model->callback != 0) + { + // We hijack the current thread to invoke the callback (we aren't doing a draw). + model->callback(model->callback_context); + return; + } + } + const char *text = model->text_buffer; canvas_clear(canvas); @@ -634,12 +653,199 @@ void uart_text_input_timer_callback(void *context) UART_TextInput *uart_text_input = context; with_view_model( - uart_text_input->view, + uart_text_input->view, + UART_TextInputModel * model, + { model->valadator_message_visible = false; }, + true); +} + +static void text_input_keyboard_callback_line(UART_TextInput *text_input, const RpcKeyboardEvent *event) +{ + with_view_model( + text_input->view, UART_TextInputModel * model, - { model->valadator_message_visible = false; }, + { + if (model->text_buffer != NULL && model->text_buffer_size > 0) + { + if (event->data.length > 0) + { + furi_mutex_acquire(event->data.mutex, FuriWaitForever); + size_t len = event->data.length; + if (len >= model->text_buffer_size) + { + len = model->text_buffer_size - 1; + } + + bool newline = false; + bool substitutions = false; + size_t copy_index = 0; + for (size_t i = 0; i < len; i++) + { + char ch = event->data.message[i]; + if ((ch >= 0x20 && ch <= 0x7E) || ch == '\n' || ch == '\r') + { + model->text_buffer[copy_index++] = ch; + if (ch == '\n' || ch == '\r') + { + newline = event->data.newline_enabled && !substitutions; // TODO: No min-length check? + break; + } + } + } + model->text_buffer[copy_index] = '\0'; + furi_mutex_release(event->data.mutex); + FURI_LOG_D("text_input", "copy: %d, %d, %s", len, copy_index, model->text_buffer); + + // Set focus on Save + model->selected_row = 3; + model->selected_column = 8; + + // Hijack the next draw to invoke the callback if newline is true. + model->invoke_callback = newline; + } + } + }, true); } +static void text_input_keyboard_type_key(UART_TextInput *text_input, char selected) +{ + with_view_model( + text_input->view, + UART_TextInputModel * model, + { + size_t text_length = strlen(model->text_buffer); + char search_key = isupper(selected) ? tolower(selected) : selected == ' ' ? '_' + : selected; + bool found = false; + for (int row = 0; row < keyboard_row_count; row++) + { + const UART_TextInputKey *keys = get_row(row); + for (int column = 0; column < get_row_size(row); column++) + { + if (keys[column].text == search_key) + { + model->selected_row = row; + model->selected_column = column; + found = true; + } + } + } + if (!found) + { + // Set focus on Backspace + model->selected_row = 2; + model->selected_column = 9; + } + + if (selected == ENTER_KEY) + { + if (model->validator_callback && (!model->validator_callback(model->text_buffer, model->validator_text, model->validator_callback_context))) + { + model->valadator_message_visible = true; + furi_timer_start(text_input->timer, furi_kernel_get_tick_frequency() * 4); + } + else if (model->callback != 0) + { // TODO: no min-length check + model->callback(model->callback_context); + } + } + else if (selected == BACKSPACE_KEY) + { + uart_text_input_backspace_cb(model); + } + else + { + if (model->clear_default_text) + { + text_length = 0; + } + if (selected == RPC_KEYBOARD_KEY_LEFT || selected == RPC_KEYBOARD_KEY_RIGHT) + { + // ignore these keys for now + } + else if (text_length < (model->text_buffer_size - 1)) + { + model->text_buffer[text_length] = selected; + model->text_buffer[text_length + 1] = 0; + } + } + model->clear_default_text = false; + }, + true); +} + +static void text_input_keyboard_callback(const void *message, void *context) +{ + UART_TextInput *text_input = context; + const RpcKeyboardEvent *event = message; + + if (event == NULL) + { + return; + } + + switch (event->type) + { + case RpcKeyboardEventTypeTextEntered: + text_input_keyboard_callback_line(text_input, event); + break; + case RpcKeyboardEventTypeCharEntered: + char ch = event->data.message[0]; + FURI_LOG_I("text_input", "char: %c", ch); + text_input_keyboard_type_key(text_input, ch); + break; + case RpcKeyboardEventTypeMacroEntered: + furi_mutex_acquire(event->data.mutex, FuriWaitForever); + FURI_LOG_I("text_input", "macro: %s", event->data.message); + for (size_t i = 0; i < event->data.length; i++) + { + text_input_keyboard_type_key(text_input, event->data.message[i]); + } + furi_mutex_release(event->data.mutex); + break; + } +} + +static void text_input_view_enter_callback(void *context) +{ + furi_assert(context); + UART_TextInput *text_input = context; + if (furi_record_exists(RECORD_RPC_KEYBOARD)) + { + RpcKeyboard *rpc_keyboard = furi_record_open(RECORD_RPC_KEYBOARD); + FuriPubSub *rpc_keyboard_pubsub = rpc_keyboard_get_pubsub(rpc_keyboard); + if (rpc_keyboard_pubsub != NULL) + { + with_view_model(text_input->view, UART_TextInputModel * model, { model->keyboard_subscription = furi_pubsub_subscribe(rpc_keyboard_pubsub, text_input_keyboard_callback, text_input); }, false); + } + furi_record_close(RECORD_RPC_KEYBOARD); + } +} + +static void text_input_view_exit_callback(void *context) +{ + furi_assert(context); + UART_TextInput *text_input = context; + if (furi_record_exists(RECORD_RPC_KEYBOARD)) + { + RpcKeyboard *rpc_keyboard = furi_record_open(RECORD_RPC_KEYBOARD); + FuriPubSub *rpc_keyboard_pubsub = rpc_keyboard_get_pubsub(rpc_keyboard); + if (rpc_keyboard_pubsub != NULL) + { + with_view_model( + text_input->view, + UART_TextInputModel * model, + { + furi_pubsub_unsubscribe(rpc_keyboard_pubsub, model->keyboard_subscription); + model->keyboard_subscription = NULL; + }, + false); + } + furi_record_close(RECORD_RPC_KEYBOARD); + } +} + UART_TextInput *uart_text_input_alloc() { UART_TextInput *uart_text_input = malloc(sizeof(UART_TextInput)); @@ -648,6 +854,8 @@ UART_TextInput *uart_text_input_alloc() view_allocate_model(uart_text_input->view, ViewModelTypeLocking, sizeof(UART_TextInputModel)); view_set_draw_callback(uart_text_input->view, uart_text_input_view_draw_callback); view_set_input_callback(uart_text_input->view, uart_text_input_view_input_callback); + view_set_enter_callback(uart_text_input->view, text_input_view_enter_callback); + view_set_exit_callback(uart_text_input->view, text_input_view_exit_callback); uart_text_input->timer = furi_timer_alloc(uart_text_input_timer_callback, FuriTimerTypeOnce, uart_text_input); @@ -732,7 +940,7 @@ void uart_text_input_set_result_callback( if (text_buffer && text_buffer[0] != '\0') { // Set focus on Save - model->selected_row = 2; + model->selected_row = 3; model->selected_column = 8; } },