Flipper Zero/Rogue AP Detector/ui.c
From charlesreid1
Main article: Flipper Zero/Rogue AP Detector
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; }