rdefeo 1 год назад
Сommit
e4a1dd83c3
21 измененных файлов с 913 добавлено и 0 удалено
  1. 29 0
      README.md
  2. 18 0
      actions/action.c
  3. 5 0
      actions/action.h
  4. 14 0
      actions/action_i.h
  5. 8 0
      actions/action_ir.c
  6. 115 0
      actions/action_rfid.c
  7. 242 0
      actions/action_subghz.c
  8. 50 0
      app_state.c
  9. 22 0
      app_state.h
  10. 17 0
      application.fam
  11. 16 0
      flipper.h
  12. 0 0
      images/.gitkeep
  13. 134 0
      item.c
  14. 42 0
      item.h
  15. 24 0
      quac.c
  16. BIN
      quac.png
  17. 6 0
      scenes/.gitignore
  18. 114 0
      scenes/scene_items.c
  19. 9 0
      scenes/scene_items.h
  20. 26 0
      scenes/scenes.c
  21. 22 0
      scenes/scenes.h

+ 29 - 0
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.
+

+ 18 - 0
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);
+    }
+}

+ 5 - 0
actions/action.h

@@ -0,0 +1,5 @@
+#pragma once
+
+struct Item;
+
+void action_tx(void* context, Item* item);

+ 14 - 0
actions/action_i.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include "../flipper.h"
+#include <furi.h>
+#include <furi_hal.h>
+
+#include <flipper_format/flipper_format.h>
+
+#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);

+ 8 - 0
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);
+}

+ 115 - 0
actions/action_rfid.c

@@ -0,0 +1,115 @@
+// Methods for RFID transmission
+
+// lfrid
+#include <lib/lfrfid/lfrfid_worker.h>
+#include <toolbox/protocols/protocol_dict.h>
+#include <lfrfid/protocols/lfrfid_protocols.h>
+#include <lfrfid/lfrfid_raw_file.h>
+#include <lib/toolbox/args.h>
+
+#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);
+}

+ 242 - 0
actions/action_subghz.c

@@ -0,0 +1,242 @@
+// Methods for Sub-GHz transmission
+
+// subghz
+#include <lib/subghz/transmitter.h>
+#include <lib/subghz/devices/devices.h>
+#include <lib/subghz/devices/cc1101_configs.h>
+#include <lib/subghz/protocols/raw.h>
+#include <lib/subghz/subghz_protocol_registry.h>
+
+#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);
+}

+ 50 - 0
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);
+}

+ 22 - 0
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);

+ 17 - 0
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
+)

+ 16 - 0
flipper.h

@@ -0,0 +1,16 @@
+#pragma once
+
+#include <furi.h>
+
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/menu.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/button_menu.h>
+#include <gui/modules/dialog_ex.h>
+
+#include <storage/storage.h>
+#include <notification/notification_messages.h>
+
+#define TAG "Quac" // log statement id

+ 0 - 0
images/.gitkeep


+ 134 - 0
item.c

@@ -0,0 +1,134 @@
+
+#include <furi.h>
+#include <storage/storage.h>
+#include <toolbox/dir_walk.h>
+#include <lib/toolbox/path.h>
+
+#include "app_state.h"
+#include "item.h"
+#include <m-array.h>
+
+// 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));
+}

+ 42 - 0
item.h

@@ -0,0 +1,42 @@
+#pragma once
+
+#include <m-array.h>
+
+#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);

+ 24 - 0
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 <quac_icons.h>
+
+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;
+}


+ 6 - 0
scenes/.gitignore

@@ -0,0 +1,6 @@
+dist/*
+.vscode
+.clang-format
+.editorconfig
+.env
+.ufbt

+ 114 - 0
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 <lib/toolbox/path.h>
+
+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);
+}

+ 9 - 0
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);

+ 26 - 0
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);
+}

+ 22 - 0
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);