Просмотр исходного кода

added playlist support, cleanup

rdefeo 1 год назад
Родитель
Сommit
2d7c8dd2c5
18 измененных файлов с 428 добавлено и 119 удалено
  1. 8 6
      actions/action.c
  2. 1 1
      actions/action.h
  3. 5 8
      actions/action_i.h
  4. 41 2
      actions/action_ir.c
  5. 160 0
      actions/action_qpl.c
  6. 15 10
      actions/action_rfid.c
  7. 25 6
      actions/action_subghz.c
  8. 0 50
      app_state.c
  9. 1 0
      app_state.h
  10. 1 1
      application.fam
  11. 17 8
      item.c
  12. 7 1
      item.h
  13. 65 4
      quac.c
  14. 35 0
      quac.h
  15. 39 13
      scenes/scene_items.c
  16. 1 1
      scenes/scene_items.h
  17. 3 2
      scenes/scenes.c
  18. 4 6
      scenes/scenes.h

+ 8 - 6
actions/action.c

@@ -1,18 +1,20 @@
 
-#include "app_state.h"
+#include "quac.h"
 #include "item.h"
 #include "action_i.h"
 
-void action_tx(void* context, Item* item) {
+void action_tx(void* context, Item* item, FuriString* error) {
     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);
+        action_subghz_tx(context, item->path, error);
     } else if(!strcmp(item->ext, ".ir")) {
-        action_ir_tx(context, item);
+        action_ir_tx(context, item->path, error);
     } else if(!strcmp(item->ext, ".rfid")) {
-        action_rfid_tx(context, item);
+        action_rfid_tx(context, item->path, error);
+    } else if(!strcmp(item->ext, ".qpl")) {
+        action_qpl_tx(context, item->path, error);
     } else {
-        FURI_LOG_E(TAG, "Unknown item type! %s", item->ext);
+        FURI_LOG_E(TAG, "Unknown item file type! %s", item->ext);
     }
 }

+ 1 - 1
actions/action.h

@@ -2,4 +2,4 @@
 
 struct Item;
 
-void action_tx(void* context, Item* item);
+void action_tx(void* context, Item* item, FuriString* error);

+ 5 - 8
actions/action_i.h

@@ -1,14 +1,11 @@
 #pragma once
 
-#include "../flipper.h"
 #include <furi.h>
 #include <furi_hal.h>
 
-#include <flipper_format/flipper_format.h>
+#define ACTION_SET_ERROR(_msg_fmt, ...) furi_string_printf(error, _msg_fmt, ##__VA_ARGS__)
 
-#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);
+void action_subghz_tx(void* context, FuriString* action_path, FuriString* error);
+void action_rfid_tx(void* context, FuriString* action_path, FuriString* error);
+void action_ir_tx(void* context, FuriString* action_path, FuriString* error);
+void action_qpl_tx(void* context, FuriString* action_path, FuriString* error);

+ 41 - 2
actions/action_ir.c

@@ -1,8 +1,47 @@
 // Methods for IR transmission
 
+#include <infrared.h>
+#include <infrared/encoder_decoder/infrared.h>
+#include <applications/services/cli/cli.h>
+
 #include "action_i.h"
+#include "quac.h"
+
+typedef struct {
+    size_t timings_size; /**< Number of elements in the timings array. */
+    uint32_t* timings; /**< Pointer to an array of timings describing the signal. */
+    uint32_t frequency; /**< Carrier frequency of the signal. */
+    float duty_cycle; /**< Duty cycle of the signal. */
+} InfraredRawSignal;
+
+typedef struct InfraredSignal {
+    bool is_raw;
+    union {
+        InfraredMessage message;
+        InfraredRawSignal raw;
+    } payload;
+} InfraredSignal;
 
-void action_ir_tx(void* context, Item* item) {
+InfraredSignal* infrared_signal_alloc() {
+    InfraredSignal* signal = malloc(sizeof(InfraredSignal));
+    signal->is_raw = false;
+    signal->payload.message.protocol = InfraredProtocolUnknown;
+    return signal;
+}
+
+void action_ir_tx(void* context, FuriString* action_path, FuriString* error) {
+    UNUSED(action_path);
+    UNUSED(error);
     UNUSED(context);
-    UNUSED(item);
+    // App* app = context;
+
+    // InfraredSignal* signal = infrared_signal_alloc();
+    // const char* ir_file = furi_string_get_cstr(action_path);
+    // bool success = infrared_parse_message(ir_file, signal) || infrared_parse_raw(ir_file, signal);
+    // if(success) {
+    //     infrared_signal_transmit(signal);
+    // } else {
+    //     ACTION_SET_ERROR("IR: Error sending signal");
+    // }
+    // infrared_signal_free(signal);
 }

+ 160 - 0
actions/action_qpl.c

@@ -0,0 +1,160 @@
+// Methods for Quac Playlist transmission
+
+#include <toolbox/stream/stream.h>
+#include <toolbox/stream/file_stream.h>
+#include <toolbox/path.h>
+#include <toolbox/args.h>
+
+#include <notification/notification_messages.h>
+
+#include "action_i.h"
+#include "quac.h"
+
+/** Open the Playlist file and then transmit each action
+ * Each line of the playlist file is one of:
+ *   <file_path>
+ *      Full SD card path, or relative path to action to be transmitted. Must be
+ *      one of the supported filetypes (.sub, .rfid, [.ir coming soon])
+ *   pause <ms> - NOT IMPLEMENTED
+ *      Pauses the playback for 'ms' milliseconds.
+ * 
+ * Blank lines, and comments (start with '#') are ignored. Whitespace is trimmed.
+ * 
+ * Not yet Implemented:
+ * - For RFID files, if they have a space followed by a number after their name,
+ *   that number will be the duration of that RFID tx
+*/
+void action_qpl_tx(void* context, FuriString* action_path, FuriString* error) {
+    App* app = context;
+
+    FuriString* buffer;
+    buffer = furi_string_alloc();
+
+    Stream* file = file_stream_alloc(app->storage);
+    if(file_stream_open(file, furi_string_get_cstr(action_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
+        while(stream_read_line(file, buffer)) {
+            furi_string_trim(buffer); // remove '\n\r' line endings, cleanup spaces
+            FURI_LOG_I(TAG, "line: %s", furi_string_get_cstr(buffer));
+
+            // Skip blank lines
+            if(furi_string_size(buffer) == 0) {
+                continue;
+            }
+
+            // Skip comments
+            char first_char = furi_string_get_char(buffer, 0);
+            if(first_char == '#') {
+                continue;
+            }
+
+            // Check if buffer is a "command", and not just a filename
+            // Commands will contain spaces
+            bool processed_special_command = false;
+            FuriString* args_tmp;
+            args_tmp = furi_string_alloc();
+            do {
+                if(!args_read_string_and_trim(buffer, args_tmp)) {
+                    // No spaces found, buffer and args_tmp are now have same contents
+                    break;
+                }
+
+                // FURI_LOG_I(
+                //     TAG,
+                //     "args_temp: '%s', buffer: '%s'",
+                //     furi_string_get_cstr(args_tmp),
+                //     furi_string_get_cstr(buffer));
+
+                // OK, there's a space, and args_tmp is the first token, buffer is the rest
+                if(furi_string_cmpi_str(args_tmp, "pause") == 0) {
+                    processed_special_command = true;
+                    uint32_t pause_length = 0;
+                    if(sscanf(furi_string_get_cstr(buffer), "%lu", &pause_length) == 1) {
+                        FURI_LOG_I(TAG, "Pausing playlist for %lu ms", pause_length);
+                        furi_delay_ms(pause_length);
+                    } else {
+                        ACTION_SET_ERROR("Playlist: Invalid or missing pause time");
+                    }
+                    break;
+                }
+
+                // FURI_LOG_I(TAG, "Still checking for commands...");
+                // FURI_LOG_I(
+                //     TAG,
+                //     "args_temp: '%s', buffer: '%s'",
+                //     furi_string_get_cstr(args_tmp),
+                //     furi_string_get_cstr(buffer));
+
+                // First token wasn't "pause", so maybe args_tmp is a .rfid filename followed
+                // by a transmit duration in ms in buffer
+                // Note: Not using path_extract_extension since it expects to find slashes in the
+                // path, and thus won't work if we have a relative path file
+                char ext[MAX_EXT_LEN + 1] = "";
+                size_t dot = furi_string_search_rchar(args_tmp, '.');
+                if(dot != FURI_STRING_FAILURE && furi_string_size(args_tmp) - dot <= MAX_EXT_LEN) {
+                    strlcpy(ext, &(furi_string_get_cstr(args_tmp))[dot], MAX_EXT_LEN);
+                }
+
+                // FURI_LOG_I(TAG, " - Found extension of %s", ext);
+                uint32_t rfid_duration = 0;
+                if(!strcmp(ext, ".rfid")) {
+                    // FURI_LOG_I(TAG, "RFID file with duration");
+                    if(sscanf(furi_string_get_cstr(buffer), "%lu", &rfid_duration) == 1) {
+                        FURI_LOG_I(TAG, "RFID duration = %lu", rfid_duration);
+                        // TODO: Need to get the duration to the action_rfid_tx command...
+                    }
+                }
+
+            } while(false);
+
+            furi_string_swap(buffer, args_tmp);
+            furi_string_free(args_tmp);
+
+            if(processed_special_command) {
+                continue;
+            }
+
+            first_char = furi_string_get_char(buffer, 0);
+            // Using relative paths? Prepend path of our playlist file
+            if(first_char != '/') {
+                FuriString* dirname;
+                dirname = furi_string_alloc();
+                path_extract_dirname(furi_string_get_cstr(action_path), dirname);
+                furi_string_cat_printf(dirname, "/%s", furi_string_get_cstr(buffer));
+                furi_string_swap(dirname, buffer);
+                furi_string_free(dirname);
+            }
+
+            char ext[MAX_EXT_LEN + 1] = "";
+            path_extract_extension(buffer, ext, MAX_EXT_LEN);
+            if(!strcmp(ext, ".sub")) {
+                action_subghz_tx(context, buffer, error);
+            } else if(!strcmp(ext, ".ir")) {
+                action_ir_tx(context, buffer, error);
+            } else if(!strcmp(ext, ".rfid")) {
+                action_rfid_tx(context, buffer, error);
+            } else if(!strcmp(ext, ".qpl")) {
+                ACTION_SET_ERROR("Playlist: Can't call playlist from playlist");
+            } else {
+                ACTION_SET_ERROR(
+                    "Playlist: Unknown file/command! %s", furi_string_get_cstr(buffer));
+            }
+
+            if(furi_string_size(error)) {
+                // Abort playing the playlist - one of our actions failed
+                break;
+            }
+
+            // Playlist action complete!
+            // TODO: Do we need a small delay (say 25ms) between actions?
+            // TODO: Should we blip the light a diff color to indicate that
+            //       we're done with one command and moving to the next?
+            // furi_delay_ms(25);
+        }
+    } else {
+        ACTION_SET_ERROR("Could not open playlist");
+    }
+
+    furi_string_free(buffer);
+    file_stream_close(file);
+    stream_free(file);
+}

+ 15 - 10
actions/action_rfid.c

@@ -7,12 +7,17 @@
 #include <lfrfid/lfrfid_raw_file.h>
 #include <lib/toolbox/args.h>
 
+#include <flipper_format/flipper_format.h>
+
 #include "action_i.h"
+#include "quac.h"
 
 // lifted from flipperzero-firmware/applications/main/lfrfid/lfrfid_cli.c
-void action_rfid_tx(void* context, Item* item) {
+void action_rfid_tx(void* context, FuriString* action_path, FuriString* error) {
+    UNUSED(error);
+
     App* app = context;
-    FuriString* file_name = item->path;
+    FuriString* file_name = action_path;
 
     FlipperFormat* fff_data_file = flipper_format_file_alloc(app->storage);
     FuriString* temp_str;
@@ -33,39 +38,39 @@ void action_rfid_tx(void* context, Item* item) {
     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));
+            ACTION_SET_ERROR("RFID: 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");
+            ACTION_SET_ERROR("RFID: 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");
+            ACTION_SET_ERROR("RFID: 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");
+            ACTION_SET_ERROR("RFID: 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));
+            ACTION_SET_ERROR("RFID: 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");
+            ACTION_SET_ERROR("RFID: Error reading data");
             break;
         }
         // FURI_LOG_I(TAG, "Data: %s", furi_string_get_cstr(data_text));
@@ -90,14 +95,14 @@ void action_rfid_tx(void* context, Item* item) {
         lfrfid_worker_start_thread(worker);
         lfrfid_worker_emulate_start(worker, protocol);
 
-        printf("Emulating RFID...\r\nPress Ctrl+C to abort\r\n");
+        FURI_LOG_I(TAG, "Emulating RFID...");
         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");
+        FURI_LOG_I(TAG, "Emulation stopped");
 
         lfrfid_worker_stop(worker);
         lfrfid_worker_stop_thread(worker);

+ 25 - 6
actions/action_subghz.c

@@ -7,7 +7,10 @@
 #include <lib/subghz/protocols/raw.h>
 #include <lib/subghz/subghz_protocol_registry.h>
 
+#include <flipper_format/flipper_format.h>
+
 #include "action_i.h"
+#include "quac.h"
 
 static FuriHalSubGhzPreset action_subghz_get_preset_name(const char* preset_name) {
     FuriHalSubGhzPreset preset = FuriHalSubGhzPresetIDLE;
@@ -22,16 +25,16 @@ static FuriHalSubGhzPreset action_subghz_get_preset_name(const char* preset_name
     } else if(!strcmp(preset_name, "FuriHalSubGhzPresetCustom")) {
         preset = FuriHalSubGhzPresetCustom;
     } else {
-        FURI_LOG_E(TAG, "Unknown preset!");
+        FURI_LOG_E(TAG, "SUBGHZ: Unknown preset!");
     }
     return preset;
 }
 
 // Lifted from flipperzero-firmware/applications/main/subghz/subghz_cli.c
-void action_subghz_tx(void* context, Item* item) {
+void action_subghz_tx(void* context, FuriString* action_path, FuriString* error) {
     App* app = context;
-    FuriString* file_name = item->path;
-    uint32_t repeat = 1; // 10?
+    FuriString* file_name = action_path;
+    uint32_t repeat = 1; //
     // uint32_t device_ind = 0; // 0 - CC1101_INT, 1 - CC1101_EXT
 
     FlipperFormat* fff_data_file = flipper_format_file_alloc(app->storage);
@@ -45,7 +48,7 @@ void action_subghz_tx(void* context, Item* item) {
     uint32_t frequency = 0;
     SubGhzTransmitter* transmitter = NULL;
 
-    FURI_LOG_I(TAG, "action_run_tx starting...");
+    FURI_LOG_I(TAG, "SUBGHZ: Action starting...");
 
     subghz_devices_init();
     SubGhzEnvironment* environment = subghz_environment_alloc();
@@ -75,11 +78,13 @@ void action_subghz_tx(void* context, Item* item) {
 
         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));
+            ACTION_SET_ERROR("SUBGHZ: 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");
+            ACTION_SET_ERROR("SUBGHZ: Missing or incorrect header");
             break;
         }
 
@@ -88,21 +93,31 @@ void action_subghz_tx(void* context, Item* item) {
            temp_data32 == SUBGHZ_KEY_FILE_VERSION) {
         } else {
             FURI_LOG_E(TAG, "Type or version mismatch");
+            ACTION_SET_ERROR("SUBGHZ: Type or version mismatch");
             break;
         }
 
         if(!flipper_format_read_uint32(fff_data_file, "Frequency", &frequency, 1)) {
             FURI_LOG_E(TAG, "Missing Frequency");
+            ACTION_SET_ERROR("SUBGHZ: Missing frequency");
             break;
         }
 
         if(!subghz_devices_is_frequency_valid(device, frequency)) {
             FURI_LOG_E(TAG, "Frequency not supported");
+            ACTION_SET_ERROR("SUBGHZ: Frequency not supported");
             break;
         }
 
         if(!flipper_format_read_string(fff_data_file, "Preset", temp_str)) {
             FURI_LOG_E(TAG, "Missing Preset");
+            ACTION_SET_ERROR("SUBGHZ: Missing preset");
+            break;
+        }
+
+        if(action_subghz_get_preset_name(furi_string_get_cstr(temp_str)) ==
+           FuriHalSubGhzPresetIDLE) {
+            ACTION_SET_ERROR("SUBGHZ: Unknown preset");
             break;
         }
 
@@ -116,6 +131,7 @@ void action_subghz_tx(void* context, Item* item) {
                 break;
             if(!temp_data32 || (temp_data32 % 2)) {
                 FURI_LOG_E(TAG, "Custom_preset_data size error");
+                ACTION_SET_ERROR("SUBGHZ: Custom_preset_data size error");
                 break;
             }
             custom_preset_data_size = sizeof(uint8_t) * temp_data32;
@@ -126,6 +142,7 @@ void action_subghz_tx(void* context, Item* item) {
                    custom_preset_data,
                    custom_preset_data_size)) {
                 FURI_LOG_E(TAG, "Custom_preset_data read error");
+                ACTION_SET_ERROR("SUBGHZ: Custom_preset_data read error");
                 break;
             }
             subghz_devices_load_preset(
@@ -143,6 +160,7 @@ void action_subghz_tx(void* context, Item* item) {
         // Load Protocol
         if(!flipper_format_read_string(fff_data_file, "Protocol", temp_str)) {
             FURI_LOG_E(TAG, "Missing protocol");
+            ACTION_SET_ERROR("SUBGHZ: Missing protocol");
             break;
         }
 
@@ -231,10 +249,11 @@ void action_subghz_tx(void* context, Item* item) {
         if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg();
 
         furi_hal_power_suppress_charge_exit();
-
         subghz_transmitter_free(transmitter);
     }
 
+    FURI_LOG_I(TAG, "SUBGHZ: Action complete.");
+
     flipper_format_free(fff_data_raw);
     furi_string_free(temp_str);
     subghz_devices_deinit();

+ 0 - 50
app_state.c

@@ -1,50 +0,0 @@
-#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);
-}

+ 1 - 0
app_state.h

@@ -9,6 +9,7 @@ typedef struct App {
     SceneManager* scene_manager;
     ViewDispatcher* view_dispatcher;
     ButtonMenu* btn_menu;
+    DialogEx* dialog;
 
     Storage* storage;
     NotificationApp* notifications;

+ 1 - 1
application.fam

@@ -8,7 +8,7 @@ App(
     stack_size=2 * 1024,
     fap_category="Tools",
     # Optional values
-    fap_version="0.1",
+    fap_version="0.2",
     fap_icon="quac.png",  # 10x10 1-bit PNG
     fap_description="Quick Action remote control app",
     fap_author="Roberto De Feo",

+ 17 - 8
item.c

@@ -2,22 +2,18 @@
 #include <furi.h>
 #include <storage/storage.h>
 #include <toolbox/dir_walk.h>
-#include <lib/toolbox/path.h>
+#include <toolbox/path.h>
 
-#include "app_state.h"
+#include "quac.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;
 
+    // Handle the app start condition
     if(input_path == NULL) {
         input_path = furi_string_alloc_set_str(QUAC_DATA_PATH);
     }
@@ -44,12 +40,23 @@ ItemsView* item_get_items_view_from_path(void* context, FuriString* input_path)
     FileArray_t flist;
     FileArray_init(flist);
 
+    FuriString* filename_tmp;
+    filename_tmp = furi_string_alloc();
+
     // 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));
+            FURI_LOG_I(TAG, "> dir_walk: %s", furi_string_get_cstr(path));
             const char* cpath = furi_string_get_cstr(path);
 
+            // Skip "hidden" files
+            path_extract_filename(path, filename_tmp, false);
+            char first_char = furi_string_get_char(filename_tmp, 0);
+            if(first_char == '.') {
+                FURI_LOG_I(TAG, ">> skipping hidden file: %s", furi_string_get_cstr(filename_tmp));
+                continue;
+            }
+
             // Insert the new file path in sorted order to flist
             uint32_t i = 0;
             FileArray_it_t it;
@@ -67,6 +74,8 @@ ItemsView* item_get_items_view_from_path(void* context, FuriString* input_path)
             }
         }
     }
+
+    furi_string_free(filename_tmp);
     furi_string_free(path);
 
     // DEBUG: Now print our array in original order

+ 7 - 1
item.h

@@ -4,6 +4,11 @@
 
 #define MAX_EXT_LEN 6
 
+/** Defines an individual item action or item group. Each object contains
+ * the relevant file and type information needed to both render correctly
+ * on-screen as well as to perform that action.
+*/
+
 typedef enum { Item_Action, Item_Group } ItemType;
 
 typedef struct Item {
@@ -22,7 +27,8 @@ typedef struct ItemsView {
 } ItemsView;
 
 /** Allocates and returns an ItemsView* which contains the list of
- * items to display for the given path.
+ * items to display for the given path. Contains everything needed
+ * to render a scene_items.
  * 
  * @param   context App*
  * @param   path    FuriString*

+ 65 - 4
quac.c

@@ -1,17 +1,78 @@
-#include "flipper.h"
-#include "app_state.h"
+#include <furi.h>
+
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/button_menu.h>
+#include <gui/modules/dialog_ex.h>
+
+#include <storage/storage.h>
+#include <notification/notification_messages.h>
+
+#include "item.h"
 #include "scenes/scenes.h"
 #include "scenes/scene_items.h"
 
+#include "quac.h"
+
 /* generated by fbt from .png files in images folder */
 #include <quac_icons.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));
+
+    app->dialog = dialog_ex_alloc();
+    view_dispatcher_add_view(app->view_dispatcher, SR_Dialog, dialog_ex_get_view(app->dialog));
+
+    // 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);
+}
+
+// FAP Entry Point
 int32_t quac_app(void* p) {
     UNUSED(p);
-    FURI_LOG_I(TAG, "QUAC QUAC!!");
+    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);

+ 35 - 0
quac.h

@@ -0,0 +1,35 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/button_menu.h>
+#include <gui/modules/dialog_ex.h>
+#include <storage/storage.h>
+#include <notification/notification_messages.h>
+
+#include "item.h"
+
+#define QUAC_NAME "Quac!"
+#define TAG "Quac" // log statement id
+
+// Location of our actions and folders
+#define QUAC_PATH "apps_data/quac"
+// Full path to actions
+#define QUAC_DATA_PATH EXT_PATH(QUAC_PATH)
+
+typedef struct App {
+    SceneManager* scene_manager;
+    ViewDispatcher* view_dispatcher;
+    ButtonMenu* btn_menu;
+    DialogEx* dialog;
+
+    Storage* storage;
+    NotificationApp* notifications;
+
+    int depth;
+    ItemsView* items_view;
+    int selected_item;
+} App;
+
+App* app_alloc();
+void app_free(App* app);

+ 39 - 13
scenes/scene_items.c

@@ -1,5 +1,13 @@
-#include "flipper.h"
-#include "app_state.h"
+#include <furi.h>
+
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/button_menu.h>
+#include <gui/modules/dialog_ex.h>
+
+#include <notification/notification_messages.h>
+
+#include "quac.h"
 #include "scenes.h"
 #include "scene_items.h"
 #include "../actions/action.h"
@@ -21,6 +29,8 @@ void scene_items_on_enter(void* context) {
     App* app = context;
     ButtonMenu* menu = app->btn_menu;
     button_menu_reset(menu);
+    DialogEx* dialog = app->dialog;
+    dialog_ex_reset(dialog);
 
     ItemsView* items_view = app->items_view;
     FURI_LOG_I(TAG, "items on_enter: [%d] %s", app->depth, furi_string_get_cstr(items_view->path));
@@ -61,7 +71,6 @@ bool scene_items_on_event(void* context, SceneManagerEvent event) {
             if(item->type == Item_Group) {
                 app->depth++;
                 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;
                 scene_manager_next_scene(app->scene_manager, SR_Scene_Items);
@@ -70,9 +79,23 @@ bool scene_items_on_event(void* context, SceneManagerEvent event) {
 
                 // LED goes blinky blinky
                 App* app = context;
-                notification_message(app->notifications, &sequence_blink_start_green);
+                notification_message(app->notifications, &sequence_blink_start_blue);
+
+                // Prepare error string for action calls
+                FuriString* error;
+                error = furi_string_alloc();
+
+                action_tx(app, item, error);
 
-                action_tx(app, item);
+                if(furi_string_size(error)) {
+                    FURI_LOG_E(TAG, furi_string_get_cstr(error));
+                    // Change LED to Red and Vibrate!
+                    notification_message(app->notifications, &sequence_error);
+
+                    // Display DialogEx popup or something?
+                }
+
+                furi_string_free(error);
 
                 // Turn off LED light
                 notification_message(app->notifications, &sequence_blink_stop);
@@ -81,22 +104,22 @@ bool scene_items_on_event(void* context, SceneManagerEvent event) {
         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);
+        consumed = false; // Ensure Back event continues to propagate
+        if(app->depth >= 0) {
+            // take our current ItemsView path, and go back up a level
+            FuriString* parent_path;
+            parent_path = furi_string_alloc();
+            path_extract_dirname(furi_string_get_cstr(app->items_view->path), parent_path);
 
             app->depth--;
-            ItemsView* new_items = item_get_items_view_from_path(app, new_path);
+            ItemsView* new_items = item_get_items_view_from_path(app, parent_path);
             item_items_view_free(app->items_view);
             app->items_view = new_items;
 
-            furi_string_free(new_path);
+            furi_string_free(parent_path);
         } else {
             FURI_LOG_W(TAG, "At the root level!");
         }
-
         break;
     default:
         break;
@@ -108,5 +131,8 @@ void scene_items_on_exit(void* context) {
     App* app = context;
     ButtonMenu* menu = app->btn_menu;
     button_menu_reset(menu);
+    DialogEx* dialog = app->dialog;
+    dialog_ex_reset(dialog);
+
     FURI_LOG_I(TAG, "on_exit. depth = %d", app->depth);
 }

+ 1 - 1
scenes/scene_items.h

@@ -1,6 +1,6 @@
 #pragma once
 
-#include "flipper.h"
+#include <gui/scene_manager.h>
 
 void scene_items_item_callback(void* context, int32_t index, InputType type);
 // For each scene, implement handler callbacks

+ 3 - 2
scenes/scenes.c

@@ -1,5 +1,6 @@
-#include "flipper.h"
-#include "app_state.h"
+
+
+#include "quac.h"
 #include "scenes.h"
 #include "scene_items.h"
 

+ 4 - 6
scenes/scenes.h

@@ -1,14 +1,12 @@
 #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
+    SR_ButtonMenu, // used on selected device, to show buttons/groups
+    SR_Dialog, // shows errors
+    SR_FileBrowser, // TODO: UNUSED!
+    SR_TextInput // TODO: UNUSED
 } appView;
 
 typedef enum { Event_DeviceSelected, Event_ButtonPressed } AppCustomEvents;