From charlesreid1

Requirements

Required Features:

  • Monitor Mode: Initializes ESP32 in station mode for passive scanning
  • Channel Scanning: Can scan specific channels or perform full spectrum sweep (1-13)
  • Beacon Parsing: Extracts SSID, BSSID, RSSI, encryption type, and channel info
  • Thread Safety: Uses mutex protection for concurrent access
  • Memory Management: Handles AP storage with configurable limits

Core Functions:

  • wifi_scanner_init() - Initialize the global scanner
  • wifi_scanner_full_scan() - Scan all channels and collect APs
  • wifi_scanner_get_results() - Retrieve discovered APs thread-safely
  • wifi_scanner_encryption_str() - Convert encryption enum to readable string

Data Structure:

  • The ap_info_t structure captures all necessary AP information including handling of hidden networks and timestamp tracking for freshness analysis.

Integration Points:

  • Uses Flipper Zero's FuriOS threading primitives
  • ESP32 WiFi APIs for hardware interface
  • Memory-efficient circular buffer approach for resource constraints


Version 1

#include <furi.h>
#include <furi_hal.h>
#include <gui/gui.h>
#include <notification/notification_messages.h>
#include <esp_wifi.h>
#include <esp_event.h>
#include <string.h>

// Maximum number of APs we can track
#define MAX_APS 50
#define SSID_MAX_LEN 32
#define SCAN_TIMEOUT_MS 5000
#define CHANNEL_DWELL_TIME_MS 200

// WiFi encryption types
typedef enum {
    WIFI_ENC_OPEN = 0,
    WIFI_ENC_WEP,
    WIFI_ENC_WPA,
    WIFI_ENC_WPA2,
    WIFI_ENC_WPA3,
    WIFI_ENC_UNKNOWN
} wifi_encryption_t;

// AP information structure
typedef struct {
    char ssid[SSID_MAX_LEN + 1];
    uint8_t bssid[6];
    int8_t rssi;
    wifi_encryption_t encryption;
    uint8_t channel;
    uint32_t timestamp;
    bool is_hidden;
} ap_info_t;

// Scanner context
typedef struct {
    ap_info_t* aps;
    uint16_t ap_count;
    uint16_t max_aps;
    uint8_t current_channel;
    bool scanning;
    FuriMutex* mutex;
} wifi_scanner_t;

// Global scanner instance
static wifi_scanner_t* scanner = NULL;

// Convert ESP WiFi auth mode to our encryption enum
static wifi_encryption_t get_encryption_type(wifi_auth_mode_t auth_mode) {
    switch(auth_mode) {
        case WIFI_AUTH_OPEN:
            return WIFI_ENC_OPEN;
        case WIFI_AUTH_WEP:
            return WIFI_ENC_WEP;
        case WIFI_AUTH_WPA_PSK:
        case WIFI_AUTH_WPA_WPA2_PSK:
            return WIFI_ENC_WPA;
        case WIFI_AUTH_WPA2_PSK:
            return WIFI_ENC_WPA2;
        case WIFI_AUTH_WPA3_PSK:
            return WIFI_ENC_WPA3;
        default:
            return WIFI_ENC_UNKNOWN;
    }
}

// Beacon frame parsing callback
static void wifi_scan_callback(void* arg, wifi_event_base_t event_base, int32_t event_id, void* event_data) {
    if(!scanner || !scanner->scanning) return;
    
    if(event_id == WIFI_EVENT_SCAN_DONE) {
        wifi_scan_config_t scan_config = {0};
        uint16_t ap_count = 0;
        
        // Get scan results count
        esp_wifi_scan_get_ap_num(&ap_count);
        
        if(ap_count > 0) {
            wifi_ap_record_t* ap_records = malloc(sizeof(wifi_ap_record_t) * ap_count);
            if(ap_records) {
                // Get actual scan results
                esp_wifi_scan_get_ap_records(&ap_count, ap_records);
                
                furi_mutex_acquire(scanner->mutex, FuriWaitForever);
                
                // Process each discovered AP
                for(uint16_t i = 0; i < ap_count && scanner->ap_count < scanner->max_aps; i++) {
                    wifi_ap_record_t* record = &ap_records[i];
                    
                    // Check if we already have this AP (by BSSID)
                    bool found = false;
                    for(uint16_t j = 0; j < scanner->ap_count; j++) {
                        if(memcmp(scanner->aps[j].bssid, record->bssid, 6) == 0) {
                            // Update existing entry with latest info
                            scanner->aps[j].rssi = record->rssi;
                            scanner->aps[j].timestamp = furi_get_tick();
                            found = true;
                            break;
                        }
                    }
                    
                    if(!found) {
                        // Add new AP
                        ap_info_t* ap = &scanner->aps[scanner->ap_count];
                        
                        // Copy SSID (handle hidden networks)
                        if(record->ssid[0] == '\0') {
                            snprintf(ap->ssid, sizeof(ap->ssid), "<Hidden>");
                            ap->is_hidden = true;
                        } else {
                            strncpy(ap->ssid, (char*)record->ssid, SSID_MAX_LEN);
                            ap->ssid[SSID_MAX_LEN] = '\0';
                            ap->is_hidden = false;
                        }
                        
                        // Copy BSSID
                        memcpy(ap->bssid, record->bssid, 6);
                        
                        // Set other fields
                        ap->rssi = record->rssi;
                        ap->encryption = get_encryption_type(record->authmode);
                        ap->channel = record->primary;
                        ap->timestamp = furi_get_tick();
                        
                        scanner->ap_count++;
                    }
                }
                
                furi_mutex_release(scanner->mutex);
                free(ap_records);
            }
        }
    }
}

// Initialize the WiFi scanner
wifi_scanner_t* wifi_scanner_alloc(uint16_t max_aps) {
    wifi_scanner_t* scanner = malloc(sizeof(wifi_scanner_t));
    if(!scanner) return NULL;
    
    scanner->aps = malloc(sizeof(ap_info_t) * max_aps);
    if(!scanner->aps) {
        free(scanner);
        return NULL;
    }
    
    scanner->max_aps = max_aps;
    scanner->ap_count = 0;
    scanner->current_channel = 1;
    scanner->scanning = false;
    scanner->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
    
    if(!scanner->mutex) {
        free(scanner->aps);
        free(scanner);
        return NULL;
    }
    
    // Initialize ESP32 WiFi
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    esp_wifi_set_mode(WIFI_MODE_STA);
    
    // Register event handler
    esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_scan_callback, NULL);
    
    return scanner;
}

// Free the scanner resources
void wifi_scanner_free(wifi_scanner_t* scanner_instance) {
    if(!scanner_instance) return;
    
    scanner_instance->scanning = false;
    esp_wifi_stop();
    esp_wifi_deinit();
    
    if(scanner_instance->mutex) {
        furi_mutex_free(scanner_instance->mutex);
    }
    
    if(scanner_instance->aps) {
        free(scanner_instance->aps);
    }
    
    free(scanner_instance);
}

// Start scanning on specific channel
bool wifi_scanner_start_channel_scan(wifi_scanner_t* scanner_instance, uint8_t channel) {
    if(!scanner_instance || channel < 1 || channel > 13) return false;
    
    scanner_instance->current_channel = channel;
    scanner_instance->scanning = true;
    
    // Configure scan parameters
    wifi_scan_config_t scan_config = {
        .ssid = NULL,           // Scan all SSIDs
        .bssid = NULL,          // Scan all BSSIDs  
        .channel = channel,     // Specific channel
        .show_hidden = true,    // Include hidden networks
        .scan_type = WIFI_SCAN_TYPE_ACTIVE,
        .scan_time = {
            .active = {
                .min = 100,     // Minimum scan time per channel
                .max = 300      // Maximum scan time per channel
            }
        }
    };
    
    esp_err_t ret = esp_wifi_start();
    if(ret != ESP_OK) return false;
    
    ret = esp_wifi_scan_start(&scan_config, false);
    return (ret == ESP_OK);
}

// Perform full spectrum scan (all channels)
bool wifi_scanner_full_scan(wifi_scanner_t* scanner_instance) {
    if(!scanner_instance) return false;
    
    // Clear existing results
    furi_mutex_acquire(scanner_instance->mutex, FuriWaitForever);
    scanner_instance->ap_count = 0;
    furi_mutex_release(scanner_instance->mutex);
    
    // Scan each channel sequentially
    for(uint8_t channel = 1; channel <= 13; channel++) {
        if(!wifi_scanner_start_channel_scan(scanner_instance, channel)) {
            return false;
        }
        
        // Wait for scan to complete
        furi_delay_ms(CHANNEL_DWELL_TIME_MS);
    }
    
    return true;
}

// Get current AP list (thread-safe)
uint16_t wifi_scanner_get_results(wifi_scanner_t* scanner_instance, ap_info_t* results, uint16_t max_results) {
    if(!scanner_instance || !results) return 0;
    
    furi_mutex_acquire(scanner_instance->mutex, FuriWaitForever);
    
    uint16_t count = (scanner_instance->ap_count < max_results) ? scanner_instance->ap_count : max_results;
    
    if(count > 0) {
        memcpy(results, scanner_instance->aps, sizeof(ap_info_t) * count);
    }
    
    furi_mutex_release(scanner_instance->mutex);
    
    return count;
}

// Get encryption type as string
const char* wifi_scanner_encryption_str(wifi_encryption_t enc) {
    switch(enc) {
        case WIFI_ENC_OPEN: return "Open";
        case WIFI_ENC_WEP: return "WEP";
        case WIFI_ENC_WPA: return "WPA";
        case WIFI_ENC_WPA2: return "WPA2";
        case WIFI_ENC_WPA3: return "WPA3";
        default: return "Unknown";
    }
}

// Check if scanner is currently active
bool wifi_scanner_is_scanning(wifi_scanner_t* scanner_instance) {
    return scanner_instance ? scanner_instance->scanning : false;
}

// Stop current scan
void wifi_scanner_stop(wifi_scanner_t* scanner_instance) {
    if(!scanner_instance) return;
    
    scanner_instance->scanning = false;
    esp_wifi_scan_stop();
}

// Initialize global scanner instance
bool wifi_scanner_init(void) {
    if(scanner) return true;  // Already initialized
    
    scanner = wifi_scanner_alloc(MAX_APS);
    return (scanner != NULL);
}

// Cleanup global scanner
void wifi_scanner_deinit(void) {
    if(scanner) {
        wifi_scanner_free(scanner);
        scanner = NULL;
    }
}

Version 2

scanner.h

#ifndef SCANNER_H
#define SCANNER_H

#include <furi.h>
#include <stdint.h>

// Define constants for AP data
#define MAX_SSID_LEN 32
#define MAX_APS_TO_STORE 50

/**
 * @brief Enum for WiFi encryption types.
 */
typedef enum {
    WifiEncryptionTypeOpen,
    WifiEncryptionTypeWep,
    WifiEncryptionTypeWpa,
    WifiEncryptionTypeWpa2,
    WifiEncryptionTypeWpa3,
    WifiEncryptionTypeUnknown,
} WifiEncryptionType;

/**
 * @brief Structure to hold all relevant information for a single Access Point.
 */
typedef struct {
    char ssid[MAX_SSID_LEN + 1];    // Network name
    uint8_t bssid[6];               // MAC address
    int8_t rssi;                    // Signal strength
    WifiEncryptionType encryption;  // Security protocol
    uint8_t channel;                // WiFi channel
    uint32_t timestamp;             // Furi tick at time of last sighting
} ap_info_t;

/**
 * @brief Main scanner state structure.
 */
typedef struct {
    ap_info_t* ap_list;       // Dynamically allocated array of found APs
    uint16_t count;           // Current number of unique APs found
    uint16_t capacity;        // Maximum number of APs to store
    FuriMutex* mutex;         // Mutex for thread-safe access to the ap_list
} WifiScanner;

/**
 * @brief Allocates and initializes a new WifiScanner instance.
 * @param capacity The maximum number of APs to store.
 * @return Pointer to the newly created WifiScanner.
 */
WifiScanner* wifi_scanner_alloc(uint16_t capacity);

/**
 * @brief Frees all resources associated with a WifiScanner instance.
 * @param scanner Pointer to the WifiScanner instance to free.
 */
void wifi_scanner_free(WifiScanner* scanner);

/**
 * @brief Initializes the ESP32 WiFi hardware. Must be called once at app startup.
 */
void wifi_scanner_init_hardware();

/**
 * @brief De-initializes the ESP32 WiFi hardware. Call when the app is closing.
 */
void wifi_scanner_deinit_hardware();

/**
 * @brief Performs a blocking scan across all 2.4GHz WiFi channels (1-13).
 * This function enables promiscuous mode, hops through channels, listens
 * for beacons, and then disables promiscuous mode.
 * @param scanner Pointer to the WifiScanner instance.
 */
void wifi_scanner_full_scan(WifiScanner* scanner);

/**
 * @brief Thread-safely retrieves the list of discovered APs.
 * @param scanner Pointer to the WifiScanner instance.
 * @param results_buffer A user-provided buffer to copy the results into.
 * @param buffer_size The size of the results_buffer.
 * @return The number of APs copied into the buffer.
 */
uint16_t wifi_scanner_get_results(WifiScanner* scanner, ap_info_t* results_buffer, uint16_t buffer_size);

/**
 * @brief Converts a WifiEncryptionType enum to a human-readable string.
 * @param encryption_type The enum value to convert.
 * @return A constant string representation (e.g., "WPA2").
 */
const char* wifi_scanner_encryption_str(WifiEncryptionType encryption_type);

#endif // SCANNER_H

scanner.c

#include "scanner.h"
#include <furi_hal.h>
#include <string.h>

// A global pointer to the scanner instance is needed for the static callback function.
static WifiScanner* g_scanner_instance = NULL;

// --- Forward Declarations for internal functions ---
static void wifi_promiscuous_rx_cb(void* buf, wifi_promiscuous_pkt_type_t type);
static void parse_beacon_frame(const wifi_promiscuous_pkt_t* pkt);
static WifiEncryptionType get_encryption_from_beacon(const uint8_t* frame_body, uint16_t body_len);
static void scanner_add_or_update_ap(WifiScanner* scanner, const ap_info_t* new_ap);

// --- Public Function Implementations ---

void wifi_scanner_init_hardware() {
    // furi_hal_wifi_init() is deprecated in recent firmwares. 
    // The wifi stack is now managed automatically.
    // We ensure the wifi module is powered on for our operations.
    furi_hal_wifi_acquire();
}

void wifi_scanner_deinit_hardware() {
    furi_hal_wifi_release();
}

WifiScanner* wifi_scanner_alloc(uint16_t capacity) {
    WifiScanner* scanner = malloc(sizeof(WifiScanner));
    furi_check(scanner != NULL);

    scanner->ap_list = malloc(sizeof(ap_info_t) * capacity);
    furi_check(scanner->ap_list != NULL);

    scanner->capacity = capacity;
    scanner->count = 0;
    scanner->mutex = furi_mutex_alloc(FuriMutexTypeRecursive);
    
    return scanner;
}

void wifi_scanner_free(WifiScanner* scanner) {
    if (!scanner) return;
    furi_mutex_free(scanner->mutex);
    free(scanner->ap_list);
    free(scanner);
    if(g_scanner_instance == scanner) {
        g_scanner_instance = NULL;
    }
}

void wifi_scanner_full_scan(WifiScanner* scanner) {
    furi_check(scanner != NULL);
    
    // Set global instance so the callback can access our scanner's state
    g_scanner_instance = scanner;

    // Clear previous results before starting a new scan
    furi_mutex_acquire(scanner->mutex, FuriWaitForever);
    scanner->count = 0;
    furi_mutex_release(scanner->mutex);
    
    // Set the callback and start promiscuous mode
    furi_hal_wifi_set_promiscuous_rx_callback(wifi_promiscuous_rx_cb);
    furi_hal_wifi_start_promiscuous_rx();

    // Sequentially hop through all 13 channels
    for (uint8_t ch = 1; ch <= 13; ch++) {
        furi_hal_wifi_set_channel(ch);
        // Wait on each channel to capture broadcasted beacon frames
        furi_delay_ms(250); 
    }

    // Stop listening and unregister the callback
    furi_hal_wifi_stop_promiscuous_rx();
    furi_hal_wifi_set_promiscuous_rx_callback(NULL);
    g_scanner_instance = NULL;
}

uint16_t wifi_scanner_get_results(WifiScanner* scanner, ap_info_t* results_buffer, uint16_t buffer_size) {
    furi_check(scanner != NULL && results_buffer != NULL);
    
    if(furi_mutex_acquire(scanner->mutex, FuriWaitForever) != FuriStatusOk) {
        return 0;
    }

    uint16_t count_to_copy = (scanner->count < buffer_size) ? scanner->count : buffer_size;
    if (count_to_copy > 0) {
        memcpy(results_buffer, scanner->ap_list, count_to_copy * sizeof(ap_info_t));
    }

    furi_mutex_release(scanner->mutex);
    return count_to_copy;
}

const char* wifi_scanner_encryption_str(WifiEncryptionType encryption_type) {
    switch(encryption_type) {
        case WifiEncryptionTypeOpen: return "Open";
        case WifiEncryptionTypeWep:  return "WEP";
        case WifiEncryptionTypeWpa:  return "WPA";
        case WifiEncryptionTypeWpa2: return "WPA2";
        case WifiEncryptionTypeWpa3: return "WPA3";
        default:                     return "Unknown";
    }
}


// --- Internal (Static) Function Implementations ---

/**
 * @brief The core callback function that processes incoming WiFi packets.
 */
static void wifi_promiscuous_rx_cb(void* buf, wifi_promiscuous_pkt_type_t type) {
    // Ensure we have a scanner instance to work with and we are interested in this packet type
    if (!g_scanner_instance || type != WIFI_PKT_MGMT) {
        return;
    }

    wifi_promiscuous_pkt_t* pkt = (wifi_promiscuous_pkt_t*)buf;
    const uint8_t* payload = pkt->payload;
    
    // Check if it's a beacon frame (Type 0, Subtype 8)
    // Frame Control field is the first byte. Subtype is bits 4-7. Type is bits 2-3.
    // 0b10000000 means Type: Mgmt (00), Subtype: Beacon (1000)
    if ((payload[0] & 0b11111100) == 0b10000000) {
        parse_beacon_frame(pkt);
    }
}

/**
 * @brief Parses the raw beacon frame to extract AP details.
 * @note This is a simplified parser. 802.11 frames can be very complex.
 */
static void parse_beacon_frame(const wifi_promiscuous_pkt_t* pkt) {
    ap_info_t ap = {0};
    const uint8_t* payload = pkt->payload;

    ap.rssi = pkt->rx_ctrl.rssi;
    ap.channel = pkt->rx_ctrl.channel;
    ap.timestamp = furi_get_tick();

    // BSSID is the "transmitter address" in a beacon frame (offset 10)
    memcpy(ap.bssid, payload + 10, 6);

    // The beacon body (tagged parameters) starts after the fixed header (24 bytes) and fixed parameters (12 bytes)
    const uint8_t* body = payload + 36;
    const uint8_t* end = payload + pkt->rx_ctrl.sig_len;

    // Iterate through tagged parameters (Element ID, Length, Value)
    const uint8_t* pos = body;
    while (pos < end - 1) { // -1 to ensure we can read EID and Length
        uint8_t eid = pos[0];
        uint8_t len = pos[1];

        // Ensure the tag length doesn't exceed packet bounds
        if (pos + 2 + len > end) break;

        if (eid == 0) { // Tag 0: SSID
            uint8_t ssid_len = (len > MAX_SSID_LEN) ? MAX_SSID_LEN : len;
            memcpy(ap.ssid, pos + 2, ssid_len);
            ap.ssid[ssid_len] = '\0';
            // Handle hidden SSIDs where length is 0 or contains null bytes
            if(ssid_len == 0 || ap.ssid[0] == '\0') {
                strcpy(ap.ssid, "[Hidden SSID]");
            }
        }
        
        pos += 2 + len;
    }

    // Determine encryption by parsing the entire beacon body for security tags
    ap.encryption = get_encryption_from_beacon(body, end - body);

    scanner_add_or_update_ap(g_scanner_instance, &ap);
}

/**
 * @brief Adds a new AP to the list or updates an existing one.
 */
static void scanner_add_or_update_ap(WifiScanner* scanner, const ap_info_t* new_ap) {
    if(furi_mutex_acquire(scanner->mutex, FuriWaitForever) != FuriStatusOk) return;

    // Check if this BSSID has been seen before
    bool found = false;
    for (uint16_t i = 0; i < scanner->count; i++) {
        if (memcmp(scanner->ap_list[i].bssid, new_ap->bssid, 6) == 0) {
            // Found: update RSSI and timestamp
            scanner->ap_list[i].rssi = new_ap->rssi;
            scanner->ap_list[i].timestamp = new_ap->timestamp;
            found = true;
            break;
        }
    }

    // Not found: add it to the list if there is space
    if (!found && scanner->count < scanner->capacity) {
        memcpy(&scanner->ap_list[scanner->count], new_ap, sizeof(ap_info_t));
        scanner->count++;
    }

    furi_mutex_release(scanner->mutex);
}

/**
 * @brief Scans the tagged parameters of a beacon for security information.
 * @return The highest level of encryption found.
 */
static WifiEncryptionType get_encryption_from_beacon(const uint8_t* body_start, uint16_t body_len) {
    bool wpa = false, wpa2 = false, wpa3 = false;
    
    // Check capabilities field (2 bytes) for the privacy bit (indicates WEP)
    const uint8_t* header_start = body_start - 12; // Go back to start of fixed params
    uint16_t capabilities = (header_start[1] << 8) | header_start[0];
    bool privacy_bit = (capabilities & 0x0010) != 0;

    const uint8_t* pos = body_start;
    const uint8_t* end = body_start + body_len;

    while (pos + 2 <= end) {
        uint8_t eid = pos[0];
        uint8_t len = pos[1];

        if (pos + 2 + len > end) break; // Avoid buffer overflow

        if (eid == 48) { // Tag 48: RSN (WPA2/WPA3)
            wpa2 = true;
            // Simple check for WPA3: look for AKM suite for SAE (00-0F-AC:8)
            if (len > 8 && pos[8] == 0x00 && pos[9] == 0x0F && pos[10] == 0xAC && pos[11] == 0x08) {
                wpa3 = true;
            }
        } else if (eid == 221) { // Tag 221: Vendor Specific (WPA1)
            // Check for Microsoft OUI for WPA1 (00-50-F2:1)
            if (len > 4 && pos[2] == 0x00 && pos[3] == 0x50 && pos[4] == 0xF2 && pos[5] == 0x01) {
                wpa = true;
            }
        }
        
        pos += 2 + len;
    }

    if (wpa3) return WifiEncryptionTypeWpa3;
    if (wpa2) return WifiEncryptionTypeWpa2;
    if (wpa) return WifiEncryptionTypeWpa;
    if (privacy_bit) return WifiEncryptionTypeWep;
    
    return WifiEncryptionTypeOpen;
}