diff --git a/README.md b/README.md index 1f8cf71cace..c93c6126824 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ - Infrared working - SubGhz working - Pause working -- IR Timing features in development +- IR Timing features working ## What this is? This app combines commands used in IR and SubGhz into playlists that can be run with one click diff --git a/application.fam b/application.fam index a6d106f7d9d..470e700c152 100644 --- a/application.fam +++ b/application.fam @@ -6,7 +6,7 @@ App( stack_size=3 * 1024, fap_icon="icons/xremote_10px.png", fap_icon_assets="icons", - fap_version="2.0", + fap_version="2.1", fap_category="Infrared", fap_author="Leedave", fap_description="One-Click, sends multiple commands", diff --git a/docs/changelog.md b/docs/changelog.md index 11129724e11..46261df4f2e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,6 @@ +## v2.1 +- Added ability to individually set IR Signal time + ## v2.0 - SubGhz Functionality added - Slight change in Storage format to enalbe individual IR timings later (feature request) diff --git a/helpers/gui/int_input.c b/helpers/gui/int_input.c new file mode 100644 index 00000000000..b93ce3126af --- /dev/null +++ b/helpers/gui/int_input.c @@ -0,0 +1,390 @@ +#include "int_input.h" + +#include +#include +#include + +/** IntInput type */ +struct IntInput { + View* view; +}; + +typedef struct { + const char text; + const uint8_t x; + const uint8_t y; +} IntInputKey; + +typedef struct { + const char* header; + char* text_buffer; + size_t text_buffer_size; + bool clear_default_text; + + IntInputCallback callback; + void* callback_context; + + int8_t selected_row; + uint8_t selected_column; +} IntInputModel; + +static const uint8_t keyboard_origin_x = 7; +static const uint8_t keyboard_origin_y = 31; +static const uint8_t keyboard_row_count = 2; +static const uint8_t enter_symbol = '\r'; +static const uint8_t backspace_symbol = '\b'; + +static const IntInputKey keyboard_keys_row_1[] = { + {'0', 0, 12}, + {'1', 11, 12}, + {'2', 22, 12}, + {'3', 33, 12}, + {'4', 44, 12}, + {backspace_symbol, 103, 4}, +}; + +static const IntInputKey keyboard_keys_row_2[] = { + {'5', 0, 26}, + {'6', 11, 26}, + {'7', 22, 26}, + {'8', 33, 26}, + {'9', 44, 26}, + {enter_symbol, 95, 17}, +}; + +/** Get row size + * + * @param row_index Index of row + * + * @return uint8_t Row size + */ +static uint8_t int_input_get_row_size(uint8_t row_index) { + uint8_t row_size = 0; + + switch(row_index + 1) { + case 1: + row_size = COUNT_OF(keyboard_keys_row_1); + break; + case 2: + row_size = COUNT_OF(keyboard_keys_row_2); + break; + default: + furi_crash(); + } + + return row_size; +} + +/** Get row pointer + * + * @param row_index Index of row + * + * @return const IntInputKey* Row pointer + */ +static const IntInputKey* int_input_get_row(uint8_t row_index) { + const IntInputKey* row = NULL; + + switch(row_index + 1) { + case 1: + row = keyboard_keys_row_1; + break; + case 2: + row = keyboard_keys_row_2; + break; + default: + furi_crash(); + } + + return row; +} + +/** Draw input box (common view) + * + * @param canvas The canvas + * @param model The model + */ +static void int_input_draw_input(Canvas* canvas, IntInputModel* model) { + const uint8_t text_x = 8; + const uint8_t text_y = 25; + + elements_slightly_rounded_frame(canvas, 6, 14, 116, 15); + + const char* text = model->text_buffer; + canvas_draw_str(canvas, text_x, text_y, text); +} + +static void int_input_backspace_cb(IntInputModel* model) { + uint8_t text_length = model->clear_default_text ? 1 : strlen(model->text_buffer); + if(text_length > 0) { + model->text_buffer[text_length - 1] = 0; + } +} + +/** Handle up button + * + * @param model The model + */ +static void int_input_handle_up(IntInputModel* model) { + if(model->selected_row > 0) { + model->selected_row--; + } +} + +/** Handle down button + * + * @param model The model + */ +static void int_input_handle_down(IntInputModel* model) { + if(model->selected_row < keyboard_row_count - 1) { + model->selected_row += 1; + } +} + +/** Handle left button + * + * @param model The model + */ +static void int_input_handle_left(IntInputModel* model) { + if(model->selected_column > 0) { + model->selected_column--; + } else { + model->selected_column = int_input_get_row_size(model->selected_row) - 1; + } +} + +/** Handle right button + * + * @param model The model + */ +static void int_input_handle_right(IntInputModel* model) { + if(model->selected_column < int_input_get_row_size(model->selected_row) - 1) { + model->selected_column++; + } else { + model->selected_column = 0; + } +} + +/** Handle OK button + * + * @param model The model + */ +static void int_input_handle_ok(IntInputModel* model) { + char selected = int_input_get_row(model->selected_row)[model->selected_column].text; + size_t text_length = strlen(model->text_buffer); + if(selected == enter_symbol) { + model->callback(model->callback_context); + } else if(selected == backspace_symbol) { + int_input_backspace_cb(model); + } else { + if(model->clear_default_text) { + text_length = 0; + } + 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; +} + +/** Draw callback + * + * @param canvas The canvas + * @param _model The model + */ +static void int_input_view_draw_callback(Canvas* canvas, void* _model) { + IntInputModel* model = _model; + uint8_t text_length = model->text_buffer ? strlen(model->text_buffer) : 0; + UNUSED(text_length); + + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + + int_input_draw_input(canvas, model); + + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 2, 9, model->header); + canvas_set_font(canvas, FontKeyboard); + // Draw keyboard + for(uint8_t row = 0; row < keyboard_row_count; row++) { + const uint8_t column_count = int_input_get_row_size(row); + const IntInputKey* keys = int_input_get_row(row); + + for(size_t column = 0; column < column_count; column++) { + if(keys[column].text == enter_symbol) { + canvas_set_color(canvas, ColorBlack); + if(model->selected_row == row && model->selected_column == column) { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeySaveSelected_24x11); + } else { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeySave_24x11); + } + } else if(keys[column].text == backspace_symbol) { + canvas_set_color(canvas, ColorBlack); + if(model->selected_row == row && model->selected_column == column) { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeyBackspaceSelected_16x9); + } else { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeyBackspace_16x9); + } + } else { + if(model->selected_row == row && model->selected_column == column) { + canvas_set_color(canvas, ColorBlack); + canvas_draw_box( + canvas, + keyboard_origin_x + keys[column].x - 3, + keyboard_origin_y + keys[column].y - 10, + 11, + 13); + canvas_set_color(canvas, ColorWhite); + } else if(model->selected_row == -1 && row == 0 && model->selected_column == column) { + canvas_set_color(canvas, ColorBlack); + canvas_draw_frame( + canvas, + keyboard_origin_x + keys[column].x - 3, + keyboard_origin_y + keys[column].y - 10, + 11, + 13); + } else { + canvas_set_color(canvas, ColorBlack); + } + + canvas_draw_glyph( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + keys[column].text); + } + } + } +} + +/** Input callback + * + * @param event The event + * @param context The context + * + * @return true + * @return false + */ +static bool int_input_view_input_callback(InputEvent* event, void* context) { + IntInput* int_input = context; + furi_assert(int_input); + + bool consumed = false; + + // Fetch the model + IntInputModel* model = view_get_model(int_input->view); + + if(event->type == InputTypeShort || event->type == InputTypeLong || + event->type == InputTypeRepeat) { + consumed = true; + switch(event->key) { + case InputKeyLeft: + int_input_handle_left(model); + break; + case InputKeyRight: + int_input_handle_right(model); + break; + case InputKeyUp: + int_input_handle_up(model); + break; + case InputKeyDown: + int_input_handle_down(model); + break; + case InputKeyOk: + int_input_handle_ok(model); + break; + default: + consumed = false; + break; + } + } + + // commit view + view_commit_model(int_input->view, consumed); + + return consumed; +} + +void int_input_reset(IntInput* int_input) { + FURI_LOG_D("INT_INPUT", "Resetting Model"); + furi_assert(int_input); + with_view_model( + int_input->view, + IntInputModel * model, + { + model->header = ""; + model->selected_row = 0; + model->selected_column = 0; + model->clear_default_text = false; + model->text_buffer = ""; + model->text_buffer_size = 0; + model->callback = NULL; + model->callback_context = NULL; + }, + true); +} + +IntInput* int_input_alloc() { + IntInput* int_input = malloc(sizeof(IntInput)); + int_input->view = view_alloc(); + view_set_context(int_input->view, int_input); + view_allocate_model(int_input->view, ViewModelTypeLocking, sizeof(IntInputModel)); + view_set_draw_callback(int_input->view, int_input_view_draw_callback); + view_set_input_callback(int_input->view, int_input_view_input_callback); + + int_input_reset(int_input); + + return int_input; +} + +void int_input_free(IntInput* int_input) { + furi_assert(int_input); + view_free(int_input->view); + free(int_input); +} + +View* int_input_get_view(IntInput* int_input) { + furi_assert(int_input); + return int_input->view; +} + +void int_input_set_result_callback( + IntInput* int_input, + IntInputCallback callback, + void* callback_context, + char* text_buffer, + size_t text_buffer_size, + bool clear_default_text) { + with_view_model( + int_input->view, + IntInputModel * model, + { + model->callback = callback; + model->callback_context = callback_context; + model->text_buffer = text_buffer; + model->text_buffer_size = text_buffer_size; + model->clear_default_text = clear_default_text; + }, + true); +} + +void int_input_set_header_text(IntInput* int_input, const char* text) { + with_view_model( + int_input->view, IntInputModel * model, { model->header = text; }, true); +} diff --git a/helpers/gui/int_input.h b/helpers/gui/int_input.h new file mode 100644 index 00000000000..9393c336028 --- /dev/null +++ b/helpers/gui/int_input.h @@ -0,0 +1,71 @@ +/** + * @file int_input.h + * GUI: Integer string keyboard view module API + */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Int input anonymous structure */ +typedef struct IntInput IntInput; + +/** callback that is executed on save button press */ +typedef void (*IntInputCallback)(void* context); + +/** callback that is executed when byte buffer is changed */ +typedef void (*IntChangedCallback)(void* context); + +/** Allocate and initialize Int input. This Int input is used to enter Ints. + * + * @return IntInput instance pointer + */ +IntInput* int_input_alloc(); + +/** Deinitialize and free byte input + * + * @param int_input Int input instance + */ +void int_input_free(IntInput* int_input); + +/** Get byte input view + * + * @param int_input byte input instance + * + * @return View instance that can be used for embedding + */ +View* int_input_get_view(IntInput* int_input); + +/** Set byte input result callback + * + * @param int_input byte input instance + * @param input_callback input callback fn + * @param changed_callback changed callback fn + * @param callback_context callback context + * @param text_buffer buffer to use + * @param text_buffer_size buffer length + * @param clear_default_text clear previous entry + */ + +void int_input_set_result_callback( + IntInput* int_input, + IntInputCallback input_callback, + void* callback_context, + char* text_buffer, + size_t text_buffer_size, + bool clear_default_text); + +/** Set byte input header text + * + * @param int_input byte input instance + * @param text text to be shown + */ +void int_input_set_header_text(IntInput* int_input, const char* text); + +#ifdef __cplusplus +} +#endif diff --git a/icons/KeyBackspaceSelected_16x9.png b/icons/KeyBackspaceSelected_16x9.png new file mode 100644 index 00000000000..7cc0759a8ca Binary files /dev/null and b/icons/KeyBackspaceSelected_16x9.png differ diff --git a/icons/KeyBackspace_16x9.png b/icons/KeyBackspace_16x9.png new file mode 100644 index 00000000000..9946232d953 Binary files /dev/null and b/icons/KeyBackspace_16x9.png differ diff --git a/icons/KeySaveSelected_24x11.png b/icons/KeySaveSelected_24x11.png new file mode 100644 index 00000000000..eeb3569d3ac Binary files /dev/null and b/icons/KeySaveSelected_24x11.png differ diff --git a/icons/KeySave_24x11.png b/icons/KeySave_24x11.png new file mode 100644 index 00000000000..e7dba987a04 Binary files /dev/null and b/icons/KeySave_24x11.png differ diff --git a/models/cross/xremote_cross_remote.c b/models/cross/xremote_cross_remote.c index 71e0f93f2ce..77cba4d24ae 100644 --- a/models/cross/xremote_cross_remote.c +++ b/models/cross/xremote_cross_remote.c @@ -144,6 +144,11 @@ void xremote_cross_remote_rename_item(CrossRemote* remote, size_t index, const c xremote_cross_remote_item_set_name(item, name); } +int16_t xremote_cross_remote_get_item_type(CrossRemote* remote, size_t index) { + CrossRemoteItem* item = xremote_cross_remote_get_item(remote, index); + return xremote_cross_remote_item_get_type(item); +} + static void xremote_cross_remote_set_name(CrossRemote* remote, const char* name) { furi_string_set(remote->name, name); } diff --git a/models/cross/xremote_cross_remote.h b/models/cross/xremote_cross_remote.h index 37542635ed0..89756276b50 100644 --- a/models/cross/xremote_cross_remote.h +++ b/models/cross/xremote_cross_remote.h @@ -23,6 +23,7 @@ void xremote_cross_remote_remove_item(CrossRemote* remote, size_t index); void xremote_cross_remote_rename_item(CrossRemote* remote, size_t index, const char* name); size_t xremote_cross_remote_get_item_count(CrossRemote* remote); CrossRemoteItem* xremote_cross_remote_get_item(CrossRemote* remote, size_t index); +int16_t xremote_cross_remote_get_item_type(CrossRemote* remote, size_t index); bool xremote_cross_remote_save_new(CrossRemote* remote, const char* name); bool xremote_cross_remote_delete(CrossRemote* remote); \ No newline at end of file diff --git a/models/cross/xremote_cross_remote_item.c b/models/cross/xremote_cross_remote_item.c index 1327f121719..bad77cab6a3 100644 --- a/models/cross/xremote_cross_remote_item.c +++ b/models/cross/xremote_cross_remote_item.c @@ -224,6 +224,10 @@ void xremote_cross_remote_item_set_sg_signal(CrossRemoteItem* item, SubGhzRemote item->sg_signal = subghz; } +int16_t xremote_cross_remote_item_get_type(CrossRemoteItem* item) { + return item->type; +} + const char* xremote_cross_remote_item_get_name(CrossRemoteItem* item) { return furi_string_get_cstr(item->name); } diff --git a/models/cross/xremote_cross_remote_item.h b/models/cross/xremote_cross_remote_item.h index 4a41f198c9e..da4c29b80b0 100644 --- a/models/cross/xremote_cross_remote_item.h +++ b/models/cross/xremote_cross_remote_item.h @@ -15,6 +15,7 @@ void xremote_cross_remote_item_set_filename(CrossRemoteItem* item, const char* f const char* xremote_cross_remote_item_get_filename(CrossRemoteItem* item); void xremote_cross_remote_item_set_type(CrossRemoteItem* item, int type); +int16_t xremote_cross_remote_item_get_type(CrossRemoteItem* item); void xremote_cross_remote_item_set_time(CrossRemoteItem* item, uint32_t time); uint32_t xremote_cross_remote_item_get_time(CrossRemoteItem* item); diff --git a/scenes/xremote_scene_config.h b/scenes/xremote_scene_config.h index 047b72d1ced..1377b164d7e 100644 --- a/scenes/xremote_scene_config.h +++ b/scenes/xremote_scene_config.h @@ -14,4 +14,5 @@ ADD_SCENE(xremote, ir_remote, IrRemote) ADD_SCENE(xremote, save_remote, SaveRemote) ADD_SCENE(xremote, save_remote_item, SaveRemoteItem) ADD_SCENE(xremote, transmit, Transmit) -ADD_SCENE(xremote, pause_set, PauseSet) \ No newline at end of file +ADD_SCENE(xremote, pause_set, PauseSet) +ADD_SCENE(xremote, ir_timer, IrTimer) \ No newline at end of file diff --git a/scenes/xremote_scene_edit_item.c b/scenes/xremote_scene_edit_item.c index 12b5cf80c65..4d1c62d5ea6 100644 --- a/scenes/xremote_scene_edit_item.c +++ b/scenes/xremote_scene_edit_item.c @@ -3,6 +3,7 @@ enum SubmenuIndexEdit { SubmenuIndexRename = 10, + SubmenuIndexTiming, SubmenuIndexDelete, }; @@ -15,6 +16,12 @@ void xremote_scene_edit_item_on_enter(void* context) { XRemote* app = context; submenu_add_item( app->editmenu, "Rename", SubmenuIndexRename, xremote_scene_edit_item_submenu_callback, app); + + if(xremote_cross_remote_get_item_type(app->cross_remote, app->edit_item) == XRemoteRemoteItemTypeInfrared) { + submenu_add_item( + app->editmenu, "Set Timing", SubmenuIndexTiming, xremote_scene_edit_item_submenu_callback, app); + } + submenu_add_item( app->editmenu, "Delete", SubmenuIndexDelete, xremote_scene_edit_item_submenu_callback, app); @@ -37,6 +44,9 @@ bool xremote_scene_edit_item_on_event(void* context, SceneManagerEvent event) { scene_manager_next_scene(app->scene_manager, XRemoteSceneSaveRemoteItem); //scene_manager_next_scene(app->scene_manager, XRemoteSceneWip); return 0; + } else if(event.event == SubmenuIndexTiming) { + scene_manager_next_scene(app->scene_manager, XRemoteSceneIrTimer); + return 0; } scene_manager_next_scene(app->scene_manager, XRemoteSceneCreate); } diff --git a/scenes/xremote_scene_ir_timer.c b/scenes/xremote_scene_ir_timer.c new file mode 100644 index 00000000000..eb962bbd4ce --- /dev/null +++ b/scenes/xremote_scene_ir_timer.c @@ -0,0 +1,58 @@ +#include "../xremote.h" +#include "../models/cross/xremote_cross_remote.h" + +void xremote_scene_ir_timer_callback(void* context) { + XRemote* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, XRemoteCustomEventTextInput); +} + +void xremote_scene_ir_timer_on_enter(void* context) { + furi_assert(context); + XRemote* app = context; + IntInput* int_input = app->int_input; + size_t enter_name_length = 5; + char* str = "Transmit in ms (0 - 9999)"; + const char* constStr = str; + CrossRemoteItem* item = xremote_cross_remote_get_item(app->cross_remote, app->edit_item); + int_input_set_header_text(int_input, constStr); + snprintf(app->text_store[1], 5, "%lu", item->time); + + int_input_set_result_callback( + int_input, + xremote_scene_ir_timer_callback, + context, + app->text_store[1], + enter_name_length, + false); + + view_dispatcher_switch_to_view(app->view_dispatcher, XRemoteViewIdIntInput); +} + +bool xremote_scene_ir_timer_on_event(void* context, SceneManagerEvent event) { + XRemote* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeBack) { + scene_manager_previous_scene(app->scene_manager); + return true; + } else if(event.type == SceneManagerEventTypeCustom) { + CrossRemoteItem* item = xremote_cross_remote_get_item(app->cross_remote, app->edit_item); + xremote_cross_remote_item_set_time(item, atoi(app->text_store[1])); + if (item->time > 9999) { + item->time = 9999; + } + //app->first_station = atoi(app->text_store[0]); + /*if(app->first_station > app->max_station) { + app->first_station = app->max_station; + snprintf(app->text_store[0], 5, "%lu", app->first_station); + }*/ + scene_manager_previous_scene(app->scene_manager); + return true; + } + return consumed; +} + +void xremote_scene_ir_timer_on_exit(void* context) { + XRemote* app = context; + UNUSED(app); +} \ No newline at end of file diff --git a/xremote.c b/xremote.c index 34685aa01e2..5a016cb4d15 100644 --- a/xremote.c +++ b/xremote.c @@ -68,6 +68,12 @@ XRemote* xremote_app_alloc() { app->subghz = subghz_alloc(); app->text_input = text_input_alloc(); + + // Custom made int keyboard + app->int_input = int_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, XRemoteViewIdIntInput, int_input_get_view(app->int_input)); + view_dispatcher_add_view( app->view_dispatcher, XRemoteViewIdTextInput, text_input_get_view(app->text_input)); @@ -141,6 +147,7 @@ void xremote_app_free(XRemote* app) { view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdTransmit); view_dispatcher_remove_view(app->view_dispatcher, XRemoteViewIdPauseSet); text_input_free(app->text_input); + int_input_free(app->int_input); button_menu_free(app->button_menu_create); button_menu_free(app->button_menu_create_add); button_menu_free(app->button_menu_ir); diff --git a/xremote.h b/xremote.h index e3484881c85..4776e4d8cca 100644 --- a/xremote.h +++ b/xremote.h @@ -9,6 +9,7 @@ #include "models/cross/xremote_cross_remote.h" #include "helpers/subghz/subghz_types.h" #include "helpers/subghz/subghz.h" +#include "helpers/gui/int_input.h" #include "xremote_i.h" typedef struct SubGhz SubGhz; @@ -49,6 +50,7 @@ typedef struct { bool stop_transmit; char text_store[XREMOTE_TEXT_STORE_NUM][XREMOTE_TEXT_STORE_SIZE + 1]; SubGhz* subghz; + IntInput* int_input; } XRemote; typedef enum { @@ -62,6 +64,7 @@ typedef enum { XRemoteViewIdIrRemote, XRemoteViewIdStack, XRemoteViewIdTextInput, + XRemoteViewIdIntInput, XRemoteViewIdTransmit, XRemoteViewIdPauseSet, } XRemoteViewId; diff --git a/xremote_i.h b/xremote_i.h index 7176b234f6c..576d8ce02d1 100644 --- a/xremote_i.h +++ b/xremote_i.h @@ -49,7 +49,7 @@ #define XREMOTE_APP_EXTENSION ".xr" #define XREMOTE_FILE_TYPE "Cross Remote File" #define XREMOTE_FILE_VERSION 1 -#define XREMOTE_TEXT_STORE_NUM 2 +#define XREMOTE_TEXT_STORE_NUM 3 #define XREMOTE_TEXT_STORE_SIZE 128 #define XREMOTE_MAX_ITEM_NAME_LENGTH 22 #define XREMOTE_MAX_REMOTE_NAME_LENGTH 22