From charlesreid1

Requirements

  • Define the data structure for a baselined AP and the public functions for managing the baseline database (baseline_ap_t struct) that is slightly different from the scanner's ap_info_t struct)
  • Include fields for tracking the observed signal strength range and the last time the AP was seen (for aging out old data and detecting signal anomalies later)
  • Handle storage limitations on Flipper Zero by using SD card for storage:
    • File Format: Use a smple binary file (short header containing a "magic number" to identify the file type and version number, followed by the raw array of baseline_ap_t structs). Very efficient for reading and writing.
    • Dirty Flag: The dirty flag is an optimization. The baseline is only written to the SD card if baseline_manager_save is called and the data in memory has actually changed. This reduces wear on the SD card.
    • Data Aging: The baseline_manager_prune_old_entries function provides a mechanism to keep the baseline relevant by removing APs that are no longer in range (e.g., from a location you visited once).

Version 1

baseline.h

#ifndef BASELINE_H
#define BASELINE_H

#include "scanner.h" // We need the ap_info_t definition
#include <furi.h>
#include <storage/storage.h>

// Define where the baseline database is stored on the SD card
#define BASELINE_FILE_PATH EXT_PATH("apps_data/rogue_ap/legitimate_aps.dat")
#define BASELINE_MAX_APS 100 // Maximum APs to store in the baseline

/**
 * @brief Structure to hold persistent information for a single legitimate AP.
 */
typedef struct {
    uint8_t bssid[6];               // MAC address (Primary Key)
    char ssid[MAX_SSID_LEN + 1];    // Last known SSID
    WifiEncryptionType encryption;  // Expected encryption
    
    // Fields for anomaly detection and data aging
    int8_t rssi_min;                // Weakest signal seen
    int8_t rssi_max;                // Strongest signal seen
    uint32_t last_seen_timestamp;   // RTC timestamp of last sighting
} baseline_ap_t;

/**
 * @brief Main baseline manager state structure.
 */
typedef struct {
    baseline_ap_t* baseline_aps; // In-memory array of legitimate APs
    uint16_t count;              // Current number of APs in the baseline
    uint16_t capacity;           // Max capacity of the in-memory array
    FuriMutex* mutex;            // Mutex for thread-safe access
    bool dirty;                  // Flag to track if the in-memory version has changed
} BaselineManager;

/**
 * @brief Allocates and initializes a new BaselineManager instance.
 * @param capacity The maximum number of baseline APs to hold in memory.
 * @return Pointer to the newly created BaselineManager.
 */
BaselineManager* baseline_manager_alloc(uint16_t capacity);

/**
 * @brief Frees all resources associated with a BaselineManager instance.
 * @param manager Pointer to the BaselineManager instance to free.
 */
void baseline_manager_free(BaselineManager* manager);

/**
 * @brief Loads the baseline database from the SD card into memory.
 * @param manager Pointer to the BaselineManager instance.
 * @return True on success or if no file exists, False on a read error.
 */
bool baseline_manager_load(BaselineManager* manager);

/**
 * @brief Saves the in-memory baseline to the SD card, if it has changed.
 * @param manager Pointer to the BaselineManager instance.
 * @return True on success, False on a write error.
 */
bool baseline_manager_save(BaselineManager* manager);

/**
 * @brief Updates the baseline with a newly scanned AP.
 * If the AP (by BSSID) is already in the baseline, its signal range and timestamp are updated.
 * If it's a new AP, it's added to the baseline. This is the core "learning" function.
 * @param manager Pointer to the BaselineManager instance.
 * @param ap_info Pointer to the AP information from the scanner.
 * @return True if the baseline was updated, False if it was full.
 */
bool baseline_manager_update(BaselineManager* manager, const ap_info_t* ap_info);

/**
 * @brief Removes entries from the baseline that haven't been seen in a long time.
 * @param manager Pointer to the BaselineManager instance.
 * @param timeout_seconds The maximum age in seconds for an entry to be kept.
 * @return The number of entries that were pruned.
 */
uint16_t baseline_manager_prune_old_entries(BaselineManager* manager, uint32_t timeout_seconds);

#endif // BASELINE_H

baseline.c

#include "baseline.h"
#include <furi_hal_rtc.h>
#include <flipper_format/flipper_format.h> // For file operations
#include <toolbox/dir_walk.h> // To ensure directory exists

// Header for our binary save file
typedef struct {
    char magic[7]; // "RGFAP_B"
    uint8_t version;
    uint16_t count;
} BaselineFileHeader;

// --- Public Function Implementations ---

BaselineManager* baseline_manager_alloc(uint16_t capacity) {
    BaselineManager* manager = malloc(sizeof(BaselineManager));
    furi_check(manager != NULL);

    manager->baseline_aps = malloc(sizeof(baseline_ap_t) * capacity);
    furi_check(manager->baseline_aps != NULL);

    manager->capacity = capacity;
    manager->count = 0;
    manager->mutex = furi_mutex_alloc(FuriMutexTypeRecursive);
    manager->dirty = false;

    return manager;
}

void baseline_manager_free(BaselineManager* manager) {
    if(!manager) return;
    furi_mutex_free(manager->mutex);
    free(manager->baseline_aps);
    free(manager);
}

bool baseline_manager_load(BaselineManager* manager) {
    furi_check(manager);

    Storage* storage = furi_record_open(RECORD_STORAGE);
    File* file = storage_file_alloc(storage);
    bool success = false;
    
    if(!storage_file_open(file, BASELINE_FILE_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) {
        // File doesn't exist, which is fine on first run. Return true.
        storage_file_free(file);
        furi_record_close(RECORD_STORAGE);
        return true; 
    }

    if(furi_mutex_acquire(manager->mutex, FuriWaitForever) != FuriStatusOk) {
        storage_file_close(file);
        storage_file_free(file);
        furi_record_close(RECORD_STORAGE);
        return false;
    }

    BaselineFileHeader header;
    if(storage_file_read(file, &header, sizeof(header)) != sizeof(header)) {
        goto cleanup; // Read error
    }

    if(strncmp(header.magic, "RGFAP_B", 7) != 0 || header.version != 1) {
        goto cleanup; // Invalid file format or version
    }

    manager->count = (header.count > manager->capacity) ? manager->capacity : header.count;
    
    if(storage_file_read(file, manager->baseline_aps, manager->count * sizeof(baseline_ap_t)) != manager->count * sizeof(baseline_ap_t)) {
        manager->count = 0; // Read error, clear data
        goto cleanup;
    }

    manager->dirty = false;
    success = true;

cleanup:
    furi_mutex_release(manager->mutex);
    storage_file_close(file);
    storage_file_free(file);
    furi_record_close(RECORD_STORAGE);
    return success;
}

bool baseline_manager_save(BaselineManager* manager) {
    furi_check(manager);

    if(furi_mutex_acquire(manager->mutex, FuriWaitForever) != FuriStatusOk) return false;
    
    // Optimization: Don't write to SD card if nothing has changed
    if(!manager->dirty) {
        furi_mutex_release(manager->mutex);
        return true;
    }

    Storage* storage = furi_record_open(RECORD_STORAGE);
    // Ensure the directory exists
    storage_simply_mkdir(storage, furi_string_get_dirname(BASELINE_FILE_PATH));

    File* file = storage_file_alloc(storage);
    bool success = false;

    if(!storage_file_open(file, BASELINE_FILE_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
        goto cleanup;
    }

    BaselineFileHeader header = {.version = 1, .count = manager->count};
    strncpy(header.magic, "RGFAP_B", 7);

    if(storage_file_write(file, &header, sizeof(header)) != sizeof(header)) {
        goto cleanup;
    }

    if(storage_file_write(file, manager->baseline_aps, manager->count * sizeof(baseline_ap_t)) != manager->count * sizeof(baseline_ap_t)) {
        goto cleanup;
    }

    manager->dirty = false; // Mark as clean after successful save
    success = true;

cleanup:
    storage_file_close(file);
    storage_file_free(file);
    furi_record_close(RECORD_STORAGE);
    furi_mutex_release(manager->mutex);
    return success;
}

bool baseline_manager_update(BaselineManager* manager, const ap_info_t* ap_info) {
    furi_check(manager && ap_info);

    if(furi_mutex_acquire(manager->mutex, FuriWaitForever) != FuriStatusOk) return false;

    bool updated = false;

    // Search for existing entry by BSSID
    for(uint16_t i = 0; i < manager->count; i++) {
        if(memcmp(manager->baseline_aps[i].bssid, ap_info->bssid, 6) == 0) {
            baseline_ap_t* entry = &manager->baseline_aps[i];
            
            // Update RSSI range
            if(ap_info->rssi < entry->rssi_min) entry->rssi_min = ap_info->rssi;
            if(ap_info->rssi > entry->rssi_max) entry->rssi_max = ap_info->rssi;

            // Update timestamp
            entry->last_seen_timestamp = furi_hal_rtc_get_timestamp();

            // Also update SSID and encryption in case they changed (e.g. guest network toggle)
            strncpy(entry->ssid, ap_info->ssid, MAX_SSID_LEN);
            entry->encryption = ap_info->encryption;
            
            manager->dirty = true;
            updated = true;
            goto exit_update;
        }
    }

    // If not found, add a new entry if there's space
    if(manager->count < manager->capacity) {
        baseline_ap_t* new_entry = &manager->baseline_aps[manager->count];
        memcpy(new_entry->bssid, ap_info->bssid, 6);
        strncpy(new_entry->ssid, ap_info->ssid, MAX_SSID_LEN);
        new_entry->encryption = ap_info->encryption;
        new_entry->rssi_min = ap_info->rssi;
        new_entry->rssi_max = ap_info->rssi;
        new_entry->last_seen_timestamp = furi_hal_rtc_get_timestamp();
        
        manager->count++;
        manager->dirty = true;
        updated = true;
    }

exit_update:
    furi_mutex_release(manager->mutex);
    return updated;
}

uint16_t baseline_manager_prune_old_entries(BaselineManager* manager, uint32_t timeout_seconds) {
    furi_check(manager);

    if(furi_mutex_acquire(manager->mutex, FuriWaitForever) != FuriStatusOk) return 0;

    uint32_t current_time = furi_hal_rtc_get_timestamp();
    uint16_t pruned_count = 0;

    // Iterate backwards to safely remove elements
    for(int i = manager->count - 1; i >= 0; i--) {
        if((current_time - manager->baseline_aps[i].last_seen_timestamp) > timeout_seconds) {
            // This entry is old. Remove it by shifting the rest of the array down.
            // This is only efficient if we don't prune many items at once.
            if(i < manager->count - 1) {
                memmove(&manager->baseline_aps[i], &manager->baseline_aps[i + 1], (manager->count - 1 - i) * sizeof(baseline_ap_t));
            }
            manager->count--;
            manager->dirty = true;
            pruned_count++;
        }
    }
    
    furi_mutex_release(manager->mutex);
    return pruned_count;
}