diff --git a/.cursor/rules b/.cursor/rules new file mode 100644 index 0000000..b82809d --- /dev/null +++ b/.cursor/rules @@ -0,0 +1,2 @@ +## Build +- use ./ctl build target diff --git a/ctl.sh b/ctl.sh index 992ffb9..02c5c0b 100755 --- a/ctl.sh +++ b/ctl.sh @@ -51,4 +51,8 @@ function cluster { ${@:-info} } +function monitor { + pio run -t monitor +} + ${@:-info} diff --git a/examples/wifiscan/README.md b/examples/wifiscan/README.md new file mode 100644 index 0000000..152986e --- /dev/null +++ b/examples/wifiscan/README.md @@ -0,0 +1,88 @@ +# WiFi Scanner Example + +This example demonstrates how to use the async WiFi scanning functionality in SPORE with periodic scanning via tasks. + +## Features + +- **Async WiFi Scanning**: Non-blocking WiFi network discovery via NetworkManager +- **Task-based Periodic Scanning**: Automatically scans for networks every minute using TaskManager +- **Event-driven**: Uses the event system to handle scan events +- **REST API**: HTTP endpoints to manually trigger scans and view results +- **Detailed Results**: Shows SSID, RSSI, channel, and encryption type for each network + +## Usage + +1. Upload this example to your ESP8266 device +2. Open the Serial Monitor at 115200 baud +3. The device will automatically start scanning for WiFi networks every minute +4. Results will be displayed in the Serial Monitor +5. Use the REST API to manually trigger scans or view current results + +## Event System + +The WiFi scanner uses the following events: + +- `wifi/scan/start`: Fired when scan starts (both manual and periodic) +- `wifi/scan/complete`: Fired when scan completes successfully +- `wifi/scan/error`: Fired when scan fails to start +- `wifi/scan/timeout`: Fired when scan times out (10 seconds) + +## Task Management + +The example registers a periodic task: +- **Task Name**: `wifi_scan_periodic` +- **Interval**: 60 seconds (60000ms) +- **Function**: Fires `wifi/scan/start` event and starts WiFi scan + +## REST API + +### Manual Scan Control +- **Start Scan**: `POST /api/wifi/scan` +- **Get Status**: `GET /api/wifi/status` + +### Example API Usage +```bash +# Start a manual scan +curl -X POST http://192.168.1.50/api/wifi/scan + +# Get current scan status and results +curl http://192.168.1.50/api/wifi/status + +# Check task status +curl http://192.168.1.50/api/tasks/status + +# Disable periodic scanning +curl -X POST http://192.168.1.50/api/tasks/control \ + -d task=wifi_scan_periodic -d action=disable +``` + +## Architecture + +- **WiFiScannerService**: Manages periodic scanning and API endpoints +- **NetworkManager**: Handles WiFi scanning operations and callbacks +- **NodeContext**: Stores the WiFi access points data +- **TaskManager**: Schedules periodic scan tasks +- **Event System**: Provides communication between components + +## WiFiAccessPoint Structure + +```cpp +struct WiFiAccessPoint { + String ssid; // Network name + int32_t rssi; // Signal strength + uint8_t encryption; // Encryption type + uint8_t* bssid; // MAC address + int32_t channel; // WiFi channel + bool isHidden; // Hidden network flag +}; +``` + +## Task Configuration + +The periodic scanning task can be controlled via the standard TaskManager API: +- Enable/disable the task +- Change the scan interval +- Monitor task status +- Start/stop the task + +This provides flexibility to adjust scanning behavior based on application needs. \ No newline at end of file diff --git a/examples/wifiscan/main.cpp b/examples/wifiscan/main.cpp new file mode 100644 index 0000000..d82f591 --- /dev/null +++ b/examples/wifiscan/main.cpp @@ -0,0 +1,183 @@ +#include +#include +#include "Globals.h" +#include "NodeContext.h" +#include "NetworkManager.h" +#include "ClusterManager.h" +#include "ApiServer.h" +#include "TaskManager.h" + +using namespace std; + +class WiFiScannerService { +public: + WiFiScannerService(NodeContext& ctx, TaskManager& taskMgr, NetworkManager& networkMgr) + : ctx(ctx), taskManager(taskMgr), network(networkMgr) { + registerTasks(); + } + + void registerApi(ApiServer& api) { + api.addEndpoint("/api/wifi/scan", HTTP_POST, [this](AsyncWebServerRequest* request) { + if (network.isWiFiScanning()) { + JsonDocument resp; + resp["success"] = false; + resp["message"] = "WiFi scan already in progress"; + String json; + serializeJson(resp, json); + request->send(409, "application/json", json); + return; + } + + network.startWiFiScan(); + + JsonDocument resp; + resp["success"] = true; + resp["message"] = "WiFi scan started"; + String json; + serializeJson(resp, json); + request->send(200, "application/json", json); + }); + + api.addEndpoint("/api/wifi/status", HTTP_GET, [this](AsyncWebServerRequest* request) { + JsonDocument doc; + doc["scanning"] = network.isWiFiScanning(); + doc["networks_found"] = ctx.wifiAccessPoints->size(); + + JsonArray networks = doc["networks"].to(); + for (const auto& ap : *ctx.wifiAccessPoints) { + JsonObject network = networks.add(); + network["ssid"] = ap.ssid; + network["rssi"] = ap.rssi; + network["channel"] = ap.channel; + network["encryption"] = ap.encryption; + network["hidden"] = ap.isHidden; + } + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); + }); + } + +private: + void registerTasks() { + // Register task to start WiFi scan every minute (60000ms) + taskManager.registerTask("wifi_scan_periodic", 60000, [this]() { + if (!network.isWiFiScanning()) { + Serial.println("[WiFiScannerService] Starting periodic WiFi scan..."); + ctx.fire("wifi/scan/start", nullptr); + network.startWiFiScan(); + } else { + Serial.println("[WiFiScannerService] Skipping scan - already in progress"); + } + }); + } + + NodeContext& ctx; + TaskManager& taskManager; + NetworkManager& network; +}; + +NodeContext ctx({ + {"app", "wifiscan"}, + {"role", "demo"} +}); +NetworkManager network(ctx); +TaskManager taskManager(ctx); +ClusterManager cluster(ctx, taskManager); +ApiServer apiServer(ctx, taskManager, ctx.config.api_server_port); +WiFiScannerService wifiScanner(ctx, taskManager, network); + +// WiFi scan start callback +void onWiFiScanStart(void* data) { + Serial.println("[WiFi] Scan started..."); +} + +// WiFi scan complete callback +void onWiFiScanComplete(void* data) { + Serial.println("\n=== WiFi Scan Results ==="); + + std::vector* accessPoints = static_cast*>(data); + + if (accessPoints->empty()) { + Serial.println("No networks found"); + return; + } + + Serial.printf("Found %d networks:\n", accessPoints->size()); + Serial.println("SSID\t\t\tRSSI\tChannel\tEncryption"); + Serial.println("----------------------------------------"); + + for (const auto& ap : *accessPoints) { + String encryptionStr = "Unknown"; + switch (ap.encryption) { + case ENC_TYPE_NONE: + encryptionStr = "Open"; + break; + case ENC_TYPE_WEP: + encryptionStr = "WEP"; + break; + case ENC_TYPE_TKIP: + encryptionStr = "WPA"; + break; + case ENC_TYPE_CCMP: + encryptionStr = "WPA2"; + break; + case ENC_TYPE_AUTO: + encryptionStr = "Auto"; + break; + } + + Serial.printf("%-20s\t%d\t%d\t%s\n", + ap.ssid.c_str(), ap.rssi, ap.channel, encryptionStr.c_str()); + } + Serial.println("========================\n"); +} + +// WiFi scan error callback +void onWiFiScanError(void* data) { + Serial.println("[WiFi] Scan failed - error occurred"); +} + +// WiFi scan timeout callback +void onWiFiScanTimeout(void* data) { + Serial.println("[WiFi] Scan failed - timeout"); +} + +void setup() { + Serial.begin(115200); + Serial.println("\n=== WiFi Scanner Example ==="); + + // Initialize task manager first + taskManager.initialize(); + ctx.taskManager = &taskManager; + + // Initialize network tasks + network.initTasks(); + + // Setup WiFi + network.setupWiFi(); + + // Start the API server + apiServer.begin(); + wifiScanner.registerApi(apiServer); + + // Register WiFi scan event callbacks + ctx.on("wifi/scan/start", onWiFiScanStart); + ctx.on("wifi/scan/complete", onWiFiScanComplete); + ctx.on("wifi/scan/error", onWiFiScanError); + ctx.on("wifi/scan/timeout", onWiFiScanTimeout); + + // Start an initial WiFi scan + Serial.println("Starting initial WiFi scan..."); + ctx.fire("wifi/scan/start", nullptr); + network.startWiFiScan(); + + // Print initial task status + taskManager.printTaskStatus(); +} + +void loop() { + taskManager.execute(); + yield(); +} \ No newline at end of file diff --git a/include/NetworkManager.h b/include/NetworkManager.h index 9a98672..6c9042b 100644 --- a/include/NetworkManager.h +++ b/include/NetworkManager.h @@ -7,6 +7,18 @@ public: NetworkManager(NodeContext& ctx); void setupWiFi(); void setHostnameFromMac(); + void startWiFiScan(); + void updateWiFiScan(); + bool isWiFiScanning() const; + void processScanResults(int networksFound); + void initTasks(); // Initialize tasks after TaskManager is ready // Public method to process scan results + private: NodeContext& ctx; + bool wifiScanInProgress; + unsigned long wifiScanStartTime; + static const unsigned long WIFI_SCAN_TIMEOUT_MS = 10000; // 10 seconds timeout + static const char* const WIFI_SCAN_MONITOR_TASK; // Task name for WiFi scan monitoring + + void onWiFiScanComplete(int networksFound); // Keep private for internal use }; diff --git a/include/NodeContext.h b/include/NodeContext.h index 76576de..f41d142 100644 --- a/include/NodeContext.h +++ b/include/NodeContext.h @@ -2,12 +2,33 @@ #include #include +#include #include "NodeInfo.h" #include #include #include #include "Config.h" +// Forward declaration +class TaskManager; + +// WiFi access point structure +struct WiFiAccessPoint { + String ssid; + int32_t rssi; + uint8_t encryption; + uint8_t* bssid; + int32_t channel; + bool isHidden; + + WiFiAccessPoint() : rssi(0), encryption(0), bssid(nullptr), channel(0), isHidden(false) {} + ~WiFiAccessPoint() { + if (bssid) { + delete[] bssid; + } + } +}; + class NodeContext { public: NodeContext(); @@ -19,10 +40,14 @@ public: NodeInfo self; std::map* memberList; Config config; + TaskManager* taskManager; using EventCallback = std::function; std::map> eventRegistry; void on(const std::string& event, EventCallback cb); void fire(const std::string& event, void* data); + + // WiFi access points storage + std::vector* wifiAccessPoints; }; diff --git a/include/TaskManager.h b/include/TaskManager.h index ffe1488..d5947a9 100644 --- a/include/TaskManager.h +++ b/include/TaskManager.h @@ -4,9 +4,11 @@ #include #include #include -#include "NodeContext.h" #include +// Forward declaration +class NodeContext; + // Define our own callback type to avoid conflict with TaskScheduler using TaskFunction = std::function; diff --git a/platformio.ini b/platformio.ini index e2bd428..7a726f2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -124,3 +124,32 @@ build_src_filter = + + + + +[env:esp01_1m_wifiscan] +platform = platformio/espressif8266@^4.2.1 +board = esp01_1m +framework = arduino +upload_speed = 115200 +monitor_speed = 115200 +board_build.partitions = partitions_ota_1M.csv +board_build.flash_mode = dout +board_build.flash_size = 1M +lib_deps = ${common.lib_deps} +build_src_filter = + + + + + + + +[env:d1_mini_wifiscan] +platform = platformio/espressif8266@^4.2.1 +board = d1_mini +framework = arduino +upload_speed = 115200 +monitor_speed = 115200 +board_build.flash_mode = dio +board_build.flash_size = 4M +lib_deps = ${common.lib_deps} +build_src_filter = + + + + + + diff --git a/src/NetworkManager.cpp b/src/NetworkManager.cpp index cae4ca0..2c50f2d 100644 --- a/src/NetworkManager.cpp +++ b/src/NetworkManager.cpp @@ -1,8 +1,29 @@ #include "NetworkManager.h" +#include "TaskManager.h" // SSID and password are now configured via Config class -NetworkManager::NetworkManager(NodeContext& ctx) : ctx(ctx) {} +const char* const NetworkManager::WIFI_SCAN_MONITOR_TASK = "wifi_scan_monitor"; + +NetworkManager::NetworkManager(NodeContext& ctx) : ctx(ctx), wifiScanInProgress(false), wifiScanStartTime(0) { + // Register scan process callback + ctx.on("wifi/scan/process", [this](void* data) { + int networksFound = reinterpret_cast(data); + this->processScanResults(networksFound); + }); +} + +void NetworkManager::initTasks() { + if (!ctx.taskManager) { + Serial.println("[WiFi] Error: TaskManager not initialized"); + return; + } + + // Register task to check scan status every 100ms (initially disabled) + ctx.taskManager->registerTask(WIFI_SCAN_MONITOR_TASK, 100, [this]() { + this->updateWiFiScan(); + }, false); // false = disabled by default +} void NetworkManager::setHostnameFromMac() { uint8_t mac[6]; @@ -65,3 +86,133 @@ void NetworkManager::setupWiFi() { // Notify listeners that the node is (re)discovered ctx.fire("node_discovered", &ctx.self); } + +void NetworkManager::startWiFiScan() { + if (wifiScanInProgress) { + Serial.println("[WiFi] Scan already in progress"); + return; + } + + // Check if we're in AP mode only + if (WiFi.getMode() == WIFI_AP) { + // Enable STA mode while keeping AP mode + WiFi.mode(WIFI_AP_STA); + delay(100); // Give some time for mode change + } + + // Ensure we have STA mode enabled + if (WiFi.getMode() != WIFI_STA && WiFi.getMode() != WIFI_AP_STA) { + Serial.println("[WiFi] Error: Cannot scan without STA mode enabled"); + ctx.fire("wifi/scan/error", nullptr); + return; + } + + Serial.println("[WiFi] Starting WiFi scan..."); + wifiScanInProgress = true; + wifiScanStartTime = millis(); + + // Enable the monitoring task if TaskManager is available + if (ctx.taskManager) { + ctx.taskManager->enableTask(WIFI_SCAN_MONITOR_TASK); + } + + // Clear previous results safely + if (ctx.wifiAccessPoints) { + ctx.wifiAccessPoints->clear(); + } + + // Disable interrupts briefly during scan start + noInterrupts(); + // Start the scan + WiFi.scanNetworksAsync([this](int networksFound) { + // Schedule callback in main loop + ctx.fire("wifi/scan/process", reinterpret_cast(networksFound)); + }, true); // Show hidden networks + interrupts(); +} + +void NetworkManager::updateWiFiScan() { + if (!wifiScanInProgress) { + return; + } + + // Check for timeout + if (millis() - wifiScanStartTime > WIFI_SCAN_TIMEOUT_MS) { + Serial.println("[WiFi] WiFi scan timeout"); + wifiScanInProgress = false; + if (ctx.taskManager) { + ctx.taskManager->disableTask(WIFI_SCAN_MONITOR_TASK); + } + ctx.fire("wifi/scan/timeout", nullptr); + } +} + +void NetworkManager::processScanResults(int networksFound) { + // This is called from the main loop context via the event system + if (!wifiScanInProgress) { + return; // Ignore if we're not expecting results + } + + wifiScanInProgress = false; + // Disable the monitoring task if TaskManager is available + if (ctx.taskManager) { + ctx.taskManager->disableTask(WIFI_SCAN_MONITOR_TASK); + } + + Serial.printf("[WiFi] Processing scan results, found %d networks\n", networksFound); + + // Create a temporary vector to hold new results + std::vector newAccessPoints; + + if (networksFound > 0) { + newAccessPoints.reserve(networksFound); // Pre-allocate space + + // Process each found network + for (int i = 0; i < networksFound; i++) { + WiFiAccessPoint ap; + ap.ssid = WiFi.SSID(i); + ap.rssi = WiFi.RSSI(i); + ap.encryption = WiFi.encryptionType(i); + ap.channel = WiFi.channel(i); + ap.isHidden = WiFi.isHidden(i); + + // Copy BSSID - with null check and bounds protection + uint8_t* bssid = WiFi.BSSID(i); + ap.bssid = nullptr; // Initialize to null first + if (bssid != nullptr) { + ap.bssid = new (std::nothrow) uint8_t[6]; // Use nothrow to prevent exceptions + if (ap.bssid != nullptr) { + memcpy(ap.bssid, bssid, 6); + } + } + + newAccessPoints.push_back(ap); + + Serial.printf("[WiFi] %d: %s (RSSI: %d, Ch: %d, Enc: %d)\n", + i, ap.ssid.c_str(), ap.rssi, ap.channel, ap.encryption); + } + } + + // Free the scan results from WiFi to prevent memory leaks + WiFi.scanDelete(); + + // Safely swap the new results with the stored results + if (ctx.wifiAccessPoints) { + ctx.wifiAccessPoints->swap(newAccessPoints); + // Fire the scan complete event with a copy of the pointer + auto accessPoints = ctx.wifiAccessPoints; + ctx.fire("wifi/scan/complete", accessPoints); + } else { + Serial.println("[WiFi] Error: wifiAccessPoints is null"); + ctx.fire("wifi/scan/error", nullptr); + } +} + +void NetworkManager::onWiFiScanComplete(int networksFound) { + // Internal callback that schedules processing in the main loop + ctx.fire("wifi/scan/process", reinterpret_cast(networksFound)); +} + +bool NetworkManager::isWiFiScanning() const { + return wifiScanInProgress; +} diff --git a/src/NodeContext.cpp b/src/NodeContext.cpp index e5833d3..4d12998 100644 --- a/src/NodeContext.cpp +++ b/src/NodeContext.cpp @@ -1,8 +1,11 @@ #include "NodeContext.h" +#include "TaskManager.h" NodeContext::NodeContext() { udp = new WiFiUDP(); memberList = new std::map(); + wifiAccessPoints = new std::vector(); + taskManager = nullptr; // Will be set by TaskManager constructor hostname = ""; self.hostname = ""; self.ip = IPAddress(); @@ -19,6 +22,7 @@ NodeContext::NodeContext(std::initializer_list> initia NodeContext::~NodeContext() { delete udp; delete memberList; + delete wifiAccessPoints; } void NodeContext::on(const std::string& event, EventCallback cb) {