jblanked hace 1 año
padre
commit
dc873878fd

BIN
.DS_Store


+ 7 - 1
CHANGELOG.md

@@ -1,2 +1,8 @@
+## 0.2 (Stability Patch)
+- Changed serial functions from synchronous to asynchronous.
+- Added error handling for GET requests and WiFi Dev Board connections.
+- Updated the WiFi Dev Board firmware (FlipperHTTP) to trigger three quick green LED blinks when the board is first connected, a solid green LED when the board receives a serial command, and a solid green LED while the board is processing.
+- Added auto-connect and auto-disconnect for WiFi when a user enters or leaves the app.
+
 ## 0.1
-- Initial Release
+- Initial release

+ 1 - 1
README.md

@@ -3,7 +3,7 @@
 **Web Crawler** is a custom application designed for the Flipper Zero device. This app allows users to configure and manage web crawling operations directly from their Flipper Zero.
 
 ## Requirements
-- WiFi Devboard for Flipper Zero with WebCrawler Flash: https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/WebCrawler-WifiDevBoard
+- WiFi Devboard for Flipper Zero with WebCrawler Flash: https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP
 - Wifi Access Point
 
 ## Features

+ 15 - 16
app.c

@@ -1,22 +1,9 @@
-#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 <flipper_http.h>
 #include <web_crawler_free.h>
+#include <web_crawler_callback.h>
 #include <web_crawler_i.h>
+#include <web_crawler_storage.h>
 /**
  * @brief      Entry point for the WebCrawler application.
  * @param      p  Input parameter - unused
@@ -33,6 +20,18 @@ int32_t web_crawler_app(void *p)
         return -1;
     }
 
+    // send settings and connect wifi
+    if (!flipper_http_connect_wifi())
+    {
+        FURI_LOG_E(TAG, "Failed to connect to WiFi");
+    }
+
+    if (!flipper_http_ping())
+    {
+        FURI_LOG_E(TAG, "Failed to ping the device");
+        return -1;
+    }
+
     // Run the application
     view_dispatcher_run(app->view_dispatcher);
 

+ 1 - 1
application.fam

@@ -8,7 +8,7 @@ App(
         "gui", 
     ],
     order=10,
-    fap_version = (0, 1),
+    fap_version = (0, 2),
     fap_icon="app.png",
     fap_category="GPIO",
     fap_author="JBlanked",

BIN
assets/.DS_Store


BIN
assets/FlipperHTTP/.DS_Store


+ 48 - 0
assets/FlipperHTTP/README.md

@@ -0,0 +1,48 @@
+# FlipperHTTP
+HTTP library for Flipper Zero. Compatible with Wifi Dev Board for Flipper Zero (ESP32S2 Dev Module).
+
+## Installation
+1. Download the `flipper_http_bootloader.bin`, `flipper_http_firmware_a.bin`, and `flipper_http_partitions.bin` files.
+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. Disconnect your Flipper Zero from your computer.
+8. Plug your Wi-Fi Devboard into the Flipper.
+9. Press and keep holding the boot button while you press the reset button once, release the boot button after 2 seconds.
+10. 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.
+11. Click on Manual Flash.
+12. Click on Bootloader and select the `flipper_http_bootloader.bin` that you downloaded earlier.
+13. Click on Part Table and select the `flipper_http_partitions.bin` that you downloaded earlier.
+14. Click on FirmwareA and select the `flipper_http_firmware_a.bin` that you downloaded earlier.
+15. Click on FLASH - fast and follow the instructions on the screen.
+16. Now you are all set to use the Web Crawler app.
+
+Guide: https://www.youtube.com/watch?v=Y2lUVTMTABE&t=19s
+
+
+# Usage
+The `flipper_http.h` file is for seamless use of the FlipperHTTp Firmware in `C` applications and is the root of the `Web Crawler` app. A Javascript library will be available soon, followed by a mPython library.
+
+## General
+- Init:
+    - `flipper_http_init(FlipperHTTP_Callback callback, void *context)`
+- DeInit:
+    - `flipper_http_deinit()`
+
+## Wifi
+- Connect To Wifi: 
+    - `flipper_http_connect_wifi()`
+- Disconnect: 
+    - `flipper_http_disconnect_wifi()`
+- Ping: 
+    - `flipper_http_ping()`
+- Save Wifi: 
+    - `flipper_http_save_wifi(const char *ssid, const char *password)`
+
+## Extras
+- Send Data:
+    - `flipper_http_send_data(const char *data)`
+- Rx Callback:
+    - `flipper_http_rx_callback(const char *line, void *context)`

+ 600 - 0
assets/FlipperHTTP/flipper_http.h

@@ -0,0 +1,600 @@
+// flipper_http.h
+#ifndef FLIPPER_HTTP_H
+#define FLIPPER_HTTP_H
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_gpio.h>
+#include <furi_hal_serial.h>
+#include <storage/storage.h>
+
+// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext
+
+#define HTTP_TAG "WebCrawler"
+#define http_tag "web_crawler_app"
+#define UART_CH (FuriHalSerialIdUsart)
+#define TIMEOUT_DURATION_TICKS (2 * 1000) // 2 seconds
+#define BAUDRATE (115200)
+#define RX_BUF_SIZE 1024
+
+// UART RX Handler Callback declaration
+void flipper_http_rx_callback(const char *line, void *context);
+
+// Function to save received data to a file
+bool flipper_http_save_received_data(size_t bytes_received, const char line_buffer[]);
+
+// Define GPIO pins for UART
+GpioPin test_pins[2] = {
+    {.port = GPIOA, .pin = LL_GPIO_PIN_7}, // USART1_RX
+    {.port = GPIOA, .pin = LL_GPIO_PIN_6}  // USART1_TX
+};
+
+// State variable to track if serial is receiving, sending, or idle
+typedef enum
+{
+    INACTIVE,  // Inactive state
+    IDLE,      // Default state
+    RECEIVING, // Receiving data
+    SENDING,   // Sending data
+    ISSUE,     // Issue with connection
+} SerialState;
+
+// Event Flags for UART Worker Thread
+typedef enum
+{
+    WorkerEvtStop = (1 << 0),
+    WorkerEvtRxDone = (1 << 1),
+} WorkerEvtFlags;
+
+// Forward declaration for callback
+typedef void (*FlipperHTTP_Callback)(const char *line, void *context);
+
+// FlipperHTTP Structure
+typedef struct
+{
+    FuriStreamBuffer *flipper_http_stream; // Stream buffer for UART communication
+    FuriHalSerialHandle *serial_handle;    // Serial handle for UART communication
+    FuriThread *rx_thread;
+    uint8_t rx_buf[RX_BUF_SIZE];
+    FuriThreadId rx_thread_id;
+    FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines
+    void *callback_context;                 // Context for the callback
+    SerialState state;
+
+    // variable to store the last received data from the UART
+    char *last_response;
+
+    // Timer-related members
+    FuriTimer *get_timeout_timer; // Timer for GET request timeout
+    bool started_receiving;       // Indicates if a GET request has started
+    bool just_started;            // Indicates if data reception has just started
+    char *received_data;          // Buffer to store received data
+} FlipperHTTP;
+
+// Declare uart as extern to prevent multiple definitions
+FlipperHTTP fhttp;
+
+// Timer callback function
+void get_timeout_timer_callback(void *context)
+{
+    UNUSED(context);
+    FURI_LOG_E(HTTP_TAG, "Timeout reached: 2 seconds without receiving [GET/END]...");
+
+    // Reset the state
+    fhttp.started_receiving = false;
+    fhttp.just_started = false;
+
+    // Free received data if any
+    if (fhttp.received_data)
+    {
+        free(fhttp.received_data);
+        fhttp.received_data = NULL;
+    }
+
+    // Update UART state
+    fhttp.state = ISSUE;
+}
+
+// UART RX Handler Callback (Interrupt Context)
+static void _flipper_http_rx_callback(FuriHalSerialHandle *handle, FuriHalSerialRxEvent event, void *context)
+{
+    UNUSED(context);
+    if (event == FuriHalSerialRxEventData)
+    {
+        uint8_t data = furi_hal_serial_async_rx(handle);
+        furi_stream_buffer_send(fhttp.flipper_http_stream, &data, 1, 0);
+        furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtRxDone);
+    }
+}
+
+// UART worker thread
+static int32_t flipper_http_worker(void *context)
+{
+    UNUSED(context);
+    size_t rx_line_pos = 0;
+    char rx_line_buffer[256]; // Buffer to collect a line
+
+    while (1)
+    {
+        uint32_t events = furi_thread_flags_wait(WorkerEvtStop | WorkerEvtRxDone, FuriFlagWaitAny, FuriWaitForever);
+        if (events & WorkerEvtStop)
+            break;
+        if (events & WorkerEvtRxDone)
+        {
+            size_t len = furi_stream_buffer_receive(fhttp.flipper_http_stream, fhttp.rx_buf, RX_BUF_SIZE * 10, 0);
+            for (size_t i = 0; i < len; i++)
+            {
+                char c = fhttp.rx_buf[i];
+                if (c == '\n' || rx_line_pos >= sizeof(rx_line_buffer) - 1)
+                {
+                    rx_line_buffer[rx_line_pos] = '\0';
+                    // Invoke the callback with the complete line
+                    if (fhttp.handle_rx_line_cb)
+                    {
+                        fhttp.handle_rx_line_cb(rx_line_buffer, fhttp.callback_context);
+                    }
+                    // Reset the line buffer
+                    rx_line_pos = 0;
+                }
+                else
+                {
+                    rx_line_buffer[rx_line_pos++] = c;
+                }
+            }
+        }
+    }
+
+    return 0;
+}
+
+// UART initialization function
+bool flipper_http_init(FlipperHTTP_Callback callback, void *context)
+{
+    fhttp.flipper_http_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1);
+    if (!fhttp.flipper_http_stream)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate UART stream buffer.");
+        return false;
+    }
+
+    fhttp.rx_thread = furi_thread_alloc();
+    if (!fhttp.rx_thread)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate UART thread.");
+        furi_stream_buffer_free(fhttp.flipper_http_stream);
+        return false;
+    }
+
+    furi_thread_set_name(fhttp.rx_thread, "FlipperHTTP_RxThread");
+    furi_thread_set_stack_size(fhttp.rx_thread, 1024);
+    furi_thread_set_context(fhttp.rx_thread, &fhttp);
+    furi_thread_set_callback(fhttp.rx_thread, flipper_http_worker);
+
+    fhttp.handle_rx_line_cb = callback;
+    fhttp.callback_context = context;
+
+    furi_thread_start(fhttp.rx_thread);
+    fhttp.rx_thread_id = furi_thread_get_id(fhttp.rx_thread);
+
+    // Initialize GPIO pins for UART
+    furi_hal_gpio_init_simple(&test_pins[0], GpioModeInput);
+    furi_hal_gpio_init_simple(&test_pins[1], GpioModeOutputPushPull);
+
+    fhttp.serial_handle = NULL;
+
+    fhttp.serial_handle = furi_hal_serial_control_acquire(UART_CH);
+    if (fhttp.serial_handle == NULL)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to acquire UART control - handle is NULL");
+        // Cleanup resources
+        furi_thread_free(fhttp.rx_thread);
+        furi_stream_buffer_free(fhttp.flipper_http_stream);
+        return false;
+    }
+
+    // Initialize UART with acquired handle
+    furi_hal_serial_init(fhttp.serial_handle, BAUDRATE);
+
+    // Enable RX direction
+    furi_hal_serial_enable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx);
+
+    // Start asynchronous RX with the callback
+    furi_hal_serial_async_rx_start(fhttp.serial_handle, _flipper_http_rx_callback, &fhttp, false);
+
+    // Wait for the TX to complete to ensure UART is ready
+    furi_hal_serial_tx_wait_complete(fhttp.serial_handle);
+
+    // Allocate the timer for handling timeouts
+    fhttp.get_timeout_timer = furi_timer_alloc(
+        get_timeout_timer_callback, // Callback function
+        FuriTimerTypeOnce,          // One-shot timer
+        &fhttp                      // Context passed to callback
+    );
+
+    if (!fhttp.get_timeout_timer)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate GET timeout timer.");
+        // Cleanup resources
+        furi_hal_serial_async_rx_stop(fhttp.serial_handle);
+        furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx);
+        furi_hal_serial_control_release(fhttp.serial_handle);
+        furi_hal_serial_deinit(fhttp.serial_handle);
+        furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop);
+        furi_thread_join(fhttp.rx_thread);
+        furi_thread_free(fhttp.rx_thread);
+        furi_stream_buffer_free(fhttp.flipper_http_stream);
+        return false;
+    }
+
+    // Set the timer thread priority if needed
+    furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
+
+    FURI_LOG_I(HTTP_TAG, "UART initialized successfully.");
+    return true;
+}
+
+// Deinitialize UART
+void flipper_http_deinit()
+{
+    if (fhttp.serial_handle == NULL)
+    {
+        FURI_LOG_E(HTTP_TAG, "UART handle is NULL. Already deinitialized?");
+        return;
+    }
+    // Stop asynchronous RX
+    furi_hal_serial_async_rx_stop(fhttp.serial_handle);
+
+    // Release and deinitialize the serial handle
+    furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx);
+    furi_hal_serial_control_release(fhttp.serial_handle);
+    furi_hal_serial_deinit(fhttp.serial_handle);
+
+    // Signal the worker thread to stop
+    furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop);
+    // Wait for the thread to finish
+    furi_thread_join(fhttp.rx_thread);
+    // Free the thread resources
+    furi_thread_free(fhttp.rx_thread);
+
+    // Free the stream buffer
+    furi_stream_buffer_free(fhttp.flipper_http_stream);
+
+    // Free the timer
+    if (fhttp.get_timeout_timer)
+    {
+        furi_timer_free(fhttp.get_timeout_timer);
+        fhttp.get_timeout_timer = NULL;
+    }
+
+    // Free received data if any
+    if (fhttp.received_data)
+    {
+        free(fhttp.received_data);
+        fhttp.received_data = NULL;
+    }
+
+    FURI_LOG_I("FlipperHTTP", "UART deinitialized successfully.");
+}
+
+// Function to send data over UART with newline termination
+bool flipper_http_send_data(const char *data)
+{
+    size_t data_length = strlen(data);
+    if (data_length == 0)
+    {
+        FURI_LOG_E("FlipperHTTP", "Attempted to send empty data.");
+        return false;
+    }
+
+    // Create a buffer with data + '\n'
+    size_t send_length = data_length + 1; // +1 for '\n'
+    if (send_length > 256)
+    { // Ensure buffer size is sufficient
+        FURI_LOG_E("FlipperHTTP", "Data too long to send over FHTTP.");
+        return false;
+    }
+
+    char send_buffer[257]; // 256 + 1 for safety
+    strncpy(send_buffer, data, 256);
+    send_buffer[data_length] = '\n';     // Append newline
+    send_buffer[data_length + 1] = '\0'; // Null-terminate
+
+    if (fhttp.state == INACTIVE && ((strstr(send_buffer, "[PING]") == NULL) && (strstr(send_buffer, "[WIFI/CONNECT]") == NULL)))
+    {
+        FURI_LOG_E("FlipperHTTP", "Cannot send data while INACTIVE.");
+        fhttp.last_response = "Cannot send data while INACTIVE.";
+        return false;
+    }
+
+    fhttp.state = SENDING;
+    furi_hal_serial_tx(fhttp.serial_handle, (const uint8_t *)send_buffer, send_length);
+
+    FURI_LOG_I("FlipperHTTP", "Sent data over UART: %s", send_buffer);
+    fhttp.state = IDLE;
+    return true;
+}
+
+bool flipper_http_ping()
+{
+    const char *command = "[PING]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send PING command.");
+        return false;
+    }
+    // set state as INACTIVE to be made IDLE if PONG is received
+    fhttp.state = INACTIVE;
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to save WiFi settings (returns true if successful)
+bool flipper_http_save_wifi(const char *ssid, const char *password)
+{
+    char buffer[256];
+    int ret = snprintf(buffer, sizeof(buffer), "[WIFI/SAVE]{\"ssid\":\"%s\",\"password\":\"%s\"}", ssid, password);
+    if (ret < 0 || ret >= (int)sizeof(buffer))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format WiFi save command.");
+        return false;
+    }
+
+    if (!flipper_http_send_data(buffer))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi save command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to disconnect from WiFi (returns true if successful)
+bool flipper_http_disconnect_wifi()
+{
+    const char *command = "[WIFI/DISCONNECT]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi disconnect command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to connect to WiFi (returns true if successful)
+bool flipper_http_connect_wifi()
+{
+    const char *command = "[WIFI/CONNECT]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi connect command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to send a GET request
+/**
+ * @brief      Send a GET request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the GET request to.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_get_request(const char *url)
+{
+    if (!url)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request.");
+        return false;
+    }
+
+    // Prepare GET request command
+    char command[256];
+    int ret = snprintf(command, sizeof(command), "[GET]%s", url);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format GET request command.");
+        return false;
+    }
+
+    // Send GET request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send GET request command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// UART RX Handler Callback
+void flipper_http_rx_callback(const char *line, void *context)
+{
+
+    if (!line || !context)
+    {
+        FURI_LOG_E(HTTP_TAG, "Invalid arguments provided to flipper_http_rx_callback.");
+        return;
+    }
+
+    fhttp.last_response = (char *)line;
+
+    // the only way for the state to change from INACTIVE to RECEIVING is if a PONG is received
+    if (fhttp.state != INACTIVE)
+    {
+        fhttp.state = RECEIVING;
+    }
+
+    // Process the received line
+    FURI_LOG_I(HTTP_TAG, "Received UART line: %s", line);
+
+    // Check if we've started receiving data from a GET request
+    if (fhttp.started_receiving)
+    {
+        // Restart the timeout timer each time new data is received
+        furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+
+        if (strstr(line, "[GET/END]") != NULL)
+        {
+            FURI_LOG_I(HTTP_TAG, "GET request completed.");
+            // Stop the timer since we've completed the GET request
+            furi_timer_stop(fhttp.get_timeout_timer);
+
+            if (fhttp.received_data)
+            {
+                flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data);
+                free(fhttp.received_data);
+                fhttp.received_data = NULL;
+            }
+            else
+            {
+                FURI_LOG_E(HTTP_TAG, "No data received.");
+            }
+            fhttp.started_receiving = false;
+            fhttp.just_started = false;
+            fhttp.state = IDLE;
+            return;
+        }
+
+        // Append the new line to the existing data
+        if (fhttp.received_data == NULL)
+        {
+            fhttp.received_data = (char *)malloc(strlen(line) + 2); // +2 for newline and null terminator
+            if (fhttp.received_data)
+            {
+                strcpy(fhttp.received_data, line);
+                strcat(fhttp.received_data, "\n");
+            }
+        }
+        else
+        {
+            size_t new_size = strlen(fhttp.received_data) + strlen(line) + 2; // +2 for newline and null terminator
+            fhttp.received_data = (char *)realloc(fhttp.received_data, new_size);
+            if (fhttp.received_data)
+            {
+                strcat(fhttp.received_data, line);
+                strcat(fhttp.received_data, "\n");
+            }
+        }
+
+        if (!fhttp.just_started)
+        {
+            fhttp.just_started = true;
+        }
+        return;
+    }
+
+    // Handle different types of responses
+    if (strstr(line, "[SUCCESS]") != NULL || strstr(line, "[CONNECTED]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "Operation succeeded.");
+    }
+    else if (strstr(line, "[INFO]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "Received info: %s", line);
+
+        if (fhttp.state == INACTIVE && strstr(line, "[INFO] Already connected to Wifi.") != NULL)
+        {
+            fhttp.state = IDLE;
+        }
+    }
+    else if (strstr(line, "[GET/SUCCESS]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "GET request succeeded.");
+        fhttp.started_receiving = true;
+        furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+    }
+    else if (strstr(line, "[DISCONNECTED]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "WiFi disconnected successfully.");
+    }
+    else if (strstr(line, "[ERROR]") != NULL)
+    {
+        FURI_LOG_E(HTTP_TAG, "Received error: %s", line);
+        fhttp.state = ISSUE;
+        return;
+    }
+    else if (strstr(line, "[PONG]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "Received PONG response: Wifi Dev Board is still alive.");
+
+        // send command to connect to WiFi
+        if (fhttp.state == INACTIVE)
+        {
+            fhttp.state = IDLE;
+            return;
+        }
+    }
+
+    if (fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL)
+    {
+        fhttp.state = IDLE;
+    }
+    else if (fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL)
+    {
+        fhttp.state = INACTIVE;
+    }
+    else
+    {
+        fhttp.state = IDLE;
+    }
+}
+
+bool flipper_http_save_received_data(size_t bytes_received, const char line_buffer[])
+{
+    const char *output_file_path = STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag "/received_data.txt";
+
+    // Ensure the directory exists
+    char directory_path[128];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag);
+
+    Storage *_storage = NULL;
+    File *_file = NULL;
+    // Open the storage if not opened already
+    // Initialize storage and create the directory if it doesn't exist
+    _storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(_storage, directory_path); // Create directory if it doesn't exist
+    _file = storage_file_alloc(_storage);
+
+    // Open file for writing and append data line by line
+    if (!storage_file_open(_file, output_file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to open output file for writing.");
+        storage_file_free(_file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Write each line received from the UART to the file
+    if (bytes_received > 0 && _file)
+    {
+        storage_file_write(_file, line_buffer, bytes_received);
+        storage_file_write(_file, "\n", 1); // Add a newline after each line
+    }
+    else
+    {
+        FURI_LOG_E(HTTP_TAG, "No data received.");
+        return false;
+    }
+
+    if (_file)
+    {
+        storage_file_close(_file);
+        storage_file_free(_file);
+        _file = NULL;
+    }
+    if (_storage)
+    {
+        furi_record_close(RECORD_STORAGE);
+        _storage = NULL;
+    }
+
+    return true;
+}
+
+#endif // FLIPPER_HTTP_H

BIN
assets/FlipperHTTP/flipper_http_bootloader.bin


BIN
assets/FlipperHTTP/flipper_http_firmware_a.bin


+ 0 - 0
assets/WebCrawler-WifiDevBoard/web_crawler_partitions.bin → assets/FlipperHTTP/flipper_http_partitions.bin


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

@@ -1,19 +0,0 @@
-## 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. Disconnect your Flipper Zero from your computer.
-8. Plug your Wi-Fi Devboard into the Flipper.
-9. Press and keep holding the boot button while you press the reset button once, release the boot button after 2 seconds.
-10. 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.
-11. Click on Manual Flash.
-12. Click on Bootloader and select the `web_crawler_bootloader.bin` that you downloaded earlier.
-13. Click on Part Table and select the `web_crawler_partitions.bin` that you downloaded earlier.
-14. Click on FirmwareA and select the `web_crawler_firmware_a.bin` that you downloaded earlier.
-15. Click on FLASH - fast and follow the instructions on the screen.
-16. Now you are all set to use the Web Crawler app.
-
-Guide: https://www.youtube.com/watch?v=Y2lUVTMTABE&t=19s

BIN
assets/WebCrawler-WifiDevBoard/web_crawler_bootloader.bin


BIN
assets/WebCrawler-WifiDevBoard/web_crawler_firmware_a.bin


+ 600 - 0
flipper_http.h

@@ -0,0 +1,600 @@
+// flipper_http.h
+#ifndef FLIPPER_HTTP_H
+#define FLIPPER_HTTP_H
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_gpio.h>
+#include <furi_hal_serial.h>
+#include <storage/storage.h>
+
+// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext
+
+#define HTTP_TAG "WebCrawler"
+#define http_tag "web_crawler_app"
+#define UART_CH (FuriHalSerialIdUsart)
+#define TIMEOUT_DURATION_TICKS (2 * 1000) // 2 seconds
+#define BAUDRATE (115200)
+#define RX_BUF_SIZE 1024
+
+// UART RX Handler Callback declaration
+void flipper_http_rx_callback(const char *line, void *context);
+
+// Function to save received data to a file
+bool flipper_http_save_received_data(size_t bytes_received, const char line_buffer[]);
+
+// Define GPIO pins for UART
+GpioPin test_pins[2] = {
+    {.port = GPIOA, .pin = LL_GPIO_PIN_7}, // USART1_RX
+    {.port = GPIOA, .pin = LL_GPIO_PIN_6}  // USART1_TX
+};
+
+// State variable to track if serial is receiving, sending, or idle
+typedef enum
+{
+    INACTIVE,  // Inactive state
+    IDLE,      // Default state
+    RECEIVING, // Receiving data
+    SENDING,   // Sending data
+    ISSUE,     // Issue with connection
+} SerialState;
+
+// Event Flags for UART Worker Thread
+typedef enum
+{
+    WorkerEvtStop = (1 << 0),
+    WorkerEvtRxDone = (1 << 1),
+} WorkerEvtFlags;
+
+// Forward declaration for callback
+typedef void (*FlipperHTTP_Callback)(const char *line, void *context);
+
+// FlipperHTTP Structure
+typedef struct
+{
+    FuriStreamBuffer *flipper_http_stream; // Stream buffer for UART communication
+    FuriHalSerialHandle *serial_handle;    // Serial handle for UART communication
+    FuriThread *rx_thread;
+    uint8_t rx_buf[RX_BUF_SIZE];
+    FuriThreadId rx_thread_id;
+    FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines
+    void *callback_context;                 // Context for the callback
+    SerialState state;
+
+    // variable to store the last received data from the UART
+    char *last_response;
+
+    // Timer-related members
+    FuriTimer *get_timeout_timer; // Timer for GET request timeout
+    bool started_receiving;       // Indicates if a GET request has started
+    bool just_started;            // Indicates if data reception has just started
+    char *received_data;          // Buffer to store received data
+} FlipperHTTP;
+
+// Declare uart as extern to prevent multiple definitions
+FlipperHTTP fhttp;
+
+// Timer callback function
+void get_timeout_timer_callback(void *context)
+{
+    UNUSED(context);
+    FURI_LOG_E(HTTP_TAG, "Timeout reached: 2 seconds without receiving [GET/END]...");
+
+    // Reset the state
+    fhttp.started_receiving = false;
+    fhttp.just_started = false;
+
+    // Free received data if any
+    if (fhttp.received_data)
+    {
+        free(fhttp.received_data);
+        fhttp.received_data = NULL;
+    }
+
+    // Update UART state
+    fhttp.state = ISSUE;
+}
+
+// UART RX Handler Callback (Interrupt Context)
+static void _flipper_http_rx_callback(FuriHalSerialHandle *handle, FuriHalSerialRxEvent event, void *context)
+{
+    UNUSED(context);
+    if (event == FuriHalSerialRxEventData)
+    {
+        uint8_t data = furi_hal_serial_async_rx(handle);
+        furi_stream_buffer_send(fhttp.flipper_http_stream, &data, 1, 0);
+        furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtRxDone);
+    }
+}
+
+// UART worker thread
+static int32_t flipper_http_worker(void *context)
+{
+    UNUSED(context);
+    size_t rx_line_pos = 0;
+    char rx_line_buffer[256]; // Buffer to collect a line
+
+    while (1)
+    {
+        uint32_t events = furi_thread_flags_wait(WorkerEvtStop | WorkerEvtRxDone, FuriFlagWaitAny, FuriWaitForever);
+        if (events & WorkerEvtStop)
+            break;
+        if (events & WorkerEvtRxDone)
+        {
+            size_t len = furi_stream_buffer_receive(fhttp.flipper_http_stream, fhttp.rx_buf, RX_BUF_SIZE * 10, 0);
+            for (size_t i = 0; i < len; i++)
+            {
+                char c = fhttp.rx_buf[i];
+                if (c == '\n' || rx_line_pos >= sizeof(rx_line_buffer) - 1)
+                {
+                    rx_line_buffer[rx_line_pos] = '\0';
+                    // Invoke the callback with the complete line
+                    if (fhttp.handle_rx_line_cb)
+                    {
+                        fhttp.handle_rx_line_cb(rx_line_buffer, fhttp.callback_context);
+                    }
+                    // Reset the line buffer
+                    rx_line_pos = 0;
+                }
+                else
+                {
+                    rx_line_buffer[rx_line_pos++] = c;
+                }
+            }
+        }
+    }
+
+    return 0;
+}
+
+// UART initialization function
+bool flipper_http_init(FlipperHTTP_Callback callback, void *context)
+{
+    fhttp.flipper_http_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1);
+    if (!fhttp.flipper_http_stream)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate UART stream buffer.");
+        return false;
+    }
+
+    fhttp.rx_thread = furi_thread_alloc();
+    if (!fhttp.rx_thread)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate UART thread.");
+        furi_stream_buffer_free(fhttp.flipper_http_stream);
+        return false;
+    }
+
+    furi_thread_set_name(fhttp.rx_thread, "FlipperHTTP_RxThread");
+    furi_thread_set_stack_size(fhttp.rx_thread, 1024);
+    furi_thread_set_context(fhttp.rx_thread, &fhttp);
+    furi_thread_set_callback(fhttp.rx_thread, flipper_http_worker);
+
+    fhttp.handle_rx_line_cb = callback;
+    fhttp.callback_context = context;
+
+    furi_thread_start(fhttp.rx_thread);
+    fhttp.rx_thread_id = furi_thread_get_id(fhttp.rx_thread);
+
+    // Initialize GPIO pins for UART
+    furi_hal_gpio_init_simple(&test_pins[0], GpioModeInput);
+    furi_hal_gpio_init_simple(&test_pins[1], GpioModeOutputPushPull);
+
+    fhttp.serial_handle = NULL;
+
+    fhttp.serial_handle = furi_hal_serial_control_acquire(UART_CH);
+    if (fhttp.serial_handle == NULL)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to acquire UART control - handle is NULL");
+        // Cleanup resources
+        furi_thread_free(fhttp.rx_thread);
+        furi_stream_buffer_free(fhttp.flipper_http_stream);
+        return false;
+    }
+
+    // Initialize UART with acquired handle
+    furi_hal_serial_init(fhttp.serial_handle, BAUDRATE);
+
+    // Enable RX direction
+    furi_hal_serial_enable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx);
+
+    // Start asynchronous RX with the callback
+    furi_hal_serial_async_rx_start(fhttp.serial_handle, _flipper_http_rx_callback, &fhttp, false);
+
+    // Wait for the TX to complete to ensure UART is ready
+    furi_hal_serial_tx_wait_complete(fhttp.serial_handle);
+
+    // Allocate the timer for handling timeouts
+    fhttp.get_timeout_timer = furi_timer_alloc(
+        get_timeout_timer_callback, // Callback function
+        FuriTimerTypeOnce,          // One-shot timer
+        &fhttp                      // Context passed to callback
+    );
+
+    if (!fhttp.get_timeout_timer)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate GET timeout timer.");
+        // Cleanup resources
+        furi_hal_serial_async_rx_stop(fhttp.serial_handle);
+        furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx);
+        furi_hal_serial_control_release(fhttp.serial_handle);
+        furi_hal_serial_deinit(fhttp.serial_handle);
+        furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop);
+        furi_thread_join(fhttp.rx_thread);
+        furi_thread_free(fhttp.rx_thread);
+        furi_stream_buffer_free(fhttp.flipper_http_stream);
+        return false;
+    }
+
+    // Set the timer thread priority if needed
+    furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
+
+    FURI_LOG_I(HTTP_TAG, "UART initialized successfully.");
+    return true;
+}
+
+// Deinitialize UART
+void flipper_http_deinit()
+{
+    if (fhttp.serial_handle == NULL)
+    {
+        FURI_LOG_E(HTTP_TAG, "UART handle is NULL. Already deinitialized?");
+        return;
+    }
+    // Stop asynchronous RX
+    furi_hal_serial_async_rx_stop(fhttp.serial_handle);
+
+    // Release and deinitialize the serial handle
+    furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx);
+    furi_hal_serial_control_release(fhttp.serial_handle);
+    furi_hal_serial_deinit(fhttp.serial_handle);
+
+    // Signal the worker thread to stop
+    furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop);
+    // Wait for the thread to finish
+    furi_thread_join(fhttp.rx_thread);
+    // Free the thread resources
+    furi_thread_free(fhttp.rx_thread);
+
+    // Free the stream buffer
+    furi_stream_buffer_free(fhttp.flipper_http_stream);
+
+    // Free the timer
+    if (fhttp.get_timeout_timer)
+    {
+        furi_timer_free(fhttp.get_timeout_timer);
+        fhttp.get_timeout_timer = NULL;
+    }
+
+    // Free received data if any
+    if (fhttp.received_data)
+    {
+        free(fhttp.received_data);
+        fhttp.received_data = NULL;
+    }
+
+    FURI_LOG_I("FlipperHTTP", "UART deinitialized successfully.");
+}
+
+// Function to send data over UART with newline termination
+bool flipper_http_send_data(const char *data)
+{
+    size_t data_length = strlen(data);
+    if (data_length == 0)
+    {
+        FURI_LOG_E("FlipperHTTP", "Attempted to send empty data.");
+        return false;
+    }
+
+    // Create a buffer with data + '\n'
+    size_t send_length = data_length + 1; // +1 for '\n'
+    if (send_length > 256)
+    { // Ensure buffer size is sufficient
+        FURI_LOG_E("FlipperHTTP", "Data too long to send over FHTTP.");
+        return false;
+    }
+
+    char send_buffer[257]; // 256 + 1 for safety
+    strncpy(send_buffer, data, 256);
+    send_buffer[data_length] = '\n';     // Append newline
+    send_buffer[data_length + 1] = '\0'; // Null-terminate
+
+    if (fhttp.state == INACTIVE && ((strstr(send_buffer, "[PING]") == NULL) && (strstr(send_buffer, "[WIFI/CONNECT]") == NULL)))
+    {
+        FURI_LOG_E("FlipperHTTP", "Cannot send data while INACTIVE.");
+        fhttp.last_response = "Cannot send data while INACTIVE.";
+        return false;
+    }
+
+    fhttp.state = SENDING;
+    furi_hal_serial_tx(fhttp.serial_handle, (const uint8_t *)send_buffer, send_length);
+
+    FURI_LOG_I("FlipperHTTP", "Sent data over UART: %s", send_buffer);
+    fhttp.state = IDLE;
+    return true;
+}
+
+bool flipper_http_ping()
+{
+    const char *command = "[PING]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send PING command.");
+        return false;
+    }
+    // set state as INACTIVE to be made IDLE if PONG is received
+    fhttp.state = INACTIVE;
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to save WiFi settings (returns true if successful)
+bool flipper_http_save_wifi(const char *ssid, const char *password)
+{
+    char buffer[256];
+    int ret = snprintf(buffer, sizeof(buffer), "[WIFI/SAVE]{\"ssid\":\"%s\",\"password\":\"%s\"}", ssid, password);
+    if (ret < 0 || ret >= (int)sizeof(buffer))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format WiFi save command.");
+        return false;
+    }
+
+    if (!flipper_http_send_data(buffer))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi save command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to disconnect from WiFi (returns true if successful)
+bool flipper_http_disconnect_wifi()
+{
+    const char *command = "[WIFI/DISCONNECT]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi disconnect command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to connect to WiFi (returns true if successful)
+bool flipper_http_connect_wifi()
+{
+    const char *command = "[WIFI/CONNECT]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi connect command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to send a GET request
+/**
+ * @brief      Send a GET request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the GET request to.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_get_request(const char *url)
+{
+    if (!url)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request.");
+        return false;
+    }
+
+    // Prepare GET request command
+    char command[256];
+    int ret = snprintf(command, sizeof(command), "[GET]%s", url);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format GET request command.");
+        return false;
+    }
+
+    // Send GET request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send GET request command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// UART RX Handler Callback
+void flipper_http_rx_callback(const char *line, void *context)
+{
+
+    if (!line || !context)
+    {
+        FURI_LOG_E(HTTP_TAG, "Invalid arguments provided to flipper_http_rx_callback.");
+        return;
+    }
+
+    fhttp.last_response = (char *)line;
+
+    // the only way for the state to change from INACTIVE to RECEIVING is if a PONG is received
+    if (fhttp.state != INACTIVE)
+    {
+        fhttp.state = RECEIVING;
+    }
+
+    // Process the received line
+    FURI_LOG_I(HTTP_TAG, "Received UART line: %s", line);
+
+    // Check if we've started receiving data from a GET request
+    if (fhttp.started_receiving)
+    {
+        // Restart the timeout timer each time new data is received
+        furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+
+        if (strstr(line, "[GET/END]") != NULL)
+        {
+            FURI_LOG_I(HTTP_TAG, "GET request completed.");
+            // Stop the timer since we've completed the GET request
+            furi_timer_stop(fhttp.get_timeout_timer);
+
+            if (fhttp.received_data)
+            {
+                flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data);
+                free(fhttp.received_data);
+                fhttp.received_data = NULL;
+            }
+            else
+            {
+                FURI_LOG_E(HTTP_TAG, "No data received.");
+            }
+            fhttp.started_receiving = false;
+            fhttp.just_started = false;
+            fhttp.state = IDLE;
+            return;
+        }
+
+        // Append the new line to the existing data
+        if (fhttp.received_data == NULL)
+        {
+            fhttp.received_data = (char *)malloc(strlen(line) + 2); // +2 for newline and null terminator
+            if (fhttp.received_data)
+            {
+                strcpy(fhttp.received_data, line);
+                strcat(fhttp.received_data, "\n");
+            }
+        }
+        else
+        {
+            size_t new_size = strlen(fhttp.received_data) + strlen(line) + 2; // +2 for newline and null terminator
+            fhttp.received_data = (char *)realloc(fhttp.received_data, new_size);
+            if (fhttp.received_data)
+            {
+                strcat(fhttp.received_data, line);
+                strcat(fhttp.received_data, "\n");
+            }
+        }
+
+        if (!fhttp.just_started)
+        {
+            fhttp.just_started = true;
+        }
+        return;
+    }
+
+    // Handle different types of responses
+    if (strstr(line, "[SUCCESS]") != NULL || strstr(line, "[CONNECTED]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "Operation succeeded.");
+    }
+    else if (strstr(line, "[INFO]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "Received info: %s", line);
+
+        if (fhttp.state == INACTIVE && strstr(line, "[INFO] Already connected to Wifi.") != NULL)
+        {
+            fhttp.state = IDLE;
+        }
+    }
+    else if (strstr(line, "[GET/SUCCESS]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "GET request succeeded.");
+        fhttp.started_receiving = true;
+        furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+    }
+    else if (strstr(line, "[DISCONNECTED]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "WiFi disconnected successfully.");
+    }
+    else if (strstr(line, "[ERROR]") != NULL)
+    {
+        FURI_LOG_E(HTTP_TAG, "Received error: %s", line);
+        fhttp.state = ISSUE;
+        return;
+    }
+    else if (strstr(line, "[PONG]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "Received PONG response: Wifi Dev Board is still alive.");
+
+        // send command to connect to WiFi
+        if (fhttp.state == INACTIVE)
+        {
+            fhttp.state = IDLE;
+            return;
+        }
+    }
+
+    if (fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL)
+    {
+        fhttp.state = IDLE;
+    }
+    else if (fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL)
+    {
+        fhttp.state = INACTIVE;
+    }
+    else
+    {
+        fhttp.state = IDLE;
+    }
+}
+
+bool flipper_http_save_received_data(size_t bytes_received, const char line_buffer[])
+{
+    const char *output_file_path = STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag "/received_data.txt";
+
+    // Ensure the directory exists
+    char directory_path[128];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag);
+
+    Storage *_storage = NULL;
+    File *_file = NULL;
+    // Open the storage if not opened already
+    // Initialize storage and create the directory if it doesn't exist
+    _storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(_storage, directory_path); // Create directory if it doesn't exist
+    _file = storage_file_alloc(_storage);
+
+    // Open file for writing and append data line by line
+    if (!storage_file_open(_file, output_file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to open output file for writing.");
+        storage_file_free(_file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Write each line received from the UART to the file
+    if (bytes_received > 0 && _file)
+    {
+        storage_file_write(_file, line_buffer, bytes_received);
+        storage_file_write(_file, "\n", 1); // Add a newline after each line
+    }
+    else
+    {
+        FURI_LOG_E(HTTP_TAG, "No data received.");
+        return false;
+    }
+
+    if (_file)
+    {
+        storage_file_close(_file);
+        storage_file_free(_file);
+        _file = NULL;
+    }
+    if (_storage)
+    {
+        furi_record_close(RECORD_STORAGE);
+        _storage = NULL;
+    }
+
+    return true;
+}
+
+#endif // FLIPPER_HTTP_H

+ 185 - 114
web_crawler_callback.h

@@ -1,19 +1,120 @@
-// 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}};
+// web_crawler_callback.h
+static bool sent_get_request = false;
+static bool get_success = false;
+static bool already_success = false;
+static WebCrawlerApp *app_instance = NULL;
 
 // 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);
+
+static void web_crawler_view_draw_callback(Canvas *canvas, void *context)
+{
+    UNUSED(context);
+
+    WebCrawlerApp *app = app_instance;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "App is NULL");
+        return;
+    }
+
+    canvas_clear(canvas);
+    canvas_set_font(canvas, FontSecondary);
+
+    if (fhttp.state == INACTIVE)
+    {
+        canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected.");
+        canvas_draw_str(canvas, 0, 17, "Please connect to the board.");
+        canvas_draw_str(canvas, 0, 32, "If you board is connected,");
+        canvas_draw_str(canvas, 0, 42, "make sure you have flashed");
+        canvas_draw_str(canvas, 0, 52, "your Dev Board with the");
+        canvas_draw_str(canvas, 0, 62, "FlipperHTTP firmware.");
+        return;
+    }
+
+    if (app->path)
+    {
+        if (!sent_get_request)
+        {
+
+            canvas_draw_str(canvas, 0, 10, "Sending GET request...");
+
+            // Perform GET request and handle the response
+            get_success = flipper_http_get_request(app->path);
+
+            canvas_draw_str(canvas, 0, 20, "Sent!");
+
+            if (get_success)
+            {
+                canvas_draw_str(canvas, 0, 30, "Receiving data...");
+                get_success = true;
+            }
+            else
+            {
+                canvas_draw_str(canvas, 0, 30, "Failed.");
+            }
+
+            sent_get_request = true;
+        }
+        else
+        {
+            if (get_success && fhttp.state == RECEIVING)
+            {
+                canvas_draw_str(canvas, 0, 10, "Receiving and parsing data...");
+                already_success = true;
+            }
+            else if (get_success && fhttp.state == IDLE && fhttp.received_data == NULL && already_success)
+            {
+                already_success = true;
+                canvas_draw_str(canvas, 0, 10, "Data saved to file.");
+                canvas_draw_str(canvas, 0, 20, "Press BACK to return.");
+            }
+            else if (get_success && fhttp.state == IDLE && fhttp.received_data == NULL && !already_success)
+            {
+                canvas_draw_str(canvas, 0, 10, "Receiving and parsing data...");
+            }
+            else
+            {
+                if (fhttp.state == ISSUE)
+                {
+                    if (strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL)
+                    {
+                        canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi.");
+                        canvas_draw_str(canvas, 0, 50, "Update your config settings.");
+                        canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+                    }
+                    else if (strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL)
+                    {
+                        canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi.");
+                        canvas_draw_str(canvas, 0, 50, "Update your config settings.");
+                        canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+                    }
+                    else
+                    {
+                        canvas_draw_str(canvas, 0, 10, "[ERROR] Failed to sync data.");
+                        canvas_draw_str(canvas, 0, 30, "If this is your third attempt,");
+                        canvas_draw_str(canvas, 0, 40, "it's likely your URL is not");
+                        canvas_draw_str(canvas, 0, 50, "compabilbe or correct.");
+                        canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+                    }
+                }
+                else
+                {
+                    canvas_draw_str(canvas, 0, 10, "GET request failed.");
+                    canvas_draw_str(canvas, 0, 20, "Press BACK to return.");
+                }
+                get_success = false;
+            }
+        }
+    }
+    else
+    {
+        canvas_draw_str(canvas, 0, 10, "URL not set.");
+    }
+}
+
 /**
  * @brief      Navigation callback to handle exiting from other views to the submenu.
  * @param      context   The context - WebCrawlerApp object.
@@ -22,6 +123,13 @@ static void web_crawler_setting_item_password_clicked(void *context, uint32_t in
 static uint32_t web_crawler_back_to_main_callback(void *context)
 {
     UNUSED(context);
+    sent_get_request = false;
+    get_success = false;
+    already_success = false;
+    if (app_instance && app_instance->textbox)
+    {
+        widget_reset(app_instance->textbox);
+    }
     return WebCrawlerViewSubmenu; // Return to the main submenu view
 }
 
@@ -44,7 +152,7 @@ static uint32_t web_crawler_back_to_configure_callback(void *context)
 static uint32_t web_crawler_exit_app_callback(void *context)
 {
     UNUSED(context);
-    return VIEW_NONE; // Exit the app
+    return VIEW_NONE;
 }
 
 /**
@@ -55,11 +163,12 @@ static uint32_t web_crawler_exit_app_callback(void *context)
 static void web_crawler_submenu_callback(void *context, uint32_t index)
 {
     WebCrawlerApp *app = (WebCrawlerApp *)context;
+    furi_check(app);
     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);
+        sent_get_request = false; // Reset the flag
+        view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewRun);
         break;
     case WebCrawlerSubmenuIndexAbout:
         view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewAbout);
@@ -67,6 +176,22 @@ static void web_crawler_submenu_callback(void *context, uint32_t index)
     case WebCrawlerSubmenuIndexSetPath:
         view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewConfigure);
         break;
+    case WebCrawlerSubmenuIndexData:
+        if (!load_received_data())
+        {
+            if (app_instance->textbox)
+            {
+                widget_reset(app_instance->textbox);
+                widget_add_text_scroll_element(
+                    app_instance->textbox,
+                    0,
+                    0,
+                    128,
+                    64, "File is empty.");
+            }
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewData);
+        break;
     default:
         FURI_LOG_E(TAG, "Unknown submenu index");
         break;
@@ -97,68 +222,6 @@ static void web_crawler_config_enter_callback(void *context, uint32_t index)
     }
 }
 
-// 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.
@@ -167,12 +230,11 @@ static void web_crawler_set_path_updated(void *context)
 {
     WebCrawlerApp *app = (WebCrawlerApp *)context;
 
+    furi_check(app);
+
     // 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);
@@ -180,15 +242,14 @@ static void web_crawler_set_path_updated(void *context)
         // Save the URL to the settings
         save_settings(app->path, app->ssid, app->password);
 
-        FURI_LOG_D(TAG, "URL saved: %s", app->path);
-    }
+        // send to UART
+        if (!flipper_http_save_wifi(app->ssid, app->password))
+        {
+            FURI_LOG_E(TAG, "Failed to save wifi settings via UART");
+            FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board");
+        }
 
-    // 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';
+        FURI_LOG_D(TAG, "URL saved: %s", app->path);
     }
 
     // Return to the Configure view
@@ -202,13 +263,10 @@ static void web_crawler_set_path_updated(void *context)
 static void web_crawler_set_ssid_updated(void *context)
 {
     WebCrawlerApp *app = (WebCrawlerApp *)context;
-
+    furi_check(app);
     // 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);
@@ -216,16 +274,16 @@ static void web_crawler_set_ssid_updated(void *context)
         // Save the SSID to the settings
         save_settings(app->path, app->ssid, app->password);
 
+        // send to UART
+        if (!flipper_http_save_wifi(app->ssid, app->password))
+        {
+            FURI_LOG_E(TAG, "Failed to save wifi settings via UART");
+            FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board");
+        }
+
         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);
 }
@@ -237,13 +295,10 @@ static void web_crawler_set_ssid_updated(void *context)
 static void web_crawler_set_password_update(void *context)
 {
     WebCrawlerApp *app = (WebCrawlerApp *)context;
-
+    furi_check(app);
     // 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);
@@ -251,16 +306,16 @@ static void web_crawler_set_password_update(void *context)
         // Save the Password to the settings
         save_settings(app->path, app->ssid, app->password);
 
+        // send to UART
+        if (!flipper_http_save_wifi(app->ssid, app->password))
+        {
+            FURI_LOG_E(TAG, "Failed to save wifi settings via UART");
+            FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board");
+        }
+
         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);
 }
@@ -273,13 +328,21 @@ static void web_crawler_set_password_update(void *context)
 static void web_crawler_setting_item_path_clicked(void *context, uint32_t index)
 {
     WebCrawlerApp *app = (WebCrawlerApp *)context;
+    furi_check(app);
     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);
+    if (app->path)
+    {
+        strncpy(app->temp_buffer_path, app->path, app->temp_buffer_size_path - 1);
+    }
+    else
+    {
+        strncpy(app->temp_buffer_path, "https://www.google.com/", app->temp_buffer_size_path - 1);
+    }
+
     app->temp_buffer_path[app->temp_buffer_size_path - 1] = '\0';
 
     // Configure the text input
@@ -309,13 +372,21 @@ 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)
 {
     WebCrawlerApp *app = (WebCrawlerApp *)context;
+    furi_check(app);
     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);
+    if (app->ssid)
+    {
+        strncpy(app->temp_buffer_ssid, app->ssid, app->temp_buffer_size_ssid - 1);
+    }
+    else
+    {
+        strncpy(app->temp_buffer_ssid, "SSID-2G-", app->temp_buffer_size_ssid - 1);
+    }
+
     app->temp_buffer_ssid[app->temp_buffer_size_ssid - 1] = '\0';
 
     // Configure the text input
@@ -345,8 +416,8 @@ 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)
 {
     WebCrawlerApp *app = (WebCrawlerApp *)context;
+    furi_check(app);
     UNUSED(index);
-
     // Set up the text input
     text_input_set_header_text(app->text_input_password, "Enter Password");
 

+ 64 - 43
web_crawler_e.h

@@ -1,56 +1,77 @@
+// web_crawler_e.h
+#ifndef WEB_CRAWLER_E
+#define WEB_CRAWLER_E
+
+#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_box.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/variable_item_list.h>
+#include <dialogs/dialogs.h>
+#include <storage/storage.h>
+
+#define TAG "WebCrawler"
+
 // Define the submenu items for our WebCrawler application
 typedef enum
 {
-    WebCrawlerSubmenuIndexRun,    // The main screen
-    WebCrawlerSubmenuIndexAbout,  // The about screen
-    WebCrawlerSubmenuIndexSetPath // The configuration screen
+    WebCrawlerSubmenuIndexRun,
+    WebCrawlerSubmenuIndexAbout,
+    WebCrawlerSubmenuIndexSetPath,
+    WebCrawlerSubmenuIndexData,
 } 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
+    WebCrawlerViewSubmenu,           // Submenu
+    WebCrawlerViewAbout,             // About screen
+    WebCrawlerViewConfigure,         // Configuration screen
+    WebCrawlerViewTextInput,         // Text input for Path
+    WebCrawlerViewTextInputSSID,     // Text input for SSID
+    WebCrawlerViewTextInputPassword, // Text input for Password
+    WebCrawlerViewRun,               // Main run view
+    WebCrawlerViewData,              // Data view
 } 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;
+    ViewDispatcher *view_dispatcher;
+    View *view_main;
+    Submenu *submenu;
+    Widget *widget_about;
+    TextInput *text_input_path;
+    TextInput *text_input_ssid;
+    TextInput *text_input_password;
+    Widget *textbox;
+    VariableItemList *variable_item_list_config;
+
+    char *path;
+    char *ssid;
+    char *password;
+    VariableItem *path_item;
+    VariableItem *ssid_item;
+    VariableItem *password_item;
+
+    char *temp_buffer_path;
+    uint32_t temp_buffer_size_path;
+
+    char *temp_buffer_ssid;
+    uint32_t temp_buffer_size_ssid;
+
+    char *temp_buffer_password;
+    uint32_t temp_buffer_size_password;
+} WebCrawlerApp;
+
+// forware declaration of storage functions
+static void save_settings(const char *path, const char *ssid, const char *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);
+static bool load_received_data();
+
+#endif // WEB_CRAWLER_E

+ 77 - 27
web_crawler_free.h

@@ -1,39 +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);
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "Invalid app context");
+        return;
+    }
+    if (app->path)
+    {
+        free(app->path);
+        app->path = NULL;
+    }
+
+    if (app->temp_buffer_path)
+    {
+        free(app->temp_buffer_path);
+        app->temp_buffer_path = NULL;
+    }
+
+    if (app->ssid)
+    {
+        free(app->ssid);
+        app->ssid = NULL;
+    }
+
+    if (app->temp_buffer_ssid)
+    {
+        free(app->temp_buffer_ssid);
+        app->temp_buffer_ssid = NULL;
+    }
+
+    if (app->password)
+    {
+        free(app->password);
+        app->password = NULL;
+    }
+
+    if (app->temp_buffer_password)
+    {
+        free(app->temp_buffer_password);
+        app->temp_buffer_password = NULL;
+    }
 }
 
 static void free_resources(WebCrawlerApp *app)
 {
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "Invalid app context");
+        return;
+    }
+
     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);
+    if (app->view_main)
+        view_free(app->view_main);
+    if (app->submenu)
+        submenu_free(app->submenu);
+    if (app->variable_item_list_config)
+        variable_item_list_free(app->variable_item_list_config);
+    if (app->view_dispatcher)
+        view_dispatcher_free(app->view_dispatcher);
+    if (app->widget_about)
+        widget_free(app->widget_about);
+    if (app->textbox)
+        widget_free(app->textbox);
+
     furi_record_close(RECORD_GUI);
     free_resources(app);
 }
-
 /**
  * @brief      Function to free the resources used by WebCrawlerApp.
  * @param      app  The WebCrawlerApp object to free.
@@ -41,12 +80,24 @@ static void free_all(WebCrawlerApp *app, char *reason)
 static void web_crawler_app_free(WebCrawlerApp *app)
 {
     if (!app)
+    {
+        FURI_LOG_E(TAG, "Invalid app context");
         return;
+    }
 
     // Remove and free Main view
-    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewMain);
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewRun);
     view_free(app->view_main);
 
+    // diconnect wifi
+    if (!flipper_http_disconnect_wifi())
+    {
+        FURI_LOG_E(TAG, "Failed to disconnect from WiFi");
+    }
+
+    // Deinitialize UART
+    flipper_http_deinit();
+
     // Remove and free Submenu
     view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewSubmenu);
     submenu_free(app->submenu);
@@ -59,20 +110,19 @@ static void web_crawler_app_free(WebCrawlerApp *app)
     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);
 
+    // Remove and free Textbox view
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewData);
+    widget_free(app->textbox);
+
     // 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);
-}
+}

+ 113 - 88
web_crawler_i.h

@@ -1,110 +1,115 @@
+#ifndef WEB_CRAWLER_I_H
+#define WEB_CRAWLER_I_H
+
 /**
  * @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()
 {
+    // Initialize the entire structure to zero to prevent undefined behavior
     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));
 
+    // 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(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);
+
+    // Initialize UART with the correct callback
+    if (!flipper_http_init(flipper_http_rx_callback, app))
+    {
+        FURI_LOG_E(TAG, "Failed to initialize UART");
+        free_all(app, "Failed to initialize UART");
+        return NULL;
+    }
+
     // Allocate and initialize temp_buffer and path
     app->temp_buffer_size_path = 128;
-    app->temp_buffer_path = (char *)malloc(app->temp_buffer_size_path);
+    app->temp_buffer_path = malloc(app->temp_buffer_size_path);
     if (!app->temp_buffer_path)
     {
         FURI_LOG_E(TAG, "Failed to allocate temp_buffer_path");
-        free(app);
+        free_all(app, "Failed to allocate temp_buffer_path");
         return NULL;
     }
     app->temp_buffer_path[0] = '\0';
 
     // Allocate path
-    app->path = (char *)malloc(app->temp_buffer_size_path);
+    app->path = malloc(app->temp_buffer_size_path);
     if (!app->path)
     {
         FURI_LOG_E(TAG, "Failed to allocate path");
-        free(app->temp_buffer_path);
-        free(app);
+        free_all(app, "Failed to allocate path");
         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);
+    app->temp_buffer_ssid = malloc(app->temp_buffer_size_ssid);
     if (!app->temp_buffer_ssid)
     {
         FURI_LOG_E(TAG, "Failed to allocate temp_buffer_ssid");
-        free_buffers(app);
+        free_all(app, "Failed to allocate temp_buffer_ssid");
         return NULL;
     }
     app->temp_buffer_ssid[0] = '\0';
 
     // Allocate ssid
-    app->ssid = (char *)malloc(app->temp_buffer_size_ssid);
+    app->ssid = malloc(app->temp_buffer_size_ssid);
     if (!app->ssid)
     {
         FURI_LOG_E(TAG, "Failed to allocate ssid");
-        free_buffers(app);
+        free_all(app, "Failed to allocate ssid");
         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);
+    app->temp_buffer_password = malloc(app->temp_buffer_size_password);
     if (!app->temp_buffer_password)
     {
         FURI_LOG_E(TAG, "Failed to allocate temp_buffer_password");
-        free_buffers(app);
+        free_all(app, "Failed to allocate temp_buffer_password");
         return NULL;
     }
     app->temp_buffer_password[0] = '\0';
 
     // Allocate password
-    app->password = (char *)malloc(app->temp_buffer_size_password);
+    app->password = malloc(app->temp_buffer_size_password);
     if (!app->password)
     {
         FURI_LOG_E(TAG, "Failed to allocate password");
-        free_buffers(app);
+        free_all(app, "Failed to allocate password");
         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)
@@ -227,21 +232,22 @@ static WebCrawlerApp *web_crawler_app_alloc()
     app->submenu = submenu_alloc();
     if (!app->submenu)
     {
+        FURI_LOG_E(TAG, "Failed to allocate Submenu");
         free_all(app, "Failed to allocate Submenu");
         return NULL;
     }
 
+    submenu_set_header(app->submenu, "Web Crawler v0.2");
+
     // 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);
+    submenu_add_item(app->submenu, "Read File", WebCrawlerSubmenuIndexData, 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,
@@ -255,81 +261,100 @@ static WebCrawlerApp *web_crawler_app_alloc()
         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);
+    view_dispatcher_add_view(app->view_dispatcher, WebCrawlerViewRun, app->view_main);
+
+    //-- WIDGET ABOUT VIEW --
 
-    // Allocate About view
+    // Allocate and add About view
     app->widget_about = widget_alloc();
     if (!app->widget_about)
     {
+        FURI_LOG_E(TAG, "Failed to allocate About widget");
         free_all(app, "Failed to allocate About widget");
         return NULL;
     }
 
-    // Add text to About widget
+    // Reset the widget before adding elements
+    widget_reset(app->widget_about);
+
     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.");
+        "Web Crawler App\n"
+        "---\n"
+        "This is a web crawler app for Flipper Zero.\n"
+        "---\n"
+        "Visit github.com/jblanked for more details.\n"
+        "---\n"
+        "Press BACK to return.");
+
+    view_set_previous_callback(widget_get_view(app->widget_about), web_crawler_back_to_main_callback);
+    view_dispatcher_add_view(app->view_dispatcher, WebCrawlerViewAbout, widget_get_view(app->widget_about));
+
+    //-- TEXTBOX DATA VIEW --
+    app->textbox = widget_alloc();
+    if (!app->textbox)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate Textbox");
+        free_all(app, "Failed to allocate Textbox");
+        return NULL;
+    }
+
+    widget_reset(app->textbox);
+
+    widget_add_text_scroll_element(
+        app->textbox,
+        0,
+        0,
+        128,
+        64, "Data will be displayed here.");
+
+    view_set_previous_callback(widget_get_view(app->textbox), web_crawler_back_to_main_callback);
+    view_dispatcher_add_view(app->view_dispatcher, WebCrawlerViewData, widget_get_view(app->textbox));
 
-    // Load settings
+    // Load Settings and Update Views
     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)
+        // Update the configuration items based on loaded settings
+        if (app->path[0] != '\0')
         {
-            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';
+            variable_item_set_current_value_text(app->path_item, app->path);
+        }
+        else
+        {
+            variable_item_set_current_value_text(app->path_item, ""); // Initialize
         }
-    }
 
-    // Set previous callback for About view
-    view_set_previous_callback(
-        widget_get_view(app->widget_about),
-        web_crawler_back_to_main_callback);
+        if (app->ssid[0] != '\0')
+        {
+            variable_item_set_current_value_text(app->ssid_item, app->ssid);
+        }
+        else
+        {
+            variable_item_set_current_value_text(app->ssid_item, ""); // Initialize
+        }
 
-    // Add About view to ViewDispatcher
-    view_dispatcher_add_view(
-        app->view_dispatcher,
-        WebCrawlerViewAbout,
-        widget_get_view(app->widget_about));
+        // Password handling can be omitted for security or handled securely
+    }
+
+    app_instance = app;
 
     // Start with the Submenu view
     view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewSubmenu);
 
     return app;
 }
+
+#endif // WEB_CRAWLER_I_H

+ 140 - 6
web_crawler_storage.h

@@ -1,6 +1,17 @@
-#include <string.h>
-#include <stdio.h>
-#include <stdbool.h>
+#ifndef WEB_CRAWLER_STORAGE_H
+#define WEB_CRAWLER_STORAGE_H
+
+#include <furi.h>
+#include <storage/storage.h>
+
+#define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag "/settings.bin"
+#define RECEIVED_DATA_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag "/received_data.txt"
+
+#define MAX_RECEIVED_DATA_SIZE 1024
+#define SHOW_MAX_FILE_SIZE 2048
+
+// Define the truncation notice
+#define TRUNCATION_NOTICE "\n\n[Data truncated due to size limits]"
 
 // Function to save settings: path, SSID, and password
 static void save_settings(const char *path, const char *ssid, const char *password)
@@ -107,8 +118,6 @@ static bool load_settings(char *path, size_t path_size, char *ssid, size_t ssid_
     }
     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);
@@ -118,4 +127,129 @@ static bool load_settings(char *path, size_t path_size, char *ssid, size_t ssid_
     storage_file_free(file);
     furi_record_close(RECORD_STORAGE);
     return true;
-}
+}
+
+static bool text_show_read_lines(File *file, FuriString *str_result)
+{
+    // Reset the FuriString to ensure it's empty before reading
+    furi_string_reset(str_result);
+
+    // Define a buffer to hold the read data
+    uint8_t buffer[SHOW_MAX_FILE_SIZE];
+
+    // Read data into the buffer
+    size_t read_count = storage_file_read(file, buffer, SHOW_MAX_FILE_SIZE);
+    if (storage_file_get_error(file) != FSE_OK)
+    {
+        FURI_LOG_E(TAG, "Error reading from file.");
+        return false;
+    }
+
+    // Append each byte to the FuriString
+    for (size_t i = 0; i < read_count; i++)
+    {
+        furi_string_push_back(str_result, buffer[i]);
+    }
+
+    return true;
+}
+
+static bool load_received_data()
+{
+    if (!app_instance)
+    {
+        FURI_LOG_E(TAG, "App instance is NULL");
+        return false;
+    }
+    if (!app_instance->textbox)
+    {
+        FURI_LOG_E(TAG, "Textbox is NULL");
+        return false;
+    }
+
+    // Open the storage record
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    if (!storage)
+    {
+        FURI_LOG_E(TAG, "Failed to open storage record");
+        return false;
+    }
+
+    // Allocate a file handle
+    File *file = storage_file_alloc(storage);
+    if (!file)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate storage file");
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Open the file for reading
+    if (!storage_file_open(file, RECEIVED_DATA_PATH, FSAM_READ, FSOM_OPEN_EXISTING))
+    {
+        FURI_LOG_E(TAG, "Failed to open received data file for reading: %s", RECEIVED_DATA_PATH);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false; // Return false if the file does not exist
+    }
+
+    // Allocate a FuriString to hold the received data
+    FuriString *str_result = furi_string_alloc();
+    if (!str_result)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate FuriString");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Read data into the FuriString
+    bool read_success = text_show_read_lines(file, str_result);
+    if (!read_success)
+    {
+        FURI_LOG_E(TAG, "Failed to read data from file");
+        furi_string_free(str_result);
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Check if there is more data beyond the maximum size
+    char extra_byte;
+    storage_file_read(file, &extra_byte, 1);
+
+    // Retrieve the C-string from FuriString
+    const char *data_cstr = furi_string_get_cstr(str_result);
+    // Set the text box with the received data
+
+    widget_reset(app_instance->textbox);
+    FURI_LOG_D(TAG, "Received data: %s", data_cstr);
+    if (str_result != NULL)
+    {
+        widget_add_text_scroll_element(
+            app_instance->textbox,
+            0,
+            0,
+            128,
+            64, data_cstr);
+    }
+    else
+    {
+        widget_add_text_scroll_element(
+            app_instance->textbox,
+            0,
+            0,
+            128,
+            64, "File is empty.");
+    }
+    // Clean up
+    furi_string_free(str_result);
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+    return true;
+}
+
+#endif // WEB_CRAWLER_STORAGE_H