jblanked 1 год назад
Родитель
Сommit
8ec862998d

+ 2 - 0
CHANGELOG.md

@@ -0,0 +1,2 @@
+## 0.1
+- Initial Release

+ 76 - 0
README.md

@@ -0,0 +1,76 @@
+## Overview
+
+**Web Crawler** is a custom application designed for the [Flipper Zero](https://flipperzero.one/) device. This app allows users to configure and manage web crawling operations directly from their Flipper Zero. 
+
+## Features
+
+- **Configurable Website Path**: Specify the URL of the website you want to crawl.
+- **Wi-Fi Configuration**: Enter your Wi-Fi SSID and password to enable network communication.
+- **Data Storage**: Automatically saves received data to the device's storage for easy access.
+
+
+## Usage
+
+1. **Launch the Web Crawler App**
+
+   Navigate to the `Apps` menu on your Flipper Zero, select `GPIO`, then scroll down and select **Web Crawler**.
+
+2. **Main Menu**
+
+   Upon launching, you'll be presented with a submenu containing the following options:
+
+   - **Run**: Initiate the web crawling operation.
+   - **About**: View information about the Web Crawler app.
+   - **Configure**: Set up the crawling parameters.
+
+3. **Configuring Settings**
+
+   Select **Configure** to enter the following parameters:
+
+   - **Path**: Enter the URL of the website you wish to crawl.
+   - **SSID**: Input your Wi-Fi network's SSID.
+   - **Password**: Provide the corresponding Wi-Fi password.
+
+   Use the Flipper Zero's navigation buttons to input and confirm your settings. Once configured, these settings will be saved and used for subsequent crawling operations.
+
+4. **Running the Crawler**
+
+   Select **Run** from the main submenu to start the web crawling process. The app will:
+
+   - **Send Settings**: Transmit the configured path, SSID, and password via UART to the Wifi Dev Board.
+   - **Receive Data**: Listen for incoming data from the UART interface.
+   - **Store Data**: Save the received data to the device's storage for later retrieval.
+
+   Monitor the operation state displayed on the main screen to track progress.
+
+5. **Accessing Crawled Data**
+
+   After the crawling operation completes, navigate to the storage directory to access the `received_data.txt` file, which contains the crawled information.
+
+## Setting Up Parameters
+
+1. **Path (URL)**
+   - Enter the complete URL of the website you intend to crawl (e.g., `https://www.example.com/`).
+
+2. **SSID (Wi-Fi Network)**
+   - Provide the name of your Wi-Fi network to enable the Flipper Zero to communicate over the network if required.
+
+3. **Password (Wi-Fi Network)**
+   - Input the corresponding password for your Wi-Fi network.
+
+## Saving Settings
+
+After entering the desired configuration parameters, the app automatically saves these settings and uses them during the web crawling process. You can update these settings at any time by navigating back to the **Configure** menu.
+
+## Logging and Debugging
+
+The Web Crawler app utilizes logging to help identify issues:
+
+- **Info Logs**: Provide general information about the app's operations (e.g., UART initialization, sending settings).
+- **Error Logs**: Indicate problems encountered during execution (e.g., failed to open settings file).
+
+Connect your Flipper Zero to a computer and use a serial terminal to view these logs for detailed troubleshooting.
+
+---
+
+*Happy Crawling! 🕷️*

+ 43 - 0
app.c

@@ -0,0 +1,43 @@
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/widget.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/widget.h>
+#include <dialogs/dialogs.h>
+#include <web_crawler_e.h>
+#include <web_crawler_uart.h>
+#include <web_crawler_storage.h>
+#include <web_crawler_callback.h>
+#include <web_crawler_free.h>
+#include <web_crawler_i.h>
+/**
+ * @brief      Entry point for the WebCrawler application.
+ * @param      p  Input parameter - unused
+ * @return     0 to indicate success, -1 on failure
+ */
+int32_t web_crawler_app(void *p)
+{
+    UNUSED(p);
+
+    WebCrawlerApp *app = web_crawler_app_alloc();
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate WebCrawlerApp");
+        return -1;
+    }
+
+    // Run the application
+    view_dispatcher_run(app->view_dispatcher);
+
+    // Free resources after the application loop ends
+    web_crawler_app_free(app);
+
+    return 0;
+}


+ 17 - 0
application.fam

@@ -0,0 +1,17 @@
+App(
+    appid="web_crawler",
+    name="Web Crawler",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="web_crawler_app",
+    stack_size=4 * 1024,
+    requires=[
+        "gui", 
+    ],
+    order=10,
+    fap_version = (0, 1),
+    fap_icon="app.png",
+    fap_category="GPIO",
+    fap_author="JBlanked",
+    fap_icon_assets="assets",
+    fap_description="Use Wifi to access the internet and web scrape",
+)

BIN
assets/.DS_Store


BIN
assets/01-run.png


BIN
assets/02-configure.png


BIN
assets/03-path.png


+ 16 - 0
assets/WebCrawler-WifiDevBoard/README.md

@@ -0,0 +1,16 @@
+## Installation
+1. Download the `web_crawler_bootloader.bin`, `web_crawler_firmware_a.bin`, and `web_crawler_partitions.bin` files from within the `WebCrawler-WifiDevBoard` folder.
+2. Connect your Flipper Zero to your computer.
+3. Open up qFlipper.
+4. Click on the File-Opener.
+5. Naviate to `SD Card/apps_data/esp_flasher/
+6. Drag all three bin files (or the entire folder) into the directory.
+7. Plug your Wi-Fi Devboard into the Flipper.
+8. Press and keep holding the boot button while you press the reset button once, release the boot button after 2 seconds.
+9. Open the ESP Flasher app on your Flipper Zero, it should be located under `Apps->GPIO` from the main menu. If not, download it from the Flipper App Store.
+10. Click on Manual Flash.
+11. Click on Bootloader and select the `web_crawler_bootloader.bin` that you downloaded earlier.
+12. Click on Part Table and select the `web_crawler_partitions.bin` that you downloaded earlier.
+13. Click on FirmwareA and select the `web_crawler_firmware_a.bin` that you downloaded earlier.
+14. Click on FLASH - fast and follow the instructions on the screen.
+15. Now you are all set to use the Web Crawler app.

BIN
assets/WebCrawler-WifiDevBoard/web_crawler_bootloader.bin


BIN
assets/WebCrawler-WifiDevBoard/web_crawler_firmware_a.bin


BIN
assets/WebCrawler-WifiDevBoard/web_crawler_partitions.bin


+ 374 - 0
web_crawler_callback.h

@@ -0,0 +1,374 @@
+// Define the GPIO pins available on the Flipper Zero
+GpioPin test_pins[9] = {
+    {.port = GPIOA, .pin = LL_GPIO_PIN_7}, // PB7 - USART1_RX
+    {.port = GPIOA, .pin = LL_GPIO_PIN_6}, // PB6 - USART1_TX
+    {.port = GPIOA, .pin = LL_GPIO_PIN_5},
+    {.port = GPIOA, .pin = LL_GPIO_PIN_4},
+    {.port = GPIOB, .pin = LL_GPIO_PIN_3},
+    {.port = GPIOB, .pin = LL_GPIO_PIN_2},
+    {.port = GPIOC, .pin = LL_GPIO_PIN_3},
+    {.port = GPIOC, .pin = LL_GPIO_PIN_1},
+    {.port = GPIOC, .pin = LL_GPIO_PIN_0}};
+
+// Forward declaration of callback functions
+static void web_crawler_setting_item_path_clicked(void *context, uint32_t index);
+static void web_crawler_setting_item_ssid_clicked(void *context, uint32_t index);
+static void web_crawler_setting_item_password_clicked(void *context, uint32_t index);
+/**
+ * @brief      Navigation callback to handle exiting from other views to the submenu.
+ * @param      context   The context - WebCrawlerApp object.
+ * @return     WebCrawlerViewSubmenu
+ */
+static uint32_t web_crawler_back_to_main_callback(void *context)
+{
+    UNUSED(context);
+    return WebCrawlerViewSubmenu; // Return to the main submenu view
+}
+
+/**
+ * @brief      Navigation callback to handle returning to the Configure screen.
+ * @param      context   The context - WebCrawlerApp object.
+ * @return     WebCrawlerViewConfigure
+ */
+static uint32_t web_crawler_back_to_configure_callback(void *context)
+{
+    UNUSED(context);
+    return WebCrawlerViewConfigure; // Return to the Configure screen
+}
+
+/**
+ * @brief      Navigation callback to handle exiting the app from the main submenu.
+ * @param      context   The context - unused
+ * @return     VIEW_NONE to exit the app
+ */
+static uint32_t web_crawler_exit_app_callback(void *context)
+{
+    UNUSED(context);
+    return VIEW_NONE; // Exit the app
+}
+
+/**
+ * @brief      Handle submenu item selection.
+ * @param      context   The context - WebCrawlerApp object.
+ * @param      index     The WebCrawlerSubmenuIndex item that was clicked.
+ */
+static void web_crawler_submenu_callback(void *context, uint32_t index)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    switch (index)
+    {
+    case WebCrawlerSubmenuIndexRun:
+        // Switch to the main view where the saved path will be displayed
+        view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewMain);
+        break;
+    case WebCrawlerSubmenuIndexAbout:
+        view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewAbout);
+        break;
+    case WebCrawlerSubmenuIndexSetPath:
+        view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewConfigure);
+        break;
+    default:
+        FURI_LOG_E(TAG, "Unknown submenu index");
+        break;
+    }
+}
+
+/**
+ * @brief      Configuration enter callback to handle different items.
+ * @param      context   The context - WebCrawlerApp object.
+ * @param      index     The index of the item that was clicked.
+ */
+static void web_crawler_config_enter_callback(void *context, uint32_t index)
+{
+    switch (index)
+    {
+    case 0:
+        web_crawler_setting_item_path_clicked(context, index);
+        break;
+    case 1:
+        web_crawler_setting_item_ssid_clicked(context, index);
+        break;
+    case 2:
+        web_crawler_setting_item_password_clicked(context, index);
+        break;
+    default:
+        FURI_LOG_E(TAG, "Unknown configuration item index");
+        break;
+    }
+}
+
+// At the top of your file, after includes and defines
+static WebCrawlerApp *app_instance = NULL;
+
+// Modify the draw callback function to match the expected signature
+static void web_crawler_view_draw_callback(Canvas *canvas, void *model)
+{
+    WebCrawlerMainModel *main_model = (WebCrawlerMainModel *)model; // Cast model to WebCrawlerMainModel
+    canvas_clear(canvas);
+    canvas_set_font(canvas, FontPrimary);
+
+    if (main_model->path[0] != '\0')
+    {
+        canvas_draw_str(canvas, 1, 10, "Sending GET request...");
+
+        // Initialize the GPIO pin for output mode
+        furi_hal_gpio_init_simple(&test_pins[1], GpioModeOutputPushPull);
+
+        // Set GPIO pin high
+        furi_hal_gpio_write(&test_pins[1], true);
+
+        canvas_draw_str(canvas, 1, 20, "Sending Wifi settings..");
+
+        // Send settings via UART
+        send_settings_via_uart(main_model->path, main_model->ssid, main_model->password);
+
+        furi_delay_ms(1000); // Delay for 1 second
+
+        // Read data from UART sent by the dev board
+        if (read_data_from_uart_and_save(canvas))
+        {
+            furi_hal_gpio_write(&test_pins[1], false); // Set GPIO pin low
+            canvas_draw_str(canvas, 1, 80, "Data received and saved");
+
+            // Switch back to submenu view
+            if (app_instance)
+            {
+                view_dispatcher_switch_to_view(app_instance->view_dispatcher, WebCrawlerViewSubmenu);
+            }
+        }
+        else
+        {
+            furi_hal_gpio_write(&test_pins[1], false); // Set GPIO pin low
+        }
+    }
+    else
+    {
+        canvas_draw_str(canvas, 1, 10, "No path saved.");
+    }
+}
+/**
+ * @brief      Input callback for the main screen.
+ * @param      event    The input event.
+ * @param      context  The context - WebCrawlerApp object.
+ * @return     true if the event was handled, false otherwise.
+ */
+static bool web_crawler_view_input_callback(InputEvent *event, void *context)
+{
+    UNUSED(event);
+    UNUSED(context);
+    return false;
+}
+
+/**
+ * @brief      Callback for when the user finishes entering the URL.
+ * @param      context   The context - WebCrawlerApp object.
+ */
+static void web_crawler_set_path_updated(void *context)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+
+    // Store the entered URL from temp_buffer_path to path
+    strncpy(app->path, app->temp_buffer_path, app->temp_buffer_size_path - 1);
+
+    // Ensure null-termination
+    app->path[app->temp_buffer_size_path - 1] = '\0';
+
+    if (app->path_item)
+    {
+        variable_item_set_current_value_text(app->path_item, app->path);
+
+        // Save the URL to the settings
+        save_settings(app->path, app->ssid, app->password);
+
+        FURI_LOG_D(TAG, "URL saved: %s", app->path);
+    }
+
+    // Update the main view's model
+    WebCrawlerMainModel *main_model = (WebCrawlerMainModel *)view_get_model(app->view_main);
+    if (main_model)
+    {
+        strncpy(main_model->path, app->path, sizeof(main_model->path) - 1);
+        main_model->path[sizeof(main_model->path) - 1] = '\0';
+    }
+
+    // Return to the Configure view
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewConfigure);
+}
+
+/**
+ * @brief      Callback for when the user finishes entering the SSID.
+ * @param      context   The context - WebCrawlerApp object.
+ */
+static void web_crawler_set_ssid_updated(void *context)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+
+    // Store the entered SSID from temp_buffer_ssid to ssid
+    strncpy(app->ssid, app->temp_buffer_ssid, app->temp_buffer_size_ssid - 1);
+
+    // Ensure null-termination
+    app->ssid[app->temp_buffer_size_ssid - 1] = '\0';
+
+    if (app->ssid_item)
+    {
+        variable_item_set_current_value_text(app->ssid_item, app->ssid);
+
+        // Save the SSID to the settings
+        save_settings(app->path, app->ssid, app->password);
+
+        FURI_LOG_D(TAG, "SSID saved: %s", app->ssid);
+    }
+
+    // Update the main view's model
+    WebCrawlerMainModel *main_model = (WebCrawlerMainModel *)view_get_model(app->view_main);
+    if (main_model)
+    {
+        strncpy(main_model->ssid, app->ssid, sizeof(main_model->ssid) - 1);
+        main_model->ssid[sizeof(main_model->ssid) - 1] = '\0';
+    }
+    // Return to the Configure view
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewConfigure);
+}
+
+/**
+ * @brief      Callback for when the user finishes entering the Password.
+ * @param      context   The context - WebCrawlerApp object.
+ */
+static void web_crawler_set_password_update(void *context)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+
+    // Store the entered Password from temp_buffer_password to password
+    strncpy(app->password, app->temp_buffer_password, app->temp_buffer_size_password - 1);
+
+    // Ensure null-termination
+    app->password[app->temp_buffer_size_password - 1] = '\0';
+
+    if (app->password_item)
+    {
+        variable_item_set_current_value_text(app->password_item, app->password);
+
+        // Save the Password to the settings
+        save_settings(app->path, app->ssid, app->password);
+
+        FURI_LOG_D(TAG, "Password saved: %s", app->password);
+    }
+
+    // Update the main view's model
+    WebCrawlerMainModel *main_model = (WebCrawlerMainModel *)view_get_model(app->view_main);
+    if (main_model)
+    {
+        strncpy(main_model->password, app->password, sizeof(main_model->password) - 1);
+        main_model->password[sizeof(main_model->password) - 1] = '\0';
+    }
+    // Return to the Configure view
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewConfigure);
+}
+
+/**
+ * @brief      Handler for Path configuration item click.
+ * @param      context  The context - WebCrawlerApp object.
+ * @param      index    The index of the item that was clicked.
+ */
+static void web_crawler_setting_item_path_clicked(void *context, uint32_t index)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    UNUSED(index);
+
+    // Set up the text input
+    text_input_set_header_text(app->text_input_path, "Enter URL");
+
+    // Initialize temp_buffer with existing path
+    strncpy(app->temp_buffer_path, "https://www.x.com/", app->temp_buffer_size_path - 1);
+    app->temp_buffer_path[app->temp_buffer_size_path - 1] = '\0';
+
+    // Configure the text input
+    bool clear_previous_text = false;
+    text_input_set_result_callback(
+        app->text_input_path,
+        web_crawler_set_path_updated,
+        app,
+        app->temp_buffer_path,
+        app->temp_buffer_size_path,
+        clear_previous_text);
+
+    // Set the previous callback to return to Configure screen
+    view_set_previous_callback(
+        text_input_get_view(app->text_input_path),
+        web_crawler_back_to_configure_callback);
+
+    // Show text input dialog
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInput);
+}
+
+/**
+ * @brief      Handler for SSID configuration item click.
+ * @param      context  The context - WebCrawlerApp object.
+ * @param      index    The index of the item that was clicked.
+ */
+static void web_crawler_setting_item_ssid_clicked(void *context, uint32_t index)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    UNUSED(index);
+
+    // Set up the text input
+    text_input_set_header_text(app->text_input_ssid, "Enter SSID");
+
+    // Initialize temp_buffer with existing SSID
+    strncpy(app->temp_buffer_ssid, app->ssid, app->temp_buffer_size_ssid - 1);
+    app->temp_buffer_ssid[app->temp_buffer_size_ssid - 1] = '\0';
+
+    // Configure the text input
+    bool clear_previous_text = false;
+    text_input_set_result_callback(
+        app->text_input_ssid,
+        web_crawler_set_ssid_updated,
+        app,
+        app->temp_buffer_ssid,
+        app->temp_buffer_size_ssid,
+        clear_previous_text);
+
+    // Set the previous callback to return to Configure screen
+    view_set_previous_callback(
+        text_input_get_view(app->text_input_ssid),
+        web_crawler_back_to_configure_callback);
+
+    // Show text input dialog
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInputSSID);
+}
+
+/**
+ * @brief      Handler for Password configuration item click.
+ * @param      context  The context - WebCrawlerApp object.
+ * @param      index    The index of the item that was clicked.
+ */
+static void web_crawler_setting_item_password_clicked(void *context, uint32_t index)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    UNUSED(index);
+
+    // Set up the text input
+    text_input_set_header_text(app->text_input_password, "Enter Password");
+
+    // Initialize temp_buffer with existing password
+    strncpy(app->temp_buffer_password, app->password, app->temp_buffer_size_password - 1);
+    app->temp_buffer_password[app->temp_buffer_size_password - 1] = '\0';
+
+    // Configure the text input
+    bool clear_previous_text = false;
+    text_input_set_result_callback(
+        app->text_input_password,
+        web_crawler_set_password_update,
+        app,
+        app->temp_buffer_password,
+        app->temp_buffer_size_password,
+        clear_previous_text);
+
+    // Set the previous callback to return to Configure screen
+    view_set_previous_callback(
+        text_input_get_view(app->text_input_password),
+        web_crawler_back_to_configure_callback);
+
+    // Show text input dialog
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInputPassword);
+}

+ 56 - 0
web_crawler_e.h

@@ -0,0 +1,56 @@
+// Define the submenu items for our WebCrawler application
+typedef enum
+{
+    WebCrawlerSubmenuIndexRun,    // The main screen
+    WebCrawlerSubmenuIndexAbout,  // The about screen
+    WebCrawlerSubmenuIndexSetPath // The configuration screen
+} WebCrawlerSubmenuIndex;
+
+// Define views for our WebCrawler application
+typedef enum
+{
+    WebCrawlerViewMain,              // The main screen
+    WebCrawlerViewSubmenu,           // The menu when the app starts
+    WebCrawlerViewAbout,             // The about screen
+    WebCrawlerViewConfigure,         // The configuration screen
+    WebCrawlerViewTextInput,         // Text input screen for Path
+    WebCrawlerViewTextInputSSID,     // Text input screen for SSID
+    WebCrawlerViewTextInputPassword, // Text input screen for Password
+} WebCrawlerView;
+
+// Define a separate model for the main view
+typedef struct
+{
+    char path[128];     // Store the entered website path
+    char ssid[128];     // Store the entered SSID
+    char password[128]; // Store the entered password
+} WebCrawlerMainModel;
+
+// Define the application structure
+typedef struct
+{
+    ViewDispatcher *view_dispatcher;             // Switches between our views
+    View *view_main;                             // The main screen that displays the main content
+    Submenu *submenu;                            // The application submenu
+    Widget *widget_about;                        // The about screen
+    TextInput *text_input_path;                  // Text input screen for Path
+    TextInput *text_input_ssid;                  // Text input screen for SSID
+    TextInput *text_input_password;              // Text input screen for Password
+    VariableItemList *variable_item_list_config; // The configuration screen
+
+    char *path;                  // The path to the website
+    char *ssid;                  // The SSID of the WiFi network
+    char *password;              // The password of the WiFi network
+    VariableItem *path_item;     // Reference to the path configuration item
+    VariableItem *ssid_item;     // Reference to the SSID configuration item
+    VariableItem *password_item; // Reference to the password configuration item
+
+    char *temp_buffer_path;         // Temporary buffer for text input (Path)
+    uint32_t temp_buffer_size_path; // Size of the temporary buffer
+
+    char *temp_buffer_ssid;         // Temporary buffer for text input (SSID)
+    uint32_t temp_buffer_size_ssid; // Size of the temporary buffer
+
+    char *temp_buffer_password;         // Temporary buffer for text input (Password)
+    uint32_t temp_buffer_size_password; // Size of the temporary buffer
+} WebCrawlerApp;

+ 78 - 0
web_crawler_free.h

@@ -0,0 +1,78 @@
+// Function to free allocated buffers
+static void free_buffers(WebCrawlerApp *app)
+{
+    free(app->path);
+    free(app->temp_buffer_path);
+    free(app->ssid);
+    free(app->temp_buffer_ssid);
+    free(app->password);
+    free(app->temp_buffer_password);
+}
+
+static void free_resources(WebCrawlerApp *app)
+{
+    free_buffers(app);
+    free(app);
+}
+
+static void free_inputs(WebCrawlerApp *app)
+{
+    free(app->temp_buffer_path);
+    free(app->temp_buffer_ssid);
+    free(app->temp_buffer_password);
+}
+
+static void free_all(WebCrawlerApp *app, char *reason)
+{
+    FURI_LOG_E(TAG, reason);
+    view_free(app->view_main);
+    submenu_free(app->submenu);
+    variable_item_list_free(app->variable_item_list_config);
+    free_inputs(app);
+    view_dispatcher_free(app->view_dispatcher);
+    furi_record_close(RECORD_GUI);
+    free_resources(app);
+}
+
+/**
+ * @brief      Function to free the resources used by WebCrawlerApp.
+ * @param      app  The WebCrawlerApp object to free.
+ */
+static void web_crawler_app_free(WebCrawlerApp *app)
+{
+    if (!app)
+        return;
+
+    // Remove and free Main view
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewMain);
+    view_free(app->view_main);
+
+    // Remove and free Submenu
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewSubmenu);
+    submenu_free(app->submenu);
+
+    // Remove and free Configuration screen
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewConfigure);
+    variable_item_list_free(app->variable_item_list_config);
+
+    // Remove and free Text Input views
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInput);
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInputSSID);
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInputPassword);
+    free_inputs(app);
+
+    // Remove and free About view
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewAbout);
+    widget_free(app->widget_about);
+
+    // Free the ViewDispatcher and close GUI
+    view_dispatcher_free(app->view_dispatcher);
+    furi_record_close(RECORD_GUI);
+
+    // deinit uart
+    uart_deinit();
+
+    // Free the application structure
+    free_buffers(app);
+    free(app);
+}

+ 335 - 0
web_crawler_i.h

@@ -0,0 +1,335 @@
+/**
+ * @brief      Function to allocate resources for the WebCrawlerApp.
+ * @return     Pointer to the initialized WebCrawlerApp, or NULL on failure.
+ */
+// In web_crawler_app_alloc, after allocating and initializing 'app'
+static WebCrawlerApp *web_crawler_app_alloc()
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)malloc(sizeof(WebCrawlerApp));
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate WebCrawlerApp");
+        return NULL;
+    }
+
+    // Initialize the entire structure to zero to prevent undefined behavior
+    memset(app, 0, sizeof(WebCrawlerApp));
+
+    // Allocate and initialize temp_buffer and path
+    app->temp_buffer_size_path = 128;
+    app->temp_buffer_path = (char *)malloc(app->temp_buffer_size_path);
+    if (!app->temp_buffer_path)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate temp_buffer_path");
+        free(app);
+        return NULL;
+    }
+    app->temp_buffer_path[0] = '\0';
+
+    // Allocate path
+    app->path = (char *)malloc(app->temp_buffer_size_path);
+    if (!app->path)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate path");
+        free(app->temp_buffer_path);
+        free(app);
+        return NULL;
+    }
+    app->path[0] = '\0';
+
+    // Allocate and initialize temp_buffer_ssid
+    app->temp_buffer_size_ssid = 128;
+    app->temp_buffer_ssid = (char *)malloc(app->temp_buffer_size_ssid);
+    if (!app->temp_buffer_ssid)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate temp_buffer_ssid");
+        free_buffers(app);
+        return NULL;
+    }
+    app->temp_buffer_ssid[0] = '\0';
+
+    // Allocate ssid
+    app->ssid = (char *)malloc(app->temp_buffer_size_ssid);
+    if (!app->ssid)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate ssid");
+        free_buffers(app);
+        return NULL;
+    }
+    app->ssid[0] = '\0';
+
+    // Allocate and initialize temp_buffer_password
+    app->temp_buffer_size_password = 128;
+    app->temp_buffer_password = (char *)malloc(app->temp_buffer_size_password);
+    if (!app->temp_buffer_password)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate temp_buffer_password");
+        free_buffers(app);
+        return NULL;
+    }
+    app->temp_buffer_password[0] = '\0';
+
+    // Allocate password
+    app->password = (char *)malloc(app->temp_buffer_size_password);
+    if (!app->password)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate password");
+        free_buffers(app);
+        return NULL;
+    }
+    app->password[0] = '\0';
+
+    // Assign to global variable
+    app_instance = app;
+
+    // Open GUI
+    Gui *gui = furi_record_open(RECORD_GUI);
+    if (!gui)
+    {
+        FURI_LOG_E(TAG, "Failed to open GUI record");
+        free_resources(app);
+        return NULL;
+    }
+
+    // Allocate ViewDispatcher
+    app->view_dispatcher = view_dispatcher_alloc();
+    if (!app->view_dispatcher)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate ViewDispatcher");
+        furi_record_close(RECORD_GUI);
+        free_resources(app);
+        return NULL;
+    }
+
+    // Attach ViewDispatcher to GUI
+    view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+
+    // Allocate TextInput views
+    app->text_input_path = text_input_alloc();
+    if (!app->text_input_path)
+    {
+        free_all(app, "Failed to allocate TextInput for Path");
+        return NULL;
+    }
+
+    app->text_input_ssid = text_input_alloc();
+    if (!app->text_input_ssid)
+    {
+        free_all(app, "Failed to allocate TextInput for SSID");
+        return NULL;
+    }
+
+    app->text_input_password = text_input_alloc();
+    if (!app->text_input_password)
+    {
+        free_all(app, "Failed to allocate TextInput for Password");
+        return NULL;
+    }
+
+    // Add TextInput views with unique view IDs
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        WebCrawlerViewTextInput,
+        text_input_get_view(app->text_input_path));
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        WebCrawlerViewTextInputSSID,
+        text_input_get_view(app->text_input_ssid));
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        WebCrawlerViewTextInputPassword,
+        text_input_get_view(app->text_input_password));
+
+    // Set previous callback for TextInput views to return to Configure screen
+    view_set_previous_callback(
+        text_input_get_view(app->text_input_path),
+        web_crawler_back_to_configure_callback);
+    view_set_previous_callback(
+        text_input_get_view(app->text_input_ssid),
+        web_crawler_back_to_configure_callback);
+    view_set_previous_callback(
+        text_input_get_view(app->text_input_password),
+        web_crawler_back_to_configure_callback);
+
+    // Allocate Configuration screen
+    app->variable_item_list_config = variable_item_list_alloc();
+    if (!app->variable_item_list_config)
+    {
+        free_all(app, "Failed to allocate VariableItemList for Configuration");
+        return NULL;
+    }
+    variable_item_list_reset(app->variable_item_list_config);
+
+    // Add "Path" item to the configuration screen
+    app->path_item = variable_item_list_add(
+        app->variable_item_list_config,
+        "Path",
+        1,    // Number of possible values (1 for a single text value)
+        NULL, // No change callback needed
+        NULL  // No context needed
+    );
+    if (!app->path_item)
+    {
+        free_all(app, "Failed to add Path item to VariableItemList");
+        return NULL;
+    }
+    variable_item_set_current_value_text(app->path_item, ""); // Initialize
+
+    // Add "SSID" item to the configuration screen
+    app->ssid_item = variable_item_list_add(
+        app->variable_item_list_config,
+        "SSID",
+        1,    // Number of possible values (1 for a single text value)
+        NULL, // No change callback needed
+        NULL  // No context needed
+    );
+    if (!app->ssid_item)
+    {
+        free_all(app, "Failed to add SSID item to VariableItemList");
+        return NULL;
+    }
+    variable_item_set_current_value_text(app->ssid_item, ""); // Initialize
+
+    // Add "Password" item to the configuration screen
+    app->password_item = variable_item_list_add(
+        app->variable_item_list_config,
+        "Password",
+        1,    // Number of possible values (1 for a single text value)
+        NULL, // No change callback needed
+        NULL  // No context needed
+    );
+    if (!app->password_item)
+    {
+        free_all(app, "Failed to add Password item to VariableItemList");
+        return NULL;
+    }
+    variable_item_set_current_value_text(app->password_item, ""); // Initialize
+
+    // Set a single enter callback for all configuration items
+    variable_item_list_set_enter_callback(
+        app->variable_item_list_config,
+        web_crawler_config_enter_callback,
+        app);
+
+    // Set previous callback for configuration screen
+    view_set_previous_callback(
+        variable_item_list_get_view(app->variable_item_list_config),
+        web_crawler_back_to_main_callback);
+
+    // Add Configuration view to ViewDispatcher
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        WebCrawlerViewConfigure,
+        variable_item_list_get_view(app->variable_item_list_config));
+
+    // Allocate Submenu view
+    app->submenu = submenu_alloc();
+    if (!app->submenu)
+    {
+        free_all(app, "Failed to allocate Submenu");
+        return NULL;
+    }
+
+    // Add items to Submenu
+    submenu_add_item(app->submenu, "Run", WebCrawlerSubmenuIndexRun, web_crawler_submenu_callback, app);
+    submenu_add_item(app->submenu, "About", WebCrawlerSubmenuIndexAbout, web_crawler_submenu_callback, app);
+    submenu_add_item(app->submenu, "Configure", WebCrawlerSubmenuIndexSetPath, web_crawler_submenu_callback, app);
+
+    // Set previous callback for Submenu
+    view_set_previous_callback(submenu_get_view(app->submenu), web_crawler_exit_app_callback);
+
+    // Initialize UART
+    uart_init();
+
+    // Add Submenu view to ViewDispatcher
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        WebCrawlerViewSubmenu,
+        submenu_get_view(app->submenu));
+
+    // Allocate Main view
+    app->view_main = view_alloc();
+    if (!app->view_main)
+    {
+        free_all(app, "Failed to allocate Main view");
+        return NULL;
+    }
+    view_set_draw_callback(app->view_main, web_crawler_view_draw_callback);
+    view_set_input_callback(app->view_main, web_crawler_view_input_callback);
+    view_set_previous_callback(app->view_main, web_crawler_back_to_main_callback);
+
+    // Allocate and initialize the main view's model
+    view_allocate_model(app->view_main, ViewModelTypeLockFree, sizeof(WebCrawlerMainModel));
+    WebCrawlerMainModel *main_model = (WebCrawlerMainModel *)view_get_model(app->view_main);
+    if (main_model)
+    {
+        strncpy(main_model->path, "", sizeof(main_model->path) - 1);         // Initialize to empty
+        strncpy(main_model->ssid, "", sizeof(main_model->ssid) - 1);         // Initialize to empty
+        strncpy(main_model->password, "", sizeof(main_model->password) - 1); // Initialize to empty
+        main_model->path[sizeof(main_model->path) - 1] = '\0';
+        main_model->ssid[sizeof(main_model->ssid) - 1] = '\0';
+        main_model->password[sizeof(main_model->password) - 1] = '\0';
+    }
+    else
+    {
+        free_all(app, "Failed to allocate main view model");
+        return NULL;
+    }
+
+    // Add Main view to ViewDispatcher
+    view_dispatcher_add_view(app->view_dispatcher, WebCrawlerViewMain, app->view_main);
+
+    // Allocate About view
+    app->widget_about = widget_alloc();
+    if (!app->widget_about)
+    {
+        free_all(app, "Failed to allocate About widget");
+        return NULL;
+    }
+
+    // Add text to About widget
+    widget_add_text_scroll_element(
+        app->widget_about,
+        0,
+        0,
+        128,
+        64,
+        "Web Crawler App\n---\nThis is a web crawler app for Flipper Zero.\n---\nVisit github.com/jblanked for more details.\n---\nPress BACK to return.");
+
+    // Load settings
+    if (!load_settings(app->path, app->temp_buffer_size_path, app->ssid, app->temp_buffer_size_ssid, app->password, app->temp_buffer_size_password, app))
+    {
+        FURI_LOG_E(TAG, "Failed to load settings");
+    }
+    else
+    {
+        // Update the main view's model
+        WebCrawlerMainModel *main_model = (WebCrawlerMainModel *)view_get_model(app->view_main);
+        if (main_model)
+        {
+            strncpy(main_model->path, app->path, sizeof(main_model->path) - 1);
+            main_model->path[sizeof(main_model->path) - 1] = '\0';
+            strncpy(main_model->ssid, app->ssid, sizeof(main_model->ssid) - 1);
+            main_model->ssid[sizeof(main_model->ssid) - 1] = '\0';
+            strncpy(main_model->password, app->password, sizeof(main_model->password) - 1);
+            main_model->password[sizeof(main_model->password) - 1] = '\0';
+        }
+    }
+
+    // Set previous callback for About view
+    view_set_previous_callback(
+        widget_get_view(app->widget_about),
+        web_crawler_back_to_main_callback);
+
+    // Add About view to ViewDispatcher
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        WebCrawlerViewAbout,
+        widget_get_view(app->widget_about));
+
+    // Start with the Submenu view
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewSubmenu);
+
+    return app;
+}

+ 121 - 0
web_crawler_storage.h

@@ -0,0 +1,121 @@
+#include <string.h>
+#include <stdio.h>
+#include <stdbool.h>
+
+// Function to save settings: path, SSID, and password
+static void save_settings(const char *path, const char *ssid, const char *password)
+{
+    // Create the directory for saving settings
+    char directory_path[256];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler_app");
+
+    // Create the directory
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(storage, directory_path);
+
+    // Open the settings file
+    File *file = storage_file_alloc(storage);
+    if (!storage_file_open(file, SETTINGS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E(TAG, "Failed to open settings file for writing: %s", SETTINGS_PATH);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return;
+    }
+
+    // Save the path length and data
+    size_t path_length = strlen(path) + 1; // Include null terminator
+    if (storage_file_write(file, &path_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, path, path_length) != path_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write path");
+    }
+
+    // Save the SSID length and data
+    size_t ssid_length = strlen(ssid) + 1; // Include null terminator
+    if (storage_file_write(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, ssid, ssid_length) != ssid_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write SSID");
+    }
+
+    // Save the password length and data
+    size_t password_length = strlen(password) + 1; // Include null terminator
+    if (storage_file_write(file, &password_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, password, password_length) != password_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write password");
+    }
+
+    FURI_LOG_I(TAG, "Settings saved: path=%s, ssid=%s, password=%s", path, ssid, password);
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+}
+
+// Function to load settings: path, SSID, and password
+static bool load_settings(char *path, size_t path_size, char *ssid, size_t ssid_size, char *password, size_t password_size, WebCrawlerApp *app)
+{
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    File *file = storage_file_alloc(storage);
+
+    if (!storage_file_open(file, SETTINGS_PATH, FSAM_READ, FSOM_OPEN_EXISTING))
+    {
+        FURI_LOG_E(TAG, "Failed to open settings file for reading: %s", SETTINGS_PATH);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false; // Return false if the file does not exist
+    }
+
+    // Load the path
+    size_t path_length;
+    if (storage_file_read(file, &path_length, sizeof(size_t)) != sizeof(size_t) || path_length > path_size ||
+        storage_file_read(file, path, path_length) != path_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read path");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    path[path_length - 1] = '\0'; // Ensure null-termination
+
+    // Load the SSID
+    size_t ssid_length;
+    if (storage_file_read(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || ssid_length > ssid_size ||
+        storage_file_read(file, ssid, ssid_length) != ssid_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read SSID");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    ssid[ssid_length - 1] = '\0'; // Ensure null-termination
+
+    // Load the password
+    size_t password_length;
+    if (storage_file_read(file, &password_length, sizeof(size_t)) != sizeof(size_t) || password_length > password_size ||
+        storage_file_read(file, password, password_length) != password_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read password");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    password[password_length - 1] = '\0'; // Ensure null-termination
+
+    FURI_LOG_I(TAG, "Settings loaded: path=%s, ssid=%s, password=%s", path, ssid, password);
+
+    // set the path, ssid, and password
+    strncpy(app->path, path, path_size);
+    strncpy(app->ssid, ssid, ssid_size);
+    strncpy(app->password, password, password_size);
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+    return true;
+}

+ 179 - 0
web_crawler_uart.h

@@ -0,0 +1,179 @@
+#include <storage/storage.h>
+#include <furi_hal_gpio.h>
+#include <furi_hal_serial.h>
+
+#define UART_CH (FuriHalSerialIdUsart)
+#define BAUDRATE (115200)
+#define RX_BUF_SIZE 256
+#define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler_app/settings.bin"
+#define SETTINGS_PATH_SD "/apps_data/web_crawler_app/settings.txt"
+#define TAG "WebCrawler"
+
+typedef struct
+{
+    FuriStreamBuffer *uart_stream; // Stream buffer for UART communication
+    FuriHalSerialHandle *serial_handle;
+} WebCrawlerUart;
+
+// Declare the UART instance
+WebCrawlerUart uart;
+
+static void uart_rx_callback(FuriHalSerialHandle *handle, FuriHalSerialRxEvent event, void *context)
+{
+    UNUSED(context);
+    if (event == FuriHalSerialRxEventData)
+    {
+        uint8_t data = furi_hal_serial_async_rx(handle);        // Read the incoming byte
+        furi_stream_buffer_send(uart.uart_stream, &data, 1, 0); // Send to stream buffer
+    }
+}
+
+// Initialize UART
+void uart_init()
+{
+    uart.uart_stream = furi_stream_buffer_alloc(RX_BUF_SIZE * 10, 1); // Increase buffer size if necessary
+    uart.serial_handle = furi_hal_serial_control_acquire(UART_CH);
+    furi_hal_serial_init(uart.serial_handle, BAUDRATE);
+    furi_hal_serial_async_rx_start(uart.serial_handle, uart_rx_callback, NULL, false);
+    FURI_LOG_I(TAG, "UART initialized.");
+}
+
+// Deinitialize UART
+void uart_deinit()
+{
+    furi_hal_serial_async_rx_stop(uart.serial_handle);
+    furi_hal_serial_deinit(uart.serial_handle);
+    furi_hal_serial_control_release(uart.serial_handle);
+    furi_stream_buffer_free(uart.uart_stream);
+}
+
+// Function to send settings via UART
+void send_settings_via_uart(const char *path, const char *ssid, const char *password)
+{
+    char buffer[512];
+    snprintf(buffer, sizeof(buffer), "{\"path\":\"%s\",\"ssid\":\"%s\",\"password\":\"%s\"}", path, ssid, password);
+    size_t data_length = strlen(buffer);
+
+    FURI_LOG_I(TAG, "Sending settings via UART: %s", buffer);
+    furi_hal_serial_tx(uart.serial_handle, (uint8_t *)buffer, data_length);
+}
+
+bool first_run = true;
+
+// Function to read data from UART after sending settings and save to a file
+bool read_data_from_uart_and_save(Canvas *canvas) // Pass the canvas context
+{
+    canvas_draw_str(canvas, 1, 35, "Initializing...");
+
+    char line_buffer[RX_BUF_SIZE + 1];
+    bool started = false;
+    size_t total_received_data = 0;
+
+    // Full path for the output file where we will save the received data
+    const char *output_file_path = STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler_app/received_data.txt";
+
+    // Ensure the directory exists
+    char directory_path[256];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler_app");
+    Storage *storage = NULL;
+    File *file = NULL;
+
+    if (first_run)
+    {
+        first_run = false;
+        return false;
+    }
+
+    FURI_LOG_I(TAG, "Waiting for data...");
+    canvas_draw_str(canvas, 1, 50, "Saving data. Please wait");
+
+    // Start reading data from UART line by line
+    while (true)
+    {
+        size_t bytes_received = furi_stream_buffer_receive(uart.uart_stream, line_buffer, RX_BUF_SIZE, 2000); // Increase the timeout to 2000 ms
+
+        if (bytes_received > 0)
+        {
+            line_buffer[bytes_received] = '\0'; // Null-terminate
+
+            // Remove any carriage returns
+            for (size_t i = 0; i < bytes_received; i++)
+            {
+                if (line_buffer[i] == '\r')
+                {
+                    memmove(&line_buffer[i], &line_buffer[i + 1], bytes_received - i);
+                    bytes_received--;
+                    i--;
+                }
+            }
+
+            // Check for the start marker
+            if (!started && strstr(line_buffer, "[BIN/STARTED]") != NULL)
+            {
+                FURI_LOG_I(TAG, "Started receiving data... please wait and do not disconnect the device or leave this screen.");
+                canvas_draw_str(canvas, 1, 60, "Receiving data...");
+                started = true;
+
+                // Now open the storage and file for writing since we've started receiving data
+                storage = furi_record_open(RECORD_STORAGE);
+                storage_common_mkdir(storage, directory_path); // Create directory if it doesn't exist
+                file = storage_file_alloc(storage);
+
+                if (!storage_file_open(file, output_file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+                {
+                    FURI_LOG_E(TAG, "Failed to open output file for writing.");
+                    canvas_draw_str(canvas, 1, 60, "Failed to open file");
+                    storage_file_free(file);
+                    furi_record_close(RECORD_STORAGE);
+                    return false;
+                }
+
+                continue; // Skip to the next iteration after opening the file
+            }
+
+            // Check for the finish marker
+            if (started && strstr(line_buffer, "[BIN/FINISHED]") != NULL)
+            {
+                FURI_LOG_I(TAG, "Finished receiving data.");
+                canvas_draw_str(canvas, 1, 70, "Finished GET request.");
+                break; // End the loop once the finish marker is detected
+            }
+
+            // If the data transfer has started, save data to the file
+            if (started && file)
+            {
+                storage_file_write(file, line_buffer, bytes_received);
+                total_received_data += bytes_received;
+            }
+        }
+        else
+        {
+            // Timeout or no data received
+            FURI_LOG_E(TAG, "Timeout.");
+            break;
+        }
+    }
+
+    // Close the file and storage only if they were opened
+    if (file)
+    {
+        storage_file_close(file);
+        storage_file_free(file);
+    }
+    if (storage)
+    {
+        furi_record_close(RECORD_STORAGE);
+    }
+
+    FURI_LOG_I(TAG, "Data reception complete. Total bytes received: %zu", total_received_data);
+
+    if (total_received_data > 0)
+    {
+        canvas_draw_str(canvas, 1, 70, "Complete!");
+        return true;
+    }
+    else
+    {
+        return false;
+    }
+}