From e4a1dd83c34e0e3b3fb0e3fd5f494cfe602e93a0 Mon Sep 17 00:00:00 2001 From: rdefeo Date: Tue, 5 Mar 2024 21:13:01 -0500 Subject: [PATCH] first commit --- README.md | 29 +++++ actions/action.c | 18 +++ actions/action.h | 5 + actions/action_i.h | 14 +++ actions/action_ir.c | 8 ++ actions/action_rfid.c | 115 +++++++++++++++++++ actions/action_subghz.c | 242 ++++++++++++++++++++++++++++++++++++++++ app_state.c | 50 +++++++++ app_state.h | 22 ++++ application.fam | 17 +++ flipper.h | 16 +++ images/.gitkeep | 0 item.c | 134 ++++++++++++++++++++++ item.h | 42 +++++++ quac.c | 24 ++++ quac.png | Bin 0 -> 153 bytes scenes/.gitignore | 6 + scenes/scene_items.c | 114 +++++++++++++++++++ scenes/scene_items.h | 9 ++ scenes/scenes.c | 26 +++++ scenes/scenes.h | 22 ++++ 21 files changed, 913 insertions(+) create mode 100644 README.md create mode 100644 actions/action.c create mode 100644 actions/action.h create mode 100644 actions/action_i.h create mode 100644 actions/action_ir.c create mode 100644 actions/action_rfid.c create mode 100644 actions/action_subghz.c create mode 100644 app_state.c create mode 100644 app_state.h create mode 100644 application.fam create mode 100644 flipper.h create mode 100644 images/.gitkeep create mode 100644 item.c create mode 100644 item.h create mode 100644 quac.c create mode 100644 quac.png create mode 100644 scenes/.gitignore create mode 100644 scenes/scene_items.c create mode 100644 scenes/scene_items.h create mode 100644 scenes/scenes.c create mode 100644 scenes/scenes.h diff --git a/README.md b/README.md new file mode 100644 index 00000000000..5d0c561bef6 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Simple On/Off Remote +## Sub-GHz + +Main Display +* Saved device 1 +* Saved device 2 +* Manage Devices + +Saved device View +* ON +* OFF +* other + + +## File System Layout +Inside the data folder, create sub-folders per device. Inside each of the device folders, store the raw files that contain the sub-ghz data, etc. + +The device order list, and button list, is based on the sorted file order. This is enforced by the following naming convention: + +``` +/data_folder + - 00_Device_1 + - 01_Device_2 + - 00_Button_1.sub + - 01_Button_2.sub +``` + +The first two digits and underscore will be stripped before display. Additionally, underscores in folder and filenames will be replaced with spaces. + diff --git a/actions/action.c b/actions/action.c new file mode 100644 index 00000000000..c770f8be42a --- /dev/null +++ b/actions/action.c @@ -0,0 +1,18 @@ + +#include "app_state.h" +#include "item.h" +#include "action_i.h" + +void action_tx(void* context, Item* item) { + FURI_LOG_I(TAG, "action_run: %s : %s", furi_string_get_cstr(item->name), item->ext); + + if(!strcmp(item->ext, ".sub")) { + action_subghz_tx(context, item); + } else if(!strcmp(item->ext, ".ir")) { + action_ir_tx(context, item); + } else if(!strcmp(item->ext, ".rfid")) { + action_rfid_tx(context, item); + } else { + FURI_LOG_E(TAG, "Unknown item type! %s", item->ext); + } +} diff --git a/actions/action.h b/actions/action.h new file mode 100644 index 00000000000..8f9c39d4776 --- /dev/null +++ b/actions/action.h @@ -0,0 +1,5 @@ +#pragma once + +struct Item; + +void action_tx(void* context, Item* item); diff --git a/actions/action_i.h b/actions/action_i.h new file mode 100644 index 00000000000..380593dcc7b --- /dev/null +++ b/actions/action_i.h @@ -0,0 +1,14 @@ +#pragma once + +#include "../flipper.h" +#include +#include + +#include + +#include "../app_state.h" +#include "../item.h" + +void action_subghz_tx(void* context, Item* item); +void action_rfid_tx(void* context, Item* item); +void action_ir_tx(void* context, Item* item); \ No newline at end of file diff --git a/actions/action_ir.c b/actions/action_ir.c new file mode 100644 index 00000000000..b471521ef0c --- /dev/null +++ b/actions/action_ir.c @@ -0,0 +1,8 @@ +// Methods for IR transmission + +#include "action_i.h" + +void action_ir_tx(void* context, Item* item) { + UNUSED(context); + UNUSED(item); +} \ No newline at end of file diff --git a/actions/action_rfid.c b/actions/action_rfid.c new file mode 100644 index 00000000000..7111d79ee2f --- /dev/null +++ b/actions/action_rfid.c @@ -0,0 +1,115 @@ +// Methods for RFID transmission + +// lfrid +#include +#include +#include +#include +#include + +#include "action_i.h" + +// lifted from flipperzero-firmware/applications/main/lfrfid/lfrfid_cli.c +void action_rfid_tx(void* context, Item* item) { + App* app = context; + FuriString* file_name = item->path; + + FlipperFormat* fff_data_file = flipper_format_file_alloc(app->storage); + FuriString* temp_str; + temp_str = furi_string_alloc(); + uint32_t temp_data32; + + FuriString* protocol_name; + FuriString* data_text; + protocol_name = furi_string_alloc(); + data_text = furi_string_alloc(); + + ProtocolDict* dict = protocol_dict_alloc(lfrfid_protocols, LFRFIDProtocolMax); + ProtocolId protocol; + size_t data_size = protocol_dict_get_max_data_size(dict); + uint8_t* data = malloc(data_size); + + FURI_LOG_I(TAG, "Max dict data size is %d", data_size); + bool successful_read = false; + do { + if(!flipper_format_file_open_existing(fff_data_file, furi_string_get_cstr(file_name))) { + FURI_LOG_E(TAG, "Error opening %s", furi_string_get_cstr(file_name)); + break; + } + FURI_LOG_I(TAG, "Opened file"); + if(!flipper_format_read_header(fff_data_file, temp_str, &temp_data32)) { + FURI_LOG_E(TAG, "Missing or incorrect header"); + break; + } + FURI_LOG_I(TAG, "Read file headers"); + // TODO: add better header checks here... + if(!strcmp(furi_string_get_cstr(temp_str), "Flipper RFID key")) { + } else { + FURI_LOG_E(TAG, "Type or version mismatch"); + break; + } + + // read and check the protocol field + if(!flipper_format_read_string(fff_data_file, "Key type", protocol_name)) { + FURI_LOG_E(TAG, "Error reading protocol"); + break; + } + protocol = protocol_dict_get_protocol_by_name(dict, furi_string_get_cstr(protocol_name)); + if(protocol == PROTOCOL_NO) { + FURI_LOG_E(TAG, "Unknown protocol: %s", furi_string_get_cstr(protocol_name)); + break; + } + FURI_LOG_I(TAG, "Protocol OK"); + + // read and check data field + size_t required_size = protocol_dict_get_data_size(dict, protocol); + FURI_LOG_I(TAG, "Protocol req data size is %d", required_size); + if(!flipper_format_read_hex(fff_data_file, "Data", data, required_size)) { + FURI_LOG_E(TAG, "Error reading data"); + break; + } + // FURI_LOG_I(TAG, "Data: %s", furi_string_get_cstr(data_text)); + + // if(data_size != required_size) { + // FURI_LOG_E( + // TAG, + // "%s data needs to be %zu bytes long", + // protocol_dict_get_name(dict, protocol), + // required_size); + // break; + // } + + protocol_dict_set_data(dict, protocol, data, data_size); + successful_read = true; + FURI_LOG_I(TAG, "protocol dict setup complete!"); + } while(false); + + if(successful_read) { + LFRFIDWorker* worker = lfrfid_worker_alloc(dict); + + lfrfid_worker_start_thread(worker); + lfrfid_worker_emulate_start(worker, protocol); + + printf("Emulating RFID...\r\nPress Ctrl+C to abort\r\n"); + int16_t time_ms = 3000; + int16_t interval_ms = 200; + while(time_ms > 0) { + furi_delay_ms(interval_ms); + time_ms -= interval_ms; + } + printf("Emulation stopped\r\n"); + + lfrfid_worker_stop(worker); + lfrfid_worker_stop_thread(worker); + lfrfid_worker_free(worker); + } + + furi_string_free(temp_str); + furi_string_free(protocol_name); + furi_string_free(data_text); + free(data); + + protocol_dict_free(dict); + + flipper_format_free(fff_data_file); +} diff --git a/actions/action_subghz.c b/actions/action_subghz.c new file mode 100644 index 00000000000..6c258ece971 --- /dev/null +++ b/actions/action_subghz.c @@ -0,0 +1,242 @@ +// Methods for Sub-GHz transmission + +// subghz +#include +#include +#include +#include +#include + +#include "action_i.h" + +static FuriHalSubGhzPreset action_subghz_get_preset_name(const char* preset_name) { + FuriHalSubGhzPreset preset = FuriHalSubGhzPresetIDLE; + if(!strcmp(preset_name, "FuriHalSubGhzPresetOok270Async")) { + preset = FuriHalSubGhzPresetOok270Async; + } else if(!strcmp(preset_name, "FuriHalSubGhzPresetOok650Async")) { + preset = FuriHalSubGhzPresetOok650Async; + } else if(!strcmp(preset_name, "FuriHalSubGhzPreset2FSKDev238Async")) { + preset = FuriHalSubGhzPreset2FSKDev238Async; + } else if(!strcmp(preset_name, "FuriHalSubGhzPreset2FSKDev476Async")) { + preset = FuriHalSubGhzPreset2FSKDev476Async; + } else if(!strcmp(preset_name, "FuriHalSubGhzPresetCustom")) { + preset = FuriHalSubGhzPresetCustom; + } else { + FURI_LOG_E(TAG, "Unknown preset!"); + } + return preset; +} + +// Lifted from flipperzero-firmware/applications/main/subghz/subghz_cli.c +void action_subghz_tx(void* context, Item* item) { + App* app = context; + FuriString* file_name = item->path; + uint32_t repeat = 1; // 10? + // uint32_t device_ind = 0; // 0 - CC1101_INT, 1 - CC1101_EXT + + FlipperFormat* fff_data_file = flipper_format_file_alloc(app->storage); + FlipperFormat* fff_data_raw = flipper_format_string_alloc(); + FuriString* temp_str; + temp_str = furi_string_alloc(); + uint32_t temp_data32; + bool check_file = false; + const SubGhzDevice* device = NULL; + + uint32_t frequency = 0; + SubGhzTransmitter* transmitter = NULL; + + FURI_LOG_I(TAG, "action_run_tx starting..."); + + subghz_devices_init(); + SubGhzEnvironment* environment = subghz_environment_alloc(); + if(!subghz_environment_load_keystore(environment, SUBGHZ_KEYSTORE_DIR_NAME)) { + FURI_LOG_E(TAG, "Load_keystore keeloq_mfcodes ERROR"); + } + if(!subghz_environment_load_keystore(environment, SUBGHZ_KEYSTORE_DIR_USER_NAME)) { + FURI_LOG_E(TAG, "Load_keystore keeloq_mfcodes_user ERROR"); + } + subghz_environment_set_came_atomo_rainbow_table_file_name( + environment, SUBGHZ_CAME_ATOMO_DIR_NAME); + subghz_environment_set_alutech_at_4n_rainbow_table_file_name( + environment, SUBGHZ_ALUTECH_AT_4N_DIR_NAME); + subghz_environment_set_nice_flor_s_rainbow_table_file_name( + environment, SUBGHZ_NICE_FLOR_S_DIR_NAME); + subghz_environment_set_protocol_registry(environment, (void*)&subghz_protocol_registry); + + do { + // SUBGHZ_DEVICE_CC1101_INT_NAME = "cc1101_int" + device = subghz_devices_get_by_name("cc1101_int"); + if(!subghz_devices_is_connect(device)) { + // power off + if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg(); + device = subghz_devices_get_by_name("cc1101_int"); + // device_ind = 0; + } + + if(!flipper_format_file_open_existing(fff_data_file, furi_string_get_cstr(file_name))) { + FURI_LOG_E(TAG, "Error opening %s", furi_string_get_cstr(file_name)); + break; + } + + if(!flipper_format_read_header(fff_data_file, temp_str, &temp_data32)) { + FURI_LOG_E(TAG, "Missing or incorrect header"); + break; + } + + if(((!strcmp(furi_string_get_cstr(temp_str), SUBGHZ_KEY_FILE_TYPE)) || + (!strcmp(furi_string_get_cstr(temp_str), SUBGHZ_RAW_FILE_TYPE))) && + temp_data32 == SUBGHZ_KEY_FILE_VERSION) { + } else { + FURI_LOG_E(TAG, "Type or version mismatch"); + break; + } + + if(!flipper_format_read_uint32(fff_data_file, "Frequency", &frequency, 1)) { + FURI_LOG_E(TAG, "Missing Frequency"); + break; + } + + if(!subghz_devices_is_frequency_valid(device, frequency)) { + FURI_LOG_E(TAG, "Frequency not supported"); + break; + } + + if(!flipper_format_read_string(fff_data_file, "Preset", temp_str)) { + FURI_LOG_E(TAG, "Missing Preset"); + break; + } + + subghz_devices_begin(device); + subghz_devices_reset(device); + + if(!strcmp(furi_string_get_cstr(temp_str), "FuriHalSubGhzPresetCustom")) { + uint8_t* custom_preset_data; + uint32_t custom_preset_data_size; + if(!flipper_format_get_value_count(fff_data_file, "Custom_preset_data", &temp_data32)) + break; + if(!temp_data32 || (temp_data32 % 2)) { + FURI_LOG_E(TAG, "Custom_preset_data size error"); + break; + } + custom_preset_data_size = sizeof(uint8_t) * temp_data32; + custom_preset_data = malloc(custom_preset_data_size); + if(!flipper_format_read_hex( + fff_data_file, + "Custom_preset_data", + custom_preset_data, + custom_preset_data_size)) { + FURI_LOG_E(TAG, "Custom_preset_data read error"); + break; + } + subghz_devices_load_preset( + device, + action_subghz_get_preset_name(furi_string_get_cstr(temp_str)), + custom_preset_data); + free(custom_preset_data); + } else { + subghz_devices_load_preset( + device, action_subghz_get_preset_name(furi_string_get_cstr(temp_str)), NULL); + } + + subghz_devices_set_frequency(device, frequency); + + // Load Protocol + if(!flipper_format_read_string(fff_data_file, "Protocol", temp_str)) { + FURI_LOG_E(TAG, "Missing protocol"); + break; + } + + SubGhzProtocolStatus status; + bool is_init_protocol = true; + if(!strcmp(furi_string_get_cstr(temp_str), "RAW")) { + FURI_LOG_I(TAG, "Protocol = RAW"); + subghz_protocol_raw_gen_fff_data( + fff_data_raw, furi_string_get_cstr(file_name), subghz_devices_get_name(device)); + transmitter = + subghz_transmitter_alloc_init(environment, furi_string_get_cstr(temp_str)); + if(transmitter == NULL) { + FURI_LOG_E(TAG, "Error transmitter"); + is_init_protocol = false; + } + + if(is_init_protocol) { + status = subghz_transmitter_deserialize(transmitter, fff_data_raw); + if(status != SubGhzProtocolStatusOk) { + FURI_LOG_E(TAG, "Error deserialize protocol"); + is_init_protocol = false; + } + } + } else { // if not RAW protocol + FURI_LOG_I(TAG, "Protocol != RAW"); + flipper_format_insert_or_update_uint32(fff_data_file, "Repeat", &repeat, 1); + transmitter = + subghz_transmitter_alloc_init(environment, furi_string_get_cstr(temp_str)); + if(transmitter == NULL) { + FURI_LOG_E(TAG, "Error transmitter"); + is_init_protocol = false; + } + if(is_init_protocol) { + status = subghz_transmitter_deserialize(transmitter, fff_data_file); + if(status != SubGhzProtocolStatusOk) { + FURI_LOG_E(TAG, "Error deserialize protocol"); + is_init_protocol = false; + } + } + flipper_format_delete_key(fff_data_file, "Repeat"); + } + + if(is_init_protocol) { + check_file = true; + } else { + subghz_devices_sleep(device); + subghz_devices_end(device); + subghz_transmitter_free(transmitter); + } + } while(false); + + flipper_format_free(fff_data_file); + + if(check_file) { + furi_hal_power_suppress_charge_enter(); + FURI_LOG_I( + TAG, + "Listening at %s. Frequency=%lu, Protocol=%s", + furi_string_get_cstr(file_name), + frequency, + furi_string_get_cstr(temp_str)); + do { + // delay in downloading files and other preparatory processes + furi_delay_ms(200); + if(subghz_devices_start_async_tx(device, subghz_transmitter_yield, transmitter)) { + while(!(subghz_devices_is_async_complete_tx( + device))) { // || cli_cmd_interrupt_received + furi_delay_ms(333); + } + subghz_devices_stop_async_tx(device); + } else { + FURI_LOG_W(TAG, "Transmission on this frequency is restricted in your region"); + } + + if(!strcmp(furi_string_get_cstr(temp_str), "RAW")) { + subghz_transmitter_stop(transmitter); + repeat--; + if(repeat) subghz_transmitter_deserialize(transmitter, fff_data_raw); + } + + } while(repeat && !strcmp(furi_string_get_cstr(temp_str), "RAW")); + + subghz_devices_sleep(device); + subghz_devices_end(device); + // power off + if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg(); + + furi_hal_power_suppress_charge_exit(); + + subghz_transmitter_free(transmitter); + } + + flipper_format_free(fff_data_raw); + furi_string_free(temp_str); + subghz_devices_deinit(); + subghz_environment_free(environment); +} diff --git a/app_state.c b/app_state.c new file mode 100644 index 00000000000..d5065e8e6f1 --- /dev/null +++ b/app_state.c @@ -0,0 +1,50 @@ +#include "flipper.h" +#include "app_state.h" +#include "scenes/scenes.h" +#include "item.h" + +App* app_alloc() { + App* app = malloc(sizeof(App)); + 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); + + // Create our UI elements + app->btn_menu = button_menu_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, SR_ButtonMenu, button_menu_get_view(app->btn_menu)); + + // Storage + app->storage = furi_record_open(RECORD_STORAGE); + + // Notifications - for LED light access + app->notifications = furi_record_open(RECORD_NOTIFICATION); + + // initialize device items list + app->depth = 0; + app->selected_item = -1; + + app->items_view = item_get_items_view_from_path(app, NULL); + + return app; +} + +void app_free(App* app) { + furi_assert(app); + + item_items_view_free(app->items_view); + + view_dispatcher_remove_view(app->view_dispatcher, SR_ButtonMenu); + + button_menu_free(app->btn_menu); + scene_manager_free(app->scene_manager); + view_dispatcher_free(app->view_dispatcher); + + furi_record_close(RECORD_STORAGE); + furi_record_close(RECORD_NOTIFICATION); + + free(app); +} \ No newline at end of file diff --git a/app_state.h b/app_state.h new file mode 100644 index 00000000000..62b9425bb16 --- /dev/null +++ b/app_state.h @@ -0,0 +1,22 @@ +#pragma once + +#include "flipper.h" +#include "item.h" + +#define QUAC_NAME "Quac!" + +typedef struct App { + SceneManager* scene_manager; + ViewDispatcher* view_dispatcher; + ButtonMenu* btn_menu; + + Storage* storage; + NotificationApp* notifications; + + int depth; + ItemsView* items_view; + int selected_item; +} App; + +App* app_alloc(); +void app_free(App* app); \ No newline at end of file diff --git a/application.fam b/application.fam new file mode 100644 index 00000000000..6ae198b290c --- /dev/null +++ b/application.fam @@ -0,0 +1,17 @@ +# For details & more options, see documentation/AppManifests.md in firmware repo + +App( + appid="quac", # Must be unique + name="Quac!", # Displayed in menus + apptype=FlipperAppType.EXTERNAL, + entry_point="quac_app", + stack_size=2 * 1024, + fap_category="Tools", + # Optional values + fap_version="0.1", + fap_icon="quac.png", # 10x10 1-bit PNG + fap_description="Quick Action remote control app", + fap_author="Roberto De Feo", + # fap_weburl="https://github.com/rdefeo/flipperzero/quac", + fap_icon_assets="images", # Image assets to compile for this application +) diff --git a/flipper.h b/flipper.h new file mode 100644 index 00000000000..6aff972d712 --- /dev/null +++ b/flipper.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define TAG "Quac" // log statement id diff --git a/images/.gitkeep b/images/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/item.c b/item.c new file mode 100644 index 00000000000..16aaff0d3b3 --- /dev/null +++ b/item.c @@ -0,0 +1,134 @@ + +#include +#include +#include +#include + +#include "app_state.h" +#include "item.h" +#include + +// Location of our actions and folders +#define QUAC_PATH "apps_data/quac" +// Full path to actions +#define QUAC_DATA_PATH EXT_PATH(QUAC_PATH) + +ARRAY_DEF(FileArray, FuriString*, FURI_STRING_OPLIST); + +ItemsView* item_get_items_view_from_path(void* context, FuriString* input_path) { + App* app = context; + + if(input_path == NULL) { + input_path = furi_string_alloc_set_str(QUAC_DATA_PATH); + } + const char* cpath = furi_string_get_cstr(input_path); + + FURI_LOG_I(TAG, "Getting items from: %s", cpath); + ItemsView* iview = malloc(sizeof(ItemsView)); + iview->path = furi_string_alloc_set(input_path); + + iview->name = furi_string_alloc(); + if(app->depth == 0) { + furi_string_set_str(iview->name, QUAC_NAME); + } else { + path_extract_basename(cpath, iview->name); + item_prettify_name(iview->name); + } + + DirWalk* dir_walk = dir_walk_alloc(app->storage); + dir_walk_set_recursive(dir_walk, false); + + FuriString* path = furi_string_alloc(); + FileArray_t flist; + FileArray_init(flist); + + // FURI_LOG_I(TAG, "About to walk the dir"); + if(dir_walk_open(dir_walk, cpath)) { + while(dir_walk_read(dir_walk, path, NULL) == DirWalkOK) { + // FURI_LOG_I(TAG, "> dir_walk: %s", furi_string_get_cstr(path)); + const char* cpath = furi_string_get_cstr(path); + + // Insert the new file path in sorted order to flist + uint32_t i = 0; + FileArray_it_t it; + for(FileArray_it(it, flist); !FileArray_end_p(it); FileArray_next(it), ++i) { + if(strcmp(cpath, furi_string_get_cstr(*FileArray_ref(it))) > 0) { + continue; + } + // FURI_LOG_I(TAG, ">> Inserting at %lu", i); + FileArray_push_at(flist, i, path); + break; + } + if(i == FileArray_size(flist)) { + // FURI_LOG_I(TAG, "Couldn't insert, so adding at the end!"); + FileArray_push_back(flist, path); + } + } + } + furi_string_free(path); + + // DEBUG: Now print our array in original order + FileArray_it_t iter; + for(FileArray_it(iter, flist); !FileArray_end_p(iter); FileArray_next(iter)) { + const char* f = furi_string_get_cstr(*FileArray_cref(iter)); + FURI_LOG_I(TAG, "Found: %s", f); + } + + FURI_LOG_I(TAG, "Creating our ItemsArray"); + ItemArray_init(iview->items); + for(FileArray_it(iter, flist); !FileArray_end_p(iter); FileArray_next(iter)) { + path = *FileArray_ref(iter); + const char* found_path = furi_string_get_cstr(path); + + Item* item = ItemArray_push_new(iview->items); + + // Action files have extensions, so item->ext starts with '.' - ehhhh + item->ext[0] = 0; + path_extract_extension(path, item->ext, MAX_EXT_LEN); + item->type = (item->ext[0] == '.') ? Item_Action : Item_Group; + + item->name = furi_string_alloc(); + path_extract_filename_no_ext(found_path, item->name); + FURI_LOG_I(TAG, "Basename: %s", furi_string_get_cstr(item->name)); + item_prettify_name(item->name); + + item->path = furi_string_alloc(); + furi_string_set(item->path, path); + FURI_LOG_I(TAG, "Path: %s", furi_string_get_cstr(item->path)); + } + + FileArray_clear(flist); + dir_walk_free(dir_walk); + + return iview; +} + +void item_items_view_free(ItemsView* items_view) { + FURI_LOG_I(TAG, "item_items_view_free - begin"); + furi_string_free(items_view->name); + furi_string_free(items_view->path); + ItemArray_it_t iter; + for(ItemArray_it(iter, items_view->items); !ItemArray_end_p(iter); ItemArray_next(iter)) { + furi_string_free(ItemArray_ref(iter)->name); + furi_string_free(ItemArray_ref(iter)->path); + } + ItemArray_clear(items_view->items); + free(items_view); + FURI_LOG_I(TAG, "item_items_view_free - end"); +} + +void item_prettify_name(FuriString* name) { + // FURI_LOG_I(TAG, "Converting %s to...", furi_string_get_cstr(name)); + if(furi_string_size(name) > 3) { + char c = furi_string_get_char(name, 2); + if(c == '_') { + char a = furi_string_get_char(name, 0); + char b = furi_string_get_char(name, 1); + if(a >= '0' && a <= '9' && b >= '0' && b <= '9') { + furi_string_right(name, 3); + } + } + } + furi_string_replace_str(name, "_", " ", 0); + // FURI_LOG_I(TAG, "... %s", furi_string_get_cstr(name)); +} \ No newline at end of file diff --git a/item.h b/item.h new file mode 100644 index 00000000000..1f7077c0fe2 --- /dev/null +++ b/item.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +#define MAX_EXT_LEN 6 + +typedef enum { Item_Action, Item_Group } ItemType; + +typedef struct Item { + ItemType type; + FuriString* name; + FuriString* path; + char ext[MAX_EXT_LEN + 1]; +} Item; + +ARRAY_DEF(ItemArray, Item, M_POD_OPLIST); + +typedef struct ItemsView { + FuriString* name; + FuriString* path; + ItemArray_t items; +} ItemsView; + +/** Allocates and returns an ItemsView* which contains the list of + * items to display for the given path. + * + * @param context App* + * @param path FuriString* + * @return ItemsView* +*/ +ItemsView* item_get_items_view_from_path(void* context, FuriString* path); + +/** Free ItemsView + * @param items_view +*/ +void item_items_view_free(ItemsView* items_view); + +/** Prettify the name by removing a leading XX_, only if both X are digits, + * as well as replace all '_' with ' '. + * @param name FuriString* +*/ +void item_prettify_name(FuriString* name); \ No newline at end of file diff --git a/quac.c b/quac.c new file mode 100644 index 00000000000..e4f3e45ee77 --- /dev/null +++ b/quac.c @@ -0,0 +1,24 @@ +#include "flipper.h" +#include "app_state.h" +#include "scenes/scenes.h" +#include "scenes/scene_items.h" + +/* generated by fbt from .png files in images folder */ +#include + +int32_t quac_app(void* p) { + UNUSED(p); + FURI_LOG_I(TAG, "QUAC QUAC!!"); + + App* app = app_alloc(); + // initialize any app state + + Gui* gui = furi_record_open(RECORD_GUI); + view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen); + scene_manager_next_scene(app->scene_manager, SR_Scene_Items); + view_dispatcher_run(app->view_dispatcher); + + furi_record_close(RECORD_GUI); + app_free(app); + return 0; +} diff --git a/quac.png b/quac.png new file mode 100644 index 0000000000000000000000000000000000000000..327ee08c509758ce7e16e60fc8eadce058fc4c86 GIT binary patch literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4F%}28J29*~C-V}>3HEex4ABVg zoe;>ypupk0`S<+DhqA%B;Ay%WOyU7{%G+}zgReZGzkPlG>+U174a5q-!jet7P| zqhbYG^90m2bOQp_^AF~`u6w@PeQVO1-$!mUC8|eVsd|0R3}`%qr>mdKI;Vst0K2<3 A0{{R3 literal 0 HcmV?d00001 diff --git a/scenes/.gitignore b/scenes/.gitignore new file mode 100644 index 00000000000..81a8981f739 --- /dev/null +++ b/scenes/.gitignore @@ -0,0 +1,6 @@ +dist/* +.vscode +.clang-format +.editorconfig +.env +.ufbt diff --git a/scenes/scene_items.c b/scenes/scene_items.c new file mode 100644 index 00000000000..7bedf51675c --- /dev/null +++ b/scenes/scene_items.c @@ -0,0 +1,114 @@ +#include "flipper.h" +#include "app_state.h" +#include "scenes.h" +#include "scene_items.h" +#include "../actions/action.h" +#include + +void scene_items_item_callback(void* context, int32_t index, InputType type) { + App* app = context; + + // FURI_LOG_I(TAG, "item_callback: %ld, %s", index, input_get_type_name(type)); + if(type == InputTypeShort || type == InputTypeRelease) { + // FURI_LOG_I(TAG, "You clicked button %li", index); + app->selected_item = index; + view_dispatcher_send_custom_event(app->view_dispatcher, Event_ButtonPressed); + } else { + // FURI_LOG_I(TAG, "[Ignored event of type %i]", type); + } +} + +// For each scene, implement handler callbacks +void scene_items_on_enter(void* context) { + App* app = context; + ButtonMenu* menu = app->btn_menu; + button_menu_reset(menu); + + ItemsView* items_view = app->items_view; + FURI_LOG_I(TAG, "items on_enter: [%d] %s", app->depth, furi_string_get_cstr(items_view->path)); + + const char* header = furi_string_get_cstr(items_view->name); + button_menu_set_header(menu, header); + + if(ItemArray_size(items_view->items)) { + ItemArray_it_t iter; + int32_t index = 0; + for(ItemArray_it(iter, items_view->items); !ItemArray_end_p(iter); + ItemArray_next(iter), ++index) { + const char* label = furi_string_get_cstr(ItemArray_cref(iter)->name); + ButtonMenuItemType type = ItemArray_cref(iter)->type == Item_Action ? + ButtonMenuItemTypeCommon : + ButtonMenuItemTypeControl; + button_menu_add_item(menu, label, index, scene_items_item_callback, type, app); + } + } else { + FURI_LOG_W(TAG, "No items for: %s", furi_string_get_cstr(items_view->path)); + // TODO: Display Error popup? + } + // ... + + view_dispatcher_switch_to_view(app->view_dispatcher, SR_ButtonMenu); +} +bool scene_items_on_event(void* context, SceneManagerEvent event) { + App* app = context; + bool consumed = false; + + FURI_LOG_I(TAG, "device on_event"); + switch(event.type) { + case SceneManagerEventTypeCustom: + if(event.event == Event_ButtonPressed) { + consumed = true; + FURI_LOG_I(TAG, "button pressed is %d", app->selected_item); + Item* item = ItemArray_get(app->items_view->items, app->selected_item); + if(item->type == Item_Group) { + ItemsView* new_items = item_get_items_view_from_path(app, item->path); + FURI_LOG_I(TAG, "calling item_items_view_free"); + item_items_view_free(app->items_view); + app->items_view = new_items; + app->depth++; + scene_manager_next_scene(app->scene_manager, SR_Scene_Items); + } else { + FURI_LOG_I(TAG, "Initiating item action: %s", furi_string_get_cstr(item->name)); + + // LED goes blinky blinky + App* app = context; + notification_message(app->notifications, &sequence_blink_start_green); + + action_tx(app, item); + + // Turn off LED light + notification_message(app->notifications, &sequence_blink_stop); + } + } + break; + case SceneManagerEventTypeBack: + FURI_LOG_I(TAG, "Back button pressed!"); + if(app->depth) { + // take our current ItemsView path, and back it up a level + FuriString* new_path; + new_path = furi_string_alloc(); + path_extract_dirname(furi_string_get_cstr(app->items_view->path), new_path); + + ItemsView* new_items = item_get_items_view_from_path(app, new_path); + item_items_view_free(app->items_view); + app->items_view = new_items; + app->depth--; + + furi_string_free(new_path); + } else { + FURI_LOG_W(TAG, "At the root level!"); + } + + break; + default: + break; + } + return consumed; +} + +void scene_items_on_exit(void* context) { + App* app = context; + ButtonMenu* menu = app->btn_menu; + button_menu_reset(menu); + FURI_LOG_I(TAG, "on_exit. depth = %d", app->depth); +} \ No newline at end of file diff --git a/scenes/scene_items.h b/scenes/scene_items.h new file mode 100644 index 00000000000..612f3f1a28a --- /dev/null +++ b/scenes/scene_items.h @@ -0,0 +1,9 @@ +#pragma once + +#include "flipper.h" + +void scene_items_item_callback(void* context, int32_t index, InputType type); +// For each scene, implement handler callbacks +void scene_items_on_enter(void* context); +bool scene_items_on_event(void* context, SceneManagerEvent event); +void scene_items_on_exit(void* context); diff --git a/scenes/scenes.c b/scenes/scenes.c new file mode 100644 index 00000000000..2325cfd9b1a --- /dev/null +++ b/scenes/scenes.c @@ -0,0 +1,26 @@ +#include "flipper.h" +#include "app_state.h" +#include "scenes.h" +#include "scene_items.h" + +// define handler callbacks - order must match appScenes enum! +void (*const app_on_enter_handlers[])(void* context) = {scene_items_on_enter}; +bool (*const app_on_event_handlers[])(void* context, SceneManagerEvent event) = { + scene_items_on_event, +}; +void (*const app_on_exit_handlers[])(void* context) = {scene_items_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 = SR_Scene_count}; + +bool app_scene_custom_callback(void* context, uint32_t custom_event_id) { + App* app = context; + return scene_manager_handle_custom_event(app->scene_manager, custom_event_id); +} +bool app_back_event_callback(void* context) { + App* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} diff --git a/scenes/scenes.h b/scenes/scenes.h new file mode 100644 index 00000000000..f81dfb292f6 --- /dev/null +++ b/scenes/scenes.h @@ -0,0 +1,22 @@ +#pragma once + +#include "flipper.h" + +typedef enum { SR_Scene_Items, SR_Scene_count } appScenes; + +typedef enum { + SR_ButtonMenu, // used on selected device, to show buttons + SR_Dialog, + SR_FileBrowser, // to find the recorded Sub-GHz data! + SR_TextInput +} appView; + +typedef enum { Event_DeviceSelected, Event_ButtonPressed } AppCustomEvents; + +extern void (*const app_on_enter_handlers[])(void*); +extern bool (*const app_on_event_handlers[])(void*, SceneManagerEvent); +extern void (*const app_on_exit_handlers[])(void*); +extern const SceneManagerHandlers app_scene_handlers; + +extern bool app_scene_custom_callback(void* context, uint32_t custom_event_id); +extern bool app_back_event_callback(void* context);