├── .gitattributes ├── .github └── workflows │ └── UploadToCDN.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── application.fam ├── docs ├── confirmation.md ├── menu.md └── settings.md ├── ghost_esp.backup.png ├── ghost_esp.png ├── gui_modules ├── mainmenu.c └── mainmenu.h ├── images ├── BLE_icon.png ├── Cog.png ├── GPS.png └── Wifi_icon.png ├── screenshots ├── connect.png ├── mainmenu.png ├── scan.png └── wifi.png └── src ├── app_state.h ├── app_types.h ├── callbacks.c ├── callbacks.h ├── confirmation_view.c ├── confirmation_view.h ├── firmware_api.c ├── firmware_api.h ├── ghost_esp_icons.h ├── log_manager.c ├── log_manager.h ├── main.c ├── menu.c ├── menu.h ├── sequential_file.c ├── sequential_file.h ├── settings_def.c ├── settings_def.h ├── settings_storage.c ├── settings_storage.h ├── settings_ui.c ├── settings_ui.h ├── settings_ui_types.h ├── uart_storage.c ├── uart_storage.h ├── uart_utils.c ├── uart_utils.h ├── utils.c └── utils.h /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/UploadToCDN.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/upload_to_r2.yml 2 | name: Upload and Notify 3 | on: 4 | workflow_dispatch: 5 | jobs: 6 | upload-and-notify: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v3 11 | 12 | - name: Get latest release info 13 | id: get_release 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | run: | 17 | release=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ 18 | https://api.github.com/repos/${{ github.repository }}/releases/latest) 19 | 20 | echo "version=$(echo "$release" | jq -r '.tag_name')" >> $GITHUB_OUTPUT 21 | echo "asset_url=$(echo "$release" | jq -r '.assets[] | select(.name | endswith(".fap")) | .browser_download_url')" >> $GITHUB_OUTPUT 22 | description=$(echo "$release" | jq -r '.body') 23 | echo "description<> $GITHUB_OUTPUT 24 | echo "$description" >> $GITHUB_OUTPUT 25 | echo "EOF" >> $GITHUB_OUTPUT 26 | 27 | - name: Download FAP 28 | run: | 29 | curl -L "${{ steps.get_release.outputs.asset_url }}" -o ghost_esp.fap 30 | 31 | - name: Install rclone 32 | run: | 33 | curl -O https://downloads.rclone.org/rclone-current-linux-amd64.zip 34 | unzip rclone-current-linux-amd64.zip 35 | sudo install -o root -g root -m 0755 rclone-*-linux-amd64/rclone /usr/local/bin/rclone 36 | 37 | - name: Configure rclone 38 | env: 39 | R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} 40 | R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} 41 | run: | 42 | mkdir -p ~/.config/rclone 43 | cat > ~/.config/rclone/rclone.conf << EOF 44 | [cloudflare] 45 | type = s3 46 | provider = Cloudflare 47 | access_key_id = $R2_ACCESS_KEY_ID 48 | secret_access_key = $R2_SECRET_ACCESS_KEY 49 | endpoint = https://fb5f7d31bedfe4f3538ddfa6db491962.r2.cloudflarestorage.com 50 | EOF 51 | 52 | - name: Upload to R2 53 | env: 54 | R2_BUCKET: "spooksapi" 55 | R2_PATH: "assets/ghost_esp.fap" 56 | run: | 57 | rclone copyto ghost_esp.fap "cloudflare:${R2_BUCKET}/${R2_PATH}" --s3-no-check-bucket --progress 58 | 59 | - name: Notify Discord 60 | if: success() 61 | env: 62 | DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} 63 | run: | 64 | # Create the Discord message payload 65 | VERSION="${{ steps.get_release.outputs.version }}" 66 | VERSION="${VERSION#v}" # Remove leading 'v' if present 67 | 68 | DESCRIPTION="${{ steps.get_release.outputs.description }}" 69 | if [ ${#DESCRIPTION} -gt 1024 ]; then 70 | DESCRIPTION="${DESCRIPTION:0:1021}..." 71 | fi 72 | 73 | # Write the payload to a file using pure JSON 74 | cat > payload.json << 'EOF' 75 | { 76 | "embeds": [{ 77 | "title": "Ghost ESP Flipper App Update", 78 | "description": "Version VERSION_PLACEHOLDER is now available", 79 | "color": 8847615, 80 | "fields": [ 81 | { 82 | "name": "Download Latest Version", 83 | "value": "[Click here to download](https://cdn.spookytools.com/assets/ghost_esp.fap)", 84 | "inline": false 85 | }, 86 | { 87 | "name": "Changes in this version", 88 | "value": "DESCRIPTION_PLACEHOLDER", 89 | "inline": false 90 | }, 91 | { 92 | "name": "GitHub", 93 | "value": "[Ghost ESP App](https://github.com/Spooks4576/ghost_esp_app)", 94 | "inline": false 95 | } 96 | ], 97 | "footer": { 98 | "text": "updated with love <3" 99 | }, 100 | "timestamp": "TIMESTAMP_PLACEHOLDER" 101 | }] 102 | } 103 | EOF 104 | 105 | # Use jq to safely inject our variables 106 | jq --arg ver "$VERSION" \ 107 | --arg desc "$DESCRIPTION" \ 108 | --arg ts "$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")" \ 109 | '.embeds[0].description = "Version " + $ver + " is now available" | 110 | .embeds[0].fields[1].value = $desc | 111 | .embeds[0].timestamp = $ts' \ 112 | payload.json > payload_final.json 113 | 114 | # Send to Discord using the file 115 | curl -H "Content-Type: application/json" \ 116 | -d @payload_final.json \ 117 | "$DISCORD_WEBHOOK_URL" 118 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | .vscode 3 | .clang-format 4 | .clangd 5 | .editorconfig 6 | .env 7 | .ufbt 8 | checks.json 9 | manifest.yml 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.2.3 4 | 5 | - Added LED Effects command 6 | 7 | ## v1.2.2 8 | 9 | - fixed race condition and blocking of UART initialization causing freeze on launch 10 | - slightly improved startup time 11 | 12 | ## v1.2.1 13 | 14 | - Added 'apcred' commands to change and reset WebUI SSID & Password 15 | 16 | ## v1.2.0 17 | 18 | - Added Variable Sniff command replacing individual commands 19 | - Added Variable Beacon Spam command replacing individual commands 20 | - Removed individual stop commands in favour of one for each section 21 | 22 | 23 | ## v1.1.9 24 | 25 | - Changed text input buffer size to 128 characters 26 | - Added Flipper Runtime Firmware detection check 27 | - Added option to disable ESP Connection Check for debugging 28 | - Revised connection error message 29 | 30 | ## v1.1.8 Polaris 📡🌍 31 | 32 | - Added GPS Info command to view real-time GPS data 33 | - Added Stop GPS Info command 34 | - UART Initialisation tweaks 35 | - Add back EAPOL capture command 36 | - Added BLE Raw Capture command 37 | - Added Stop BLE Raw Capture command 38 | - Added BLE Skimmer Detection command 39 | - Expanded Stop on Back to include all stop commands 40 | - Added wrap-around scrolling in command menus 41 | - Add Scan Local command to scan for devices on connected network 42 | - Respect Momentum settings for ESP UART Channel 43 | - Added PCAP and Wardrive clearing options in Settings menu 44 | - Added BLE Wardriving command 45 | - Added GPS Track (GPX) command for recording GPS tracks in GPX format 46 | - Miscellaneous bug fixes and improvements 47 | 48 | ## v1.1.7 49 | 50 | - added null checks before freeing resources 51 | - remove unused commands from menu.c and cleaned up command details 52 | - initialise uart in esp connection check if needed 53 | 54 | ## v1.1.6 55 | 56 | - sync files more frequently 57 | 58 | ## v1.1.5 59 | 60 | - expanded quick help menu with more context-specific tips 61 | - swapped order of AT and Stop commands in ESP Check 62 | - cleaned up general UI text 63 | - improved handling of corrupted or missing log files 64 | - general code cleanup and improvements 65 | 66 | ## v1.1.4 67 | 68 | - Added confirmation dialog for Cast Video command 69 | - Fixed view locking issues with confirmation dialog 70 | 71 | ## v1.1.3 72 | 73 | **Added specific buttondown icon for help menu button to not be dependent on firmware icons** 74 | 75 | ## v1.1.2 76 | 77 | - improved ESP connection check reliability by trying AT command first with shorter timeouts, while keeping original 'stop' command as fallback 78 | 79 | ## v1.1.1 80 | 81 | - add sniff pwnagotchi command 82 | 83 | ## v1.1.0 🕸️👻 84 | 85 | - Ring buffer implementation for text handling 86 | - New view buffer management 87 | - Added proper locking mechanisms 88 | - **Remove Filtering due to Firmware updates!** 89 | - Made exiting views more consistent for UE 90 | - **Replaced select a utility text with prompt to show NEW Help Menu** 91 | - Refactored and simplified uart_utils 92 | - Made PCAP file handling more robust 93 | - **Add GPS Menu and commands with saving to .csv** 94 | - Miscellaneous bug fixes 95 | 96 | ## v1.0.9 97 | 98 | - Fixed log file corruption when stopping captures 99 | - Added proper bounds checking for oversized messages 100 | - Improved text display buffer management 101 | - **Added automatic prefix tagging for WiFi, BLE and system messages** 102 | - Improved storage init speed 103 | 104 | ## v1.0.8 105 | 106 | ### 🔴 CRITICAL FIX - PCAP capture 107 | 108 | - **Fixed PCAP file handling and storage system** 109 | - Resolved PCAP file stream corruption issues 110 | - Added proper storage system initialization 111 | - Removed the line buffering logic for PCAP data 112 | 113 | ### Improvements 114 | 115 | - Added error checking for storage operations 116 | - Filtering majorly improved 117 | - Improved stop on back to be much more reliable by added type-specific stop commands with delays between operations 118 | 119 | ## v1.0.7a 120 | 121 | - Disable the expansion interface before trying to use UART 122 | 123 | ## v1.0.7 124 | 125 | - **Increased buffers and stacks: MAX_BUFFER_SIZE to 8KB, INITIAL_BUFFER_SIZE to 4KB, BUFFER_CLEAR_SIZE to 128B, uart/app stacks to 4KB/6KB** 126 | - Added buffer_mutex with proper timeout handling 127 | - **Added Marauder-style data handling** 128 | - Improved ESP connection reliability 129 | - Added view log from start/end configuration setting 130 | - **Added line buffering with overflow detection, boundary protection and pre-flush on mode switches** 131 | 132 | ## v1.0.6 133 | 134 | - Replaced 'Info' command in ESP Check with 'Stop' 135 | - Slightly improved optional UART filtering 136 | - Memory safety improvements. 137 | - Improved Clear Logs to be faster and more efficient 138 | - **Added details view to each command accessible with hold of center button. (Like BLE Spam)** 139 | - **Made ESP Not Connected screen more helpful with prompts to reboot/reflash if issues persist.** 140 | - Renamed CONF menu option to SET to better align with actual Settings menu since its header is "Settings" and there is a configuration submenu 141 | - Replaced textbox for ESP Connection Check with scrollable Confirmation View 142 | 143 | ## v1.0.5 144 | 145 | - **Commands will silently fail if UART isn't working rather than crashing** 146 | - **Fixed double-free memory issue by removing stream buffer cleanup from the worker thread** 147 | - Reorganized initialization order 148 | - **UART initialization happens in background** 149 | - **Serial operations don't block app startup** 150 | - Optimized storage initialization by deferring file operations until needed 151 | - Improved directory creation efficiency in storage handling 152 | 153 | ## v1.0.4 154 | 155 | - Refined confirmation view line breaks for readability 156 | - Improved ESP Connectivity check to decrease false negatives 157 | - **Added optional filtering to UART output to improve readability (BETA)** 158 | - **Added 'App Info' Button in Settings** 159 | - Misc Changes (mostly to UI) 160 | 161 | ## v1.0.3 162 | 163 | - Enhanced confirmation view structure and readability with better text alignment 164 | - **Added confirmation for "Clear Log Files" with a permanent action warning** 165 | - **Enabled back press exit on confirmation views with callback context handling** 166 | - Improved memory management with context cleanup, view state tracking, and transition fixes 167 | - Added NULL checks, fixed memory leaks, and added state tracking for dialogs 168 | 169 | ## v1.0.2 170 | 171 | - **Added confirmation dialogs for WebUI-dependent features in the UI** 172 | - Improved settings menu with actions submenu, NVS clearing, and log clearing 173 | - Enhanced memory management and improved settings storage/loading robustness 174 | - **Added contextual help for WebUI configuration and confirmation dialogs for command safety** 175 | - Improved view navigation, state management, and memory cleanup processes 176 | - **Added safeguards against `furi_check` failures with NULL checks and memory initialization** 177 | 178 | ## v1.0.1 179 | 180 | - **Revamped menu structure with logical grouping (scanning, beacon spam, attacks, etc.)** 181 | - Simplified command addition and cleaned up documentation in `menu.c` 182 | - **Centralized and enum-based settings metadata for improved validation and extensibility** 183 | - **Enhanced settings with Stop-on-Back feature and ESP reboot command** 184 | - **Enabled automatic connectivity check and error recovery for ESP issues** 185 | - **Unified UI with metadata-driven consistency and better type safety** 186 | - Simplified UI view switching and improved error display 187 | - Refined code organization, separating concerns, removing redundancy, and standardizing error handling 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ghost ESP 👻 2 | A Flipper Zero application for interfacing with the Ghost ESP32 firmware 3 | 4 | ## Preview 5 | ![image](https://github.com/user-attachments/assets/dbff6546-24ed-4d20-af6e-0e01e1643385) 6 | 7 | ## Download Latest Release [Here](https://cdn.spookytools.com/assets/ghost_esp.fap) 8 | 9 | ## Features 10 | 11 | ### 📶 WiFi Operations 12 | - **Scan and List**: Discover WiFi access points and stations 13 | - **Beacon Spam**: Choose from random, rickroll, or custom beacon spamming 14 | - **Deauthentication Attacks**: Perform targeted deauthentication attacks 15 | - **Packet Capture**: Capture PMKID, Probe, WPS, Raw packets, and more 16 | - **Evil Portal**: Create a captive portal 17 | - **Network Connection**: Access comprehensive network connection features 18 | - **Printer Power Control**: Control printer power remotely 19 | 20 | ### 📡 Bluetooth Operations 21 | - **Flipper Discovery**: Locate other Flipper devices 22 | - **BLE Spam Detection**: Detect and manage BLE spam (COMING SOON…) 23 | - **AirTag Scanning**: Find and scan nearby AirTags 24 | - **Raw Packet Capture**: Capture raw Bluetooth packets for analysis 25 | 26 | ### 🌍 GPS 27 | - **GPS Information**: View real-time GPS data including position, altitude, speed and signal quality 28 | - **Wardriving Capabilities**: Enable Wardriving for location-based data collection 29 | 30 | ### ⚙️ Configuration Options 31 | - **RGB LED Control**: Customize RGB LED settings 32 | - **Channel Hopping**: Adjust channel hopping behavior 33 | - **BLE MAC Randomization**: Enable MAC address randomization for Bluetooth 34 | - **Auto-Stop**: Automatically stop operations on back button press 35 | - **Clear Logs**: Easily clear stored logs 36 | - **ESP Reboot**: Reboot the ESP with a single command 37 | - **NVS Clearing**: Clear NVS data with a confirmation prompt 38 | 39 | 40 | ## Credits 🙏 41 | - Made by [Spooky](https://github.com/Spooks4576) 42 | - Additional contributions by [Jay Candel](https://github.com/jaylikesbunda) 43 | 44 | 45 | ## Support 46 | - For support, please open an [issue](https://github.com/Spooks4576/ghost_esp_app/issues) on the repository or contact [Jay](https://github.com/jaylikesbunda) (@fuckyoudeki on Discord) or [Spooky](https://github.com/Spooks4576). 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /application.fam: -------------------------------------------------------------------------------- 1 | # For details & more options, see documentation/AppManifests.md in firmware repo 2 | 3 | App( 4 | appid="ghost_esp", # Must be unique 5 | name="[ESP32] Ghost ESP", # Displayed in menus 6 | apptype=FlipperAppType.EXTERNAL, # type: ignore 7 | entry_point="ghost_esp_app", 8 | stack_size=6 * 1024, 9 | fap_category="GPIO/ESP", 10 | # Optional values 11 | icon="A_GhostESP_14", 12 | fap_version="1.2.3", 13 | fap_icon="ghost_esp.png", # 10x10 1-bit PNG 14 | fap_icon_assets="images", # Image assets to compile for this application 15 | ) 16 | -------------------------------------------------------------------------------- /docs/confirmation.md: -------------------------------------------------------------------------------- 1 | # Ghost ESP App Confirmation View Guide 2 | 3 | ## Overview 4 | The Confirmation View is a UI component that provides a modal dialog for confirming user actions. It features a header, message text, and OK/Cancel functionality with callbacks. 5 | 6 | ## Component Structure 7 | ```c 8 | typedef struct { 9 | View* view; // Base view 10 | ConfirmationViewCallback ok_callback; // OK button callback 11 | ConfirmationViewCallback cancel_callback; // Cancel button callback 12 | void* callback_context; // Context passed to callbacks 13 | } ConfirmationView; 14 | 15 | typedef struct { 16 | const char* header; // Dialog header text 17 | const char* text; // Dialog message text 18 | } ConfirmationViewModel; 19 | ``` 20 | 21 | ## Using Confirmation View 22 | 23 | ### 1. Create Instance 24 | ```c 25 | // Allocate and initialize the view 26 | ConfirmationView* confirmation = confirmation_view_alloc(); 27 | 28 | // Add to view dispatcher 29 | view_dispatcher_add_view(dispatcher, view_id, confirmation_view_get_view(confirmation)); 30 | ``` 31 | 32 | ### 2. Configure Dialog 33 | ```c 34 | // Set the header and message 35 | confirmation_view_set_header(confirmation, "Warning"); 36 | confirmation_view_set_text(confirmation, "This action cannot be undone.\nContinue?"); 37 | 38 | // Set callbacks 39 | confirmation_view_set_ok_callback(confirmation, ok_handler, context); 40 | confirmation_view_set_cancel_callback(confirmation, cancel_handler, context); 41 | ``` 42 | 43 | ### 3. Show Dialog 44 | ```c 45 | // Switch to confirmation view 46 | view_dispatcher_switch_to_view(dispatcher, confirmation_view_id); 47 | ``` 48 | 49 | ## Callback Implementation 50 | 51 | ### Basic Callbacks 52 | ```c 53 | static void ok_callback(void* context) { 54 | AppState* state = context; 55 | // Handle confirmation 56 | // Return to previous view 57 | view_dispatcher_switch_to_view(state->view_dispatcher, previous_view); 58 | } 59 | 60 | static void cancel_callback(void* context) { 61 | AppState* state = context; 62 | // Handle cancellation 63 | // Return to previous view 64 | view_dispatcher_switch_to_view(state->view_dispatcher, previous_view); 65 | } 66 | ``` 67 | 68 | ### Command Execution Callback 69 | ```c 70 | static void command_ok_callback(void* context) { 71 | MenuCommandContext* cmd_ctx = context; 72 | if(cmd_ctx && cmd_ctx->state && cmd_ctx->command) { 73 | // Execute command 74 | send_uart_command(cmd_ctx->command->command, cmd_ctx->state); 75 | // Handle response 76 | uart_receive_data( 77 | cmd_ctx->state->uart_context, 78 | cmd_ctx->state->view_dispatcher, 79 | cmd_ctx->state, 80 | cmd_ctx->command->capture_prefix, 81 | cmd_ctx->command->file_ext, 82 | cmd_ctx->command->folder); 83 | } 84 | free(cmd_ctx); // Clean up context 85 | } 86 | ``` 87 | 88 | ## Visual Layout 89 | ```c 90 | static void confirmation_view_draw_callback(Canvas* canvas, void* _model) { 91 | // Draw outer frame 92 | canvas_draw_rframe(canvas, 0, 0, 128, 64, 3); 93 | canvas_draw_rframe(canvas, 1, 1, 126, 62, 2); 94 | 95 | // Draw header 96 | canvas_set_font(canvas, FontPrimary); 97 | elements_multiline_text_aligned(canvas, 64, 8, AlignCenter, AlignTop, header); 98 | 99 | // Draw message 100 | canvas_set_font(canvas, FontSecondary); 101 | elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignTop, text); 102 | 103 | // Draw OK button 104 | elements_button_center(canvas, "OK"); 105 | } 106 | ``` 107 | 108 | ## Input Handling 109 | ```c 110 | static bool confirmation_view_input_callback(InputEvent* event, void* context) { 111 | ConfirmationView* instance = context; 112 | bool consumed = false; 113 | 114 | if(event->type == InputTypeShort) { 115 | switch(event->key) { 116 | case InputKeyOk: 117 | if(instance->ok_callback) { 118 | instance->ok_callback(instance->callback_context); 119 | } 120 | consumed = true; 121 | break; 122 | 123 | case InputKeyBack: 124 | if(instance->cancel_callback) { 125 | instance->cancel_callback(instance->callback_context); 126 | } 127 | consumed = true; 128 | break; 129 | } 130 | } 131 | return consumed; 132 | } 133 | ``` 134 | 135 | ## Best Practices 136 | 137 | 1. Message Format 138 | - Clear, concise headers 139 | - Detailed but brief messages 140 | - Use newlines for readability 141 | - Keep text within view bounds 142 | 143 | 2. Callback Handling 144 | - Always check context validity 145 | - Clean up resources 146 | - Return to appropriate view 147 | - Handle errors gracefully 148 | 149 | 3. Memory Management 150 | - Free command context after use 151 | - Check for NULL pointers 152 | - Initialize all pointers 153 | - Clean up on view free 154 | 155 | 4. Visual Design 156 | - Consistent spacing 157 | - Clear visual hierarchy 158 | - Readable text size 159 | - Visible button indicator 160 | 161 | ## Example Usage 162 | 163 | ### Dangerous Action 164 | ```c 165 | void show_dangerous_action_confirmation(AppState* state) { 166 | confirmation_view_set_header(state->confirmation_view, "Warning"); 167 | confirmation_view_set_text(state->confirmation_view, 168 | "This will affect all devices.\nContinue?"); 169 | confirmation_view_set_ok_callback(state->confirmation_view, 170 | dangerous_action_ok, state); 171 | confirmation_view_set_cancel_callback(state->confirmation_view, 172 | dangerous_action_cancel, state); 173 | view_dispatcher_switch_to_view(state->view_dispatcher, 174 | CONFIRMATION_VIEW_ID); 175 | } 176 | ``` 177 | 178 | ### Settings Reset 179 | ```c 180 | void show_reset_confirmation(AppState* state) { 181 | confirmation_view_set_header(state->confirmation_view, "Reset Settings"); 182 | confirmation_view_set_text(state->confirmation_view, 183 | "All settings will be reset\nto default values.\n\nContinue?"); 184 | confirmation_view_set_ok_callback(state->confirmation_view, 185 | reset_settings_ok, state); 186 | confirmation_view_set_cancel_callback(state->confirmation_view, 187 | reset_settings_cancel, state); 188 | view_dispatcher_switch_to_view(state->view_dispatcher, 189 | CONFIRMATION_VIEW_ID); 190 | } 191 | ``` -------------------------------------------------------------------------------- /docs/menu.md: -------------------------------------------------------------------------------- 1 | # Ghost ESP App Menu Guide 2 | 3 | ## Overview 4 | This guide explains how to add new menu items and commands to the Ghost ESP application. The app uses a structured approach for menu organization, command execution, and UI feedback. 5 | 6 | ## Menu Command Structure 7 | ```c 8 | typedef struct { 9 | const char* label; // Display label in menu 10 | const char* command; // UART command to send 11 | const char* capture_prefix; // Prefix for capture files (NULL if none) 12 | const char* file_ext; // File extension for captures (NULL if none) 13 | const char* folder; // Folder for captures (NULL if none) 14 | bool needs_input; // Whether command requires text input 15 | const char* input_text; // Text to show in input box (NULL if none) 16 | bool needs_confirmation; // Whether command needs confirmation 17 | const char* confirm_header; // Confirmation dialog header 18 | const char* confirm_text; // Confirmation dialog text 19 | } MenuCommand; 20 | ``` 21 | 22 | ## Adding New Menu Commands 23 | 24 | ### 1. Define the Command 25 | Add your command to the appropriate menu array: 26 | ```c 27 | static const MenuCommand wifi_commands[] = { 28 | // Basic command without any special features 29 | {"My Command", "mycmd\n", NULL, NULL, NULL, false, NULL, false, NULL, NULL}, 30 | 31 | // Command that captures data 32 | {"Capture Data", "capture\n", "capture_prefix", "pcap", GHOST_ESP_APP_FOLDER_PCAPS, 33 | false, NULL, false, NULL, NULL}, 34 | 35 | // Command that needs user input 36 | {"Custom Command", "cmd", NULL, NULL, NULL, true, "Enter value:", 37 | false, NULL, NULL}, 38 | 39 | // Command with confirmation dialog 40 | {"Dangerous Command", "danger\n", NULL, NULL, NULL, false, NULL, true, 41 | "Warning", "This action cannot be undone.\nAre you sure?"}, 42 | }; 43 | ``` 44 | 45 | ### 2. Menu Types and Features 46 | 47 | #### Basic Command 48 | ```c 49 | {"Label", "command\n", NULL, NULL, NULL, false, NULL, false, NULL, NULL} 50 | ``` 51 | 52 | #### Command with File Capture 53 | ```c 54 | {"Label", "command\n", "prefix", "ext", FOLDER, false, NULL, false, NULL, NULL} 55 | ``` 56 | 57 | #### Command with User Input 58 | ```c 59 | {"Label", "command", NULL, NULL, NULL, true, "Input prompt", false, NULL, NULL} 60 | ``` 61 | 62 | #### Command with Confirmation 63 | ```c 64 | {"Label", "command\n", NULL, NULL, NULL, false, NULL, true, 65 | "Header", "Confirmation message"} 66 | ``` 67 | 68 | ## Command Execution Flow 69 | 70 | ### Regular Commands 71 | 1. Command validation 72 | 2. ESP connection check 73 | 3. Send command via UART 74 | 4. Process response 75 | 76 | ### Input Commands 77 | 1. Show text input dialog 78 | 2. Wait for user input 79 | 3. Send command + input 80 | 4. Process response 81 | 82 | ### Confirmation Commands 83 | 1. Show confirmation dialog 84 | 2. Wait for user response 85 | 3. Execute on confirm 86 | 4. Return to menu on cancel 87 | 88 | ### Capture Commands 89 | 1. Execute command 90 | 2. Create capture file 91 | 3. Save data to file 92 | 4. Close file on completion 93 | 94 | ## Example Implementations 95 | 96 | ### WiFi Command Group 97 | ```c 98 | static const MenuCommand wifi_commands[] = { 99 | // Scanning Operations 100 | {"Scan WiFi APs", "scanap\n", NULL, NULL, NULL, false, NULL, false, NULL, NULL}, 101 | {"Stop Scan", "stopscan\n", NULL, NULL, NULL, false, NULL, false, NULL, NULL}, 102 | 103 | // Capture Operations 104 | {"Sniff Packets", "capture\n", "packet_capture", "pcap", 105 | GHOST_ESP_APP_FOLDER_PCAPS, false, NULL, false, NULL, NULL}, 106 | 107 | // Interactive Operations 108 | {"Connect WiFi", "connect", NULL, NULL, NULL, true, "SSID,Password", 109 | false, NULL, NULL}, 110 | 111 | // Dangerous Operations 112 | {"Deauth", "attack -d\n", NULL, NULL, NULL, false, NULL, true, 113 | "Warning", "This will disconnect devices.\nContinue?"}, 114 | }; 115 | ``` 116 | 117 | ### BLE Command Group 118 | ```c 119 | static const MenuCommand ble_commands[] = { 120 | {"Find Flippers", "blescan -f\n", NULL, NULL, NULL, false, NULL, false, NULL, NULL}, 121 | {"Stop Scan", "blescan -s\n", NULL, NULL, NULL, false, NULL, false, NULL, NULL}, 122 | }; 123 | ``` 124 | 125 | ## Best Practices 126 | 127 | 1. Command String Format 128 | - Add newline for automatic commands: `command\n` 129 | - Omit newline for input commands: `command` 130 | 131 | 2. Input Commands 132 | - Clear input prompts 133 | - Show expected format 134 | - Validate input where possible 135 | 136 | 3. Confirmation Dialogs 137 | - Clear warning messages 138 | - Explain consequences 139 | - Allow easy cancellation 140 | 141 | 4. Capture Files 142 | - Descriptive prefixes 143 | - Appropriate extensions 144 | - Organized folders 145 | 146 | 5. Error Handling 147 | - Check ESP connection 148 | - Validate inputs 149 | - Handle failures gracefully 150 | 151 | 6. UI Feedback 152 | - Show operation status 153 | - Clear error messages 154 | - Return to appropriate menu 155 | 156 | ## Common Patterns 157 | 158 | ### Command with Input and Confirmation 159 | ```c 160 | {"Dangerous Input", "danger", NULL, NULL, NULL, true, "Enter target:", true, 161 | "Warning", "This will affect target.\nContinue?"} 162 | ``` 163 | 164 | ### Capture with Auto-naming 165 | ```c 166 | {"Capture Data", "capture\n", "auto_capture", "bin", GHOST_ESP_APP_FOLDER_PCAPS, 167 | false, NULL, false, NULL, NULL} 168 | ``` 169 | 170 | ### Toggle Command 171 | ```c 172 | {"Toggle Feature", "toggle\n", NULL, NULL, NULL, false, NULL, true, 173 | "Toggle", "Feature will be switched.\nContinue?"} 174 | ``` -------------------------------------------------------------------------------- /docs/settings.md: -------------------------------------------------------------------------------- 1 | # Ghost ESP App Settings Guide 2 | 3 | ## Overview 4 | This document explains how to add new settings to the Ghost ESP application. The app uses a structured approach to handle settings with metadata, storage, and UI integration. 5 | 6 | ## Adding a New Setting 7 | 8 | ### 1. Define Setting Key 9 | Add your setting to the `SettingKey` enum in `settings_def.h`: 10 | ```c 11 | typedef enum { 12 | SETTING_RGB_MODE, 13 | SETTING_CHANNEL_HOP_DELAY, 14 | // Add your new setting here 15 | YOUR_NEW_SETTING, 16 | SETTINGS_COUNT // Keep this last 17 | } SettingKey; 18 | ``` 19 | 20 | ### 2. Add Setting Storage 21 | Add a field to the `Settings` struct in `settings_def.h`: 22 | ```c 23 | typedef struct { 24 | uint8_t rgb_mode_index; 25 | // Add your new setting storage 26 | uint8_t your_new_setting_index; 27 | } Settings; 28 | ``` 29 | 30 | ### 3. Define Setting Values 31 | If your setting has predefined values, add them in `settings_def.c`: 32 | ```c 33 | // For enum-based settings 34 | const char* const SETTING_VALUE_NAMES_YOUR_SETTING[] = {"Value1", "Value2", "Value3"}; 35 | ``` 36 | 37 | ### 4. Add Setting Metadata 38 | Add metadata in `settings_def.c` `SETTING_METADATA` array: 39 | 40 | #### For Regular Settings: 41 | ```c 42 | [YOUR_NEW_SETTING] = { 43 | .name = "Your Setting Name", 44 | .data.setting = { 45 | .max_value = YOUR_SETTING_COUNT - 1, 46 | .value_names = SETTING_VALUE_NAMES_YOUR_SETTING, 47 | .uart_command = "setsetting -i X -v" // If ESP needs to know about it 48 | }, 49 | .is_action = false 50 | }, 51 | ``` 52 | 53 | #### For Action Buttons: 54 | ```c 55 | [YOUR_NEW_ACTION] = { 56 | .name = "Your Action Name", 57 | .data.action = { 58 | .name = "Button Label", 59 | .command = "your_command", // UART command if needed 60 | .callback = &your_callback_function // Function to call when pressed 61 | }, 62 | .is_action = true 63 | }, 64 | ``` 65 | 66 | ### 5. Implement Setting Handler 67 | Add case in `settings_set` function in `settings_ui.c`: 68 | ```c 69 | case YOUR_NEW_SETTING: 70 | if(settings->your_new_setting_index != value) { 71 | settings->your_new_setting_index = value; 72 | changed = true; 73 | } 74 | break; 75 | ``` 76 | 77 | And in `settings_get`: 78 | ```c 79 | case YOUR_NEW_SETTING: 80 | return settings->your_new_setting_index; 81 | ``` 82 | 83 | ## Setting Types 84 | 85 | ### Regular Settings 86 | - Used for configurable options 87 | - Can have multiple values 88 | - Automatically saved to storage 89 | - Can trigger UART commands 90 | 91 | Example: 92 | ```c 93 | [SETTING_RGB_MODE] = { 94 | .name = "RGB Mode", 95 | .data.setting = { 96 | .max_value = RGB_MODE_COUNT - 1, 97 | .value_names = SETTING_VALUE_NAMES_RGB_MODE, 98 | .uart_command = "setsetting -i 1 -v" 99 | }, 100 | .is_action = false 101 | }, 102 | ``` 103 | 104 | ### Action Buttons 105 | - Used for triggering actions 106 | - Can have UART commands 107 | - Can have callback functions 108 | - Support confirmation dialogs 109 | 110 | Example: 111 | ```c 112 | [SETTING_CLEAR_LOGS] = { 113 | .name = "Clear Log Files", 114 | .data.action = { 115 | .name = "Clear Log Files", 116 | .command = NULL, 117 | .callback = &clear_log_files 118 | }, 119 | .is_action = true 120 | }, 121 | ``` 122 | 123 | ## Storage 124 | Settings are automatically saved to the file specified by `GHOST_ESP_APP_SETTINGS_FILE` when changed. The storage system: 125 | - Maintains settings between app launches 126 | - Includes version control 127 | - Handles file corruption 128 | - Creates default settings if no file exists 129 | 130 | ## Best Practices 131 | 1. Always initialize new settings to 0 in defaults 132 | 2. Use enum for predefined values 133 | 3. Add proper validation in settings_set 134 | 4. Keep setting names clear and concise 135 | 5. Group related settings together 136 | 6. Document UART commands if used 137 | 7. Add NULL checks in callbacks 138 | 8. Use logging for debugging 139 | 9. Consider adding confirmation for destructive actions 140 | 141 | ## Example: Full Setting Implementation 142 | Here's a complete example of adding a new setting: 143 | 144 | ```c 145 | // In settings_def.h 146 | typedef enum { 147 | SETTING_EXAMPLE_MODE, 148 | // ... other settings ... 149 | SETTINGS_COUNT 150 | } SettingKey; 151 | 152 | typedef enum { 153 | EXAMPLE_MODE_OFF, 154 | EXAMPLE_MODE_LOW, 155 | EXAMPLE_MODE_HIGH, 156 | EXAMPLE_MODE_COUNT 157 | } ExampleMode; 158 | 159 | // In settings_def.c 160 | const char* const SETTING_VALUE_NAMES_EXAMPLE[] = {"Off", "Low", "High"}; 161 | 162 | [SETTING_EXAMPLE_MODE] = { 163 | .name = "Example Setting", 164 | .data.setting = { 165 | .max_value = EXAMPLE_MODE_COUNT - 1, 166 | .value_names = SETTING_VALUE_NAMES_EXAMPLE, 167 | .uart_command = "setsetting -i 9 -v" 168 | }, 169 | .is_action = false 170 | }, 171 | ``` -------------------------------------------------------------------------------- /ghost_esp.backup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spooks4576/ghost_esp_app/0a2a2215c9e03908c827ade4215f78523b38715b/ghost_esp.backup.png -------------------------------------------------------------------------------- /ghost_esp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spooks4576/ghost_esp_app/0a2a2215c9e03908c827ade4215f78523b38715b/ghost_esp.png -------------------------------------------------------------------------------- /gui_modules/mainmenu.c: -------------------------------------------------------------------------------- 1 | #include "mainmenu.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #define CARD_WIDTH 28 10 | #define CARD_HEIGHT 44 11 | #define CARD_MARGIN 1 12 | #define BASE_Y_POSITION 5 13 | #define SELECTED_OFFSET 3 14 | #define ICON_WIDTH 20 15 | #define ICON_PADDING 3 16 | #define TEXT_BOTTOM_MARGIN 8 17 | #define BUTTON_HEIGHT 12 18 | 19 | typedef struct { 20 | uint8_t x; 21 | uint8_t y; 22 | uint8_t width; 23 | uint8_t height; 24 | } CardLayout; 25 | 26 | struct MainMenu { 27 | View* view; 28 | FuriTimer* locked_timer; 29 | MainMenuItemCallback help_callback; 30 | void* help_context; 31 | }; 32 | 33 | typedef struct { 34 | FuriString* label; 35 | uint32_t index; 36 | MainMenuItemCallback callback; 37 | void* callback_context; 38 | bool locked; 39 | FuriString* locked_message; 40 | } MainMenuItem; 41 | 42 | static void MainMenuItem_init(MainMenuItem* item) { 43 | item->label = furi_string_alloc(); 44 | item->index = 0; 45 | item->callback = NULL; 46 | item->callback_context = NULL; 47 | item->locked = false; 48 | item->locked_message = furi_string_alloc(); 49 | } 50 | 51 | static void MainMenuItem_init_set(MainMenuItem* item, const MainMenuItem* src) { 52 | item->label = furi_string_alloc_set(src->label); 53 | item->index = src->index; 54 | item->callback = src->callback; 55 | item->callback_context = src->callback_context; 56 | item->locked = src->locked; 57 | item->locked_message = furi_string_alloc_set(src->locked_message); 58 | } 59 | 60 | static void MainMenuItem_set(MainMenuItem* item, const MainMenuItem* src) { 61 | furi_string_set(item->label, src->label); 62 | item->index = src->index; 63 | item->callback = src->callback; 64 | item->callback_context = src->callback_context; 65 | item->locked = src->locked; 66 | furi_string_set(item->locked_message, src->locked_message); 67 | } 68 | 69 | static void MainMenuItem_clear(MainMenuItem* item) { 70 | furi_string_free(item->label); 71 | furi_string_free(item->locked_message); 72 | } 73 | 74 | ARRAY_DEF( 75 | MainMenuItemArray, 76 | MainMenuItem, 77 | (INIT(API_2(MainMenuItem_init)), 78 | SET(API_6(MainMenuItem_set)), 79 | INIT_SET(API_6(MainMenuItem_init_set)), 80 | CLEAR(API_2(MainMenuItem_clear)))) 81 | 82 | typedef struct { 83 | MainMenuItemArray_t items; 84 | FuriString* header; 85 | size_t position; 86 | size_t window_position; 87 | bool locked_message_visible; 88 | bool is_vertical; 89 | } MainMenuModel; 90 | 91 | static void main_menu_process_up(MainMenu* main_menu); 92 | static void main_menu_process_down(MainMenu* main_menu); 93 | static void main_menu_process_ok(MainMenu* main_menu); 94 | 95 | static size_t main_menu_items_on_screen(MainMenuModel* model) { 96 | UNUSED(model); 97 | size_t res = 8; 98 | return res; 99 | } 100 | 101 | static bool is_valid_icon_index(size_t position) { 102 | return position <= 4; 103 | } 104 | 105 | // Add the new helper function: 106 | void main_menu_set_help_callback(MainMenu* main_menu, MainMenuItemCallback callback, void* context) { 107 | furi_assert(main_menu); 108 | main_menu->help_callback = callback; 109 | main_menu->help_context = context; 110 | } 111 | 112 | static void draw_help_button(Canvas* canvas) { 113 | const char* str = "Help"; 114 | const size_t vertical_offset = 3; 115 | const size_t horizontal_offset = 3; 116 | const size_t string_width = canvas_string_width(canvas, str); 117 | 118 | // Create a small arrow icon directly 119 | const uint8_t arrow_width = 7; 120 | const uint8_t arrow_height = 4; 121 | const int32_t icon_h_offset = 3; 122 | const int32_t icon_width_with_offset = arrow_width + icon_h_offset; 123 | const size_t button_width = string_width + horizontal_offset * 2 + icon_width_with_offset; 124 | 125 | const int32_t x = (canvas_width(canvas) - button_width) / 2; 126 | const int32_t y = canvas_height(canvas); 127 | 128 | // Draw text 129 | canvas_draw_str(canvas, x + horizontal_offset, y - vertical_offset, str); 130 | 131 | // Draw small down arrow 132 | const int32_t arrow_x = x + horizontal_offset + string_width + icon_h_offset; 133 | const int32_t arrow_y = y - vertical_offset - arrow_height; 134 | canvas_draw_line(canvas, arrow_x + 3, arrow_y + 3, arrow_x + 6, arrow_y); 135 | canvas_draw_line(canvas, arrow_x + 3, arrow_y + 3, arrow_x, arrow_y); 136 | } 137 | 138 | static CardLayout calculate_card_layout( 139 | Canvas* canvas, 140 | size_t total_cards, 141 | size_t visible_cards, 142 | size_t position, 143 | bool is_selected) { 144 | 145 | UNUSED(total_cards); 146 | CardLayout layout = {0}; 147 | 148 | if(visible_cards == 0) { 149 | return layout; 150 | } 151 | 152 | const uint8_t total_width = visible_cards * CARD_WIDTH + (visible_cards - 1) * CARD_MARGIN; 153 | layout.x = (canvas_width(canvas) - total_width) / 2 + 154 | (position < visible_cards ? position : 0) * (CARD_WIDTH + CARD_MARGIN); 155 | 156 | layout.y = is_selected ? BASE_Y_POSITION - SELECTED_OFFSET : BASE_Y_POSITION; 157 | layout.width = CARD_WIDTH; 158 | layout.height = CARD_HEIGHT; 159 | 160 | return layout; 161 | } 162 | 163 | static void draw_card_background( 164 | Canvas* canvas, 165 | const CardLayout* layout, 166 | bool is_selected) { 167 | // Draw shadow effect 168 | canvas_set_color(canvas, ColorBlack); 169 | elements_slightly_rounded_box( 170 | canvas, 171 | layout->x + 2, 172 | layout->y + 2, 173 | layout->width, 174 | layout->height); 175 | 176 | // Draw main background 177 | canvas_set_color(canvas, is_selected ? ColorBlack : ColorWhite); 178 | elements_slightly_rounded_box( 179 | canvas, 180 | layout->x, 181 | layout->y, 182 | layout->width, 183 | layout->height); 184 | 185 | // Draw outline 186 | canvas_set_color(canvas, ColorBlack); 187 | elements_slightly_rounded_frame( 188 | canvas, 189 | layout->x, 190 | layout->y, 191 | layout->width, 192 | layout->height); 193 | } 194 | 195 | static void draw_card_icon( 196 | Canvas* canvas, 197 | const CardLayout* layout, 198 | const Icon* icon, 199 | bool is_selected) { 200 | const uint8_t icon_x = layout->x + (layout->width - ICON_WIDTH) / 2; 201 | const uint8_t icon_y = layout->y + ICON_PADDING; 202 | // Draw main icon 203 | canvas_set_color(canvas, is_selected ? ColorWhite : ColorBlack); 204 | canvas_draw_icon(canvas, icon_x, icon_y, icon); 205 | } 206 | 207 | static void draw_card_label( 208 | Canvas* canvas, 209 | const CardLayout* layout, 210 | FuriString* label, 211 | bool is_selected, 212 | bool is_last_card) { 213 | 214 | elements_string_fit_width(canvas, label, layout->width + 40); 215 | 216 | // Draw text shadow first 217 | if(!is_selected) { 218 | canvas_set_color(canvas, ColorWhite); 219 | canvas_draw_str( 220 | canvas, 221 | is_last_card ? layout->x + 4 : layout->x + 6, // Offset by 1 for shadow 222 | layout->y + layout->height - TEXT_BOTTOM_MARGIN + 1, 223 | furi_string_get_cstr(label)); 224 | } 225 | 226 | // Draw main text 227 | canvas_set_color(canvas, is_selected ? ColorWhite : ColorBlack); 228 | canvas_draw_str( 229 | canvas, 230 | is_last_card ? layout->x + 3 : layout->x + 5, 231 | layout->y + layout->height - TEXT_BOTTOM_MARGIN, 232 | furi_string_get_cstr(label)); 233 | } 234 | static void main_menu_view_draw_callback(Canvas* canvas, void* _model) { 235 | MainMenuModel* model = _model; 236 | const size_t total_cards = MainMenuItemArray_size(model->items); 237 | const size_t visible_cards = total_cards; 238 | 239 | canvas_clear(canvas); 240 | 241 | // Draw all menu items first 242 | size_t position = 0; 243 | MainMenuItemArray_it_t it; 244 | for(MainMenuItemArray_it(it, model->items); !MainMenuItemArray_end_p(it); 245 | MainMenuItemArray_next(it)) { 246 | const bool is_selected = (position == model->position); 247 | const bool is_last_card = position + 1 == total_cards; 248 | 249 | CardLayout layout = calculate_card_layout( 250 | canvas, 251 | total_cards, 252 | visible_cards, 253 | position, 254 | is_selected); 255 | 256 | draw_card_background(canvas, &layout, is_selected); 257 | 258 | const Icon* icon = NULL; 259 | if(is_valid_icon_index(position)) { 260 | switch(position) { 261 | case 0: icon = &I_Wifi_icon; break; 262 | case 1: icon = &I_BLE_icon; break; 263 | case 2: icon = &I_GPS; break; // Use GPS icon for position 2 264 | case 3: icon = &I_Cog; break; // Use Settings icon for position 3 265 | } 266 | } 267 | 268 | if(icon) { 269 | draw_card_icon(canvas, &layout, icon, is_selected); 270 | } 271 | 272 | FuriString* disp_str = furi_string_alloc_set(MainMenuItemArray_cref(it)->label); 273 | draw_card_label(canvas, &layout, disp_str, is_selected, is_last_card); 274 | furi_string_free(disp_str); 275 | 276 | position++; 277 | } 278 | 279 | // Draw help button last 280 | canvas_set_font(canvas, FontSecondary); 281 | canvas_set_color(canvas, ColorBlack); 282 | draw_help_button(canvas); 283 | canvas_set_color(canvas, ColorBlack); 284 | } 285 | static bool main_menu_view_input_callback(InputEvent* event, void* context) { 286 | MainMenu* main_menu = context; 287 | furi_assert(main_menu); 288 | bool consumed = false; 289 | 290 | bool locked_message_visible = false; 291 | with_view_model( 292 | main_menu->view, 293 | MainMenuModel * model, 294 | { locked_message_visible = model->locked_message_visible; }, 295 | false); 296 | 297 | if((event->type != InputTypePress && event->type != InputTypeRelease) && 298 | locked_message_visible) { 299 | with_view_model( 300 | main_menu->view, 301 | MainMenuModel * model, 302 | { model->locked_message_visible = false; }, 303 | true); 304 | consumed = true; 305 | } else if(event->type == InputTypeShort) { 306 | switch(event->key) { 307 | case InputKeyLeft: 308 | consumed = true; 309 | main_menu_process_up(main_menu); 310 | break; 311 | case InputKeyRight: 312 | consumed = true; 313 | main_menu_process_down(main_menu); 314 | break; 315 | case InputKeyOk: 316 | consumed = true; 317 | main_menu_process_ok(main_menu); 318 | break; 319 | case InputKeyDown: 320 | // Handle help button press 321 | if(main_menu->help_callback) { 322 | main_menu->help_callback(main_menu->help_context, 0); 323 | consumed = true; 324 | } 325 | break; 326 | default: 327 | break; 328 | } 329 | } else if(event->type == InputTypeRepeat) { 330 | if(event->key == InputKeyUp) { 331 | consumed = true; 332 | main_menu_process_up(main_menu); 333 | } else if(event->key == InputKeyDown) { 334 | consumed = true; 335 | main_menu_process_down(main_menu); 336 | } 337 | } 338 | 339 | return consumed; 340 | } 341 | 342 | 343 | void main_menu_timer_callback(void* context) { 344 | furi_assert(context); 345 | MainMenu* main_menu = context; 346 | 347 | with_view_model( 348 | main_menu->view, MainMenuModel * model, { model->locked_message_visible = false; }, true); 349 | } 350 | 351 | MainMenu* main_menu_alloc(void) { 352 | MainMenu* main_menu = malloc(sizeof(MainMenu)); 353 | main_menu->view = view_alloc(); 354 | view_set_context(main_menu->view, main_menu); 355 | view_allocate_model(main_menu->view, ViewModelTypeLocking, sizeof(MainMenuModel)); 356 | view_set_draw_callback(main_menu->view, main_menu_view_draw_callback); 357 | view_set_input_callback(main_menu->view, main_menu_view_input_callback); 358 | 359 | main_menu->locked_timer = 360 | furi_timer_alloc(main_menu_timer_callback, FuriTimerTypeOnce, main_menu); 361 | 362 | with_view_model( 363 | main_menu->view, 364 | MainMenuModel * model, 365 | { 366 | MainMenuItemArray_init(model->items); 367 | model->position = 0; 368 | model->window_position = 0; 369 | model->header = furi_string_alloc(); 370 | }, 371 | true); 372 | 373 | return main_menu; 374 | } 375 | 376 | void main_menu_free(MainMenu* main_menu) { 377 | furi_check(main_menu); 378 | 379 | with_view_model( 380 | main_menu->view, 381 | MainMenuModel * model, 382 | { 383 | furi_string_free(model->header); 384 | MainMenuItemArray_clear(model->items); 385 | }, 386 | true); 387 | furi_timer_stop(main_menu->locked_timer); 388 | furi_timer_free(main_menu->locked_timer); 389 | view_free(main_menu->view); 390 | free(main_menu); 391 | } 392 | 393 | View* main_menu_get_view(MainMenu* main_menu) { 394 | furi_check(main_menu); 395 | return main_menu->view; 396 | } 397 | 398 | void main_menu_add_item( 399 | MainMenu* main_menu, 400 | const char* label, 401 | uint32_t index, 402 | MainMenuItemCallback callback, 403 | void* callback_context) { 404 | main_menu_add_lockable_item(main_menu, label, index, callback, callback_context, false, NULL); 405 | } 406 | 407 | void main_menu_add_lockable_item( 408 | MainMenu* main_menu, 409 | const char* label, 410 | uint32_t index, 411 | MainMenuItemCallback callback, 412 | void* callback_context, 413 | bool locked, 414 | const char* locked_message) { 415 | MainMenuItem* item = NULL; 416 | furi_check(label); 417 | furi_check(main_menu); 418 | if(locked) { 419 | furi_check(locked_message); 420 | } 421 | 422 | with_view_model( 423 | main_menu->view, 424 | MainMenuModel * model, 425 | { 426 | item = MainMenuItemArray_push_new(model->items); 427 | furi_string_set_str(item->label, label); 428 | item->index = index; 429 | item->callback = callback; 430 | item->callback_context = callback_context; 431 | item->locked = locked; 432 | if(locked) { 433 | furi_string_set_str(item->locked_message, locked_message); 434 | } 435 | }, 436 | true); 437 | } 438 | 439 | void main_menu_change_item_label(MainMenu* main_menu, uint32_t index, const char* label) { 440 | furi_check(main_menu); 441 | furi_check(label); 442 | 443 | with_view_model( 444 | main_menu->view, 445 | MainMenuModel * model, 446 | { 447 | MainMenuItemArray_it_t it; 448 | for(MainMenuItemArray_it(it, model->items); !MainMenuItemArray_end_p(it); 449 | MainMenuItemArray_next(it)) { 450 | if(index == MainMenuItemArray_cref(it)->index) { 451 | furi_string_set_str(MainMenuItemArray_cref(it)->label, label); 452 | break; 453 | } 454 | } 455 | }, 456 | true); 457 | } 458 | 459 | void main_menu_reset(MainMenu* main_menu) { 460 | furi_check(main_menu); 461 | view_set_orientation(main_menu->view, ViewOrientationHorizontal); 462 | 463 | with_view_model( 464 | main_menu->view, 465 | MainMenuModel * model, 466 | { 467 | MainMenuItemArray_reset(model->items); 468 | model->position = 0; 469 | model->window_position = 0; 470 | model->is_vertical = false; 471 | furi_string_reset(model->header); 472 | }, 473 | true); 474 | } 475 | 476 | uint32_t main_menu_get_selected_item(MainMenu* main_menu) { 477 | furi_check(main_menu); 478 | 479 | uint32_t selected_item_index = 0; 480 | 481 | with_view_model( 482 | main_menu->view, 483 | MainMenuModel * model, 484 | { 485 | if(model->position < MainMenuItemArray_size(model->items)) { 486 | const MainMenuItem* item = MainMenuItemArray_cget(model->items, model->position); 487 | selected_item_index = item->index; 488 | } 489 | }, 490 | false); 491 | 492 | return selected_item_index; 493 | } 494 | 495 | void main_menu_set_selected_item(MainMenu* main_menu, uint32_t index) { 496 | furi_check(main_menu); 497 | with_view_model( 498 | main_menu->view, 499 | MainMenuModel * model, 500 | { 501 | size_t position = 0; 502 | MainMenuItemArray_it_t it; 503 | for(MainMenuItemArray_it(it, model->items); !MainMenuItemArray_end_p(it); 504 | MainMenuItemArray_next(it)) { 505 | if(index == MainMenuItemArray_cref(it)->index) { 506 | break; 507 | } 508 | position++; 509 | } 510 | 511 | const size_t items_size = MainMenuItemArray_size(model->items); 512 | 513 | if(position >= items_size) { 514 | position = 0; 515 | } 516 | 517 | model->position = position; 518 | model->window_position = position; 519 | 520 | if(model->window_position > 0) { 521 | model->window_position -= 1; 522 | } 523 | 524 | const size_t items_on_screen = main_menu_items_on_screen(model); 525 | 526 | if(items_size <= items_on_screen) { 527 | model->window_position = 0; 528 | } else { 529 | const size_t pos = items_size - items_on_screen; 530 | if(model->window_position > pos) { 531 | model->window_position = pos; 532 | } 533 | } 534 | }, 535 | true); 536 | } 537 | 538 | void main_menu_process_up(MainMenu* main_menu) { 539 | with_view_model( 540 | main_menu->view, 541 | MainMenuModel * model, 542 | { 543 | const size_t items_on_screen = main_menu_items_on_screen(model); 544 | const size_t items_size = MainMenuItemArray_size(model->items); 545 | 546 | if(model->position > 0) { 547 | model->position--; 548 | if((model->position == model->window_position) && (model->window_position > 0)) { 549 | model->window_position--; 550 | } 551 | } else { 552 | model->position = items_size - 1; 553 | if(model->position > items_on_screen - 1) { 554 | model->window_position = model->position - (items_on_screen - 1); 555 | } 556 | } 557 | }, 558 | true); 559 | } 560 | 561 | void main_menu_process_down(MainMenu* main_menu) { 562 | with_view_model( 563 | main_menu->view, 564 | MainMenuModel * model, 565 | { 566 | const size_t items_on_screen = main_menu_items_on_screen(model); 567 | const size_t items_size = MainMenuItemArray_size(model->items); 568 | 569 | if(model->position < items_size - 1) { 570 | model->position++; 571 | if((model->position - model->window_position > items_on_screen - 2) && 572 | (model->window_position < items_size - items_on_screen)) { 573 | model->window_position++; 574 | } 575 | } else { 576 | model->position = 0; 577 | model->window_position = 0; 578 | } 579 | }, 580 | true); 581 | } 582 | 583 | void main_menu_process_ok(MainMenu* main_menu) { 584 | MainMenuItem* item = NULL; 585 | 586 | with_view_model( 587 | main_menu->view, 588 | MainMenuModel * model, 589 | { 590 | const size_t items_size = MainMenuItemArray_size(model->items); 591 | if(model->position < items_size) { 592 | item = MainMenuItemArray_get(model->items, model->position); 593 | } 594 | if(item && item->locked) { 595 | model->locked_message_visible = true; 596 | furi_timer_start(main_menu->locked_timer, furi_kernel_get_tick_frequency() * 3); 597 | } 598 | }, 599 | true); 600 | 601 | if(item && !item->locked && item->callback) { 602 | item->callback(item->callback_context, item->index); 603 | } 604 | } 605 | 606 | void main_menu_set_header(MainMenu* main_menu, const char* header) { 607 | furi_check(main_menu); 608 | 609 | with_view_model( 610 | main_menu->view, 611 | MainMenuModel * model, 612 | { 613 | if(header == NULL) { 614 | furi_string_reset(model->header); 615 | } else { 616 | furi_string_set_str(model->header, header); 617 | } 618 | }, 619 | true); 620 | } 621 | 622 | void main_menu_set_orientation(MainMenu* main_menu, ViewOrientation orientation) { 623 | furi_check(main_menu); 624 | const bool is_vertical = orientation == ViewOrientationVertical || 625 | orientation == ViewOrientationVerticalFlip; 626 | 627 | view_set_orientation(main_menu->view, orientation); 628 | 629 | with_view_model( 630 | main_menu->view, 631 | MainMenuModel * model, 632 | { 633 | model->is_vertical = is_vertical; 634 | 635 | // Recalculating the position 636 | // Need if _set_orientation is called after _set_selected_item 637 | size_t position = model->position; 638 | const size_t items_size = MainMenuItemArray_size(model->items); 639 | const size_t items_on_screen = main_menu_items_on_screen(model); 640 | 641 | if(position >= items_size) { 642 | position = 0; 643 | } 644 | 645 | model->position = position; 646 | model->window_position = position; 647 | 648 | if(model->window_position > 0) { 649 | model->window_position -= 1; 650 | } 651 | 652 | if(items_size <= items_on_screen) { 653 | model->window_position = 0; 654 | } else { 655 | const size_t pos = items_size - items_on_screen; 656 | if(model->window_position > pos) { 657 | model->window_position = pos; 658 | } 659 | } 660 | }, 661 | true); 662 | } 663 | -------------------------------------------------------------------------------- /gui_modules/mainmenu.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file main_menu.h 3 | * GUI: MainMenu view module API 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | #ifdef __cplusplus 11 | extern "C" { 12 | #endif 13 | 14 | /** MainMenu anonymous structure */ 15 | typedef struct MainMenu MainMenu; 16 | typedef void (*MainMenuItemCallback)(void* context, uint32_t index); 17 | 18 | /** Allocate and initialize main menu 19 | * 20 | * This main menu is used to select one option 21 | * 22 | * @return MainMenu instance 23 | */ 24 | MainMenu* main_menu_alloc(void); 25 | 26 | /** Deinitialize and free main menu 27 | * 28 | * @param main_menu MainMenu instance 29 | */ 30 | void main_menu_free(MainMenu* main_menu); 31 | 32 | /** Get main menu view 33 | * 34 | * @param main_menu MainMenu instance 35 | * 36 | * @return View instance that can be used for embedding 37 | */ 38 | View* main_menu_get_view(MainMenu* main_menu); 39 | 40 | /** Add item to main menu 41 | * 42 | * @param main_menu MainMenu instance 43 | * @param label menu item label 44 | * @param index menu item index, used for callback, may be 45 | * the same with other items 46 | * @param callback menu item callback 47 | * @param callback_context menu item callback context 48 | */ 49 | void main_menu_add_item( 50 | MainMenu* main_menu, 51 | const char* label, 52 | uint32_t index, 53 | MainMenuItemCallback callback, 54 | void* callback_context); 55 | 56 | /** Add lockable item to main menu 57 | * 58 | * @param main_menu MainMenu instance 59 | * @param label menu item label 60 | * @param index menu item index, used for callback, may be 61 | * the same with other items 62 | * @param callback menu item callback 63 | * @param callback_context menu item callback context 64 | * @param locked menu item locked status 65 | * @param locked_message menu item locked message 66 | */ 67 | void main_menu_add_lockable_item( 68 | MainMenu* main_menu, 69 | const char* label, 70 | uint32_t index, 71 | MainMenuItemCallback callback, 72 | void* callback_context, 73 | bool locked, 74 | const char* locked_message); 75 | 76 | /** Change label of an existing item 77 | * 78 | * @param main_menu MainMenu instance 79 | * @param index The index of the item 80 | * @param label The new label 81 | */ 82 | void main_menu_change_item_label(MainMenu* main_menu, uint32_t index, const char* label); 83 | 84 | /** Remove all items from main menu 85 | * 86 | * @param main_menu MainMenu instance 87 | */ 88 | void main_menu_reset(MainMenu* main_menu); 89 | 90 | /** Get main menu selected item index 91 | * 92 | * @param main_menu MainMenu instance 93 | * 94 | * @return Index of the selected item 95 | */ 96 | uint32_t main_menu_get_selected_item(MainMenu* main_menu); 97 | 98 | /** Set main menu selected item by index 99 | * 100 | * @param main_menu MainMenu instance 101 | * @param index The index of the selected item 102 | */ 103 | void main_menu_set_selected_item(MainMenu* main_menu, uint32_t index); 104 | 105 | /** Set optional header for main menu 106 | * Must be called before adding items OR after adding items and before set_selected_item() 107 | * 108 | * @param main_menu MainMenu instance 109 | * @param header header to set 110 | */ 111 | void main_menu_set_header(MainMenu* main_menu, const char* header); 112 | 113 | /** Set main menu orientation 114 | * 115 | * @param main_menu MainMenu instance 116 | * @param orientation either vertical or horizontal 117 | */ 118 | void main_menu_set_orientation(MainMenu* main_menu, ViewOrientation orientation); 119 | 120 | void main_menu_set_help_callback(MainMenu* main_menu, MainMenuItemCallback callback, void* context); 121 | 122 | 123 | #ifdef __cplusplus 124 | } 125 | #endif -------------------------------------------------------------------------------- /images/BLE_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spooks4576/ghost_esp_app/0a2a2215c9e03908c827ade4215f78523b38715b/images/BLE_icon.png -------------------------------------------------------------------------------- /images/Cog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spooks4576/ghost_esp_app/0a2a2215c9e03908c827ade4215f78523b38715b/images/Cog.png -------------------------------------------------------------------------------- /images/GPS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spooks4576/ghost_esp_app/0a2a2215c9e03908c827ade4215f78523b38715b/images/GPS.png -------------------------------------------------------------------------------- /images/Wifi_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spooks4576/ghost_esp_app/0a2a2215c9e03908c827ade4215f78523b38715b/images/Wifi_icon.png -------------------------------------------------------------------------------- /screenshots/connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spooks4576/ghost_esp_app/0a2a2215c9e03908c827ade4215f78523b38715b/screenshots/connect.png -------------------------------------------------------------------------------- /screenshots/mainmenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spooks4576/ghost_esp_app/0a2a2215c9e03908c827ade4215f78523b38715b/screenshots/mainmenu.png -------------------------------------------------------------------------------- /screenshots/scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spooks4576/ghost_esp_app/0a2a2215c9e03908c827ade4215f78523b38715b/screenshots/scan.png -------------------------------------------------------------------------------- /screenshots/wifi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spooks4576/ghost_esp_app/0a2a2215c9e03908c827ade4215f78523b38715b/screenshots/wifi.png -------------------------------------------------------------------------------- /src/app_state.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "gui_modules/mainmenu.h" 10 | #include "settings_def.h" 11 | #include "app_types.h" 12 | #include "settings_ui_types.h" 13 | 14 | typedef struct { 15 | bool enabled; // Master switch for filtering 16 | bool show_ble_status; 17 | bool show_wifi_status; 18 | bool show_flipper_devices; 19 | bool show_wifi_networks; 20 | bool strip_ansi_codes; 21 | bool add_prefixes; // Whether to add [BLE], [WIFI] etc prefixes 22 | } FilterConfig; 23 | 24 | struct AppState { 25 | // Views 26 | ViewDispatcher* view_dispatcher; 27 | MainMenu* main_menu; 28 | Submenu* wifi_menu; 29 | Submenu* ble_menu; 30 | Submenu* gps_menu; 31 | VariableItemList* settings_menu; 32 | TextBox* text_box; 33 | TextInput* text_input; 34 | ConfirmationView* confirmation_view; 35 | FuriMutex* buffer_mutex; 36 | // UART Context 37 | UartContext* uart_context; 38 | FilterConfig* filter_config; 39 | 40 | // Settings 41 | Settings settings; 42 | SettingsUIContext settings_ui_context; 43 | Submenu* settings_actions_menu; 44 | 45 | // State 46 | uint32_t current_index; 47 | uint8_t current_view; 48 | uint8_t previous_view; 49 | uint32_t last_wifi_index; 50 | uint32_t last_ble_index; 51 | uint32_t last_gps_index; 52 | char* input_buffer; 53 | const char* uart_command; 54 | char* textBoxBuffer; 55 | size_t buffer_length; 56 | size_t buffer_capacity; 57 | size_t buffer_size; 58 | }; -------------------------------------------------------------------------------- /src/app_types.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Forward declarations only 4 | typedef struct AppState AppState; 5 | typedef struct UartContext UartContext; 6 | typedef struct StorageContext StorageContext; 7 | typedef struct SettingsUIContext SettingsUIContext; 8 | typedef struct ConfirmationView ConfirmationView; 9 | typedef struct UartStorageContext UartStorageContext; -------------------------------------------------------------------------------- /src/callbacks.c: -------------------------------------------------------------------------------- 1 | #include "callbacks.h" 2 | #include "app_state.h" 3 | #include "menu.h" 4 | #include "settings_def.h" 5 | #include "settings_ui.h" 6 | #include "uart_utils.h" 7 | 8 | 9 | 10 | 11 | void on_rgb_mode_changed(VariableItem* item) { 12 | AppState* app = variable_item_get_context(item); 13 | uint8_t index = variable_item_get_current_value_index(item); 14 | app->settings.rgb_mode_index = index; 15 | variable_item_set_current_value_text(item, SETTING_VALUE_NAMES_RGB_MODE[index]); 16 | char command[32]; 17 | snprintf(command, sizeof(command), "setsetting -i 1 -v %d\n", index + 1); 18 | send_uart_command(command, app); 19 | } 20 | 21 | void on_channelswitchdelay_changed(VariableItem* item) { 22 | AppState* app = variable_item_get_context(item); 23 | uint8_t index = variable_item_get_current_value_index(item); 24 | app->settings.channel_hop_delay_index = index; 25 | variable_item_set_current_value_text(item, SETTING_VALUE_NAMES_CHANNEL_HOP[index]); 26 | char command[32]; 27 | snprintf(command, sizeof(command), "setsetting -i 2 -v %d\n", index + 1); 28 | send_uart_command(command, app); 29 | } 30 | 31 | void on_togglechannelhopping_changed(VariableItem* item) { 32 | AppState* app = variable_item_get_context(item); 33 | uint8_t index = variable_item_get_current_value_index(item); 34 | app->settings.enable_channel_hopping_index = index; 35 | variable_item_set_current_value_text(item, SETTING_VALUE_NAMES_BOOL[index]); 36 | char command[32]; 37 | snprintf(command, sizeof(command), "setsetting -i 3 -v %d\n", index + 1); 38 | send_uart_command(command, app); 39 | } 40 | 41 | void on_ble_mac_changed(VariableItem* item) { 42 | AppState* app = variable_item_get_context(item); 43 | uint8_t index = variable_item_get_current_value_index(item); 44 | app->settings.enable_random_ble_mac_index = index; 45 | variable_item_set_current_value_text(item, SETTING_VALUE_NAMES_BOOL[index]); 46 | char command[32]; 47 | snprintf(command, sizeof(command), "setsetting -i 4 -v %d\n", index + 1); 48 | send_uart_command(command, app); 49 | } 50 | 51 | void on_stop_on_back_changed(VariableItem* item) { 52 | AppState* app = variable_item_get_context(item); 53 | uint8_t index = variable_item_get_current_value_index(item); 54 | app->settings.stop_on_back_index = index; 55 | variable_item_set_current_value_text(item, SETTING_VALUE_NAMES_BOOL[index]); 56 | } 57 | 58 | void on_reboot_esp_changed(VariableItem* item) { 59 | AppState* app = variable_item_get_context(item); 60 | uint8_t index = variable_item_get_current_value_index(item); 61 | variable_item_set_current_value_text(item, SETTING_VALUE_NAMES_ACTION[index]); 62 | send_uart_command("handle_reboot\n", app); 63 | } 64 | 65 | void logs_clear_confirmed_callback(void* context) { 66 | FURI_LOG_D("ClearLogs", "Confirmed callback started, context: %p", context); 67 | 68 | SettingsConfirmContext* ctx = context; 69 | if(!ctx) { 70 | FURI_LOG_E("ClearLogs", "Null context"); 71 | return; 72 | } 73 | 74 | if(!ctx->state) { 75 | FURI_LOG_E("ClearLogs", "Null state in context"); 76 | free(ctx); 77 | return; 78 | } 79 | 80 | AppState* app_state = ctx->state; 81 | uint32_t prev_view = app_state->previous_view; 82 | 83 | FURI_LOG_D("ClearLogs", "Previous view: %lu", prev_view); 84 | clear_log_files(ctx->state); 85 | 86 | // Reset callbacks 87 | FURI_LOG_D("ClearLogs", "Resetting callbacks"); 88 | confirmation_view_set_ok_callback(app_state->confirmation_view, NULL, NULL); 89 | confirmation_view_set_cancel_callback(app_state->confirmation_view, NULL, NULL); 90 | 91 | // Free context first 92 | free(ctx); 93 | 94 | // Switch view last and update current_view 95 | FURI_LOG_D("ClearLogs", "Switching to view: %lu", prev_view); 96 | view_dispatcher_switch_to_view(app_state->view_dispatcher, prev_view); 97 | app_state->current_view = prev_view; // Add this line 98 | } 99 | 100 | void logs_clear_cancelled_callback(void* context) { 101 | FURI_LOG_D("ClearLogs", "Cancel callback started, context: %p", context); 102 | 103 | SettingsConfirmContext* ctx = context; 104 | if(!ctx) { 105 | FURI_LOG_E("ClearLogs", "Null context"); 106 | return; 107 | } 108 | 109 | if(!ctx->state) { 110 | FURI_LOG_E("ClearLogs", "Null state in context"); 111 | free(ctx); 112 | return; 113 | } 114 | 115 | AppState* app_state = ctx->state; 116 | uint32_t prev_view = app_state->previous_view; 117 | 118 | FURI_LOG_D("ClearLogs", "Previous view: %lu", prev_view); 119 | 120 | // Reset callbacks before freeing context 121 | FURI_LOG_D("ClearLogs", "Resetting callbacks"); 122 | confirmation_view_set_ok_callback(app_state->confirmation_view, NULL, NULL); 123 | confirmation_view_set_cancel_callback(app_state->confirmation_view, NULL, NULL); 124 | 125 | // Free context 126 | free(ctx); 127 | 128 | // Switch view last and update current_view 129 | FURI_LOG_D("ClearLogs", "Switching to view: %lu", prev_view); 130 | view_dispatcher_switch_to_view(app_state->view_dispatcher, prev_view); 131 | app_state->current_view = prev_view; // Add this line 132 | } 133 | 134 | void on_clear_logs_changed(VariableItem* item) { 135 | AppState* app = variable_item_get_context(item); 136 | uint8_t index = variable_item_get_current_value_index(item); 137 | variable_item_set_current_value_text(item, SETTING_VALUE_NAMES_ACTION[index]); 138 | 139 | if(index == 0) { 140 | show_confirmation_dialog_ex( 141 | app, 142 | "Clear Logs", 143 | "Are you sure you want to clear the logs?\nThis action cannot be undone.", 144 | logs_clear_confirmed_callback, 145 | logs_clear_cancelled_callback); 146 | } 147 | } 148 | 149 | 150 | 151 | void on_clear_nvs_changed(VariableItem* item) { 152 | AppState* app = variable_item_get_context(item); 153 | uint8_t index = variable_item_get_current_value_index(item); 154 | variable_item_set_current_value_text(item, SETTING_VALUE_NAMES_ACTION[index]); 155 | 156 | if(index == 0) { 157 | show_confirmation_dialog_ex( // Changed to _ex version 158 | app, 159 | "Clear NVS", 160 | "Are you sure you want to clear NVS?\nThis will reset all ESP settings.", 161 | nvs_clear_confirmed_callback, 162 | nvs_clear_cancelled_callback); 163 | } 164 | } 165 | 166 | void nvs_clear_confirmed_callback(void* context) { 167 | SettingsConfirmContext* ctx = context; 168 | if(ctx && ctx->state) { 169 | AppState* app_state = ctx->state; 170 | uint32_t prev_view = app_state->previous_view; 171 | 172 | send_uart_command("handle_clearnvs\n", ctx->state); 173 | 174 | confirmation_view_set_ok_callback(app_state->confirmation_view, NULL, NULL); 175 | confirmation_view_set_cancel_callback(app_state->confirmation_view, NULL, NULL); 176 | 177 | free(ctx); 178 | 179 | view_dispatcher_switch_to_view(app_state->view_dispatcher, prev_view); 180 | app_state->current_view = prev_view; // Add this line 181 | } 182 | } 183 | 184 | void nvs_clear_cancelled_callback(void* context) { 185 | SettingsConfirmContext* ctx = context; 186 | if(ctx && ctx->state) { 187 | AppState* app_state = ctx->state; 188 | uint32_t prev_view = app_state->previous_view; 189 | 190 | confirmation_view_set_ok_callback(app_state->confirmation_view, NULL, NULL); 191 | confirmation_view_set_cancel_callback(app_state->confirmation_view, NULL, NULL); 192 | 193 | free(ctx); 194 | 195 | view_dispatcher_switch_to_view(app_state->view_dispatcher, prev_view); 196 | app_state->current_view = prev_view; // Add this line 197 | } 198 | } 199 | void show_app_info(void* context) { 200 | SettingsUIContext* settings_context = (SettingsUIContext*)context; 201 | AppState* app = (AppState*)settings_context->context; 202 | 203 | FURI_LOG_D("AppInfo", "Show app info called, context: %p", app); 204 | 205 | const char* info_text = 206 | ""; 207 | 208 | if(app && app->confirmation_view) { 209 | // Create a new context for the confirmation dialog 210 | SettingsConfirmContext* confirm_ctx = malloc(sizeof(SettingsConfirmContext)); 211 | if(!confirm_ctx) { 212 | FURI_LOG_E("AppInfo", "Failed to allocate confirmation context"); 213 | return; 214 | } 215 | confirm_ctx->state = app; 216 | 217 | // Save current view before switching 218 | app->previous_view = app->current_view; 219 | FURI_LOG_D("AppInfo", "Saved previous view: %d", app->previous_view); 220 | 221 | confirmation_view_set_header(app->confirmation_view, "App Info"); 222 | confirmation_view_set_text(app->confirmation_view, info_text); 223 | 224 | // Set up callbacks with proper context 225 | confirmation_view_set_ok_callback( 226 | app->confirmation_view, 227 | app_info_ok_callback, 228 | confirm_ctx); 229 | confirmation_view_set_cancel_callback( 230 | app->confirmation_view, 231 | app_info_cancel_callback, 232 | confirm_ctx); 233 | 234 | // Switch to confirmation view 235 | FURI_LOG_D("AppInfo", "Switching to confirmation view"); 236 | view_dispatcher_switch_to_view(app->view_dispatcher, 7); // 7 is confirmation view 237 | app->current_view = 7; 238 | } else { 239 | FURI_LOG_E("AppInfo", "Invalid app state or confirmation view"); 240 | } 241 | } 242 | 243 | // Update callback functions to use proper context 244 | void app_info_ok_callback(void* context) { 245 | SettingsConfirmContext* ctx = (SettingsConfirmContext*)context; 246 | if(!ctx || !ctx->state) { 247 | FURI_LOG_E("AppInfo", "Invalid callback context"); 248 | return; 249 | } 250 | 251 | AppState* app_state = ctx->state; 252 | uint32_t prev_view = app_state->previous_view; 253 | 254 | FURI_LOG_D("AppInfo", "OK callback, returning to view: %lu", prev_view); 255 | 256 | // Reset callbacks 257 | confirmation_view_set_ok_callback(app_state->confirmation_view, NULL, NULL); 258 | confirmation_view_set_cancel_callback(app_state->confirmation_view, NULL, NULL); 259 | 260 | // Free the context 261 | free(ctx); 262 | 263 | // Return to previous view 264 | view_dispatcher_switch_to_view(app_state->view_dispatcher, prev_view); 265 | app_state->current_view = prev_view; 266 | } 267 | 268 | void app_info_cancel_callback(void* context) { 269 | // Use same callback as OK since both just return to previous view 270 | app_info_ok_callback(context); 271 | } 272 | 273 | // Add these new callback declarations 274 | void wardrive_clear_confirmed_callback(void* context) { 275 | FURI_LOG_D("ClearWardrive", "Confirmed callback started, context: %p", context); 276 | 277 | SettingsConfirmContext* ctx = context; 278 | if(!ctx) { 279 | FURI_LOG_E("ClearWardrive", "Null context"); 280 | return; 281 | } 282 | 283 | if(!ctx->state) { 284 | FURI_LOG_E("ClearWardrive", "Null state in context"); 285 | free(ctx); 286 | return; 287 | } 288 | 289 | AppState* app_state = ctx->state; 290 | uint32_t prev_view = app_state->previous_view; 291 | 292 | FURI_LOG_D("ClearWardrive", "Previous view: %lu", prev_view); 293 | clear_wardrive_files(ctx->state); 294 | 295 | // Reset callbacks 296 | confirmation_view_set_ok_callback(app_state->confirmation_view, NULL, NULL); 297 | confirmation_view_set_cancel_callback(app_state->confirmation_view, NULL, NULL); 298 | 299 | free(ctx); 300 | 301 | view_dispatcher_switch_to_view(app_state->view_dispatcher, prev_view); 302 | app_state->current_view = prev_view; 303 | } 304 | 305 | void wardrive_clear_cancelled_callback(void* context) { 306 | // Similar to logs_clear_cancelled_callback 307 | SettingsConfirmContext* ctx = context; 308 | if(!ctx || !ctx->state) { 309 | FURI_LOG_E("ClearWardrive", "Invalid context"); 310 | free(ctx); 311 | return; 312 | } 313 | 314 | AppState* app_state = ctx->state; 315 | uint32_t prev_view = app_state->previous_view; 316 | 317 | confirmation_view_set_ok_callback(app_state->confirmation_view, NULL, NULL); 318 | confirmation_view_set_cancel_callback(app_state->confirmation_view, NULL, NULL); 319 | 320 | free(ctx); 321 | 322 | view_dispatcher_switch_to_view(app_state->view_dispatcher, prev_view); 323 | app_state->current_view = prev_view; 324 | } 325 | 326 | void pcap_clear_confirmed_callback(void* context) { 327 | FURI_LOG_D("ClearPCAP", "Confirmed callback started, context: %p", context); 328 | 329 | SettingsConfirmContext* ctx = context; 330 | if(!ctx) { 331 | FURI_LOG_E("ClearPCAP", "Null context"); 332 | return; 333 | } 334 | 335 | if(!ctx->state) { 336 | FURI_LOG_E("ClearPCAP", "Null state in context"); 337 | free(ctx); 338 | return; 339 | } 340 | 341 | AppState* app_state = ctx->state; 342 | uint32_t prev_view = app_state->previous_view; 343 | 344 | FURI_LOG_D("ClearPCAP", "Previous view: %lu", prev_view); 345 | clear_pcap_files(ctx->state); 346 | 347 | confirmation_view_set_ok_callback(app_state->confirmation_view, NULL, NULL); 348 | confirmation_view_set_cancel_callback(app_state->confirmation_view, NULL, NULL); 349 | 350 | free(ctx); 351 | 352 | view_dispatcher_switch_to_view(app_state->view_dispatcher, prev_view); 353 | app_state->current_view = prev_view; 354 | } 355 | 356 | void pcap_clear_cancelled_callback(void* context) { 357 | // Similar to logs_clear_cancelled_callback 358 | SettingsConfirmContext* ctx = context; 359 | if(!ctx || !ctx->state) { 360 | FURI_LOG_E("ClearPCAP", "Invalid context"); 361 | free(ctx); 362 | return; 363 | } 364 | 365 | AppState* app_state = ctx->state; 366 | uint32_t prev_view = app_state->previous_view; 367 | 368 | confirmation_view_set_ok_callback(app_state->confirmation_view, NULL, NULL); 369 | confirmation_view_set_cancel_callback(app_state->confirmation_view, NULL, NULL); 370 | 371 | free(ctx); 372 | 373 | view_dispatcher_switch_to_view(app_state->view_dispatcher, prev_view); 374 | app_state->current_view = prev_view; 375 | } 376 | 377 | // Add these variable item callbacks 378 | void on_clear_wardrive_changed(VariableItem* item) { 379 | AppState* app = variable_item_get_context(item); 380 | uint8_t index = variable_item_get_current_value_index(item); 381 | variable_item_set_current_value_text(item, SETTING_VALUE_NAMES_ACTION[index]); 382 | 383 | if(index == 0) { 384 | show_confirmation_dialog_ex( 385 | app, 386 | "Clear Wardrives", 387 | "Are you sure you want to clear\nall wardrive files?\nThis action cannot be undone.", 388 | wardrive_clear_confirmed_callback, 389 | wardrive_clear_cancelled_callback); 390 | } 391 | } 392 | 393 | void on_clear_pcaps_changed(VariableItem* item) { 394 | AppState* app = variable_item_get_context(item); 395 | uint8_t index = variable_item_get_current_value_index(item); 396 | variable_item_set_current_value_text(item, SETTING_VALUE_NAMES_ACTION[index]); 397 | 398 | if(index == 0) { 399 | show_confirmation_dialog_ex( 400 | app, 401 | "Clear PCAPs", 402 | "Are you sure you want to clear\nall PCAP files?\nThis action cannot be undone.", 403 | pcap_clear_confirmed_callback, 404 | pcap_clear_cancelled_callback); 405 | } 406 | } 407 | 408 | void on_disable_esp_check_changed(VariableItem* item) { 409 | AppState* app = variable_item_get_context(item); 410 | uint8_t index = variable_item_get_current_value_index(item); 411 | app->settings.disable_esp_check_index = index; 412 | variable_item_set_current_value_text(item, SETTING_VALUE_NAMES_BOOL[index]); 413 | } -------------------------------------------------------------------------------- /src/callbacks.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "settings_def.h" 3 | #include "app_state.h" 4 | #include 5 | #include 6 | #include 7 | #include "utils.h" 8 | #include 9 | 10 | 11 | 12 | // Function declarations 13 | void update_settings_and_write(AppState* app, Settings* settings); 14 | void on_rgb_mode_changed(VariableItem* item); 15 | void on_channelswitchdelay_changed(VariableItem* item); 16 | void on_togglechannelhopping_changed(VariableItem* item); 17 | void on_ble_mac_changed(VariableItem* item); 18 | void on_stop_on_back_changed(VariableItem* item); 19 | void on_reboot_esp_changed(VariableItem* item); 20 | void on_clear_logs_changed(VariableItem* item); 21 | void on_clear_nvs_changed(VariableItem* item); 22 | void logs_clear_confirmed_callback(void* context); 23 | void logs_clear_cancelled_callback(void* context); 24 | void nvs_clear_confirmed_callback(void* context); 25 | void nvs_clear_cancelled_callback(void* context); 26 | void show_app_info(void* context); 27 | void app_info_ok_callback(void* context); 28 | void app_info_cancel_callback(void* context); 29 | void wardrive_clear_confirmed_callback(void* context); 30 | void wardrive_clear_cancelled_callback(void* context); 31 | void pcap_clear_confirmed_callback(void* context); 32 | void pcap_clear_cancelled_callback(void* context); 33 | void on_disable_esp_check_changed(VariableItem* item); -------------------------------------------------------------------------------- /src/confirmation_view.c: -------------------------------------------------------------------------------- 1 | #include "confirmation_view.h" 2 | #include 3 | #include 4 | 5 | struct ConfirmationView { 6 | View* view; 7 | ConfirmationViewCallback ok_callback; 8 | void* ok_callback_context; 9 | ConfirmationViewCallback cancel_callback; 10 | void* cancel_callback_context; 11 | }; 12 | 13 | typedef struct { 14 | const char* header; 15 | const char* text; 16 | uint8_t scroll_position; 17 | uint8_t total_lines; 18 | bool can_scroll; 19 | // Simplified easter egg tracking 20 | bool easter_egg_active; 21 | uint8_t sequence_position; // Track position in sequence 22 | } ConfirmationViewModel; 23 | 24 | static void confirmation_view_draw_callback(Canvas* canvas, void* _model) { 25 | if(!canvas || !_model) return; 26 | 27 | ConfirmationViewModel* model = (ConfirmationViewModel*)_model; 28 | 29 | // Border 30 | canvas_draw_rframe(canvas, 0, 0, 128, 64, 2); 31 | 32 | // Header 33 | if(model->header) { 34 | canvas_set_font(canvas, FontPrimary); 35 | elements_multiline_text_aligned(canvas, 64, 5, AlignCenter, AlignTop, model->header); 36 | } 37 | 38 | // Text area 39 | canvas_set_font(canvas, FontSecondary); 40 | 41 | // Calculate visible area for text (leave space for header and OK button) 42 | uint8_t text_y = 20; 43 | 44 | if(model->text) { 45 | // Create a temporary buffer for visible text 46 | const size_t max_visible_chars = 128; 47 | char visible_text[max_visible_chars]; 48 | uint8_t current_line = 0; 49 | uint8_t visible_lines = 0; 50 | const char* p = model->text; 51 | size_t buffer_pos = 0; 52 | 53 | // Skip lines before scroll position 54 | while(*p && current_line < model->scroll_position) { 55 | if(*p == '\n') current_line++; 56 | p++; 57 | } 58 | 59 | // Copy only the visible portion of text 60 | while(*p && visible_lines < 4 && buffer_pos < (max_visible_chars - 1)) { 61 | if(*p == '\n') { 62 | visible_lines++; 63 | if(visible_lines >= 4) break; 64 | } 65 | visible_text[buffer_pos++] = *p++; 66 | } 67 | visible_text[buffer_pos] = '\0'; 68 | 69 | // Draw visible portion 70 | elements_multiline_text_aligned( 71 | canvas, 72 | 63, 73 | text_y, 74 | AlignCenter, 75 | AlignTop, 76 | visible_text); 77 | 78 | // Draw scroll indicators if needed 79 | if(model->can_scroll) { 80 | if(model->scroll_position > 0) { 81 | canvas_draw_str(canvas, 120, text_y, "^"); 82 | } 83 | if(model->scroll_position < model->total_lines - 4) { 84 | canvas_draw_str(canvas, 120, 52, "v"); 85 | } 86 | } 87 | } 88 | 89 | // Draw OK button at bottom 90 | canvas_set_font(canvas, FontSecondary); 91 | elements_button_center(canvas, "OK"); 92 | } 93 | 94 | static bool confirmation_view_input_callback(InputEvent* event, void* context) { 95 | if(!event || !context) return false; 96 | 97 | ConfirmationView* instance = (ConfirmationView*)context; 98 | ConfirmationViewModel* model = view_get_model(instance->view); 99 | bool consumed = false; 100 | 101 | // Handle regular scroll behavior 102 | if(event->type == InputTypeShort || event->type == InputTypeRepeat) { 103 | if(event->key == InputKeyUp && model->scroll_position > 0) { 104 | model->scroll_position--; 105 | consumed = true; 106 | } else if(event->key == InputKeyDown && 107 | model->can_scroll && 108 | model->scroll_position < model->total_lines - 4) { 109 | model->scroll_position++; 110 | consumed = true; 111 | } else if(event->key == InputKeyOk) { 112 | if(instance->ok_callback) { 113 | instance->ok_callback(instance->ok_callback_context); 114 | } 115 | consumed = true; 116 | } else if(event->key == InputKeyBack) { 117 | if(instance->cancel_callback) { 118 | instance->cancel_callback(instance->cancel_callback_context); 119 | } 120 | consumed = true; 121 | } 122 | } 123 | 124 | view_commit_model(instance->view, consumed); 125 | return consumed; 126 | } 127 | __attribute__((used)) ConfirmationView* confirmation_view_alloc(void) { 128 | ConfirmationView* instance = malloc(sizeof(ConfirmationView)); 129 | if(!instance) return NULL; 130 | 131 | instance->view = view_alloc(); 132 | if(!instance->view) { 133 | free(instance); 134 | return NULL; 135 | } 136 | 137 | view_set_context(instance->view, instance); 138 | view_set_draw_callback(instance->view, confirmation_view_draw_callback); 139 | view_set_input_callback(instance->view, confirmation_view_input_callback); 140 | 141 | view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(ConfirmationViewModel)); 142 | 143 | with_view_model( 144 | instance->view, 145 | ConfirmationViewModel* model, 146 | { 147 | model->header = NULL; 148 | model->text = NULL; 149 | model->scroll_position = 0; 150 | model->total_lines = 0; 151 | model->can_scroll = false; 152 | model->easter_egg_active = false; 153 | model->sequence_position = 0; 154 | }, 155 | true); 156 | 157 | return instance; 158 | } 159 | 160 | __attribute__((used)) void confirmation_view_free(ConfirmationView* instance) { 161 | if(!instance) return; 162 | if(instance->view) view_free(instance->view); 163 | free(instance); 164 | } 165 | 166 | __attribute__((used)) View* confirmation_view_get_view(ConfirmationView* instance) { 167 | return instance ? instance->view : NULL; 168 | } 169 | 170 | __attribute__((used)) void confirmation_view_set_header(ConfirmationView* instance, const char* text) { 171 | if(!instance || !instance->view) return; 172 | with_view_model( 173 | instance->view, 174 | ConfirmationViewModel* model, 175 | { 176 | model->header = text; 177 | }, 178 | true); 179 | } 180 | 181 | __attribute__((used)) void confirmation_view_set_text(ConfirmationView* instance, const char* text) { 182 | if(!instance || !instance->view) return; 183 | with_view_model( 184 | instance->view, 185 | ConfirmationViewModel* model, 186 | { 187 | model->text = text; 188 | model->scroll_position = 0; 189 | 190 | if(text) { 191 | uint8_t lines = 1; 192 | const char* p = text; 193 | while(*p) { 194 | if(*p == '\n') lines++; 195 | p++; 196 | } 197 | model->total_lines = lines; 198 | model->can_scroll = (lines > 4); 199 | } else { 200 | model->total_lines = 0; 201 | model->can_scroll = false; 202 | } 203 | }, 204 | true); 205 | } 206 | 207 | __attribute__((used)) void confirmation_view_set_ok_callback( 208 | ConfirmationView* instance, 209 | ConfirmationViewCallback callback, 210 | void* context) { 211 | if(!instance) { 212 | FURI_LOG_E("ConfView", "Null instance in set_ok_callback"); 213 | return; 214 | } 215 | FURI_LOG_D("ConfView", "Setting OK callback: %p with context: %p", callback, context); 216 | instance->ok_callback = callback; 217 | instance->ok_callback_context = context; 218 | } 219 | 220 | __attribute__((used)) void confirmation_view_set_cancel_callback( 221 | ConfirmationView* instance, 222 | ConfirmationViewCallback callback, 223 | void* context) { 224 | if(!instance) { 225 | FURI_LOG_E("ConfView", "Null instance in set_cancel_callback"); 226 | return; 227 | } 228 | FURI_LOG_D("ConfView", "Setting Cancel callback: %p with context: %p", callback, context); 229 | instance->cancel_callback = callback; 230 | instance->cancel_callback_context = context; 231 | } -------------------------------------------------------------------------------- /src/confirmation_view.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct ConfirmationView ConfirmationView; 6 | typedef void (*ConfirmationViewCallback)(void* context); 7 | 8 | ConfirmationView* confirmation_view_alloc(void); 9 | void confirmation_view_free(ConfirmationView* instance); 10 | View* confirmation_view_get_view(ConfirmationView* instance); 11 | 12 | void confirmation_view_set_header(ConfirmationView* instance, const char* text); 13 | void confirmation_view_set_text(ConfirmationView* instance, const char* text); 14 | void confirmation_view_set_ok_callback( 15 | ConfirmationView* instance, 16 | ConfirmationViewCallback callback, 17 | void* context); 18 | void confirmation_view_set_cancel_callback( 19 | ConfirmationView* instance, 20 | ConfirmationViewCallback callback, 21 | void* context); -------------------------------------------------------------------------------- /src/firmware_api.c: -------------------------------------------------------------------------------- 1 | #include "firmware_api.h" 2 | #include 3 | 4 | bool is_momentum_firmware(void) { 5 | #ifdef HAS_VERSION_CUSTOM_NAME 6 | const Version* version = furi_hal_version_get_firmware_version(); 7 | const char* firmware_origin = version_get_custom_name(version); 8 | return (firmware_origin != NULL) && (strcmp(firmware_origin, "Momentum") == 0); 9 | #else 10 | return false; 11 | #endif 12 | } 13 | 14 | bool has_momentum_features(void) { 15 | #ifdef HAS_MOMENTUM_SUPPORT 16 | return is_momentum_firmware(); 17 | #else 18 | return false; 19 | #endif 20 | } 21 | -------------------------------------------------------------------------------- /src/firmware_api.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | // Check if we're building for Momentum firmware 6 | #if defined(FAP_MOMENTUM) && defined(__has_include) 7 | #if __has_include() 8 | #include 9 | #define HAS_MOMENTUM_SUPPORT 1 10 | #endif 11 | #endif 12 | 13 | // Check for version.h and custom name function 14 | #if defined(__has_include) 15 | #if __has_include() 16 | #include 17 | // Only define if the function prototype exists 18 | #if defined(version_get_custom_name) 19 | #define HAS_VERSION_CUSTOM_NAME 1 20 | #endif 21 | #endif 22 | #endif 23 | 24 | /** 25 | * @brief Check if currently running on Momentum firmware 26 | * @return true if running on Momentum firmware 27 | */ 28 | bool is_momentum_firmware(void); 29 | 30 | /** 31 | * @brief Check if Momentum features are available 32 | * @return true if Momentum features can be used 33 | */ 34 | bool has_momentum_features(void); 35 | -------------------------------------------------------------------------------- /src/ghost_esp_icons.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | extern const Icon I_Wifi_icon; 5 | extern const Icon I_BLE_icon; 6 | extern const Icon I_GPS_icon; 7 | extern const Icon I_Cog; -------------------------------------------------------------------------------- /src/log_manager.c: -------------------------------------------------------------------------------- 1 | #include "log_manager.h" 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | typedef struct { 8 | char* filename; 9 | int index; 10 | } LogFile; 11 | 12 | static void sort_log_files(LogFile* files, size_t count) { 13 | // Simple bubble sort implementation 14 | for(size_t i = 0; i < count - 1; i++) { 15 | for(size_t j = 0; j < count - i - 1; j++) { 16 | if(files[j].index < files[j + 1].index) { 17 | // Swap the elements 18 | LogFile temp = files[j]; 19 | files[j] = files[j + 1]; 20 | files[j + 1] = temp; 21 | } 22 | } 23 | } 24 | } 25 | 26 | static bool parse_log_index(const char* filename, const char* prefix, int* index) { 27 | // Expected format: prefix_NUMBER.txt 28 | size_t prefix_len = strlen(prefix); 29 | if(strncmp(filename, prefix, prefix_len) != 0) return false; 30 | 31 | const char* num_start = filename + prefix_len; 32 | if(*num_start != '_') return false; 33 | num_start++; 34 | 35 | char* end; 36 | *index = strtol(num_start, &end, 10); 37 | 38 | // Debug print to see what's happening 39 | FURI_LOG_D("LogManager", "Parsing '%s': num_start='%s', end='%s'", filename, num_start, end); 40 | 41 | // Check for valid number and .txt extension 42 | if(*index < 0 || (end == num_start) || // Invalid number 43 | strncmp(end, ".txt", 4) != 0 || // Must end with .txt 44 | *(end + 4) != '\0') { // Nothing should follow .txt 45 | return false; 46 | } 47 | 48 | return true; 49 | } 50 | 51 | bool get_latest_log_file(Storage* storage, const char* dir, const char* prefix, char* out_path) { 52 | if(!storage || !dir || !prefix || !out_path) return false; 53 | 54 | File* dir_handle = storage_file_alloc(storage); 55 | if(!storage_dir_open(dir_handle, dir)) { 56 | storage_file_free(dir_handle); 57 | return false; 58 | } 59 | 60 | LogFile* files = NULL; 61 | size_t files_count = 0; 62 | size_t files_capacity = 16; 63 | files = malloc(sizeof(LogFile) * files_capacity); 64 | 65 | FileInfo file_info; 66 | char* filename = malloc(MAX_FILENAME_LEN); 67 | 68 | // Collect all matching log files 69 | while(storage_dir_read(dir_handle, &file_info, filename, MAX_FILENAME_LEN)) { 70 | if(file_info.flags & FSF_DIRECTORY) continue; 71 | 72 | int index; 73 | if(!parse_log_index(filename, prefix, &index)) continue; 74 | 75 | if(files_count >= files_capacity) { 76 | files_capacity *= 2; 77 | LogFile* new_files = realloc(files, sizeof(LogFile) * files_capacity); 78 | if(!new_files) { 79 | // Handle realloc failure 80 | free(files); 81 | free(filename); 82 | storage_dir_close(dir_handle); 83 | storage_file_free(dir_handle); 84 | return false; 85 | } 86 | files = new_files; 87 | } 88 | 89 | files[files_count].filename = strdup(filename); 90 | files[files_count].index = index; 91 | files_count++; 92 | } 93 | 94 | bool result = false; 95 | if(files_count > 0) { 96 | // Sort files by index (descending order) 97 | sort_log_files(files, files_count); 98 | 99 | // Get the latest file (highest index) 100 | snprintf(out_path, MAX_FILENAME_LEN, "%s/%s", dir, files[0].filename); 101 | result = true; 102 | } 103 | 104 | // Cleanup 105 | for(size_t i = 0; i < files_count; i++) { 106 | free(files[i].filename); 107 | } 108 | free(files); 109 | free(filename); 110 | storage_dir_close(dir_handle); 111 | storage_file_free(dir_handle); 112 | 113 | if(result) { 114 | File* test_file = storage_file_alloc(storage); 115 | if(!storage_file_open(test_file, out_path, FSAM_READ, FSOM_OPEN_EXISTING)) { 116 | FURI_LOG_W("LogManager", "Latest log file cannot be opened: %s", out_path); 117 | result = false; 118 | } 119 | storage_file_close(test_file); 120 | storage_file_free(test_file); 121 | } 122 | 123 | return result; 124 | } -------------------------------------------------------------------------------- /src/log_manager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #define MAX_LOGS_TO_KEEP 5 7 | #define MAX_FILENAME_LEN 256 8 | 9 | /** 10 | * @brief Get the path to the latest log file 11 | * 12 | * @param storage Storage instance 13 | * @param dir Directory to search in 14 | * @param prefix Prefix of log files to search for 15 | * @param out_path Buffer to store the resulting path 16 | * @return true if a log file was found, false otherwise 17 | */ 18 | bool get_latest_log_file(Storage* storage, const char* dir, const char* prefix, char* out_path); -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include "menu.h" 18 | #include "uart_utils.h" 19 | #include "settings_storage.h" 20 | #include "settings_def.h" 21 | #include "settings_ui.h" 22 | #include "app_state.h" 23 | #include "callbacks.h" 24 | #include "confirmation_view.h" 25 | #include "utils.h" 26 | 27 | // Include the header where settings_custom_event_callback is declared 28 | #include "settings_ui.h" 29 | 30 | #define UART_INIT_STACK_SIZE 2048 31 | #define UART_INIT_TIMEOUT_MS 1500 // ms 32 | 33 | static int32_t init_uart_task(void* context) { 34 | AppState* state = context; 35 | 36 | // Add some delay to let system stabilize 37 | furi_delay_ms(50); 38 | 39 | state->uart_context = uart_init(state); 40 | if (state->uart_context) { 41 | FURI_LOG_I("Ghost_ESP", "UART initialized successfully"); 42 | } else { 43 | FURI_LOG_E("Ghost_ESP", "UART initialization failed"); 44 | } 45 | return 0; 46 | } 47 | 48 | int32_t ghost_esp_app(void* p) { 49 | UNUSED(p); 50 | 51 | // Disable expansion protocol to avoid UART interference 52 | Expansion* expansion = furi_record_open(RECORD_EXPANSION); 53 | expansion_disable(expansion); 54 | 55 | // Modified power initialization 56 | uint8_t attempts = 0; 57 | bool otg_was_enabled = furi_hal_power_is_otg_enabled(); 58 | 59 | // Simply try to enable OTG if not already enabled 60 | while(!furi_hal_power_is_otg_enabled() && attempts++ < 3) { 61 | furi_hal_power_enable_otg(); 62 | furi_delay_ms(20); 63 | } 64 | furi_delay_ms(50); // Reduced stabilization time 65 | 66 | // Set up bare minimum UI state 67 | AppState* state = malloc(sizeof(AppState)); 68 | if (!state) return -1; 69 | memset(state, 0, sizeof(AppState)); // Zero all memory first 70 | 71 | // Initialize menu selection indices 72 | state->last_wifi_index = 0; 73 | state->last_ble_index = 0; 74 | state->last_gps_index = 0; 75 | state->current_index = 0; 76 | state->current_view = 0; 77 | state->previous_view = 0; 78 | 79 | // Initialize essential text buffers with minimal size 80 | state->textBoxBuffer = malloc(1); 81 | if (state->textBoxBuffer) { 82 | state->textBoxBuffer[0] = '\0'; 83 | } 84 | state->buffer_length = 0; 85 | state->input_buffer = malloc(32); 86 | if (state->input_buffer) { 87 | memset(state->input_buffer, 0, 32); 88 | } 89 | 90 | // Initialize UI components - core components first 91 | state->view_dispatcher = view_dispatcher_alloc(); 92 | state->main_menu = main_menu_alloc(); 93 | if (!state->view_dispatcher || !state->main_menu) { 94 | // Clean up and exit if core components fail 95 | if (state->view_dispatcher) view_dispatcher_free(state->view_dispatcher); 96 | if (state->main_menu) main_menu_free(state->main_menu); 97 | free(state->textBoxBuffer); 98 | free(state->input_buffer); 99 | free(state); 100 | return -1; 101 | } 102 | 103 | // Allocate remaining UI components 104 | state->wifi_menu = submenu_alloc(); 105 | state->ble_menu = submenu_alloc(); 106 | state->gps_menu = submenu_alloc(); 107 | state->text_box = text_box_alloc(); 108 | state->settings_menu = variable_item_list_alloc(); 109 | state->text_input = text_input_alloc(); 110 | state->confirmation_view = confirmation_view_alloc(); 111 | state->settings_actions_menu = submenu_alloc(); 112 | 113 | // Set headers - only for successfully allocated components 114 | if(state->main_menu) main_menu_set_header(state->main_menu, "Select a Utility"); 115 | if(state->wifi_menu) submenu_set_header(state->wifi_menu, "Select a Wifi Utility"); 116 | if(state->ble_menu) submenu_set_header(state->ble_menu, "Select a Bluetooth Utility"); 117 | if(state->gps_menu) submenu_set_header(state->gps_menu, "Select a GPS Utility"); 118 | if(state->text_input) text_input_set_header_text(state->text_input, "Enter Your Text"); 119 | if(state->settings_actions_menu) submenu_set_header(state->settings_actions_menu, "Settings"); 120 | 121 | // Initialize settings and configuration early 122 | settings_storage_init(); 123 | if(settings_storage_load(&state->settings, GHOST_ESP_APP_SETTINGS_FILE) != SETTINGS_OK) { 124 | memset(&state->settings, 0, sizeof(Settings)); 125 | state->settings.stop_on_back_index = 1; 126 | settings_storage_save(&state->settings, GHOST_ESP_APP_SETTINGS_FILE); 127 | } 128 | 129 | // Initialize filter config 130 | state->filter_config = malloc(sizeof(FilterConfig)); 131 | if(state->filter_config) { 132 | state->filter_config->enabled = state->settings.enable_filtering_index; 133 | state->filter_config->show_ble_status = true; 134 | state->filter_config->show_wifi_status = true; 135 | state->filter_config->show_flipper_devices = true; 136 | state->filter_config->show_wifi_networks = true; 137 | state->filter_config->strip_ansi_codes = true; 138 | state->filter_config->add_prefixes = true; 139 | } 140 | 141 | // Set up settings UI context 142 | state->settings_ui_context.settings = &state->settings; 143 | state->settings_ui_context.send_uart_command = send_uart_command; 144 | state->settings_ui_context.switch_to_view = NULL; 145 | state->settings_ui_context.show_confirmation_view = show_confirmation_view_wrapper; 146 | state->settings_ui_context.context = state; 147 | 148 | // Initialize settings menu 149 | settings_setup_gui(state->settings_menu, &state->settings_ui_context); 150 | 151 | // Start UART init in background thread 152 | FuriThread* uart_init_thread = furi_thread_alloc_ex( 153 | "UartInit", 154 | UART_INIT_STACK_SIZE, 155 | init_uart_task, 156 | state); 157 | furi_thread_start(uart_init_thread); 158 | 159 | bool uart_init_success = false; 160 | uint32_t start_time = furi_get_tick(); 161 | while(furi_get_tick() - start_time < UART_INIT_TIMEOUT_MS) { 162 | if(furi_thread_join(uart_init_thread) == 0) { 163 | uart_init_success = true; 164 | break; 165 | } 166 | furi_delay_ms(50); 167 | } 168 | 169 | if(!uart_init_success) { 170 | FURI_LOG_E("Main", "UART init timeout! OTG not ready?"); 171 | furi_thread_flags_set(furi_thread_get_id(uart_init_thread), WorkerEvtStop); 172 | furi_thread_join(uart_init_thread); 173 | } 174 | 175 | // Add views to dispatcher - check each component before adding 176 | if(state->view_dispatcher) { 177 | if(state->main_menu) view_dispatcher_add_view(state->view_dispatcher, 0, main_menu_get_view(state->main_menu)); 178 | if(state->wifi_menu) view_dispatcher_add_view(state->view_dispatcher, 1, submenu_get_view(state->wifi_menu)); 179 | if(state->ble_menu) view_dispatcher_add_view(state->view_dispatcher, 2, submenu_get_view(state->ble_menu)); 180 | if(state->gps_menu) view_dispatcher_add_view(state->view_dispatcher, 3, submenu_get_view(state->gps_menu)); 181 | if(state->settings_menu) view_dispatcher_add_view(state->view_dispatcher, 4, variable_item_list_get_view(state->settings_menu)); 182 | if(state->text_box) view_dispatcher_add_view(state->view_dispatcher, 5, text_box_get_view(state->text_box)); 183 | if(state->text_input) view_dispatcher_add_view(state->view_dispatcher, 6, text_input_get_view(state->text_input)); 184 | if(state->confirmation_view) view_dispatcher_add_view(state->view_dispatcher, 7, confirmation_view_get_view(state->confirmation_view)); 185 | if(state->settings_actions_menu) view_dispatcher_add_view(state->view_dispatcher, 8, submenu_get_view(state->settings_actions_menu)); 186 | 187 | view_dispatcher_set_custom_event_callback(state->view_dispatcher, settings_custom_event_callback); 188 | } 189 | 190 | if(!state->text_box) { 191 | FURI_LOG_E("Main", "Text box allocation failed!"); 192 | return -1; // Don't try to fuck with broken UI 193 | } 194 | 195 | // Show main menu immediately 196 | show_main_menu(state); 197 | 198 | // Set up and run GUI 199 | Gui* gui = furi_record_open("gui"); 200 | if(gui && state->view_dispatcher) { 201 | view_dispatcher_attach_to_gui(state->view_dispatcher, gui, ViewDispatcherTypeFullscreen); 202 | view_dispatcher_set_navigation_event_callback(state->view_dispatcher, back_event_callback); 203 | view_dispatcher_set_event_callback_context(state->view_dispatcher, state); 204 | view_dispatcher_run(state->view_dispatcher); 205 | } 206 | furi_record_close("gui"); 207 | 208 | // Start cleanup - first remove views 209 | if(state->view_dispatcher) { 210 | for(size_t i = 0; i <= 8; i++) { 211 | view_dispatcher_remove_view(state->view_dispatcher, i); 212 | } 213 | } 214 | 215 | // Clear callbacks before cleanup 216 | if(state->confirmation_view) { 217 | confirmation_view_set_ok_callback(state->confirmation_view, NULL, NULL); 218 | confirmation_view_set_cancel_callback(state->confirmation_view, NULL, NULL); 219 | } 220 | 221 | // Clean up UART first 222 | if(state->uart_context) { 223 | uart_free(state->uart_context); 224 | state->uart_context = NULL; 225 | } 226 | 227 | // Cleanup UI components in reverse order 228 | if(state->confirmation_view) { 229 | confirmation_view_free(state->confirmation_view); 230 | state->confirmation_view = NULL; 231 | } 232 | if(state->text_input) { 233 | text_input_free(state->text_input); 234 | state->text_input = NULL; 235 | } 236 | if(state->text_box) { 237 | text_box_free(state->text_box); 238 | state->text_box = NULL; 239 | } 240 | if(state->settings_actions_menu) { 241 | submenu_free(state->settings_actions_menu); 242 | state->settings_actions_menu = NULL; 243 | } 244 | if(state->settings_menu) { 245 | variable_item_list_free(state->settings_menu); 246 | state->settings_menu = NULL; 247 | } 248 | if(state->wifi_menu) { 249 | submenu_free(state->wifi_menu); 250 | state->wifi_menu = NULL; 251 | } 252 | if(state->ble_menu) { 253 | submenu_free(state->ble_menu); 254 | state->ble_menu = NULL; 255 | } 256 | if(state->gps_menu) { 257 | submenu_free(state->gps_menu); 258 | state->gps_menu = NULL; 259 | } 260 | if(state->main_menu) { 261 | main_menu_free(state->main_menu); 262 | state->main_menu = NULL; 263 | } 264 | 265 | // Free view dispatcher last after all views are removed 266 | if(state->view_dispatcher) { 267 | view_dispatcher_free(state->view_dispatcher); 268 | state->view_dispatcher = NULL; 269 | } 270 | 271 | // Cleanup buffers 272 | if(state->input_buffer) { 273 | free(state->input_buffer); 274 | state->input_buffer = NULL; 275 | } 276 | if(state->textBoxBuffer) { 277 | free(state->textBoxBuffer); 278 | state->textBoxBuffer = NULL; 279 | } 280 | // Add filter config cleanup 281 | if(state->filter_config) { 282 | free(state->filter_config); 283 | state->filter_config = NULL; 284 | } 285 | 286 | // Final state cleanup 287 | free(state); 288 | 289 | // Return previous state of expansion 290 | expansion_enable(expansion); 291 | furi_record_close(RECORD_EXPANSION); 292 | 293 | // Power cleanup 294 | if(furi_hal_power_is_otg_enabled() && !otg_was_enabled) { 295 | furi_hal_power_disable_otg(); 296 | } 297 | 298 | return 0; 299 | } 300 | 301 | // 6675636B796F7564656B69 -------------------------------------------------------------------------------- /src/menu.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "gui_modules/mainmenu.h" 7 | #include 8 | #include 9 | #include "app_state.h" 10 | 11 | 12 | // Function declarations 13 | void send_uart_command(const char* command, void* state); // Changed from AppState* to void* 14 | void send_uart_command_with_text(const char* command, char* text, AppState* state); 15 | void send_uart_command_with_bytes( 16 | const char* command, 17 | const uint8_t* bytes, 18 | size_t length, 19 | AppState* state); 20 | 21 | bool back_event_callback(void* context); 22 | void submenu_callback(void* context, uint32_t index); 23 | void handle_wifi_commands(AppState* state, uint32_t index, const char** wifi_commands); 24 | void show_main_menu(AppState* state); 25 | void handle_wifi_menu(AppState* state, uint32_t index); 26 | void handle_ble_menu(AppState* state, uint32_t index); 27 | void handle_gps_menu(AppState* state, uint32_t index); 28 | 29 | void show_wifi_menu(AppState* state); 30 | void show_ble_menu(AppState* state); 31 | void show_gps_menu(AppState* state); 32 | 33 | // 6675636B796F7564656B69 -------------------------------------------------------------------------------- /src/sequential_file.c: -------------------------------------------------------------------------------- 1 | #include "sequential_file.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | char* sequential_file_resolve_path( 10 | Storage* storage, 11 | const char* dir, 12 | const char* prefix, 13 | const char* extension) { 14 | if(storage == NULL || dir == NULL || prefix == NULL || extension == NULL) { 15 | FURI_LOG_E("SequentialFile", "Invalid parameters passed to resolve_path"); 16 | return NULL; 17 | } 18 | 19 | // Allocate a file handle for directory operations 20 | File* dir_handle = storage_file_alloc(storage); 21 | if(!dir_handle) { 22 | FURI_LOG_E("SequentialFile", "Failed to allocate file handle for directory"); 23 | return NULL; 24 | } 25 | 26 | // Open the directory 27 | if(!storage_dir_open(dir_handle, dir)) { 28 | FURI_LOG_E("SequentialFile", "Failed to open directory: %s", dir); 29 | storage_file_free(dir_handle); 30 | return NULL; 31 | } 32 | 33 | FileInfo file_info; 34 | char filename[256]; 35 | int highest_index = -1; 36 | size_t prefix_len = strlen(prefix); 37 | size_t extension_len = strlen(extension); 38 | 39 | // Iterate over files in the directory 40 | while(storage_dir_read(dir_handle, &file_info, filename, sizeof(filename))) { 41 | // Skip directories 42 | if(file_info.flags & FSF_DIRECTORY) continue; 43 | 44 | // Check if filename matches the expected pattern 45 | if(strncmp(filename, prefix, prefix_len) != 0) continue; 46 | size_t filename_len = strlen(filename); 47 | 48 | // Verify extension 49 | if(filename_len <= extension_len + 1) continue; // +1 for '.' 50 | if(strcmp(&filename[filename_len - extension_len], extension) != 0) continue; 51 | if(filename[filename_len - extension_len - 1] != '.') continue; 52 | 53 | // Extract the index part of the filename 54 | const char* index_start = filename + prefix_len; 55 | if(*index_start != '_') continue; 56 | index_start++; // Skip the '_' 57 | 58 | // Calculate the length of the index string 59 | size_t index_len = filename_len - prefix_len - extension_len - 2; // -2 for '_' and '.' 60 | 61 | // Copy the index string 62 | char index_str[32]; 63 | if(index_len >= sizeof(index_str)) continue; // Prevent buffer overflow 64 | strncpy(index_str, index_start, index_len); 65 | index_str[index_len] = '\0'; 66 | 67 | // Convert index string to integer 68 | char* endptr; 69 | long index = strtol(index_str, &endptr, 10); 70 | if(*endptr != '\0' || index < 0) continue; // Ensure full conversion and non-negative index 71 | 72 | if(index > highest_index) { 73 | highest_index = (int)index; 74 | } 75 | } 76 | 77 | storage_dir_close(dir_handle); 78 | storage_file_free(dir_handle); 79 | 80 | // Determine the new index 81 | int new_index = highest_index + 1; 82 | 83 | // Construct the new file path 84 | char file_path[256]; 85 | int snprintf_result = snprintf(file_path, sizeof(file_path), "%s/%s_%d.%s", dir, prefix, new_index, extension); 86 | if(snprintf_result < 0 || (size_t)snprintf_result >= sizeof(file_path)) { 87 | FURI_LOG_E("SequentialFile", "snprintf failed or output truncated in resolve_path"); 88 | return NULL; 89 | } 90 | 91 | FURI_LOG_I("SequentialFile", "Resolved new file path: %s", file_path); 92 | 93 | return strdup(file_path); 94 | } 95 | 96 | 97 | bool sequential_file_open( 98 | Storage* storage, 99 | File* file, 100 | const char* dir, 101 | const char* prefix, 102 | const char* extension) { 103 | if(storage == NULL || file == NULL || dir == NULL || prefix == NULL || extension == NULL) { 104 | FURI_LOG_E("SequentialFile", "Invalid parameters passed to open"); 105 | return false; 106 | } 107 | 108 | char* file_path = sequential_file_resolve_path(storage, dir, prefix, extension); 109 | if(file_path == NULL) { 110 | FURI_LOG_E("SequentialFile", "Failed to resolve file path"); 111 | return false; 112 | } 113 | 114 | // Open the file with the resolved path 115 | bool success = storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS); 116 | if(success) { 117 | FURI_LOG_I("SequentialFile", "Opened log file: %s", file_path); 118 | } else { 119 | FURI_LOG_E("SequentialFile", "Failed to open log file: %s", file_path); 120 | } 121 | 122 | free(file_path); 123 | return success; 124 | } -------------------------------------------------------------------------------- /src/sequential_file.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | char* sequential_file_resolve_path( 6 | Storage* storage, 7 | const char* dir, 8 | const char* prefix, 9 | const char* extension); 10 | bool sequential_file_open( 11 | Storage* storage, 12 | File* file, 13 | const char* dir, 14 | const char* prefix, 15 | const char* extension); -------------------------------------------------------------------------------- /src/settings_def.c: -------------------------------------------------------------------------------- 1 | // settings_def.c 2 | #include "settings_def.h" 3 | #include 4 | #include "callbacks.h" 5 | const char* const SETTING_VALUE_NAMES_RGB_MODE[] = {"Stealth", "Normal", "Rainbow"}; 6 | const char* const SETTING_VALUE_NAMES_CHANNEL_HOP[] = {"500ms", "1000ms", "2000ms", "3000ms", "4000ms"}; 7 | const char* const SETTING_VALUE_NAMES_BOOL[] = {"False", "True"}; 8 | const char* const SETTING_VALUE_NAMES_ACTION[] = {"Press OK", "Press OK"}; 9 | const char* const SETTING_VALUE_NAMES_LOG_VIEW[] = {"End", "Start"}; 10 | 11 | #include "settings_ui.h" 12 | 13 | const SettingMetadata SETTING_METADATA[SETTINGS_COUNT] = { 14 | [SETTING_RGB_MODE] = { 15 | .name = "RGB Mode", 16 | .data.setting = { 17 | .max_value = RGB_MODE_COUNT - 1, 18 | .value_names = SETTING_VALUE_NAMES_RGB_MODE, 19 | .uart_command = "setsetting -i 1 -v" 20 | }, 21 | .is_action = false 22 | }, 23 | [SETTING_CHANNEL_HOP_DELAY] = { 24 | .name = "Channel Switch Delay", 25 | .data.setting = { 26 | .max_value = CHANNEL_HOP_COUNT - 1, 27 | .value_names = SETTING_VALUE_NAMES_CHANNEL_HOP, 28 | .uart_command = "setsetting -i 2 -v" 29 | }, 30 | .is_action = false 31 | }, 32 | [SETTING_ENABLE_CHANNEL_HOPPING] = { 33 | .name = "Enable Channel Hopping", 34 | .data.setting = { 35 | .max_value = 1, 36 | .value_names = SETTING_VALUE_NAMES_BOOL, 37 | .uart_command = "setsetting -i 3 -v" 38 | }, 39 | .is_action = false 40 | }, 41 | [SETTING_ENABLE_RANDOM_BLE_MAC] = { 42 | .name = "Enable Random BLE Mac", 43 | .data.setting = { 44 | .max_value = 1, 45 | .value_names = SETTING_VALUE_NAMES_BOOL, 46 | .uart_command = "setsetting -i 4 -v" 47 | }, 48 | .is_action = false 49 | }, 50 | [SETTING_STOP_ON_BACK] = { 51 | .name = "Send Stop On Back", 52 | .data.setting = { 53 | .max_value = 1, 54 | .value_names = SETTING_VALUE_NAMES_BOOL, 55 | .uart_command = NULL 56 | }, 57 | .is_action = false 58 | }, 59 | [SETTING_ENABLE_FILTERING] = { 60 | .name = "(BETA) Enable UART Filtering", 61 | .data.setting = { 62 | .max_value = 1, 63 | .value_names = SETTING_VALUE_NAMES_BOOL, 64 | .uart_command = NULL 65 | }, 66 | .is_action = false 67 | }, 68 | [SETTING_SHOW_INFO] = { 69 | .name = "App Info", 70 | .data.action = { 71 | .name = "Show Info", 72 | .command = NULL, 73 | .callback = &show_app_info 74 | }, 75 | .is_action = true 76 | }, 77 | [SETTING_REBOOT_ESP] = { 78 | .name = "Reboot ESP", 79 | .data.action = { 80 | .name = "Reboot ESP", 81 | .command = "handle_reboot", 82 | .callback = NULL 83 | }, 84 | .is_action = true 85 | }, 86 | [SETTING_CLEAR_LOGS] = { 87 | .name = "Clear Log Files", 88 | .data.action = { 89 | .name = "Clear Log Files", 90 | .command = NULL, 91 | .callback = &clear_log_files 92 | }, 93 | .is_action = true 94 | }, 95 | [SETTING_CLEAR_NVS] = { 96 | .name = "Clear NVS", 97 | .data.action = { 98 | .name = "Clear NVS", 99 | .command = "handle_clearnvs", 100 | .callback = NULL 101 | }, 102 | .is_action = true 103 | }, 104 | [SETTING_VIEW_LOGS_FROM_START] = { 105 | .name = "View Logs From", 106 | .data.setting = { 107 | .max_value = 1, 108 | .value_names = SETTING_VALUE_NAMES_LOG_VIEW, 109 | .uart_command = NULL 110 | }, 111 | .is_action = false 112 | }, 113 | [SETTING_CLEAR_PCAPS] = { 114 | .name = "Clear PCAPs", 115 | .data.action = { 116 | .name = "Clear PCAPs", 117 | .command = NULL, 118 | .callback = &clear_pcap_files 119 | }, 120 | .is_action = true 121 | }, 122 | [SETTING_CLEAR_WARDRIVE] = { 123 | .name = "Clear Wardrives", 124 | .data.action = { 125 | .name = "Clear Wardrives", 126 | .command = NULL, 127 | .callback = &clear_wardrive_files 128 | }, 129 | .is_action = true 130 | }, 131 | [SETTING_DISABLE_ESP_CHECK] = { 132 | .name = "Disable ESP Check", 133 | .data.setting = { 134 | .max_value = 1, 135 | .value_names = SETTING_VALUE_NAMES_BOOL, 136 | .uart_command = NULL 137 | }, 138 | .is_action = false 139 | } 140 | }; 141 | 142 | bool setting_is_visible(SettingKey key) { 143 | if(key == SETTING_ENABLE_FILTERING) { 144 | return false; 145 | } 146 | return true; 147 | } 148 | 149 | const SettingMetadata* settings_get_metadata(SettingKey key) { 150 | if(key >= SETTINGS_COUNT) { 151 | return NULL; 152 | } 153 | return &SETTING_METADATA[key]; 154 | } 155 | 156 | // 6675636B796F7564656B69 -------------------------------------------------------------------------------- /src/settings_def.h: -------------------------------------------------------------------------------- 1 | // settings_def.h 2 | #pragma once 3 | #include 4 | #include 5 | 6 | // Settings keys - add new settings here 7 | typedef enum { 8 | SETTING_RGB_MODE, 9 | SETTING_CHANNEL_HOP_DELAY, 10 | SETTING_ENABLE_CHANNEL_HOPPING, 11 | SETTING_ENABLE_RANDOM_BLE_MAC, 12 | SETTING_STOP_ON_BACK, 13 | SETTING_ENABLE_FILTERING, // Keep for settings file compatibility 14 | SETTING_VIEW_LOGS_FROM_START, 15 | SETTING_SHOW_INFO, 16 | SETTING_REBOOT_ESP, 17 | SETTING_CLEAR_LOGS, 18 | SETTING_CLEAR_NVS, 19 | SETTING_CLEAR_PCAPS, 20 | SETTING_CLEAR_WARDRIVE, 21 | SETTING_DISABLE_ESP_CHECK, 22 | SETTINGS_COUNT 23 | } SettingKey; 24 | 25 | 26 | // Settings operations result 27 | typedef enum { 28 | SETTINGS_OK, 29 | SETTINGS_INVALID_VALUE, 30 | SETTINGS_FILE_ERROR, 31 | SETTINGS_PARSE_ERROR 32 | } SettingsResult; 33 | 34 | // Setting value definitions 35 | typedef enum { 36 | RGB_MODE_STEALTH, 37 | RGB_MODE_NORMAL, 38 | RGB_MODE_RAINBOW, 39 | RGB_MODE_COUNT 40 | } RGBMode; 41 | 42 | typedef enum { 43 | CHANNEL_HOP_500MS, 44 | CHANNEL_HOP_1000MS, 45 | CHANNEL_HOP_2000MS, 46 | CHANNEL_HOP_3000MS, 47 | CHANNEL_HOP_4000MS, 48 | CHANNEL_HOP_COUNT 49 | } ChannelHopDelay; 50 | 51 | typedef struct { 52 | uint8_t rgb_mode_index; 53 | uint8_t channel_hop_delay_index; 54 | uint8_t enable_channel_hopping_index; 55 | uint8_t enable_random_ble_mac_index; 56 | uint8_t stop_on_back_index; 57 | uint8_t enable_filtering_index; 58 | uint8_t view_logs_from_start_index; 59 | uint8_t reboot_esp_index; 60 | uint8_t clear_logs_index; 61 | uint8_t clear_nvs_index; 62 | uint8_t disable_esp_check_index; 63 | } Settings; 64 | 65 | // Add this to settings_def.h 66 | typedef struct { 67 | const char* name; 68 | const char* command; 69 | void (*callback)(void* context); // Function pointer for the action 70 | } SettingAction; 71 | 72 | typedef struct { 73 | const char* name; 74 | union { 75 | struct { 76 | uint8_t max_value; 77 | const char* const* value_names; 78 | const char* uart_command; 79 | } setting; 80 | SettingAction action; 81 | } data; 82 | bool is_action; 83 | } SettingMetadata; 84 | 85 | 86 | 87 | // Value name arrays 88 | extern const char* const SETTING_VALUE_NAMES_RGB_MODE[]; 89 | extern const char* const SETTING_VALUE_NAMES_CHANNEL_HOP[]; 90 | extern const char* const SETTING_VALUE_NAMES_BOOL[]; 91 | extern const char* const SETTING_VALUE_NAMES_ACTION[]; 92 | 93 | // Function declarations 94 | const SettingMetadata* settings_get_metadata(SettingKey key); 95 | bool setting_is_visible(SettingKey key); 96 | 97 | // Common definitions 98 | #define GHOST_ESP_APP_FOLDER "/ext/apps_data/ghost_esp" 99 | #define GHOST_ESP_APP_FOLDER_PCAPS "/ext/apps_data/ghost_esp/pcaps" 100 | #define GHOST_ESP_APP_FOLDER_WARDRIVE "/ext/apps_data/ghost_esp/wardrive" 101 | #define GHOST_ESP_APP_FOLDER_LOGS "/ext/apps_data/ghost_esp/logs" 102 | #define GHOST_ESP_APP_SETTINGS_FILE "/ext/apps_data/ghost_esp/settings.ini" 103 | 104 | #define SETTINGS_HEADER_MAGIC 0xDEADBEEF 105 | #define SETTINGS_FILE_VERSION 1 106 | 107 | 108 | // SettingsHeader structure 109 | typedef struct { 110 | uint32_t magic; 111 | uint16_t version; 112 | uint16_t settings_count; 113 | } SettingsHeader; -------------------------------------------------------------------------------- /src/settings_storage.c: -------------------------------------------------------------------------------- 1 | #include "settings_storage.h" 2 | #include // for logging 3 | #include "uart_storage.h" 4 | static Storage* storage = NULL; 5 | 6 | 7 | 8 | // Forward declarations of static functions 9 | static bool write_header(File* file); 10 | static bool verify_header(File* file); 11 | 12 | bool settings_storage_init() { 13 | uint32_t start_time = furi_get_tick(); 14 | FURI_LOG_I("SettingsStorage", "Starting storage initialization"); 15 | 16 | if(storage != NULL) { 17 | FURI_LOG_I("SettingsStorage", "Storage already initialized"); 18 | return true; 19 | } 20 | 21 | storage = furi_record_open(RECORD_STORAGE); 22 | if(storage == NULL) { 23 | FURI_LOG_E("SettingsStorage", "Failed to open RECORD_STORAGE"); 24 | return false; 25 | } 26 | 27 | // Attempt to create directory without checking if it exists 28 | if(!storage_simply_mkdir(storage, GHOST_ESP_APP_FOLDER)) { 29 | furi_crash("Can't mkdir! Fuck this SD card!"); 30 | } 31 | 32 | uint32_t duration = furi_get_tick() - start_time; 33 | FURI_LOG_I("SettingsStorage", "Storage initialization complete (Time taken: %lu ms)", duration); 34 | 35 | if(!storage) { 36 | FURI_LOG_E("Storage", "Storage system failure!"); 37 | furi_crash("Storage fucked"); 38 | } 39 | 40 | return true; 41 | } 42 | 43 | void uart_storage_sync_file(UartStorageContext* ctx) { 44 | if(ctx && ctx->current_file) { 45 | storage_file_sync(ctx->current_file); 46 | } 47 | } 48 | 49 | static bool write_header(File* file) { 50 | SettingsHeader header = { 51 | .magic = SETTINGS_HEADER_MAGIC, 52 | .version = SETTINGS_FILE_VERSION, 53 | .settings_count = SETTINGS_COUNT 54 | }; 55 | return storage_file_write(file, &header, sizeof(header)) == sizeof(header); 56 | } 57 | 58 | static bool verify_header(File* file) { 59 | SettingsHeader header; 60 | if(storage_file_read(file, &header, sizeof(header)) != sizeof(header)) { 61 | return false; 62 | } 63 | return header.magic == SETTINGS_HEADER_MAGIC && 64 | header.version == SETTINGS_FILE_VERSION && 65 | header.settings_count == SETTINGS_COUNT; 66 | } 67 | 68 | SettingsResult settings_storage_save(Settings* settings, const char* path) { 69 | FURI_LOG_I("SettingsStorage", "Starting to save settings to %s", path); 70 | 71 | if(!storage) { 72 | FURI_LOG_E("SettingsStorage", "Storage not initialized"); 73 | return SETTINGS_FILE_ERROR; 74 | } 75 | 76 | File* file = storage_file_alloc(storage); 77 | if(!storage_file_open(file, path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { 78 | FURI_LOG_E("SettingsStorage", "Failed to open file for writing: %s", path); 79 | storage_file_free(file); 80 | return SETTINGS_FILE_ERROR; 81 | } 82 | 83 | bool success = write_header(file); 84 | if(!success) { 85 | FURI_LOG_E("SettingsStorage", "Failed to write header"); 86 | } else { 87 | size_t bytes_written = storage_file_write(file, settings, sizeof(Settings)); 88 | if(bytes_written != sizeof(Settings)) { 89 | FURI_LOG_E("SettingsStorage", "Failed to write settings data"); 90 | success = false; 91 | } else { 92 | FURI_LOG_I("SettingsStorage", "Settings saved successfully"); 93 | } 94 | } 95 | 96 | storage_file_close(file); 97 | storage_file_free(file); 98 | return success ? SETTINGS_OK : SETTINGS_FILE_ERROR; 99 | } 100 | 101 | SettingsResult settings_storage_load(Settings* settings, const char* path) { 102 | FURI_LOG_D("SettingsStorage", "Loading settings from %s", path); 103 | 104 | if(!storage) { 105 | FURI_LOG_E("SettingsStorage", "Storage not initialized"); 106 | return SETTINGS_FILE_ERROR; 107 | } 108 | 109 | File* file = storage_file_alloc(storage); 110 | if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) { 111 | FURI_LOG_W("SettingsStorage", "Settings file doesn't exist, creating with defaults"); 112 | storage_file_free(file); 113 | memset(settings, 0, sizeof(Settings)); 114 | return settings_storage_save(settings, path); 115 | } 116 | 117 | bool success = verify_header(file); 118 | if(!success) { 119 | FURI_LOG_E("SettingsStorage", "Invalid header in settings file"); 120 | storage_file_close(file); 121 | storage_file_free(file); 122 | return SETTINGS_PARSE_ERROR; 123 | } 124 | 125 | success = storage_file_read(file, settings, sizeof(Settings)) == sizeof(Settings); 126 | if(!success) { 127 | FURI_LOG_E("SettingsStorage", "Failed to read settings data"); 128 | } 129 | 130 | storage_file_close(file); 131 | storage_file_free(file); 132 | return success ? SETTINGS_OK : SETTINGS_PARSE_ERROR; 133 | } -------------------------------------------------------------------------------- /src/settings_storage.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "settings_def.h" 3 | #include 4 | #include 5 | #include 6 | 7 | // Forward declare the struct to avoid circular dependencies 8 | struct UartStorageContext; 9 | 10 | // Initialize settings storage 11 | bool settings_storage_init(); 12 | void uart_storage_sync_file(struct UartStorageContext* ctx); 13 | __attribute__((used)) SettingsResult settings_storage_save(Settings* settings, const char* path); 14 | __attribute__((used)) SettingsResult settings_storage_load(Settings* settings, const char* path); -------------------------------------------------------------------------------- /src/settings_ui.c: -------------------------------------------------------------------------------- 1 | #include "settings_ui.h" 2 | #include "settings_ui_types.h" 3 | #include "settings_def.h" 4 | #include "app_state.h" 5 | #include "sequential_file.h" 6 | #include "uart_utils.h" 7 | #include 8 | #include 9 | #include 10 | #include "settings_storage.h" 11 | #include "utils.h" 12 | #include "callbacks.h" 13 | 14 | typedef struct { 15 | SettingsUIContext* settings_ui_context; 16 | SettingKey key; 17 | } VariableItemContext; 18 | 19 | #define MAX_FILENAME_LEN 256 20 | #define MAX_PATH_LEN 512 21 | 22 | static inline void close_current_log(AppState* app) { 23 | if(app && app->uart_context && app->uart_context->storageContext && 24 | app->uart_context->storageContext->log_file) { 25 | storage_file_close(app->uart_context->storageContext->log_file); 26 | } 27 | } 28 | 29 | static inline void create_new_log(AppState* app) { 30 | if(app && app->uart_context && app->uart_context->storageContext) { 31 | sequential_file_open( 32 | app->uart_context->storageContext->storage_api, 33 | app->uart_context->storageContext->log_file, 34 | GHOST_ESP_APP_FOLDER_LOGS, 35 | "ghost_logs", 36 | "txt"); 37 | } 38 | } 39 | 40 | void clear_log_files(void* context) { 41 | AppState* app = (AppState*)context; 42 | if(!app) return; 43 | 44 | // Close current log file 45 | close_current_log(app); 46 | 47 | // Stack allocation for better performance 48 | char filename[MAX_FILENAME_LEN]; 49 | char full_path[MAX_PATH_LEN]; 50 | FileInfo file_info; 51 | int deleted_count = 0; 52 | 53 | // Open storage once 54 | Storage* storage = furi_record_open(RECORD_STORAGE); 55 | File* dir = storage_file_alloc(storage); 56 | 57 | if(!storage_dir_open(dir, GHOST_ESP_APP_FOLDER_LOGS)) { 58 | FURI_LOG_E("ClearLogs", "Failed to open logs directory"); 59 | goto cleanup; 60 | } 61 | 62 | // Batch process files 63 | while(storage_dir_read(dir, &file_info, filename, MAX_FILENAME_LEN)) { 64 | if(file_info.flags & FSF_DIRECTORY) continue; 65 | 66 | // Construct path only for files 67 | snprintf(full_path, MAX_PATH_LEN, "%s/%s", GHOST_ESP_APP_FOLDER_LOGS, filename); 68 | 69 | // Remove file directly without extra existence check 70 | if(storage_simply_remove(storage, full_path)) { 71 | deleted_count++; 72 | } 73 | } 74 | 75 | FURI_LOG_I("ClearLogs", "Deleted %d files", deleted_count); 76 | 77 | cleanup: 78 | // Cleanup resources 79 | storage_dir_close(dir); 80 | storage_file_free(dir); 81 | furi_record_close(RECORD_STORAGE); 82 | 83 | // Create new log file 84 | create_new_log(app); 85 | } 86 | 87 | void clear_pcap_files(void* context) { 88 | AppState* app = (AppState*)context; 89 | if(!app) return; 90 | 91 | // Close current file if open 92 | if(app->uart_context && app->uart_context->storageContext && 93 | app->uart_context->storageContext->current_file) { 94 | storage_file_close(app->uart_context->storageContext->current_file); 95 | } 96 | 97 | // Stack allocation for better performance 98 | char filename[MAX_FILENAME_LEN]; 99 | char full_path[MAX_PATH_LEN]; 100 | FileInfo file_info; 101 | int deleted_count = 0; 102 | 103 | // Open storage once 104 | Storage* storage = furi_record_open(RECORD_STORAGE); 105 | File* dir = storage_file_alloc(storage); 106 | 107 | if(!storage_dir_open(dir, GHOST_ESP_APP_FOLDER_PCAPS)) { 108 | FURI_LOG_E("ClearPCAPs", "Failed to open pcaps directory"); 109 | goto cleanup; 110 | } 111 | 112 | // Batch process files 113 | while(storage_dir_read(dir, &file_info, filename, MAX_FILENAME_LEN)) { 114 | if(file_info.flags & FSF_DIRECTORY) continue; 115 | 116 | snprintf(full_path, MAX_PATH_LEN, "%s/%s", GHOST_ESP_APP_FOLDER_PCAPS, filename); 117 | 118 | if(storage_simply_remove(storage, full_path)) { 119 | deleted_count++; 120 | } 121 | } 122 | 123 | FURI_LOG_I("ClearPCAPs", "Deleted %d files", deleted_count); 124 | 125 | cleanup: 126 | storage_dir_close(dir); 127 | storage_file_free(dir); 128 | furi_record_close(RECORD_STORAGE); 129 | } 130 | 131 | void clear_wardrive_files(void* context) { 132 | AppState* app = (AppState*)context; 133 | if(!app) return; 134 | 135 | // Stack allocation for better performance 136 | char filename[MAX_FILENAME_LEN]; 137 | char full_path[MAX_PATH_LEN]; 138 | FileInfo file_info; 139 | int deleted_count = 0; 140 | 141 | // Open storage once 142 | Storage* storage = furi_record_open(RECORD_STORAGE); 143 | File* dir = storage_file_alloc(storage); 144 | 145 | if(!storage_dir_open(dir, GHOST_ESP_APP_FOLDER_WARDRIVE)) { 146 | FURI_LOG_E("ClearWardrive", "Failed to open wardrive directory"); 147 | goto cleanup; 148 | } 149 | 150 | // Batch process files 151 | while(storage_dir_read(dir, &file_info, filename, MAX_FILENAME_LEN)) { 152 | if(file_info.flags & FSF_DIRECTORY) continue; 153 | 154 | snprintf(full_path, MAX_PATH_LEN, "%s/%s", GHOST_ESP_APP_FOLDER_WARDRIVE, filename); 155 | 156 | if(storage_simply_remove(storage, full_path)) { 157 | deleted_count++; 158 | } 159 | } 160 | 161 | FURI_LOG_I("ClearWardrive", "Deleted %d files", deleted_count); 162 | 163 | cleanup: 164 | storage_dir_close(dir); 165 | storage_file_free(dir); 166 | furi_record_close(RECORD_STORAGE); 167 | } 168 | 169 | bool settings_set(Settings* settings, SettingKey key, uint8_t value, void* context) { 170 | FURI_LOG_D("SettingsSet", "Entering settings_set function for key: %d, value: %d", key, value); 171 | 172 | if(key >= SETTINGS_COUNT) return false; 173 | bool changed = false; 174 | 175 | switch(key) { 176 | case SETTING_RGB_MODE: 177 | if(settings->rgb_mode_index != value) { 178 | settings->rgb_mode_index = value; 179 | changed = true; 180 | } 181 | break; 182 | 183 | case SETTING_CHANNEL_HOP_DELAY: 184 | if(settings->channel_hop_delay_index != value) { 185 | settings->channel_hop_delay_index = value; 186 | changed = true; 187 | } 188 | break; 189 | 190 | case SETTING_ENABLE_CHANNEL_HOPPING: 191 | if(settings->enable_channel_hopping_index != value) { 192 | settings->enable_channel_hopping_index = value; 193 | changed = true; 194 | } 195 | break; 196 | 197 | case SETTING_ENABLE_RANDOM_BLE_MAC: 198 | if(settings->enable_random_ble_mac_index != value) { 199 | settings->enable_random_ble_mac_index = value; 200 | changed = true; 201 | } 202 | break; 203 | 204 | case SETTING_STOP_ON_BACK: 205 | if(settings->stop_on_back_index != value) { 206 | settings->stop_on_back_index = value; 207 | changed = true; 208 | } 209 | break; 210 | 211 | case SETTING_ENABLE_FILTERING: 212 | if(settings->enable_filtering_index != value) { 213 | settings->enable_filtering_index = value; 214 | if(context) { 215 | SettingsUIContext* settings_context = (SettingsUIContext*)context; 216 | if(settings_context->context) { 217 | AppState* app_state = (AppState*)settings_context->context; 218 | if(app_state->filter_config) { 219 | app_state->filter_config->enabled = value; 220 | } 221 | } 222 | } 223 | changed = true; 224 | } 225 | break; 226 | case SETTING_SHOW_INFO: 227 | if(value == 0) { // Execute on press 228 | SettingsUIContext* settings_context = (SettingsUIContext*)context; 229 | if(settings_context && settings_context->context) { 230 | FURI_LOG_I("SettingsSet", "Posting custom event to show app info dialog"); 231 | AppState* app_state = (AppState*)settings_context->context; 232 | // Post a custom event with the key 233 | view_dispatcher_send_custom_event(app_state->view_dispatcher, key); 234 | } 235 | } 236 | break; 237 | 238 | case SETTING_REBOOT_ESP: 239 | if(value == 0) { // Execute on press 240 | SettingsUIContext* settings_context = (SettingsUIContext*)context; 241 | if(settings_context && settings_context->send_uart_command) { 242 | FURI_LOG_I("SettingsSet", "Executing reboot command"); 243 | settings_context->send_uart_command("handle_reboot\n", settings_context->context); 244 | } 245 | } 246 | break; 247 | 248 | case SETTING_CLEAR_LOGS: 249 | case SETTING_CLEAR_NVS: 250 | case SETTING_CLEAR_PCAPS: 251 | case SETTING_CLEAR_WARDRIVE: 252 | if(value == 0) { // Execute on press 253 | SettingsUIContext* settings_context = (SettingsUIContext*)context; 254 | if(settings_context && settings_context->context) { 255 | FURI_LOG_I("SettingsSet", "Posting custom event to show confirmation dialog"); 256 | AppState* app_state = (AppState*)settings_context->context; 257 | // Post a custom event with the key 258 | view_dispatcher_send_custom_event(app_state->view_dispatcher, key); 259 | } 260 | } 261 | break; 262 | case SETTING_VIEW_LOGS_FROM_START: 263 | if(settings->view_logs_from_start_index != value) { 264 | settings->view_logs_from_start_index = value; 265 | changed = true; 266 | } 267 | break; 268 | 269 | case SETTING_DISABLE_ESP_CHECK: 270 | if(settings->disable_esp_check_index != value) { 271 | settings->disable_esp_check_index = value; 272 | changed = true; 273 | } 274 | break; 275 | 276 | default: 277 | return false; 278 | } 279 | 280 | if(changed) { 281 | FURI_LOG_I("SettingsSet", "Setting changed, saving to storage"); 282 | // Save settings to storage 283 | settings_storage_save(settings, GHOST_ESP_APP_SETTINGS_FILE); 284 | } 285 | 286 | return true; 287 | } 288 | 289 | uint8_t settings_get(const Settings* settings, SettingKey key) { 290 | FURI_LOG_D("SettingsGet", "Getting setting for key: %d", key); 291 | 292 | switch(key) { 293 | case SETTING_RGB_MODE: 294 | return settings->rgb_mode_index; 295 | 296 | case SETTING_CHANNEL_HOP_DELAY: 297 | return settings->channel_hop_delay_index; 298 | 299 | case SETTING_ENABLE_CHANNEL_HOPPING: 300 | return settings->enable_channel_hopping_index; 301 | 302 | case SETTING_ENABLE_RANDOM_BLE_MAC: 303 | return settings->enable_random_ble_mac_index; 304 | 305 | case SETTING_STOP_ON_BACK: 306 | return settings->stop_on_back_index; 307 | 308 | case SETTING_ENABLE_FILTERING: 309 | return settings->enable_filtering_index; 310 | 311 | case SETTING_VIEW_LOGS_FROM_START: 312 | return settings->view_logs_from_start_index; 313 | 314 | case SETTING_DISABLE_ESP_CHECK: 315 | return settings->disable_esp_check_index; 316 | 317 | case SETTING_REBOOT_ESP: 318 | case SETTING_CLEAR_LOGS: 319 | case SETTING_CLEAR_NVS: 320 | return 0; 321 | 322 | default: 323 | return 0; 324 | } 325 | } 326 | 327 | static void settings_item_change_callback(VariableItem* item) { 328 | FURI_LOG_D("SettingsChange", "Settings item change callback triggered"); 329 | 330 | VariableItemContext* item_context = variable_item_get_context(item); 331 | if(!item_context) { 332 | FURI_LOG_E("SettingsChange", "Invalid item context"); 333 | return; 334 | } 335 | 336 | SettingsUIContext* context = item_context->settings_ui_context; 337 | SettingKey key = item_context->key; 338 | 339 | FURI_LOG_D("SettingsChange", "Handling key: %d", key); 340 | 341 | const SettingMetadata* metadata = settings_get_metadata(key); 342 | if(!metadata) { 343 | FURI_LOG_E("SettingsChange", "Invalid metadata for key: %d", key); 344 | return; 345 | } 346 | 347 | if(metadata->is_action) { 348 | // Action button pressed 349 | FURI_LOG_D("SettingsChange", "Action button detected for key: %d", key); 350 | 351 | // Reset the action button's value to allow future presses 352 | variable_item_set_current_value_index(item, 0); 353 | variable_item_set_current_value_text(item, metadata->data.action.name); 354 | FURI_LOG_D("SettingsChange", "Action button state reset for key: %d", key); 355 | 356 | // Execute the action 357 | if(metadata->data.action.callback) { 358 | FURI_LOG_D("SettingsChange", "Executing callback for action key: %d", key); 359 | metadata->data.action.callback(context); 360 | FURI_LOG_I("SettingsChange", "Action callback executed for key: %d", key); 361 | } else if(metadata->data.action.command && context->send_uart_command) { 362 | FURI_LOG_D("SettingsChange", "Executing command for action key: %d", key); 363 | context->send_uart_command(metadata->data.action.command, context->context); 364 | FURI_LOG_I("SettingsChange", "Action command executed for key: %d", key); 365 | } else { 366 | FURI_LOG_I("SettingsChange", "No action handler found for key: %d", key); 367 | } 368 | 369 | return; 370 | } 371 | 372 | // Handle regular settings 373 | uint8_t value = variable_item_get_current_value_index(item); 374 | FURI_LOG_D("SettingsChange", "Attempting to set setting: key=%d, value=%d", key, value); 375 | 376 | if(settings_set(context->settings, key, value, context)) { 377 | variable_item_set_current_value_text(item, metadata->data.setting.value_names[value]); 378 | if(metadata->data.setting.uart_command && context->send_uart_command) { 379 | char command[64]; 380 | snprintf( 381 | command, 382 | sizeof(command), 383 | "%s %d\n", 384 | metadata->data.setting.uart_command, 385 | value + 1); 386 | FURI_LOG_D("SettingsChange", "Sending UART command: %s", command); 387 | context->send_uart_command(command, context->context); 388 | } 389 | } 390 | } 391 | 392 | static void settings_menu_callback(void* context, uint32_t index) { 393 | UNUSED(index); 394 | AppState* app_state = context; 395 | view_dispatcher_switch_to_view(app_state->view_dispatcher, 4); // Switch to settings view 396 | app_state->current_view = 4; 397 | } 398 | 399 | static void settings_action_callback(void* context, uint32_t index) { 400 | SettingsUIContext* settings_context = context; 401 | // Execute the action directly without value simulation 402 | settings_set(settings_context->settings, index, 0, settings_context); 403 | } 404 | 405 | void settings_setup_gui(VariableItemList* list, SettingsUIContext* context) { 406 | FURI_LOG_D("SettingsSetup", "Entering settings_setup_gui"); 407 | AppState* app_state = (AppState*)context->context; 408 | 409 | // Add "Configuration" submenu item 410 | submenu_add_item( 411 | app_state->settings_actions_menu, 412 | "Configuration >", 413 | SETTINGS_COUNT, 414 | settings_menu_callback, 415 | app_state); 416 | 417 | // Iterate over all settings 418 | for(SettingKey key = 0; key < SETTINGS_COUNT; key++) { 419 | // Skip hidden settings 420 | if(!setting_is_visible(key)) { 421 | continue; 422 | } 423 | 424 | const SettingMetadata* metadata = settings_get_metadata(key); 425 | if(!metadata) continue; 426 | 427 | if(metadata->is_action) { 428 | // Add all action items to the actions submenu 429 | submenu_add_item( 430 | app_state->settings_actions_menu, 431 | metadata->name, 432 | key, 433 | settings_action_callback, 434 | context); 435 | FURI_LOG_D("SettingsSetup", "Added action button: %s", metadata->name); 436 | } else { 437 | // Handle regular settings 438 | VariableItemContext* item_context = malloc(sizeof(VariableItemContext)); 439 | if(!item_context) { 440 | FURI_LOG_E("SettingsSetup", "Failed to allocate memory for item context"); 441 | continue; 442 | } 443 | item_context->settings_ui_context = context; 444 | item_context->key = key; 445 | 446 | VariableItem* item = variable_item_list_add( 447 | list, 448 | metadata->name, 449 | metadata->data.setting.max_value + 1, 450 | settings_item_change_callback, 451 | item_context); 452 | 453 | if(item) { 454 | uint8_t current_value = settings_get(context->settings, key); 455 | variable_item_set_current_value_index(item, current_value); 456 | variable_item_set_current_value_text( 457 | item, metadata->data.setting.value_names[current_value]); 458 | FURI_LOG_D("SettingsSetup", "Added setting item: %s", metadata->name); 459 | } 460 | } 461 | } 462 | } 463 | 464 | bool settings_custom_event_callback(void* context, uint32_t event_id) { 465 | AppState* app_state = (AppState*)context; 466 | if(!app_state) return false; 467 | 468 | switch(event_id) { 469 | case SETTING_CLEAR_LOGS: 470 | show_confirmation_dialog_ex( 471 | app_state, 472 | "Clear Logs", 473 | "Clear all log files?\n" 474 | "This cannot be undone.\n" 475 | "Files located at:\n" 476 | "apps_data/ghost_esp/logs\n", 477 | logs_clear_confirmed_callback, 478 | logs_clear_cancelled_callback); 479 | return true; 480 | 481 | case SETTING_CLEAR_PCAPS: 482 | show_confirmation_dialog_ex( 483 | app_state, 484 | "Clear PCAPs", 485 | "Clear all PCAP files?\n" 486 | "This cannot be undone.\n" 487 | "Files located at:\n" 488 | "apps_data/ghost_esp/pcaps\n", 489 | pcap_clear_confirmed_callback, 490 | pcap_clear_cancelled_callback); 491 | return true; 492 | 493 | case SETTING_CLEAR_WARDRIVE: 494 | show_confirmation_dialog_ex( 495 | app_state, 496 | "Clear Wardrives", 497 | "Clear all wardrive files?\n" 498 | "This cannot be undone.\n" 499 | "Files located at:\n" 500 | "apps_data/ghost_esp/wardrive\n", 501 | wardrive_clear_confirmed_callback, 502 | wardrive_clear_cancelled_callback); 503 | return true; 504 | 505 | case SETTING_CLEAR_NVS: 506 | show_confirmation_dialog_ex( 507 | app_state, 508 | "Clear NVS", 509 | "Clear NVS settings?\n" 510 | "This will reset all ESP\n" 511 | "settings to default.\n" 512 | "This cannot be undone.", 513 | nvs_clear_confirmed_callback, 514 | nvs_clear_cancelled_callback); 515 | return true; 516 | 517 | case SETTING_SHOW_INFO: { 518 | // Create a new context for the confirmation dialog 519 | SettingsConfirmContext* confirm_ctx = malloc(sizeof(SettingsConfirmContext)); 520 | if(!confirm_ctx) return false; 521 | confirm_ctx->state = app_state; 522 | 523 | const char* info_text = "Created by: Spooky\n" 524 | "Updated by: Jay Candel\n" 525 | "Built with <3"; 526 | 527 | confirmation_view_set_header(app_state->confirmation_view, "Ghost ESP v1.2.3"); 528 | confirmation_view_set_text(app_state->confirmation_view, info_text); 529 | 530 | // Save current view before switching 531 | app_state->previous_view = app_state->current_view; 532 | 533 | confirmation_view_set_ok_callback( 534 | app_state->confirmation_view, app_info_ok_callback, confirm_ctx); 535 | confirmation_view_set_cancel_callback( 536 | app_state->confirmation_view, app_info_cancel_callback, confirm_ctx); 537 | 538 | view_dispatcher_switch_to_view(app_state->view_dispatcher, 7); 539 | app_state->current_view = 7; 540 | break; 541 | } 542 | 543 | default: 544 | return false; 545 | } 546 | return false; 547 | } 548 | 549 | // 6675636B796F7564656B69 550 | -------------------------------------------------------------------------------- /src/settings_ui.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "app_types.h" 4 | #include "settings_def.h" 5 | #include 6 | 7 | // Function pointer types 8 | typedef void (*SendUartCommandCallback)(const char* command, void* context); 9 | typedef void (*SwitchToViewCallback)(void* context, uint32_t view_id); 10 | typedef void (*ShowConfirmationViewCallback)(void* context, ConfirmationView* view); 11 | 12 | // Forward declare the struct 13 | typedef struct SettingsUIContext SettingsUIContext; 14 | // Add this near the top of settings_ui.h 15 | typedef struct { 16 | AppState* state; 17 | SettingKey key; 18 | } SettingsConfirmContext; 19 | 20 | // Function declarations 21 | void clear_log_files(void* context); 22 | void settings_setup_gui(VariableItemList* list, SettingsUIContext* context); 23 | bool settings_set(Settings* settings, SettingKey key, uint8_t value, void* context); 24 | uint8_t settings_get(const Settings* settings, SettingKey key); 25 | bool settings_custom_event_callback(void* context, uint32_t event); 26 | -------------------------------------------------------------------------------- /src/settings_ui_types.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "settings_def.h" 3 | #include "settings_ui.h" 4 | 5 | // Full structure definition 6 | struct SettingsUIContext { 7 | Settings* settings; 8 | SendUartCommandCallback send_uart_command; 9 | SwitchToViewCallback switch_to_view; 10 | ShowConfirmationViewCallback show_confirmation_view; 11 | void* context; 12 | }; -------------------------------------------------------------------------------- /src/uart_storage.c: -------------------------------------------------------------------------------- 1 | #include "uart_utils.h" 2 | #include "log_manager.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "sequential_file.h" 8 | 9 | #define COMMAND_BUFFER_SIZE 128 10 | #define PCAP_WRITE_CHUNK_SIZE 1024u 11 | 12 | // Define directories in an array for loop-based creation 13 | static const char* GHOST_DIRECTORIES[] = { 14 | GHOST_ESP_APP_FOLDER, 15 | GHOST_ESP_APP_FOLDER_PCAPS, 16 | GHOST_ESP_APP_FOLDER_LOGS, 17 | GHOST_ESP_APP_FOLDER_WARDRIVE 18 | }; 19 | 20 | // Helper macro for error handling and cleanup 21 | #define CLEANUP_AND_RETURN(ctx, msg, retval) \ 22 | do { \ 23 | FURI_LOG_E("Storage", msg); \ 24 | if (ctx) { \ 25 | uart_storage_free(ctx); \ 26 | } \ 27 | return retval; \ 28 | } while(0) 29 | 30 | void uart_storage_safe_cleanup(UartStorageContext* ctx) { 31 | if(!ctx) return; 32 | 33 | // Safely close current file if open 34 | if(ctx->current_file) { 35 | if(storage_file_is_open(ctx->current_file)) { 36 | storage_file_sync(ctx->current_file); 37 | storage_file_close(ctx->current_file); 38 | } 39 | ctx->HasOpenedFile = false; 40 | } 41 | 42 | // Safely close log file if open 43 | if(ctx->log_file) { 44 | if(storage_file_is_open(ctx->log_file)) { 45 | storage_file_sync(ctx->log_file); 46 | storage_file_close(ctx->log_file); 47 | } 48 | } 49 | } 50 | 51 | UartStorageContext* uart_storage_init(UartContext* parentContext) { 52 | uint32_t func_start_time = furi_get_tick(); 53 | FURI_LOG_D("Storage", "Starting storage initialization"); 54 | 55 | uint32_t step_start = furi_get_tick(); 56 | // Allocate and initialize context 57 | UartStorageContext* ctx = malloc(sizeof(UartStorageContext)); 58 | uint32_t elapsed_step = furi_get_tick() - step_start; 59 | if(!ctx) { 60 | FURI_LOG_E("Storage", "Failed to allocate UartStorageContext (Time taken: %lu ms)", elapsed_step); 61 | return NULL; 62 | } 63 | FURI_LOG_I("Storage", "Allocated UartStorageContext (Time taken: %lu ms)", elapsed_step); 64 | 65 | // Initialize context fields 66 | step_start = furi_get_tick(); 67 | memset(ctx, 0, sizeof(UartStorageContext)); 68 | ctx->parentContext = parentContext; 69 | elapsed_step = furi_get_tick() - step_start; 70 | FURI_LOG_I("Storage", "Initialized UartStorageContext fields (Time taken: %lu ms)", elapsed_step); 71 | 72 | // Open storage API 73 | step_start = furi_get_tick(); 74 | ctx->storage_api = furi_record_open(RECORD_STORAGE); 75 | elapsed_step = furi_get_tick() - step_start; 76 | if(!ctx->storage_api) { 77 | FURI_LOG_E("Storage", "Failed to open RECORD_STORAGE (Time taken: %lu ms)", elapsed_step); 78 | free(ctx); 79 | return NULL; 80 | } 81 | FURI_LOG_I("Storage", "Opened RECORD_STORAGE (Time taken: %lu ms)", elapsed_step); 82 | 83 | // Allocate file handles 84 | step_start = furi_get_tick(); 85 | ctx->current_file = storage_file_alloc(ctx->storage_api); 86 | ctx->log_file = storage_file_alloc(ctx->storage_api); 87 | elapsed_step = furi_get_tick() - step_start; 88 | if(!ctx->current_file || !ctx->log_file) { 89 | FURI_LOG_E("Storage", "Failed to allocate file handles (Time taken: %lu ms)", elapsed_step); 90 | uart_storage_free(ctx); 91 | return NULL; 92 | } 93 | FURI_LOG_I("Storage", "Allocated current_file and log_file (Time taken: %lu ms)", elapsed_step); 94 | 95 | // Create directories 96 | step_start = furi_get_tick(); 97 | size_t num_directories = sizeof(GHOST_DIRECTORIES) / sizeof(GHOST_DIRECTORIES[0]); 98 | for(size_t i = 0; i < num_directories; i++) { 99 | uint32_t dir_step_start = furi_get_tick(); 100 | if(!storage_simply_mkdir(ctx->storage_api, GHOST_DIRECTORIES[i])) { 101 | FURI_LOG_W("Storage", "Failed to create or confirm directory: %s (Time taken: %lu ms)", GHOST_DIRECTORIES[i], furi_get_tick() - dir_step_start); 102 | } else { 103 | FURI_LOG_I("Storage", "Created/Confirmed directory: %s (Time taken: %lu ms)", GHOST_DIRECTORIES[i], furi_get_tick() - dir_step_start); 104 | } 105 | } 106 | elapsed_step = furi_get_tick() - step_start; 107 | FURI_LOG_I("Storage", "Directory creation/confirmation completed (Time taken: %lu ms)", elapsed_step); 108 | 109 | // Ensure all directories are accessible 110 | step_start = furi_get_tick(); 111 | bool dirs_ok = true; 112 | if(!storage_dir_exists(ctx->storage_api, GHOST_ESP_APP_FOLDER_PCAPS)) { 113 | FURI_LOG_E("Storage", "PCAP directory not accessible"); 114 | dirs_ok = false; 115 | } 116 | if(!storage_dir_exists(ctx->storage_api, GHOST_ESP_APP_FOLDER_LOGS)) { 117 | FURI_LOG_E("Storage", "Logs directory not accessible"); 118 | dirs_ok = false; 119 | } 120 | if(!storage_dir_exists(ctx->storage_api, GHOST_ESP_APP_FOLDER_WARDRIVE)) { 121 | FURI_LOG_E("Storage", "Wardrive directory not accessible"); 122 | dirs_ok = false; 123 | } 124 | elapsed_step = furi_get_tick() - step_start; 125 | FURI_LOG_I("Storage", "Checked directory accessibility (Time taken: %lu ms)", elapsed_step); 126 | 127 | if(!dirs_ok) { 128 | step_start = furi_get_tick(); 129 | FURI_LOG_E("Storage", "Critical directories missing, attempting cleanup"); 130 | uart_storage_safe_cleanup(ctx); 131 | // Retry directory creation 132 | for(size_t i = 0; i < num_directories; i++) { 133 | uint32_t retry_dir_start = furi_get_tick(); 134 | storage_simply_mkdir(ctx->storage_api, GHOST_DIRECTORIES[i]); 135 | FURI_LOG_I("Storage", "Retried creating directory: %s (Time taken: %lu ms)", GHOST_DIRECTORIES[i], furi_get_tick() - retry_dir_start); 136 | } 137 | elapsed_step = furi_get_tick() - step_start; 138 | FURI_LOG_I("Storage", "Retry directory creation completed (Time taken: %lu ms)", elapsed_step); 139 | } 140 | 141 | // Initialize log file 142 | step_start = furi_get_tick(); 143 | ctx->HasOpenedFile = sequential_file_open( 144 | ctx->storage_api, 145 | ctx->log_file, 146 | GHOST_ESP_APP_FOLDER_LOGS, 147 | "ghost_logs", 148 | "txt" 149 | ); 150 | elapsed_step = furi_get_tick() - step_start; 151 | if(!ctx->HasOpenedFile) { 152 | FURI_LOG_W("Storage", "Failed to open log file, attempting cleanup (Time taken: %lu ms)", elapsed_step); 153 | uart_storage_safe_cleanup(ctx); 154 | // Retry log file creation 155 | step_start = furi_get_tick(); 156 | ctx->HasOpenedFile = sequential_file_open( 157 | ctx->storage_api, 158 | ctx->log_file, 159 | GHOST_ESP_APP_FOLDER_LOGS, 160 | "ghost_logs", 161 | "txt" 162 | ); 163 | elapsed_step = furi_get_tick() - step_start; 164 | if(!ctx->HasOpenedFile) { 165 | FURI_LOG_W("Storage", "Log init failed after cleanup, continuing without logging (Time taken: %lu ms)", elapsed_step); 166 | } else { 167 | FURI_LOG_I("Storage", "Successfully opened log file after retry (Time taken: %lu ms)", elapsed_step); 168 | } 169 | } else { 170 | FURI_LOG_I("Storage", "Opened log file successfully (Time taken: %lu ms)", elapsed_step); 171 | } 172 | 173 | step_start = furi_get_tick(); 174 | ctx->IsWritingToFile = false; 175 | elapsed_step = furi_get_tick() - step_start; 176 | FURI_LOG_I("Storage", "Initialized IsWritingToFile flag (Time taken: %lu ms)", elapsed_step); 177 | 178 | uint32_t func_elapsed = furi_get_tick() - func_start_time; 179 | FURI_LOG_I("Storage", "Storage initialization completed in %lu ms", func_elapsed); 180 | return ctx; 181 | } 182 | 183 | 184 | void uart_storage_rx_callback(uint8_t *buf, size_t len, void *context) { 185 | UartContext *app = (UartContext *)context; 186 | 187 | // Basic sanity checks with detailed logging 188 | if(!app || !app->storageContext || !buf || len == 0) { 189 | FURI_LOG_E("Storage", "Invalid parameters in storage callback: app=%p, storageContext=%p, buf=%p, len=%zu", 190 | (void*)app, (void*)app->storageContext, (void*)buf, len); 191 | return; 192 | } 193 | 194 | // **Ensure PCAP File is Open** 195 | if(!app->storageContext->current_file || !app->storageContext->HasOpenedFile) { 196 | FURI_LOG_E("Storage", "PCAP file is not open. Data cannot be written."); 197 | return; 198 | } 199 | 200 | FURI_LOG_D("Storage", "Received %zu bytes for PCAP write", len); 201 | 202 | // Log the first few bytes for debugging 203 | size_t bytes_to_log = (len < 16) ? len : 16; 204 | FURI_LOG_D("Storage", "First %zu bytes of buffer:", bytes_to_log); 205 | for(size_t i = 0; i < bytes_to_log; i++) { 206 | FURI_LOG_D("Storage", "Byte %zu: 0x%02X", i, buf[i]); 207 | } 208 | if(len > bytes_to_log) { 209 | FURI_LOG_D("Storage", "... (%zu more bytes)", len - bytes_to_log); 210 | } 211 | 212 | // Write data and verify with detailed logging 213 | size_t written = storage_file_write(app->storageContext->current_file, buf, len); 214 | if(written != len) { 215 | FURI_LOG_E("Storage", "Failed to write PCAP data: Expected %zu, Written %zu", len, written); 216 | app->pcap = false; // Reset PCAP state on write failure 217 | return; 218 | } 219 | 220 | FURI_LOG_D("Storage", "Successfully wrote %zu bytes to PCAP file", written); 221 | 222 | // Optionally, calculate and log a checksum for data integrity 223 | uint8_t checksum = 0; 224 | for(size_t i = 0; i < len; i++) { 225 | checksum ^= buf[i]; 226 | } 227 | FURI_LOG_D("Storage", "Data Checksum (XOR): 0x%02X", checksum); 228 | 229 | // Periodic sync every ~8KB to balance between safety and performance with detailed logging 230 | static size_t bytes_since_sync = 0; 231 | bytes_since_sync += len; 232 | FURI_LOG_D("Storage", "Accumulated %zu bytes since last sync", bytes_since_sync); 233 | if(bytes_since_sync >= 8192) { 234 | storage_file_sync(app->storageContext->current_file); 235 | FURI_LOG_D("Storage", "PCAP file synced to storage"); 236 | bytes_since_sync = 0; 237 | } 238 | } 239 | 240 | 241 | 242 | void uart_storage_reset_logs(UartStorageContext *ctx) { 243 | if(!ctx || !ctx->storage_api) return; 244 | 245 | if(ctx->log_file) { 246 | storage_file_close(ctx->log_file); 247 | storage_file_free(ctx->log_file); 248 | ctx->log_file = storage_file_alloc(ctx->storage_api); 249 | } 250 | 251 | ctx->HasOpenedFile = sequential_file_open( 252 | ctx->storage_api, 253 | ctx->log_file, 254 | GHOST_ESP_APP_FOLDER_LOGS, 255 | "ghost_logs", 256 | "txt" 257 | ); 258 | 259 | if(!ctx->HasOpenedFile) { 260 | FURI_LOG_E("Storage", "Failed to reset log file"); 261 | } 262 | } 263 | 264 | void uart_storage_free(UartStorageContext *ctx) { 265 | if(!ctx) return; 266 | 267 | // Do safe cleanup first 268 | uart_storage_safe_cleanup(ctx); 269 | 270 | // Free resources 271 | if(ctx->current_file) { 272 | storage_file_free(ctx->current_file); 273 | } 274 | 275 | if(ctx->log_file) { 276 | storage_file_free(ctx->log_file); 277 | } 278 | 279 | if(ctx->storage_api) { 280 | furi_record_close(RECORD_STORAGE); 281 | } 282 | 283 | free(ctx); 284 | } -------------------------------------------------------------------------------- /src/uart_storage.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "app_types.h" 4 | #include 5 | #include 6 | 7 | struct UartStorageContext { 8 | Storage* storage_api; 9 | File* current_file; 10 | File* log_file; 11 | File* settings_file; 12 | UartContext* parentContext; 13 | bool HasOpenedFile; 14 | bool IsWritingToFile; 15 | bool view_logs_from_start; 16 | }; 17 | 18 | UartStorageContext* uart_storage_init(UartContext* parentContext); 19 | void uart_storage_free(UartStorageContext* ctx); 20 | void uart_storage_rx_callback(uint8_t* buf, size_t len, void* context); -------------------------------------------------------------------------------- /src/uart_utils.c: -------------------------------------------------------------------------------- 1 | #include "uart_utils.h" 2 | #include 3 | #include 4 | #include 5 | #include "sequential_file.h" 6 | #include 7 | #include 8 | 9 | #define WORKER_ALL_RX_EVENTS (WorkerEvtStop | WorkerEvtRxDone | WorkerEvtPcapDone) 10 | #define PCAP_WRITE_CHUNK_SIZE 1024 11 | #define AP_LIST_TIMEOUT_MS 5000 12 | #define INITIAL_BUFFER_SIZE 2048 13 | #define BUFFER_GROWTH_FACTOR 1.5 14 | #define MAX_BUFFER_SIZE (8 * 1024) // 8KB max 15 | #define MUTEX_TIMEOUT_MS 2500 16 | #define BUFFER_CLEAR_SIZE 128 17 | #define BUFFER_RESIZE_CHUNK 1024 18 | #define TEXT_SCROLL_GUARD_SIZE 64 19 | 20 | typedef enum { 21 | MARKER_STATE_IDLE, 22 | MARKER_STATE_BEGIN, 23 | MARKER_STATE_CLOSE 24 | } MarkerState; 25 | 26 | static TextBufferManager* text_buffer_alloc(void) { 27 | TextBufferManager* manager = malloc(sizeof(TextBufferManager)); 28 | if(!manager) return NULL; 29 | 30 | manager->ring_buffer = malloc(RING_BUFFER_SIZE); 31 | manager->view_buffer = malloc(VIEW_BUFFER_SIZE); 32 | manager->mutex = furi_mutex_alloc(FuriMutexTypeNormal); 33 | 34 | if(!manager->ring_buffer || !manager->view_buffer || !manager->mutex) { 35 | free(manager->ring_buffer); 36 | free(manager->view_buffer); 37 | free(manager->mutex); 38 | free(manager); 39 | return NULL; 40 | } 41 | 42 | manager->ring_read_index = 0; 43 | manager->ring_write_index = 0; 44 | manager->view_buffer_len = 0; 45 | manager->buffer_full = false; 46 | 47 | memset(manager->ring_buffer, 0, RING_BUFFER_SIZE); 48 | memset(manager->view_buffer, 0, VIEW_BUFFER_SIZE); 49 | 50 | return manager; 51 | } 52 | static void text_buffer_free(TextBufferManager* manager) { 53 | if(!manager) return; 54 | if(manager->mutex) furi_mutex_free(manager->mutex); 55 | free(manager->ring_buffer); 56 | free(manager->view_buffer); 57 | free(manager); 58 | } 59 | 60 | static void text_buffer_add(TextBufferManager* manager, const char* data, size_t len) { 61 | if(!manager || !data || !len) return; 62 | 63 | if(furi_mutex_acquire(manager->mutex, 300) != FuriStatusOk) { 64 | FURI_LOG_E("UART", "Mutex timeout! Dropping data"); 65 | return; 66 | } 67 | 68 | for(size_t i = 0; i < len; i++) { 69 | manager->ring_buffer[manager->ring_write_index] = data[i]; 70 | manager->ring_write_index = (manager->ring_write_index + 1) % RING_BUFFER_SIZE; 71 | 72 | if(manager->ring_write_index == manager->ring_read_index) { 73 | manager->ring_read_index = (manager->ring_read_index + 1) % RING_BUFFER_SIZE; 74 | manager->buffer_full = true; 75 | } 76 | } 77 | 78 | furi_mutex_release(manager->mutex); 79 | } 80 | static void text_buffer_update_view(TextBufferManager* manager, bool view_from_start) { 81 | if(!manager) return; 82 | 83 | furi_mutex_acquire(manager->mutex, FuriWaitForever); 84 | 85 | // Calculate available data 86 | size_t available; 87 | if(manager->buffer_full) { 88 | available = RING_BUFFER_SIZE; 89 | } else if(manager->ring_write_index >= manager->ring_read_index) { 90 | available = manager->ring_write_index - manager->ring_read_index; 91 | } else { 92 | available = RING_BUFFER_SIZE - manager->ring_read_index + manager->ring_write_index; 93 | } 94 | 95 | // Limit to view buffer size 96 | size_t copy_size = (available > VIEW_BUFFER_SIZE - 1) ? VIEW_BUFFER_SIZE - 1 : available; 97 | 98 | // Choose starting point based on view preference 99 | size_t start; 100 | if(view_from_start) { 101 | // Start from oldest data 102 | start = manager->ring_read_index; 103 | } else { 104 | // Start from newest data that will fit in view 105 | size_t data_to_skip = available > copy_size ? available - copy_size : 0; 106 | start = (manager->ring_read_index + data_to_skip) % RING_BUFFER_SIZE; 107 | } 108 | 109 | 110 | // Copy data to view buffer 111 | size_t j = 0; 112 | for(size_t i = 0; i < copy_size; i++) { 113 | size_t idx = (start + i) % RING_BUFFER_SIZE; 114 | manager->view_buffer[j++] = manager->ring_buffer[idx]; 115 | } 116 | 117 | manager->view_buffer[j] = '\0'; 118 | manager->view_buffer_len = j; 119 | 120 | furi_mutex_release(manager->mutex); 121 | } 122 | static void uart_rx_callback(FuriHalSerialHandle* handle, FuriHalSerialRxEvent event, void* context) { 123 | UartContext* uart = (UartContext*)context; 124 | const char* mark_begin = "[BUF/BEGIN]"; 125 | const char* mark_close = "[BUF/CLOSE]"; 126 | size_t mark_len = 11; 127 | 128 | if(!uart || !uart->text_manager || event != FuriHalSerialRxEventData) { 129 | return; 130 | } 131 | 132 | uint8_t data = furi_hal_serial_async_rx(handle); 133 | 134 | // Check if we're collecting a marker 135 | if(uart->mark_test_idx > 0) { 136 | // Prevent buffer overflow 137 | if(uart->mark_test_idx >= sizeof(uart->mark_test_buf)) { 138 | uart->mark_test_idx = 0; 139 | return; 140 | } 141 | 142 | if(uart->mark_test_idx < mark_len && 143 | (data == mark_begin[uart->mark_test_idx] || data == mark_close[uart->mark_test_idx])) { 144 | uart->mark_test_buf[uart->mark_test_idx++] = data; 145 | 146 | if(uart->mark_test_idx == mark_len) { 147 | furi_mutex_acquire(uart->text_manager->mutex, FuriWaitForever); 148 | 149 | if(!memcmp(uart->mark_test_buf, mark_begin, mark_len)) { 150 | uart->pcap = true; 151 | FURI_LOG_I("UART", "Capture started"); 152 | } else if(!memcmp(uart->mark_test_buf, mark_close, mark_len)) { 153 | uart->pcap = false; 154 | FURI_LOG_I("UART", "Capture ended"); 155 | } 156 | 157 | furi_mutex_release(uart->text_manager->mutex); 158 | uart->mark_test_idx = 0; 159 | return; 160 | } 161 | // Don't process marker bytes 162 | return; 163 | } else { 164 | // Mismatch occurred, handle buffered bytes atomically 165 | furi_mutex_acquire(uart->text_manager->mutex, FuriWaitForever); 166 | 167 | // Ensure valid thread and stream before sending 168 | if(uart->rx_thread && (uart->pcap ? uart->pcap_stream : uart->rx_stream)) { 169 | if(uart->pcap) { 170 | if(furi_stream_buffer_send(uart->pcap_stream, uart->mark_test_buf, 171 | uart->mark_test_idx, 0) == uart->mark_test_idx) { 172 | furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtPcapDone); 173 | } 174 | } else { 175 | if(furi_stream_buffer_send(uart->rx_stream, uart->mark_test_buf, 176 | uart->mark_test_idx, 0) == uart->mark_test_idx) { 177 | furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtRxDone); 178 | } 179 | } 180 | } 181 | 182 | furi_mutex_release(uart->text_manager->mutex); 183 | uart->mark_test_idx = 0; 184 | } 185 | } 186 | 187 | // Start of a potential marker 188 | if(data == mark_begin[0] || data == mark_close[0]) { 189 | uart->mark_test_buf[0] = data; 190 | uart->mark_test_idx = 1; 191 | return; 192 | } 193 | 194 | // Handle regular data atomically 195 | furi_mutex_acquire(uart->text_manager->mutex, FuriWaitForever); 196 | 197 | bool current_pcap = uart->pcap; 198 | bool success = false; 199 | 200 | // Ensure valid thread and stream before sending 201 | if(uart->rx_thread && (current_pcap ? uart->pcap_stream : uart->rx_stream)) { 202 | if(current_pcap) { 203 | if(furi_stream_buffer_send(uart->pcap_stream, &data, 1, 0) == 1) { 204 | furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtPcapDone); 205 | success = true; 206 | FURI_LOG_D("UART", "Captured data byte (pcap=true): 0x%02X", data); 207 | } 208 | } else { 209 | if(furi_stream_buffer_send(uart->rx_stream, &data, 1, 0) == 1) { 210 | furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtRxDone); 211 | success = true; 212 | FURI_LOG_D("UART", "Logged data byte (pcap=false): 0x%02X", data); 213 | } 214 | } 215 | } 216 | 217 | if(!success) { 218 | FURI_LOG_W("UART", "Failed to send data byte: 0x%02X", data); 219 | } 220 | 221 | furi_mutex_release(uart->text_manager->mutex); 222 | } 223 | 224 | void handle_uart_rx_data(uint8_t *buf, size_t len, void *context) { 225 | AppState *state = (AppState *)context; 226 | if(!state || !state->uart_context || !state->uart_context->is_serial_active || 227 | !buf || len == 0) { 228 | FURI_LOG_W("UART", "Invalid parameters in handle_uart_rx_data"); 229 | return; 230 | } 231 | 232 | // Only log data if NOT in PCAP mode 233 | if(!state->uart_context->pcap && 234 | state->uart_context->storageContext && 235 | state->uart_context->storageContext->log_file && 236 | state->uart_context->storageContext->HasOpenedFile) { 237 | static size_t bytes_since_sync = 0; 238 | 239 | size_t written = storage_file_write( 240 | state->uart_context->storageContext->log_file, 241 | buf, 242 | len 243 | ); 244 | 245 | if(written != len) { 246 | FURI_LOG_E("UART", "Failed to write log data: expected %zu, wrote %zu", len, written); 247 | } else { 248 | bytes_since_sync += written; 249 | if(bytes_since_sync >= 1024) { // Sync every 1KB 250 | storage_file_sync(state->uart_context->storageContext->log_file); 251 | bytes_since_sync = 0; 252 | FURI_LOG_D("UART", "Synced log file to storage"); 253 | } 254 | } 255 | } 256 | 257 | // Update text display 258 | text_buffer_add(state->uart_context->text_manager, (char*)buf, len); 259 | text_buffer_update_view(state->uart_context->text_manager, 260 | state->settings.view_logs_from_start_index); 261 | 262 | bool view_from_start = state->settings.view_logs_from_start_index; 263 | text_box_set_text(state->text_box, state->uart_context->text_manager->view_buffer); 264 | text_box_set_focus(state->text_box, 265 | view_from_start ? TextBoxFocusStart : TextBoxFocusEnd); 266 | } 267 | 268 | static int32_t uart_worker(void* context) { 269 | UartContext* uart = (UartContext*)context; 270 | 271 | FURI_LOG_I("Worker", "UART worker thread started"); 272 | 273 | while(1) { 274 | uint32_t events = furi_thread_flags_wait( 275 | WORKER_ALL_RX_EVENTS, 276 | FuriFlagWaitAny, 277 | FuriWaitForever); 278 | 279 | FURI_LOG_D("Worker", "Received events: 0x%08lX", (unsigned long)events); 280 | 281 | if(events & WorkerEvtStop) { 282 | FURI_LOG_I("Worker", "Stopping worker thread"); 283 | break; 284 | } 285 | 286 | if(events & WorkerEvtRxDone) { 287 | size_t len = furi_stream_buffer_receive( 288 | uart->rx_stream, 289 | uart->rx_buf, 290 | RX_BUF_SIZE, 291 | 0); 292 | 293 | FURI_LOG_D("Worker", "Processing rx_stream data: %zu bytes", len); 294 | 295 | if(len > 0 && uart->handle_rx_data_cb) { 296 | FURI_LOG_D("Worker", "Invoking handle_rx_data_cb with %zu bytes", len); 297 | uart->handle_rx_data_cb(uart->rx_buf, len, uart->state); 298 | FURI_LOG_D("Worker", "rx_stream callback invoked successfully"); 299 | } else { 300 | if(len == 0) { 301 | FURI_LOG_W("Worker", "Received zero bytes from rx_stream"); 302 | } 303 | if(!uart->handle_rx_data_cb) { 304 | FURI_LOG_E("Worker", "handle_rx_data_cb is NULL"); 305 | } 306 | } 307 | } 308 | 309 | if(events & WorkerEvtPcapDone) { 310 | size_t len = furi_stream_buffer_receive( 311 | uart->pcap_stream, 312 | uart->rx_buf, 313 | RX_BUF_SIZE, 314 | 0); 315 | 316 | FURI_LOG_D("Worker", "Processing pcap_stream data: %zu bytes", len); 317 | 318 | if(len > 0 && uart->handle_rx_pcap_cb) { 319 | FURI_LOG_D("Worker", "Invoking handle_rx_pcap_cb with %zu bytes", len); 320 | uart->handle_rx_pcap_cb(uart->rx_buf, len, uart); // Corrected context 321 | FURI_LOG_D("Worker", "pcap_stream callback invoked successfully"); 322 | } else { 323 | if(len == 0) { 324 | FURI_LOG_W("Worker", "Received zero bytes from pcap_stream"); 325 | } 326 | if(!uart->handle_rx_pcap_cb) { 327 | FURI_LOG_E("Worker", "handle_rx_pcap_cb is NULL"); 328 | } 329 | } 330 | } 331 | } 332 | 333 | // Clean up streams with detailed logging 334 | FURI_LOG_I("Worker", "Cleaning up rx_stream and pcap_stream buffers"); 335 | furi_stream_buffer_free(uart->rx_stream); 336 | furi_stream_buffer_free(uart->pcap_stream); 337 | 338 | FURI_LOG_I("Worker", "Worker thread exited"); 339 | return 0; 340 | } 341 | 342 | 343 | void update_text_box_view(AppState* state) { 344 | if(!state || !state->text_box || !state->uart_context || !state->uart_context->text_manager) return; 345 | 346 | text_buffer_update_view(state->uart_context->text_manager, 347 | state->settings.view_logs_from_start_index); 348 | 349 | bool view_from_start = state->settings.view_logs_from_start_index; 350 | text_box_set_text(state->text_box, state->uart_context->text_manager->view_buffer); 351 | text_box_set_focus(state->text_box, 352 | view_from_start ? TextBoxFocusStart : TextBoxFocusEnd); 353 | } 354 | UartContext* uart_init(AppState* state) { 355 | uint32_t start_time = furi_get_tick(); 356 | FURI_LOG_I("UART", "Starting UART initialization"); 357 | 358 | UartContext* uart = malloc(sizeof(UartContext)); 359 | if(!uart) { 360 | FURI_LOG_E("UART", "Failed to allocate UART context"); 361 | return NULL; 362 | } 363 | memset(uart, 0, sizeof(UartContext)); 364 | 365 | uart->state = state; 366 | uart->is_serial_active = false; 367 | uart->pcap = false; 368 | uart->mark_test_idx = 0; 369 | uart->pcap_buf_len = 0; 370 | 371 | // Initialize rx/pcap streams 372 | uart->rx_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1); 373 | uart->pcap_stream = furi_stream_buffer_alloc(PCAP_BUF_SIZE, 1); 374 | 375 | if(!uart->rx_stream || !uart->pcap_stream) { 376 | FURI_LOG_E("UART", "Failed to allocate stream buffers"); 377 | uart_free(uart); 378 | return NULL; 379 | } 380 | 381 | // Set callbacks 382 | uart->handle_rx_data_cb = handle_uart_rx_data; 383 | uart->handle_rx_pcap_cb = uart_storage_rx_callback; 384 | 385 | // Initialize thread 386 | uart->rx_thread = furi_thread_alloc(); 387 | if(uart->rx_thread) { 388 | furi_thread_set_name(uart->rx_thread, "UART_Receive"); 389 | furi_thread_set_stack_size(uart->rx_thread, 2048); 390 | furi_thread_set_context(uart->rx_thread, uart); 391 | furi_thread_set_callback(uart->rx_thread, uart_worker); 392 | furi_thread_start(uart->rx_thread); 393 | } else { 394 | FURI_LOG_E("UART", "Failed to allocate rx thread"); 395 | uart_free(uart); 396 | return NULL; 397 | } 398 | 399 | // Initialize storage 400 | uart->storageContext = uart_storage_init(uart); 401 | if(!uart->storageContext) { 402 | FURI_LOG_E("UART", "Failed to initialize storage"); 403 | uart_free(uart); 404 | return NULL; 405 | } 406 | 407 | // Initialize serial with firmware-aware channel selection 408 | FuriHalSerialId uart_channel; 409 | if(has_momentum_features()) { 410 | uart_channel = UART_CH_ESP; 411 | } else { 412 | uart_channel = FuriHalSerialIdUsart; 413 | } 414 | 415 | uart->serial_handle = furi_hal_serial_control_acquire(uart_channel); 416 | if(uart->serial_handle) { 417 | furi_hal_serial_init(uart->serial_handle, 115200); 418 | uart->is_serial_active = true; 419 | furi_hal_serial_async_rx_start(uart->serial_handle, uart_rx_callback, uart, false); 420 | } else { 421 | FURI_LOG_E("UART", "Failed to acquire serial handle"); 422 | uart_free(uart); 423 | return NULL; 424 | } 425 | 426 | // Initialize text manager 427 | uart->text_manager = text_buffer_alloc(); 428 | if(!uart->text_manager) { 429 | FURI_LOG_E("UART", "Failed to allocate text manager"); 430 | uart_free(uart); 431 | return NULL; 432 | } 433 | 434 | uint32_t duration = furi_get_tick() - start_time; 435 | FURI_LOG_I("UART", "UART initialization complete (Time taken: %lu ms)", duration); 436 | 437 | return uart; 438 | } 439 | 440 | void uart_free(UartContext *uart) { 441 | if(!uart) return; 442 | 443 | // Stop the worker thread 444 | if(uart->rx_thread) { 445 | furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtStop); 446 | furi_thread_join(uart->rx_thread); 447 | furi_thread_free(uart->rx_thread); 448 | uart->rx_thread = NULL; 449 | } 450 | 451 | // Clean up serial 452 | if(uart->serial_handle) { 453 | furi_hal_serial_async_rx_stop(uart->serial_handle); 454 | furi_hal_serial_deinit(uart->serial_handle); 455 | furi_hal_serial_control_release(uart->serial_handle); 456 | uart->serial_handle = NULL; 457 | } 458 | 459 | // Free streams 460 | if(uart->rx_stream) { 461 | furi_stream_buffer_free(uart->rx_stream); 462 | uart->rx_stream = NULL; 463 | } 464 | if(uart->pcap_stream) { 465 | furi_stream_buffer_free(uart->pcap_stream); 466 | uart->pcap_stream = NULL; 467 | } 468 | 469 | // Clean up storage context 470 | if(uart->storageContext) { 471 | uart_storage_free(uart->storageContext); 472 | uart->storageContext = NULL; 473 | } 474 | 475 | // Free text manager 476 | if(uart->text_manager) { 477 | text_buffer_free(uart->text_manager); 478 | uart->text_manager = NULL; 479 | } 480 | 481 | free(uart); 482 | } 483 | 484 | 485 | // Stop the UART thread (typically when exiting) 486 | void uart_stop_thread(UartContext *uart) 487 | { 488 | if (uart && uart->rx_thread) 489 | { 490 | furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtStop); 491 | } 492 | } 493 | 494 | // Send data over UART 495 | void uart_send(UartContext *uart, const uint8_t *data, size_t len) { 496 | if(!uart || !uart->serial_handle || !uart->is_serial_active || !data || len == 0) { 497 | return; 498 | } 499 | 500 | // Send data directly without mutex lock for basic commands 501 | furi_hal_serial_tx(uart->serial_handle, data, len); 502 | 503 | // Small delay to ensure transmission 504 | furi_delay_ms(5); 505 | } 506 | 507 | 508 | bool uart_is_esp_connected(UartContext* uart) { 509 | FURI_LOG_D("UART", "Checking ESP connection..."); 510 | 511 | if(!uart || !uart->serial_handle || !uart->text_manager) { 512 | FURI_LOG_E("UART", "Invalid UART context"); 513 | return false; 514 | } 515 | 516 | // Check if ESP check is disabled 517 | if(uart->state && uart->state->settings.disable_esp_check_index) { 518 | FURI_LOG_D("UART", "ESP connection check disabled by setting"); 519 | return true; 520 | } 521 | 522 | // Temporarily disable callbacks 523 | furi_hal_serial_async_rx_stop(uart->serial_handle); 524 | 525 | // Clear and reset buffers atomically 526 | furi_mutex_acquire(uart->text_manager->mutex, FuriWaitForever); 527 | memset(uart->text_manager->ring_buffer, 0, RING_BUFFER_SIZE); 528 | memset(uart->text_manager->view_buffer, 0, VIEW_BUFFER_SIZE); 529 | uart->text_manager->ring_read_index = 0; 530 | uart->text_manager->ring_write_index = 0; 531 | uart->text_manager->buffer_full = false; 532 | uart->text_manager->view_buffer_len = 0; 533 | furi_mutex_release(uart->text_manager->mutex); 534 | 535 | // Re-enable callbacks with clean state 536 | furi_hal_serial_async_rx_start(uart->serial_handle, uart_rx_callback, uart, false); 537 | 538 | // Quick flush 539 | furi_hal_serial_tx(uart->serial_handle, (uint8_t*)"\r\n", 2); 540 | furi_delay_ms(50); 541 | 542 | const char* test_commands[] = { 543 | "stop\n", // Try stop command first 544 | "AT\r\n", // AT command as backup 545 | }; 546 | bool connected = false; 547 | const uint32_t CMD_TIMEOUT_MS = 250; // Shorter timeout per command 548 | 549 | for(uint8_t cmd_idx = 0; cmd_idx < sizeof(test_commands)/sizeof(test_commands[0]) && !connected; cmd_idx++) { 550 | // Send test command 551 | uart_send(uart, (uint8_t*)test_commands[cmd_idx], strlen(test_commands[cmd_idx])); 552 | FURI_LOG_D("UART", "Sent command: %s", test_commands[cmd_idx]); 553 | 554 | uint32_t start_time = furi_get_tick(); 555 | while(furi_get_tick() - start_time < CMD_TIMEOUT_MS) { 556 | furi_mutex_acquire(uart->text_manager->mutex, FuriWaitForever); 557 | 558 | size_t available = uart->text_manager->buffer_full ? RING_BUFFER_SIZE : 559 | (uart->text_manager->ring_write_index >= uart->text_manager->ring_read_index) ? 560 | uart->text_manager->ring_write_index - uart->text_manager->ring_read_index : 561 | RING_BUFFER_SIZE - uart->text_manager->ring_read_index + uart->text_manager->ring_write_index; 562 | 563 | if(available > 0) { 564 | connected = true; 565 | FURI_LOG_D("UART", "Received %d bytes response", available); 566 | } 567 | 568 | furi_mutex_release(uart->text_manager->mutex); 569 | 570 | if(connected) break; 571 | furi_delay_ms(5); // Shorter sleep interval 572 | } 573 | } 574 | 575 | FURI_LOG_I("UART", "ESP connection check: %s", connected ? "Success" : "Failed"); 576 | return connected; 577 | } 578 | 579 | bool uart_receive_data( 580 | UartContext* uart, 581 | ViewDispatcher* view_dispatcher, 582 | AppState* state, 583 | const char* prefix, 584 | const char* extension, 585 | const char* TargetFolder) { 586 | 587 | if(!uart || !uart->storageContext || !view_dispatcher || !state) { 588 | FURI_LOG_E("UART", "Invalid parameters to uart_receive_data"); 589 | return false; 590 | } 591 | 592 | // Close any existing file 593 | if(uart->storageContext->HasOpenedFile) { 594 | storage_file_sync(uart->storageContext->current_file); 595 | storage_file_close(uart->storageContext->current_file); 596 | uart->storageContext->HasOpenedFile = false; 597 | } 598 | 599 | uart->pcap = false; // Reset capture state 600 | furi_stream_buffer_reset(uart->pcap_stream); 601 | 602 | // Clear display before switching view 603 | text_box_set_text(state->text_box, ""); 604 | text_box_set_focus(state->text_box, TextBoxFocusEnd); 605 | 606 | // Open new file if needed 607 | if(prefix && extension && TargetFolder && strlen(prefix) > 1) { 608 | uart->storageContext->HasOpenedFile = sequential_file_open( 609 | uart->storageContext->storage_api, 610 | uart->storageContext->current_file, 611 | TargetFolder, 612 | prefix, 613 | extension); 614 | 615 | if(!uart->storageContext->HasOpenedFile) { 616 | FURI_LOG_E("UART", "Failed to open file"); 617 | return false; 618 | } 619 | } 620 | 621 | // Set the view state before switching 622 | state->previous_view = state->current_view; 623 | state->current_view = 5; 624 | 625 | // Process any pending events before view switch 626 | furi_delay_ms(5); 627 | 628 | view_dispatcher_switch_to_view(view_dispatcher, 5); 629 | 630 | return true; 631 | } 632 | 633 | // 6675636B796F7564656B69 -------------------------------------------------------------------------------- /src/uart_utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UART_UTILS_H 2 | #define UART_UTILS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "menu.h" 12 | #include "uart_storage.h" 13 | #include 14 | #include "firmware_api.h" 15 | 16 | #ifdef HAS_MOMENTUM_SUPPORT 17 | #define UART_CH_ESP (momentum_settings.uart_esp_channel) 18 | #define UART_CH_GPS (momentum_settings.uart_nmea_channel) 19 | #else 20 | #define UART_CH_ESP FuriHalSerialIdUsart 21 | #define UART_CH_GPS FuriHalSerialIdLpuart 22 | #endif 23 | 24 | #define BAUDRATE (115200) 25 | 26 | #define TEXT_BOX_STORE_SIZE (4096) // 4KB text box buffer size 27 | #define RX_BUF_SIZE 2048 28 | #define PCAP_BUF_SIZE 4096 29 | #define STORAGE_BUF_SIZE 4096 30 | #define GHOST_ESP_APP_FOLDER "/ext/apps_data/ghost_esp" 31 | #define GHOST_ESP_APP_FOLDER_PCAPS "/ext/apps_data/ghost_esp/pcaps" 32 | #define GHOST_ESP_APP_FOLDER_WARDRIVE "/ext/apps_data/ghost_esp/wardrive" 33 | #define GHOST_ESP_APP_FOLDER_LOGS "/ext/apps_data/ghost_esp/logs" 34 | #define GHOST_ESP_APP_SETTINGS_FILE "/ext/apps_data/ghost_esp/settings.ini" 35 | #define ESP_CHECK_TIMEOUT_MS 100 36 | #define VIEW_BUFFER_SIZE (16 * 1024) // 16KB for view 37 | #define RING_BUFFER_SIZE (8 * 1024) // 8KB for incoming data 38 | #define PCAP_GLOBAL_HEADER_SIZE 24 39 | #define PCAP_PACKET_HEADER_SIZE 16 40 | #define PCAP_TEMP_BUFFER_SIZE 4096 41 | 42 | 43 | void update_text_box_view(AppState* state); 44 | void handle_uart_rx_data(uint8_t *buf, size_t len, void *context); 45 | 46 | typedef struct { 47 | char* ring_buffer; // Ring buffer for incoming data 48 | char* view_buffer; // Buffer for current view 49 | size_t ring_read_index; // Read position in ring buffer 50 | size_t ring_write_index; // Write position in ring buffer 51 | size_t view_buffer_len; // Length of current view content 52 | bool buffer_full; // Ring buffer state 53 | FuriMutex* mutex; // Synchronization mutex 54 | } TextBufferManager; 55 | 56 | typedef enum { 57 | WorkerEvtStop = (1 << 0), 58 | WorkerEvtRxDone = (1 << 1), 59 | WorkerEvtPcapDone = (1 << 2), 60 | WorkerEvtStorage = (1 << 3), 61 | } WorkerEvtFlags; 62 | 63 | typedef struct { 64 | char bssid[18]; 65 | char ssid[33]; 66 | double latitude; 67 | double longitude; 68 | int8_t rssi; 69 | uint8_t channel; 70 | char encryption_type[20]; 71 | int64_t timestamp; 72 | } wardriving_data_t; 73 | 74 | typedef struct UartContext { 75 | FuriHalSerialHandle* serial_handle; 76 | FuriHalSerialHandle* gps_handle; 77 | FuriStreamBuffer* gps_stream; 78 | FuriThread* rx_thread; 79 | FuriStreamBuffer* rx_stream; 80 | FuriStreamBuffer* pcap_stream; 81 | bool pcap; 82 | uint8_t mark_test_buf[11]; // Fixed size for markers 83 | uint8_t mark_test_idx; 84 | uint8_t rx_buf[RX_BUF_SIZE + 1]; // Add +1 for null termination 85 | void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context); 86 | void (*handle_rx_pcap_cb)(uint8_t* buf, size_t len, void* context); 87 | AppState* state; 88 | UartStorageContext* storageContext; 89 | bool is_serial_active; 90 | TextBufferManager* text_manager; 91 | uint8_t pcap_rx_buf[RX_BUF_SIZE]; // Buffer for PCAP data 92 | size_t pcap_buf_len; // Current PCAP buffer length 93 | } UartContext; 94 | 95 | 96 | 97 | // Function prototypes 98 | UartContext* uart_init(AppState* state); 99 | void uart_free(UartContext* uart); 100 | void uart_stop_thread(UartContext* uart); 101 | void uart_send(UartContext* uart, const uint8_t* data, size_t len); 102 | bool uart_is_marauder_firmware(UartContext* uart); 103 | bool uart_receive_data( 104 | UartContext* uart, 105 | ViewDispatcher* view_dispatcher, 106 | AppState* current_view, 107 | const char* prefix, 108 | const char* extension, 109 | const char* TargetFolder); 110 | bool uart_is_esp_connected(UartContext* uart); 111 | void uart_storage_reset_logs(UartStorageContext *ctx); 112 | void uart_storage_safe_cleanup(UartStorageContext* ctx); 113 | 114 | #endif 115 | 116 | // 6675636B796F7564656B69 -------------------------------------------------------------------------------- /src/utils.c: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | #include "app_state.h" 3 | #include 4 | #include 5 | #include // For EXT_PATH 6 | 7 | // Structure to hold confirmation dialog data 8 | typedef struct { 9 | const char* header; 10 | const char* text; 11 | ConfirmationViewCallback ok_callback; 12 | ConfirmationViewCallback cancel_callback; 13 | } ConfirmationDialogData; 14 | 15 | static ConfirmationDialogData current_dialog; 16 | 17 | void show_confirmation_dialog_ex( 18 | void* context, 19 | const char* header, 20 | const char* text, 21 | ConfirmationViewCallback ok_callback, 22 | ConfirmationViewCallback cancel_callback) { 23 | 24 | FURI_LOG_D("ConfDialog", "Starting dialog, context: %p", context); 25 | 26 | AppState* state = (AppState*)context; 27 | if(!state) { 28 | FURI_LOG_E("ConfDialog", "Null state"); 29 | return; 30 | } 31 | 32 | // Don't try to show dialog if we're already in confirmation view 33 | if(state->current_view == 7) { 34 | FURI_LOG_W("ConfDialog", "Already in confirmation view, ignoring"); 35 | return; 36 | } 37 | 38 | FURI_LOG_D("ConfDialog", "Previous view: %d, Current view: %d", 39 | state->previous_view, state->current_view); 40 | 41 | // Allocate new context 42 | SettingsConfirmContext* confirm_ctx = malloc(sizeof(SettingsConfirmContext)); 43 | if(!confirm_ctx) { 44 | FURI_LOG_E("ConfDialog", "Failed to allocate context"); 45 | return; 46 | } 47 | 48 | confirm_ctx->state = state; 49 | 50 | // Set up the confirmation dialog 51 | confirmation_view_set_header(state->confirmation_view, header); 52 | confirmation_view_set_text(state->confirmation_view, text); 53 | confirmation_view_set_ok_callback(state->confirmation_view, ok_callback, confirm_ctx); 54 | confirmation_view_set_cancel_callback(state->confirmation_view, cancel_callback, confirm_ctx); 55 | 56 | FURI_LOG_D("ConfDialog", "Set callbacks - OK: %p, Cancel: %p", ok_callback, cancel_callback); 57 | 58 | // Save current view before switching 59 | state->previous_view = state->current_view; 60 | FURI_LOG_D("ConfDialog", "Saved previous view: %d", state->previous_view); 61 | 62 | // Switch to confirmation view 63 | view_dispatcher_switch_to_view(state->view_dispatcher, 7); 64 | state->current_view = 7; 65 | FURI_LOG_D("ConfDialog", "Switched to confirmation view"); 66 | } 67 | 68 | void show_confirmation_view_wrapper(void* context, ConfirmationView* view) { 69 | (void)view; // Mark parameter as unused to avoid compiler warning 70 | // Redirect to show_confirmation_dialog_ex with the stored dialog data 71 | show_confirmation_dialog_ex(context, current_dialog.header, current_dialog.text, current_dialog.ok_callback, current_dialog.cancel_callback); 72 | } 73 | 74 | // 6675636B796F7564656B69 -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | // utils.h 2 | #pragma once 3 | #include "app_state.h" 4 | #include "confirmation_view.h" 5 | 6 | // Function declarations 7 | void show_confirmation_dialog_ex( 8 | void* context, 9 | const char* header, 10 | const char* text, 11 | ConfirmationViewCallback ok_callback, 12 | ConfirmationViewCallback cancel_callback); 13 | 14 | void show_confirmation_view_wrapper(void* context, ConfirmationView* view); // Added declaration 15 | 16 | void clear_pcap_files(void* context); 17 | void clear_wardrive_files(void* context); 18 | --------------------------------------------------------------------------------