From charlesreid1

Requirements

  • The UI is how the user interacts with the powerful backend. This UI should use specific Flipper Zero GUI toolkit to create a responsive and intuitive experience.
  • UI is tightly integrated with application main lifecycle and threading, so implement as an application file (rogue_ap_app.c)
  • Multi-threaded Design: The UI runs on the main Flipper OS thread. All heavy lifting (WiFi scanning, analysis) is offloaded to a separate worker thread. This is crucial to prevent the UI from freezing during scans.
  • View Dispatcher: Flipper's ViewDispatcher is used to manage and switch between different screens (views), such as the main menu, the settings page, and the live dashboard.
  • State Management: A central App struct holds the state of the entire application, including pointers to our backend modules (scanner, baseline, detector) and data to be displayed on the screen.

Version 1

rogue_ap_app.c

#include <furi.h>
#include <furi_hal.h>
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <gui/modules/submenu.h>
#include <gui/modules/widget.h>
#include <gui/modules/popup.h>
#include <gui/modules/variable_item_list.h>
#include <notification/notification_messages.h>

// Include our backend modules
#include "scanner.h"
#include "baseline.h"
#include "analyzer.h"
#include "detector.h"

#define TAG "RogueAP"

// --- Application State and Views ---

typedef enum {
    AppViewMainMenu,
    AppViewDashboard,
    AppViewSettings,
    AppViewPopup,
} AppView;

typedef enum {
    AppModeIdle,
    AppModeLearning,
    AppModeDetecting,
} AppMode;

// The main application context structure
typedef struct {
    Gui* gui;
    ViewDispatcher* view_dispatcher;
    Submenu* submenu;
    Widget* dashboard_widget;
    VariableItemList* settings_list;
    Popup* popup;
    NotificationApp* notifications;

    FuriThread* worker_thread;
    AppMode mode;

    // Backend modules
    WifiScanner* scanner;
    BaselineManager* baseline;
    Detector* detector;

    // Data for UI display
    FuriMutex* ui_mutex;
    uint16_t baseline_count;
    uint16_t scan_count;
    uint16_t detection_count;
    char status_text[64];
    char last_alert_text[128];

} RogueApApp;


// --- Forward Declarations for Callbacks and Functions ---

// Main menu callback
uint32_t rogue_ap_navigation_exit_callback(void* context);
void rogue_ap_main_menu_callback(void* context, uint32_t index);

// Dashboard view
void rogue_ap_dashboard_render_callback(Canvas* canvas, void* context);

// Worker thread
int32_t rogue_ap_worker_thread(void* context);
void rogue_ap_start_worker(RogueApApp* app, AppMode new_mode);
void rogue_ap_stop_worker(RogueApApp* app);


// --- Main Logic ---

/**
 * @brief Main allocation function for the application.
 */
RogueApApp* rogue_ap_app_alloc() {
    RogueApApp* app = malloc(sizeof(RogueApApp));

    // --- Initialize Modules ---
    wifi_scanner_init_hardware();
    app->scanner = wifi_scanner_alloc(MAX_APS_TO_STORE);
    app->baseline = baseline_manager_alloc(BASELINE_MAX_APS);
    app->detector = detector_alloc();
    baseline_manager_load(app->baseline); // Load known APs from SD card

    // --- Initialize UI ---
    app->gui = furi_record_open(RECORD_GUI);
    app->notifications = furi_record_open(RECORD_NOTIFICATION);
    app->view_dispatcher = view_dispatcher_alloc();
    view_dispatcher_enable_queue(app->view_dispatcher);
    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
    view_dispatcher_set_navigation_event_callback(app->view_dispatcher, rogue_ap_navigation_exit_callback);

    // --- UI Mutex for thread-safe data access ---
    app->ui_mutex = furi_mutex_alloc(FuriMutexTypeNormal);

    // Main Menu
    app->submenu = submenu_alloc();
    submenu_add_item(app->submenu, "Start Detection", 0, rogue_ap_main_menu_callback, app);
    submenu_add_item(app->submenu, "Start Learning Mode", 1, rogue_ap_main_menu_callback, app);
    submenu_add_item(app->submenu, "Settings", 2, rogue_ap_main_menu_callback, app);
    view_dispatcher_add_view(app->view_dispatcher, AppViewMainMenu, submenu_get_view(app->submenu));
    
    // Dashboard Widget
    app->dashboard_widget = widget_alloc();
    widget_set_draw_callback(app->dashboard_widget, rogue_ap_dashboard_render_callback, app);
    view_dispatcher_add_view(app->view_dispatcher, AppViewDashboard, widget_get_view(app->dashboard_widget));

    // Alert Popup
    app->popup = popup_alloc();
    view_dispatcher_add_view(app->view_dispatcher, AppViewPopup, popup_get_view(app->popup));

    // Settings (To be implemented)
    // app->settings_list = variable_item_list_alloc();
    // view_dispatcher_add_view(app->view_dispatcher, AppViewSettings, variable_item_list_get_view(app->settings_list));

    // Start on the main menu
    view_dispatcher_switch_to_view(app->view_dispatcher, AppViewMainMenu);
    
    return app;
}

/**
 * @brief Main free function for the application.
 */
void rogue_ap_app_free(RogueApApp* app) {
    rogue_ap_stop_worker(app);
    baseline_manager_save(app->baseline); // Save any learned APs to SD card

    // Free UI components
    view_dispatcher_remove_view(app->view_dispatcher, AppViewMainMenu);
    view_dispatcher_remove_view(app->view_dispatcher, AppViewDashboard);
    view_dispatcher_remove_view(app->view_dispatcher, AppViewPopup);
    submenu_free(app->submenu);
    widget_free(app->dashboard_widget);
    popup_free(app->popup);
    view_dispatcher_free(app->view_dispatcher);
    furi_record_close(RECORD_GUI);
    furi_record_close(RECORD_NOTIFICATION);
    furi_mutex_free(app->ui_mutex);

    // Free backend modules
    detector_free(app->detector);
    baseline_manager_free(app->baseline);
    wifi_scanner_free(app->scanner);
    wifi_scanner_deinit_hardware();

    free(app);
}

/**
 * @brief Main menu item selection callback.
 */
void rogue_ap_main_menu_callback(void* context, uint32_t index) {
    RogueApApp* app = context;
    if(index == 0) { // Start Detection
        rogue_ap_start_worker(app, AppModeDetecting);
        view_dispatcher_switch_to_view(app->view_dispatcher, AppViewDashboard);
    } else if(index == 1) { // Start Learning
        rogue_ap_start_worker(app, AppModeLearning);
        view_dispatcher_switch_to_view(app->view_dispatcher, AppViewDashboard);
    }
    // else if (index == 2) { ... navigate to settings ... }
}

/**
 * @brief Handles the 'back' button press to exit views.
 */
uint32_t rogue_ap_navigation_exit_callback(void* context) {
    RogueApApp* app = context;
    // Stop scanning when backing out of the dashboard
    if(app->mode != AppModeIdle) {
        rogue_ap_stop_worker(app);
        view_dispatcher_switch_to_view(app->view_dispatcher, AppViewMainMenu);
        return VIEW_NONE; // Prevent exit
    }
    return VIEW_IGNORE; // Allow exit
}


// --- Dashboard Display ---

/**
 * @brief Renders the main dashboard screen.
 */
void rogue_ap_dashboard_render_callback(Canvas* canvas, void* context) {
    RogueApApp* app = context;
    furi_mutex_acquire(app->ui_mutex, FuriWaitForever);

    canvas_set_font(canvas, FontPrimary);
    if(app->mode == AppModeDetecting) {
        canvas_draw_str(canvas, 4, 12, "Mode: Detection");
    } else if(app->mode == AppModeLearning) {
        canvas_draw_str(canvas, 4, 12, "Mode: Learning");
    }

    char buffer[64];
    canvas_set_font(canvas, FontSecondary);
    snprintf(buffer, sizeof(buffer), "Status: %s", app->status_text);
    canvas_draw_str(canvas, 4, 26, buffer);

    snprintf(buffer, sizeof(buffer), "Baseline APs: %u", app->baseline_count);
    canvas_draw_str(canvas, 4, 38, buffer);
    
    snprintf(buffer, sizeof(buffer), "Detections: %u", app->detection_count);
    canvas_draw_str(canvas, 4, 50, buffer);

    furi_mutex_release(app->ui_mutex);
}

// --- Worker Thread for Background Scanning ---

void rogue_ap_start_worker(RogueApApp* app, AppMode new_mode) {
    app->mode = new_mode;
    app->worker_thread = furi_thread_alloc_ex("RogueAPWorker", 4096, rogue_ap_worker_thread, app);
    furi_thread_start(app->worker_thread);
}

void rogue_ap_stop_worker(RogueApApp* app) {
    if(app->mode != AppModeIdle) {
        furi_thread_join(app->worker_thread);
        furi_thread_free(app->worker_thread);
        app->mode = AppModeIdle;
    }
}

/**
 * @brief The main function for the background worker thread.
 */
int32_t rogue_ap_worker_thread(void* context) {
    RogueApApp* app = context;
    ap_info_t scan_results[MAX_APS_TO_STORE];
    AnalysisResult new_detections[DETECTOR_MAX_NEW_ALERTS];

    while(app->mode != AppModeIdle) {
        // --- 1. Scan Phase ---
        furi_mutex_acquire(app->ui_mutex, FuriWaitForever);
        snprintf(app->status_text, sizeof(app->status_text), "Scanning...");
        furi_mutex_release(app->ui_mutex);
        widget_update(app->dashboard_widget); // Force a repaint

        wifi_scanner_full_scan(app->scanner);
        uint16_t result_count = wifi_scanner_get_results(app->scanner, scan_results, MAX_APS_TO_STORE);

        // --- 2. Process Phase ---
        furi_mutex_acquire(app->ui_mutex, FuriWaitForever);
        snprintf(app->status_text, sizeof(app->status_text), "Analyzing %u APs...", result_count);
        furi_mutex_release(app->ui_mutex);
        widget_update(app->dashboard_widget);

        detector_clear_new_detections(app->detector);

        for(uint16_t i = 0; i < result_count; i++) {
            if(app->mode == AppModeLearning) {
                baseline_manager_update(app->baseline, &scan_results[i]);
            } else if(app->mode == AppModeDetecting) {
                AnalysisResult result = analyze_ap(app->baseline, &scan_results[i]);
                detector_process_analysis(app->detector, &result);
            }
        }
        
        // --- 3. Alert Phase ---
        uint16_t new_detection_count = detector_get_new_detections(app->detector, new_detections, DETECTOR_MAX_NEW_ALERTS);
        if(new_detection_count > 0) {
            // A new threat was found! Show a popup and vibrate.
            furi_mutex_acquire(app->ui_mutex, FuriWaitForever);
            snprintf(app->last_alert_text, sizeof(app->last_alert_text), 
                "%s!\nSSID: %s", 
                threat_type_to_string(new_detections[0].threat_type),
                new_detections[0].scanned_ap.ssid);
            furi_mutex_release(app->ui_mutex);
            
            popup_set_text(app->popup, app->last_alert_text, 64, 20, AlignCenter, AlignTop);
            popup_set_header(app->popup, "ROGUE AP ALERT", 64, 5, AlignCenter, AlignTop);
            view_dispatcher_switch_to_view(app->view_dispatcher, AppViewPopup);
            notification_message(app->notifications, &sequence_double_vibro);
        }

        // --- 4. Update UI Data and Sleep ---
        furi_mutex_acquire(app->ui_mutex, FuriWaitForever);
        app->baseline_count = app->baseline->count;
        app->detection_count += new_detection_count;
        snprintf(app->status_text, sizeof(app->status_text), "Idle");
        furi_mutex_release(app->ui_mutex);
        widget_update(app->dashboard_widget);
        
        furi_delay_ms(5000); // Wait 5 seconds before next scan cycle
    }

    return 0;
}


// --- Application Entry Point ---

int32_t rogue_ap_app_entry(void* p) {
    UNUSED(p);
    RogueApApp* app = rogue_ap_app_alloc();
    
    // This is the main message loop
    view_dispatcher_run(app->view_dispatcher);

    rogue_ap_app_free(app);
    return 0;
}