Ver código fonte

Merge branch 'kbf-32' of kraxor/kbf into develop

Bence Balint 2 anos atrás
pai
commit
bd61533f66

+ 4 - 1
CMakeLists.txt

@@ -21,7 +21,10 @@ idf_component_register(
         "src/web_service.cpp"
         "src/wifi/ap.cpp"
         "src/wifi/sta.cpp"
-        "src/wifi/driver.cpp"
+        "src/wifi/wifi.cpp"
+        "src/wifi_legacy/ap.cpp"
+        "src/wifi_legacy/sta.cpp"
+        "src/wifi_legacy/driver.cpp"
 
         INCLUDE_DIRS "include" "lib"
 

+ 10 - 0
include/kbf/exception.h

@@ -41,6 +41,16 @@ namespace kbf::exception {
 
     class OutOfMemoryError : public std::exception {
     };
+
+    class InvalidArgument : public std::exception {
+        const char *msg;
+    public:
+        explicit InvalidArgument(const char *msg) : msg(msg) {}
+
+        [[nodiscard]] const char *what() const _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_USE_NOEXCEPT override {
+            return msg;
+        }
+    };
 }
 
 #endif //KBF_EXCEPTION_H

+ 16 - 0
include/kbf/internal/wifi.h

@@ -0,0 +1,16 @@
+#ifdef KBF_INTERNAL
+#ifndef KBF_INTERNAL_WIFI_H
+#define KBF_INTERNAL_WIFI_H
+
+namespace kbf::wifi::internal {
+    void init();
+
+    extern bool initialized;
+
+    extern Mode mode;
+
+    void setMode(Mode m);
+}
+
+#endif //KBF_INTERNAL_WIFI_H
+#endif //KBF_INTERNAL

+ 54 - 342
include/kbf/wifi.h

@@ -2,385 +2,97 @@
 #define KBF_WIFI_H
 
 #include <string>
-#include <memory>
-#include <map>
 #include <vector>
 
-#include <esp_wifi.h>
-#include <esp_log.h>
-
 #include "net.h"
 
-using std::string;
-using std::shared_ptr;
-using std::map;
-using std::vector;
-
-/** @brief WiFi functions */
 namespace kbf::wifi {
-    /** @brief Tag used for logging */
-    static constexpr const char *TAG = "kbf::wifi";
-
-    class AP;
-
-    class STA;
-
-    /**
-     * @brief Starts WiFi in soft-AP mode.
-     *
-     * @see #kbf::wifi::AP::create()
-     *
-     * @param ap AP pointer
-     */
-    void start(shared_ptr<AP> ap);
-
-    /**
-     * @brief Starts WiFi in STA mode.
-     *
-     * @param sta STA pointer; if nullptr, kbf::wifi::STA::create() will be called.
-     */
-    void start(shared_ptr<STA> sta = nullptr);
-
-    /**
-     * @brief Starts WiFi in AP+STA dual mode.
-     *
-     * @param ap
-     * @param sta
-     */
-    void start(shared_ptr<AP> ap, shared_ptr<STA> sta);
-
-    /**
-     * @brief Stops WiFi.
-     */
     void stop();
 
-    /**
-     * @brief Disables AP.
-     *
-     * If the driver was in dual mode, it will change to STA mode.
-     * If the driver was in softAP mode, it will change to NULL mode.
-     */
-    void stopAP();
-
-    /**
-     * @brief Returns the currently used AP instance.
-     *
-     * @return AP pointer or nullptr if not in AP mode
-     */
-    shared_ptr<AP> getAP();
-
-    /**
-     * @brief Returns the currently used STA instance.
-     *
-     * @return STA pointer or nullptr if not in STA mode
-     */
-    shared_ptr<STA> getSTA();
-
-    /**
-     * @brief Returns whether the WiFi is started.
-     *
-     * @note Does not differentiate between modes.
-     *
-     * @return true if WiFi is running; false otherwise
-     */
-    bool isRunning();
-
-    /**
-     * @brief Holds information about an access point.
-     */
-    struct APInfo {
-        /** @brief SSID */
-        string ssid;
-
-        /** @brief signal strength */
-        int8_t rssi;
+    enum class Mode {
+        OFF,
+        STA,
+        AP,
+        DUAL
     };
 
-    /**
-     * @brief WiFi station (STA) mode.
-     */
-    class STA {
-        /** @see #kbf::wifi::start() */
-        friend void kbf::wifi::start(shared_ptr<STA>);
-
-        /** @see #kbf::wifi::start() */
-        friend void kbf::wifi::start(shared_ptr<AP>, shared_ptr<STA>);
-
-        /** @see #kbf::wifi::stop() */
-        friend void kbf::wifi::stop();
+    Mode mode();
 
+    class STA {
     public:
-        /** @brief Tag used for logging. */
-        static constexpr const char *TAG = "kbf::wifi::STA";
-
-        /**
-         * @brief Creates STA instance.
-         *
-         * @note WiFi requires NVS. If NVS hasn't been initialized yet, the constructor will call #kbf::nvs::init()
-         * and try again.
-         *
-         * @see #kbf::wifi::start()
-         *
-         * @return shared_ptr<STA>
-         */
-        static shared_ptr<STA> create();
-
-        /**
-         * Destructor. Unregisters event handlers from the default event loop.
-         */
-        ~STA();
-
-        /**
-         * @brief Initiates connection to an AP.
-         *
-         * @param ssid SSID of AP
-         * @param password Password
-         * @param maxRetryAttempts maximum number of retry attempts if the connection fails
-         */
-        void connect(const string &ssid, const string &password, int maxRetryAttempts = 3);
-
-        /**
-         * @brief Disconnects from the AP.
-         */
-        void disconnect();
-
-        /**
-         * @brief Starts async scan.
-         *
-         * @param data custom data passed to the event handler
-         *
-         * @see onScanDone()
-         */
-        void startScan(void *data);
-
-        /**
-         * @brief Called when the WiFi has started.
-         */
-        void (*onStart)(){};
-
-        /**
-         * @brief Called when the WiFi has stopped.
-         */
-        void (*onStop)(){};
-
-        /**
-         * @brief Called when the connection to the AP was successful.
-         *
-         * @note Connection may be useless until an IP address is assigned by the AP. See onIp().
-         * @see onIp()
-         */
-        void (*onConnect)(){};
-
-        /**
-         * @brief Called once an IP address is assigned by the AP. The connection should be ready to use at this point.
-         */
-        void (*onIp)(){};
-
-        /**
-         * @brief Called before reconnect attempts.
-         *
-         * Reconnect is attempted after disconnect events if retryMax hasn't been reached yet.
-         *
-         * @note The retry attempt counter is reset after successful connection.
-         *
-         * @param attempt number of current attempt
-         * @param max maximum number of attempts
-         * @return if false is returned, reconnect will be cancelled
-         */
-        bool (*onReconnecting)(int attempt, int max){};
-
-        /**
-         * @brief Called if disconnected and retryMax has been reached.
-         */
-        void (*onDisconnect)(){};
-
-        /**
-         * @brief Called when scanning has finished.
-         *
-         * @param aps list of detected access points
-         * @param data custom data
-         */
-        void (*onScanDone)(vector<APInfo> &aps, void *data){};
-
-        /** @brief Returns the IP address for the connection. */
-        [[nodiscard]] const kbf::net::IP &ip() const { return m_ip; };
+        STA() : aid(0), mac{} {};
 
-    private:
-        STA();
+        explicit STA(wifi_event_ap_staconnected_t &data) : aid(data.aid), mac(data.mac) {};
 
-        int retryNum = 0;
-        int retryMax{};
-
-        void init();
-
-        void registerEventHandlers();
-
-        void unregisterEventHandlers();
-
-        static void handleStart(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
-
-        static void handleStop(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
-
-        static void handleConnect(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
-
-        static void handleDisconnect(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
-
-        static void handleScanDone(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
-
-        static void handleGotIp(void *arg, esp_event_base_t baseType, int32_t eventId, void *pEventData);
-
-        esp_netif_obj     *netif = nullptr;
-        wifi_sta_config_t config = {};
-
-        esp_event_handler_instance_t startHandler      = {};
-        esp_event_handler_instance_t stopHandler       = {};
-        esp_event_handler_instance_t connectHandler    = {};
-        esp_event_handler_instance_t disconnectHandler = {};
-        esp_event_handler_instance_t scanDoneHandler   = {};
-
-        esp_event_handler_instance_t gotIpHandler = {};
-
-        kbf::net::IP m_ip;
+        const int           aid;
+        const kbf::net::MAC mac;
     };
 
-    /**
-     * @brief WiFi Software-enabled AccessPoint (AP) mode
-     *
-     * To start AP, use the #create() method to create an instance of this class, then pass it to #kbf::wifi::start().
-     */
-    class AP {
-        /** @see #kbf::wifi::start() */
-        friend void kbf::wifi::start(shared_ptr<AP>);
-
-        /** @see #kbf::wifi::start() */
-        friend void kbf::wifi::start(shared_ptr<AP>, shared_ptr<STA>);
-
-        /** @see #kbf::wifi::stop() */
-        friend void kbf::wifi::stop();
-
-        /** @see #kbf::wifi::stopAP() */
-        friend void kbf::wifi::stopAP();
-
-    public:
-        /** @brief Tag used for logging. */
-        static constexpr const char *TAG = "kbf::wifi::AP";
-
-        /**
-         * @brief Creates an instance of the class.
-         *
-         * @param ssid
-         * @param password
-         * @param ip address and gateway
-         * @param netmask
-         * @return shared_ptr<AP>
-         */
-        static shared_ptr<AP> create(
-                const string &ssid,
-                const string &password,
-                const net::IP &ip = net::IP("192.168.4.1"),
-                const net::IP &netmask = net::IP("255.255.255.0")
+    namespace ap {
+        void start(
+                std::string ssid,
+                std::string password,
+                net::IP ip = net::IP("192.168.4.1"),
+                net::IP netmask = net::IP("255.255.255.0")
         );
 
-        // TODO figure out why this doesn't work then use make_shared
-//        template<typename ...Arg> std::shared_ptr<AP> static create(Arg&&...arg) {
-//            struct EnableMakeShared : public AP {
-//                explicit EnableMakeShared(Arg&&...arg) :AP(std::forward<Arg>(arg)...) {}
-//            };
-//            return std::make_shared<EnableMakeShared>(std::forward<Arg>(arg)...);
-//        }
-
-        /**
-         * @brief Destructor; will unregister event handlers from the default event loop.
-         */
-        ~AP();
-
-        /** @brief SSID to use. */
-        const string ssid;
+        void stop();
 
-        /** @brief WPA2/PSK password. */
-        const string password;
+        [[nodiscard]] const std::string &ssid();
 
-        /** @brief IP address and gateway for this AP */
-        const net::IP ip;
+        [[nodiscard]] const std::string &password();
 
-        /** @brief netmask for this AP */
-        const net::IP netmask;
-
-        /**
-         * @brief Represents a station connected to this AP.
-         */
-        class STA {
-        public:
-            STA() : aid(0), mac{} {};
-
-            explicit STA(wifi_event_ap_staconnected_t &);
-
-            const int           aid;
-            const kbf::net::MAC mac;
-        };
+        extern void (*onConnect)(STA &);
 
-        /** @brief aid mapped to STA instances */
-        map<int, STA> stations{};
+        extern void (*onDisconnect)(STA &);
+    }
 
-        /**
-         * @brief Called when the AP has started.
-         *
-         * @param ap
-         */
-        void (*onStart)(){};
+    struct APInfo {
+        std::string ssid;
+        int8_t rssi;
+    };
 
-        /**
-         * @brief Called when the AP has stopped.
-         *
-         * @param ap
-         */
-        void (*onStop)(){};
+    namespace sta {
+        void start();
 
-        /**
-         * @brief Called when a station connects to the AP.
-         *
-         * @param ap
-         * @param sta
-         */
-        void (*onConnect)(STA &sta){};
+        void stop();
 
-        /**
-         * @brief Called when a station disconnects from the AP.
-         *
-         * @param ap
-         * @param sta
-         */
-        void (*onDisconnect)(STA &sta){};
+        bool connect(std::string ssid, std::string password, bool async = false, int maxAttempts = 3);
 
-    private:
-        AP() { ESP_LOGW(TAG, "running default constructor!"); }
+        void disconnect();
 
-        AP(string ssid, string password, kbf::net::IP ip, kbf::net::IP netmask);
+        [[nodiscard]] bool connected();
 
-        void init();
+        [[nodiscard]] const std::string &ssid();
 
-        void setupDhcp();
+        [[nodiscard]] const std::string &password();
 
-        void registerEventHandlers();
+        [[nodiscard]] const net::IP &ip();
 
-        void unregisterEventHandlers();
+        extern void (*onConnect)();
 
-        static void handleStart(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+        extern void (*onIp)();
 
-        static void handleStop(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+        extern void (*onDisconnect)();
 
-        static void handleConnect(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+        void startScan(void *data);
 
-        static void handleDisconnect(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+        extern void (*onScanDone)(std::vector<APInfo> &apList, void *data);
+    }
 
-        esp_netif_obj                *netif            = nullptr;
-        wifi_ap_config_t             config            = {};
-        esp_event_handler_instance_t startHandler      = {};
-        esp_event_handler_instance_t stopHandler       = {};
-        esp_event_handler_instance_t connectHandler    = {};
-        esp_event_handler_instance_t disconnectHandler = {};
-    };
+    namespace dual {
+        void start(
+                std::string ssid,
+                std::string password,
+                net::IP ip = net::IP("192.168.4.1"),
+                net::IP netmask = net::IP("255.255.255.0")
+        );
+    }
 
+    namespace exception {
+        class InvalidMode : public std::exception {
+        };
+    }
 }
 
 #endif //KBF_WIFI_H

+ 390 - 0
include/kbf/wifi_legacy.h

@@ -0,0 +1,390 @@
+#ifndef KBF_WIFI_LEGACY_H
+#define KBF_WIFI_LEGACY_H
+
+#include <string>
+#include <memory>
+#include <map>
+#include <vector>
+
+#include <esp_wifi.h>
+#include <esp_log.h>
+
+#include "net.h"
+
+using std::string;
+using std::shared_ptr;
+using std::map;
+using std::vector;
+
+/**
+ * @brief WiFi functions
+ *
+ * @deprecated use kbf::wifi instead
+ */
+namespace kbf::wifi_legacy {
+    /** @brief Tag used for logging */
+    static constexpr const char *TAG = "kbf::wifi";
+
+    class AP;
+
+    class STA;
+
+    /**
+     * @brief Starts WiFi in soft-AP mode.
+     *
+     * @see #kbf::wifi_legacy::AP::create()
+     *
+     * @param ap AP pointer
+     */
+    void start(shared_ptr<AP> ap);
+
+    /**
+     * @brief Starts WiFi in STA mode.
+     *
+     * @param sta STA pointer; if nullptr, kbf::wifi_legacy::STA::create() will be called.
+     */
+    void start(shared_ptr<STA> sta = nullptr);
+
+    /**
+     * @brief Starts WiFi in AP+STA dual mode.
+     *
+     * @param ap
+     * @param sta
+     */
+    void start(shared_ptr<AP> ap, shared_ptr<STA> sta);
+
+    /**
+     * @brief Stops WiFi.
+     */
+    void stop();
+
+    /**
+     * @brief Disables AP.
+     *
+     * If the driver was in dual mode, it will change to STA mode.
+     * If the driver was in softAP mode, it will change to NULL mode.
+     */
+    void stopAP();
+
+    /**
+     * @brief Returns the currently used AP instance.
+     *
+     * @return AP pointer or nullptr if not in AP mode
+     */
+    shared_ptr<AP> getAP();
+
+    /**
+     * @brief Returns the currently used STA instance.
+     *
+     * @return STA pointer or nullptr if not in STA mode
+     */
+    shared_ptr<STA> getSTA();
+
+    /**
+     * @brief Returns whether the WiFi is started.
+     *
+     * @note Does not differentiate between modes.
+     *
+     * @return true if WiFi is running; false otherwise
+     */
+    bool isRunning();
+
+    /**
+     * @brief Holds information about an access point.
+     */
+    struct APInfo {
+        /** @brief SSID */
+        string ssid;
+
+        /** @brief signal strength */
+        int8_t rssi;
+    };
+
+    /**
+     * @brief WiFi station (STA) mode.
+     */
+    class STA {
+        /** @see #kbf::wifi_legacy::start() */
+        friend void kbf::wifi_legacy::start(shared_ptr<STA>);
+
+        /** @see #kbf::wifi_legacy::start() */
+        friend void kbf::wifi_legacy::start(shared_ptr<AP>, shared_ptr<STA>);
+
+        /** @see #kbf::wifi_legacy::stop() */
+        friend void kbf::wifi_legacy::stop();
+
+    public:
+        /** @brief Tag used for logging. */
+        static constexpr const char *TAG = "kbf::wifi_legacy::STA";
+
+        /**
+         * @brief Creates STA instance.
+         *
+         * @note WiFi requires NVS. If NVS hasn't been initialized yet, the constructor will call #kbf::nvs::init()
+         * and try again.
+         *
+         * @see #kbf::wifi_legacy::start()
+         *
+         * @return shared_ptr<STA>
+         */
+        static shared_ptr<STA> create();
+
+        /**
+         * Destructor. Unregisters event handlers from the default event loop.
+         */
+        ~STA();
+
+        /**
+         * @brief Initiates connection to an AP.
+         *
+         * @param ssid SSID of AP
+         * @param password Password
+         * @param maxRetryAttempts maximum number of retry attempts if the connection fails
+         */
+        void connect(const string &ssid, const string &password, int maxRetryAttempts = 3);
+
+        /**
+         * @brief Disconnects from the AP.
+         */
+        void disconnect();
+
+        /**
+         * @brief Starts async scan.
+         *
+         * @param data custom data passed to the event handler
+         *
+         * @see onScanDone()
+         */
+        void startScan(void *data);
+
+        /**
+         * @brief Called when the WiFi has started.
+         */
+        void (*onStart)(){};
+
+        /**
+         * @brief Called when the WiFi has stopped.
+         */
+        void (*onStop)(){};
+
+        /**
+         * @brief Called when the connection to the AP was successful.
+         *
+         * @note Connection may be useless until an IP address is assigned by the AP. See onIp().
+         * @see onIp()
+         */
+        void (*onConnect)(){};
+
+        /**
+         * @brief Called once an IP address is assigned by the AP. The connection should be ready to use at this point.
+         */
+        void (*onIp)(){};
+
+        /**
+         * @brief Called before reconnect attempts.
+         *
+         * Reconnect is attempted after disconnect events if retryMax hasn't been reached yet.
+         *
+         * @note The retry attempt counter is reset after successful connection.
+         *
+         * @param attempt number of current attempt
+         * @param max maximum number of attempts
+         * @return if false is returned, reconnect will be cancelled
+         */
+        bool (*onReconnecting)(int attempt, int max){};
+
+        /**
+         * @brief Called if disconnected and retryMax has been reached.
+         */
+        void (*onDisconnect)(){};
+
+        /**
+         * @brief Called when scanning has finished.
+         *
+         * @param aps list of detected access points
+         * @param data custom data
+         */
+        void (*onScanDone)(vector<APInfo> &aps, void *data){};
+
+        /** @brief Returns the IP address for the connection. */
+        [[nodiscard]] const kbf::net::IP &ip() const { return m_ip; };
+
+    private:
+        STA();
+
+        int retryNum = 0;
+        int retryMax{};
+
+        void init();
+
+        void registerEventHandlers();
+
+        void unregisterEventHandlers();
+
+        static void handleStart(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+
+        static void handleStop(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+
+        static void handleConnect(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+
+        static void handleDisconnect(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+
+        static void handleScanDone(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+
+        static void handleGotIp(void *arg, esp_event_base_t baseType, int32_t eventId, void *pEventData);
+
+        esp_netif_obj     *netif = nullptr;
+        wifi_sta_config_t config = {};
+
+        esp_event_handler_instance_t startHandler      = {};
+        esp_event_handler_instance_t stopHandler       = {};
+        esp_event_handler_instance_t connectHandler    = {};
+        esp_event_handler_instance_t disconnectHandler = {};
+        esp_event_handler_instance_t scanDoneHandler   = {};
+
+        esp_event_handler_instance_t gotIpHandler = {};
+
+        kbf::net::IP m_ip;
+    };
+
+    /**
+     * @brief WiFi Software-enabled AccessPoint (AP) mode
+     *
+     * To start AP, use the #create() method to create an instance of this class, then pass it to #kbf::wifi_legacy::start().
+     */
+    class AP {
+        /** @see #kbf::wifi_legacy::start() */
+        friend void kbf::wifi_legacy::start(shared_ptr<AP>);
+
+        /** @see #kbf::wifi_legacy::start() */
+        friend void kbf::wifi_legacy::start(shared_ptr<AP>, shared_ptr<STA>);
+
+        /** @see #kbf::wifi_legacy::stop() */
+        friend void kbf::wifi_legacy::stop();
+
+        /** @see #kbf::wifi_legacy::stopAP() */
+        friend void kbf::wifi_legacy::stopAP();
+
+    public:
+        /** @brief Tag used for logging. */
+        static constexpr const char *TAG = "kbf::wifi_legacy::AP";
+
+        /**
+         * @brief Creates an instance of the class.
+         *
+         * @param ssid
+         * @param password
+         * @param ip address and gateway
+         * @param netmask
+         * @return shared_ptr<AP>
+         */
+        static shared_ptr<AP> create(
+                const string &ssid,
+                const string &password,
+                const net::IP &ip = net::IP("192.168.4.1"),
+                const net::IP &netmask = net::IP("255.255.255.0")
+        );
+
+        // TODO figure out why this doesn't work then use make_shared
+//        template<typename ...Arg> std::shared_ptr<AP> static create(Arg&&...arg) {
+//            struct EnableMakeShared : public AP {
+//                explicit EnableMakeShared(Arg&&...arg) :AP(std::forward<Arg>(arg)...) {}
+//            };
+//            return std::make_shared<EnableMakeShared>(std::forward<Arg>(arg)...);
+//        }
+
+        /**
+         * @brief Destructor; will unregister event handlers from the default event loop.
+         */
+        ~AP();
+
+        /** @brief SSID to use. */
+        const string ssid;
+
+        /** @brief WPA2/PSK password. */
+        const string password;
+
+        /** @brief IP address and gateway for this AP */
+        const net::IP ip;
+
+        /** @brief netmask for this AP */
+        const net::IP netmask;
+
+        /**
+         * @brief Represents a station connected to this AP.
+         */
+        class STA {
+        public:
+            STA() : aid(0), mac{} {};
+
+            explicit STA(wifi_event_ap_staconnected_t &);
+
+            const int           aid;
+            const kbf::net::MAC mac;
+        };
+
+        /** @brief aid mapped to STA instances */
+        map<int, STA> stations{};
+
+        /**
+         * @brief Called when the AP has started.
+         *
+         * @param ap
+         */
+        void (*onStart)(){};
+
+        /**
+         * @brief Called when the AP has stopped.
+         *
+         * @param ap
+         */
+        void (*onStop)(){};
+
+        /**
+         * @brief Called when a station connects to the AP.
+         *
+         * @param ap
+         * @param sta
+         */
+        void (*onConnect)(STA &sta){};
+
+        /**
+         * @brief Called when a station disconnects from the AP.
+         *
+         * @param ap
+         * @param sta
+         */
+        void (*onDisconnect)(STA &sta){};
+
+    private:
+        AP() { ESP_LOGW(TAG, "running default constructor!"); }
+
+        AP(string ssid, string password, kbf::net::IP ip, kbf::net::IP netmask);
+
+        void init();
+
+        void setupDhcp();
+
+        void registerEventHandlers();
+
+        void unregisterEventHandlers();
+
+        static void handleStart(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+
+        static void handleStop(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+
+        static void handleConnect(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+
+        static void handleDisconnect(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+
+        esp_netif_obj                *netif            = nullptr;
+        wifi_ap_config_t             config            = {};
+        esp_event_handler_instance_t startHandler      = {};
+        esp_event_handler_instance_t stopHandler       = {};
+        esp_event_handler_instance_t connectHandler    = {};
+        esp_event_handler_instance_t disconnectHandler = {};
+    };
+
+}
+
+#endif //KBF_WIFI_LEGACY_H

+ 131 - 89
src/wifi/ap.cpp

@@ -1,121 +1,163 @@
 #include "kbf/wifi.h"
 
+#include <esp_log.h>
+#include <esp_wifi.h>
+
+#define KBF_INTERNAL
+
+#include "kbf/internal/wifi.h"
 #include "kbf/macros.h"
+#include "kbf/exception.h"
 
-using namespace kbf;
+using namespace std;
 
-wifi::AP::AP(string ssid, string password, net::IP ip, net::IP netmask) :
-        ssid(std::move(ssid)), password(std::move(password)), ip(ip), netmask(netmask) {
-    ESP_LOGI(TAG, "AP()");
+namespace {
+    constexpr const char *const TAG = "kbf::wifi::ap";
 
-    std::copy(this->ssid.begin(), this->ssid.end() + 1, std::begin(config.ssid));
-    std::copy(this->password.begin(), this->password.end() + 1, std::begin(config.password));
+    string ssid;
+    string password;
 
-    config.ssid_len       = this->ssid.length();
-    config.max_connection = 5; // TODO use Kconfig
-    config.authmode       = WIFI_AUTH_WPA2_PSK; // TODO use AUTH_OPEN if no password is provided
-}
+    bool dhcpInit = false;
 
-wifi::AP::~AP() {
-    ESP_LOGI(TAG, "~AP()");
-    unregisterEventHandlers();
-}
+    esp_netif_obj    *interface = nullptr;
+    wifi_ap_config_t apConfig   = {};
 
-shared_ptr<wifi::AP>
-wifi::AP::create(const string &ssid, const string &password, const net::IP &ip, const net::IP &netmask) {
-    if (password.length() < 8) {
-        ABORT("WiFi password needs to be at least 8 characters long.");
-    }
+    esp_event_handler_instance_t connectHandler    = {};
+    esp_event_handler_instance_t disconnectHandler = {};
+
+    void handleConnect(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+
+    void handleDisconnect(void *arg, esp_event_base_t baseType, int32_t eventId, void *);
+
+    void initDhcp(const kbf::net::IP &ip, const kbf::net::IP &netmask);
 
-    auto ap = new AP(ssid, password, ip, netmask);
-    return shared_ptr<AP>(ap);
+    void registerEventHandlers();
+
+    void unregisterEventHandlers();
 }
 
-void wifi::AP::init() {
-    netif = esp_netif_create_default_wifi_ap();
+using namespace kbf;
+using namespace kbf::wifi;
+
+const std::string &ap::ssid() { return ::ssid; }
+
+const std::string &ap::password() { return ::password; }
+
+void (*ap::onConnect)(STA &) = nullptr;
 
-    setupDhcp();
+void (*ap::onDisconnect)(STA &) = nullptr;
+
+void ap::start(string ssid, string password, net::IP ip, net::IP netmask) {
+    ESP_LOGI(TAG, "%s(%s, <omitted>, %s, %s)", __func__, ssid.c_str(), ip.str().c_str(), netmask.str().c_str());
+
+    if (internal::mode == Mode::AP || internal::mode == Mode::DUAL) throw exception::InvalidMode();
+    if (ssid.empty()) throw kbf::exception::InvalidArgument("no SSID");
+    if (password.length() < 8) throw kbf::exception::InvalidArgument("password.length() < 8");
+
+    internal::init();
+    interface = esp_netif_create_default_wifi_ap();
+    initDhcp(ip, netmask);
     registerEventHandlers();
-}
 
-void wifi::AP::setupDhcp() {
-    ESP_LOGD(TAG, "setupDhcp()");
+    ::ssid     = std::move(ssid);
+    ::password = std::move(password);
 
-    // TODO this should only be run once, before bringing up the interface for the first time; if the interface is already up, DHCP seems to fail silently
+    std::copy(::ssid.begin(), ::ssid.end() + 1, std::begin(apConfig.ssid));
+    std::copy(::password.begin(), ::password.end() + 1, std::begin(apConfig.password));
 
-    esp_netif_ip_info_t ipInfo;
-    IP4_ADDR(&ipInfo.ip, ip[0], ip[1], ip[2], ip[3]);
-    IP4_ADDR(&ipInfo.gw, ip[0], ip[1], ip[2], ip[3]);
-    IP4_ADDR(&ipInfo.netmask, netmask[0], netmask[1], netmask[2], netmask[3]);
-    CHECK(esp_netif_dhcps_stop(netif));
-    CHECK(esp_netif_set_ip_info(netif, &ipInfo));
-    CHECK(esp_netif_dhcps_start(netif));
-}
+    apConfig.ssid_len       = ::ssid.length();
+    apConfig.max_connection = 5; // TODO use Kconfig
+    apConfig.authmode       = WIFI_AUTH_WPA2_PSK; // TODO use AUTH_OPEN if no password is provided
 
-void wifi::AP::registerEventHandlers() {
-    ESP_LOGD(TAG, "registerEventHandlers()");
-    CHECK(esp_event_handler_instance_register(
-            WIFI_EVENT, WIFI_EVENT_AP_START, &handleStart, this, &startHandler
-    ));
-    CHECK(esp_event_handler_instance_register(
-            WIFI_EVENT, WIFI_EVENT_AP_STOP, &handleStop, this, &stopHandler
-    ));
-    CHECK(esp_event_handler_instance_register(
-            WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, &handleConnect, this, &connectHandler
-    ));
-    CHECK(esp_event_handler_instance_register(
-            WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, &handleDisconnect, this, &disconnectHandler
-    ));
+    wifi_config_t config = {.ap = apConfig};
+
+    if (internal::mode == Mode::OFF) {
+        CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
+        CHECK(esp_wifi_set_config(WIFI_IF_AP, &config));
+        CHECK(esp_wifi_start());
+        internal::setMode(Mode::AP);
+    } else {
+        CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
+        CHECK(esp_wifi_set_config(WIFI_IF_AP, &config));
+        internal::setMode(Mode::DUAL);
+    }
 }
 
-void wifi::AP::unregisterEventHandlers() {
-    ESP_LOGD(TAG, "unregisterEventHandlers()");
+void ap::stop() {
+    ESP_LOGI(TAG, "%s()", __func__);
 
-    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_AP_START, &startHandler));
-    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_AP_STOP, &stopHandler));
-    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, &connectHandler));
-    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, &disconnectHandler));
+    if (internal::mode != Mode::AP && internal::mode != Mode::DUAL) throw wifi::exception::InvalidMode();
 
-    // TODO why are these necessary? shouldn't instance unregistration be enough?
-    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_AP_START, &handleStart));
-    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_AP_STOP, &handleStop));
-    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, &handleConnect));
-    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, &handleDisconnect));
-}
+    unregisterEventHandlers();
 
-void wifi::AP::handleStart(void *arg, esp_event_base_t, int32_t, void *) {
-    ESP_LOGD(TAG, "handleStart()");
-    auto ap = static_cast<AP *>(arg);
-    if (ap->onStart) ap->onStart();
-}
+    if (internal::mode == Mode::DUAL) {
+        CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
+        internal::setMode(Mode::STA);
+    } else {
+        CHECK(esp_wifi_stop());
+        internal::setMode(Mode::OFF);
+    }
 
-void wifi::AP::handleStop(void *arg, esp_event_base_t, int32_t, void *) {
-    ESP_LOGD(TAG, "handleStop()");
-    auto ap = static_cast<AP *>(arg);
-    if (ap->onStop) ap->onStop();
+    esp_netif_destroy(interface);
 }
 
-void wifi::AP::handleConnect(void *arg, esp_event_base_t, int32_t, void *eventData) {
-    ESP_LOGD(TAG, "handleConnect()");
-    auto ap  = static_cast<AP *>(arg);
-    auto sta = STA(*static_cast<wifi_event_ap_staconnected_t *>(eventData));
+namespace {
+    void initDhcp(const net::IP &ip, const net::IP &netmask) {
+        ESP_LOGI(TAG, "%s()", __func__);
+
+        if (dhcpInit) {
+            // TODO do something with this; DHCP will fail silently if interface is already up
+            ESP_LOGI(TAG, "DHCP already initialized");
+            return;
+        }
+
+        esp_netif_ip_info_t ipInfo;
+        IP4_ADDR(&ipInfo.ip, ip[0], ip[1], ip[2], ip[3]);
+        IP4_ADDR(&ipInfo.gw, ip[0], ip[1], ip[2], ip[3]);
+        IP4_ADDR(&ipInfo.netmask, netmask[0], netmask[1], netmask[2], netmask[3]);
 
-    if (ap->stations.find(sta.aid) != ap->stations.end()) {
-        ESP_LOGE(TAG, "sta.aid already in use: %d", sta.aid);
-        ABORT("debug me");
+        CHECK(esp_netif_dhcps_stop(interface));
+        CHECK(esp_netif_set_ip_info(interface, &ipInfo));
+        CHECK(esp_netif_dhcps_start(interface));
+
+        dhcpInit = true;
     }
 
-    if (ap->onConnect) ap->onConnect(sta);
-    ap->stations.insert(std::pair<int, STA>(sta.aid, sta));
-}
+    void registerEventHandlers() {
+        ESP_LOGI(TAG, "%s()", __func__);
 
-void wifi::AP::handleDisconnect(void *arg, esp_event_base_t, int32_t, void *eventData) {
-    ESP_LOGD(TAG, "handleDisconnect()");
-    auto ap  = static_cast<AP *>(arg);
-    auto aid = (static_cast<wifi_event_ap_stadisconnected_t *>(eventData))->aid;
+        CHECK(esp_event_handler_instance_register(
+                WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, &handleConnect, nullptr, &connectHandler
+        ));
+        CHECK(esp_event_handler_instance_register(
+                WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, &handleDisconnect, nullptr, &disconnectHandler
+        ));
+    }
 
-    if (ap->onDisconnect) ap->onDisconnect(ap->stations[aid]);
-    ap->stations.erase(aid);
-}
+    void unregisterEventHandlers() {
+        ESP_LOGI(TAG, "%s()", __func__);
+
+        CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, &connectHandler));
+        CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, &disconnectHandler));
+        CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, &handleConnect));
+        CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, &handleDisconnect));
+    }
+
+    void handleConnect(void *, esp_event_base_t, int32_t, void *data) {
+        ESP_LOGI(TAG, "%s()", __func__);
 
-wifi::AP::STA::STA(wifi_event_ap_staconnected_t &eventData) : aid(eventData.aid), mac(eventData.mac) {}
+        if (ap::onConnect) {
+            auto sta = STA(*static_cast<wifi_event_ap_staconnected_t *>(data));
+            ap::onConnect(sta);
+        }
+    }
+
+    void handleDisconnect(void *, esp_event_base_t, int32_t, void *data) {
+        ESP_LOGI(TAG, "%s()", __func__);
+
+        if (ap::onDisconnect) {
+            auto sta = STA(*static_cast<wifi_event_ap_staconnected_t *>(data));
+            ap::onDisconnect(sta);
+        }
+    }
+}

+ 194 - 117
src/wifi/sta.cpp

@@ -1,167 +1,244 @@
 #include "kbf/wifi.h"
 
+#include <esp_log.h>
+#include <esp_wifi.h>
+
+#define KBF_INTERNAL
+
+#include "kbf/internal/wifi.h"
 #include "kbf/macros.h"
-#include "kbf/nvs.h"
+#include "kbf/exception.h"
+#include "kbf/rtos.h"
 
 using namespace kbf;
+using namespace kbf::wifi;
+using std::string;
+using std::vector;
 
-wifi::STA::STA() {
-    ESP_LOGI(TAG, "STA()");
-    config.pmf_cfg = {true, false};
-}
+constexpr const char *const TAG = "kbf::wifi::sta";
 
-wifi::STA::~STA() {
-    ESP_LOGI(TAG, "~STA()");
-    unregisterEventHandlers();
-}
+namespace {
+    string ssid;
+    string password;
+
+    int retryNum                           = 0;
+    int retryMax                           = 0;
+
+    bool         connected;
+    kbf::net::IP ip; // NOLINT(cert-err58-cpp)
+
+    kbf::rtos::EventGroup event; // NOLINT(cert-err58-cpp)
+    const int             CONNECT_FINISHED = 0;
 
-shared_ptr<wifi::STA> wifi::STA::create() {
-    return shared_ptr<STA>(new STA());
+    esp_netif_obj     *interface = nullptr;
+    wifi_sta_config_t staConfig  = {};
+
+    esp_event_handler_instance_t connectHandler    = {};
+    esp_event_handler_instance_t ipHandler         = {};
+    esp_event_handler_instance_t disconnectHandler = {};
+    esp_event_handler_instance_t scanDoneHandler   = {};
+
+    void handleConnect(void *, esp_event_base_t, int32_t, void *);
+
+    void handleDisconnect(void *, esp_event_base_t, int32_t, void *);
+
+    void handleIp(void *, esp_event_base_t, int32_t, void *);
+
+    void handleScanDone(void *, esp_event_base_t, int32_t, void *);
+
+    void registerEventHandlers();
+
+    void unregisterEventHandlers();
 }
 
-void wifi::STA::init() {
-    netif = esp_netif_create_default_wifi_sta();
+bool sta::connected() { return ::connected; }
+
+const string &sta::ssid() { return ::ssid; }
+
+const string &sta::password() { return ::password; }
+
+const net::IP &sta::ip() { return ::ip; }
+
+void (*sta::onConnect)() = nullptr;
+
+void (*sta::onIp)() = nullptr;
+
+void (*sta::onDisconnect)() = nullptr;
+
+void (*sta::onScanDone)(vector<APInfo> &apList, void *data) = nullptr;
+
+void sta::start() {
+    ESP_LOGI(TAG, "%s()", __func__);
+
+    if (internal::mode == Mode::STA || internal::mode == Mode::DUAL) throw exception::InvalidMode();
+
+    internal::init();
+    interface = esp_netif_create_default_wifi_sta();
     registerEventHandlers();
+
+    staConfig.pmf_cfg = {true, false};
+    wifi_config_t config = {.sta = staConfig};
+
+    if (internal::mode == Mode::OFF) {
+        CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
+        CHECK(esp_wifi_set_config(WIFI_IF_STA, &config));
+        CHECK(esp_wifi_start());
+        internal::setMode(Mode::STA);
+    } else {
+        CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
+        CHECK(esp_wifi_set_config(WIFI_IF_STA, &config));
+        internal::setMode(Mode::DUAL);
+    }
 }
 
+void sta::stop() {
+    ESP_LOGI(TAG, "%s()", __func__);
 
-void wifi::STA::registerEventHandlers() {
-    ESP_LOGD(TAG, "registerEventHandlers");
+    if (internal::mode != Mode::STA && internal::mode != Mode::DUAL) throw wifi::exception::InvalidMode();
 
-    CHECK(esp_event_handler_instance_register(
-            WIFI_EVENT, WIFI_EVENT_STA_START, &handleStart, this, &startHandler));
-    CHECK(esp_event_handler_instance_register(
-            WIFI_EVENT, WIFI_EVENT_STA_STOP, &handleStop, this, &stopHandler));
-    CHECK(esp_event_handler_instance_register(
-            WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &handleConnect, this, &connectHandler));
-    CHECK(esp_event_handler_instance_register(
-            WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &handleDisconnect, this, &disconnectHandler));
+    unregisterEventHandlers();
 
-    CHECK(esp_event_handler_instance_register(
-            IP_EVENT, IP_EVENT_STA_GOT_IP, &handleGotIp, this, &gotIpHandler));
-}
+    if (internal::mode == Mode::DUAL) {
+        CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
+        internal::setMode(Mode::AP);
+    } else {
+        CHECK(esp_wifi_stop());
+        internal::setMode(Mode::OFF);
+    }
 
-void wifi::STA::unregisterEventHandlers() {
-    ESP_LOGD(TAG, "unregisterEventHandlers");
-
-    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_STA_START, &startHandler));
-    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_STA_STOP, &stopHandler));
-    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &connectHandler));
-    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disconnectHandler));
-    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disconnectHandler));
-    CHECK(esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &gotIpHandler));
-
-    // TODO why are these necessary? shouldn't instance unregistration be enough?
-    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_START, &handleStart));
-    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_STOP, &handleStop));
-    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &handleConnect));
-    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &handleDisconnect));
-    CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &handleGotIp));
+    ::connected = false;
+    esp_netif_destroy(interface);
 }
 
-void wifi::STA::connect(const string &ssid, const string &password, int maxRetryAttempts) {
-    ESP_LOGI(TAG, "connect()");
+bool sta::connect(string ssid, string password, bool async, int maxAttempts) {
+    ESP_LOGI(TAG, "%s(%s, <omitted>, %d, %d)", __func__, ssid.c_str(), async, maxAttempts);
+
+    if (internal::mode != Mode::STA && internal::mode != Mode::DUAL) throw wifi::exception::InvalidMode();
 
-    wifi_config_t wifiConfig = {.sta = config};
-    std::copy(ssid.begin(), ssid.end() + 1, std::begin(wifiConfig.sta.ssid));
-    std::copy(password.begin(), password.end() + 1, std::begin(wifiConfig.sta.password));
     retryNum = 0;
-    retryMax = maxRetryAttempts;
+    retryMax = maxAttempts;
+
+    event.clear();
+
+    ::ssid     = std::move(ssid);
+    ::password = std::move(password);
 
-    ESP_LOGD(TAG, "SSID: \"%s\"; pass: \"%s\"; maxRetry = %d", wifiConfig.sta.ssid, wifiConfig.sta.password,
-             maxRetryAttempts);
+    std::copy(::ssid.begin(), ::ssid.end() + 1, std::begin(staConfig.ssid));
+    std::copy(::password.begin(), ::password.end() + 1, std::begin(staConfig.password));
 
-    // TODO check station_example_main.c; they use ESP_IF_WIFI_STA which doesn't compile in C++
-    CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifiConfig));
+    wifi_config_t config = {.sta = staConfig};
+    CHECK(esp_wifi_set_config(WIFI_IF_STA, &config));
     CHECK(esp_wifi_connect());
+
+    if (!async) {
+        // TODO set timeout for the case when we don't get an IP address
+        event.waitForBit(CONNECT_FINISHED);
+    }
+
+    return true;
 }
 
-void wifi::STA::disconnect() {
-    ESP_LOGI(TAG, "disconnect()");
-    retryMax = 0;
+void sta::disconnect() {
+    ESP_LOGI(TAG, "%s()", __func__);
+
+    retryMax    = 0;
+    ::connected = false;
     CHECK(esp_wifi_disconnect());
 }
 
-void wifi::STA::handleStart(void *, esp_event_base_t, int32_t, void *) {
-    ESP_LOGD(TAG, "handleStart()");
-    auto instance = getSTA();
-    if (instance->onStart) instance->onStart();
-}
+void sta::startScan(void *data) {
+    ESP_LOGI(TAG, "%s()", __func__);
 
-void wifi::STA::handleStop(void *, esp_event_base_t, int32_t, void *) {
-    ESP_LOGD(TAG, "handleStop()");
-    auto instance = getSTA();
-    if (instance->onStop) instance->onStop();
-}
+    if (!onScanDone) { ESP_LOGW(TAG, "%s: onScanDone not set", __func__); }
 
-void wifi::STA::handleConnect(void *, esp_event_base_t, int32_t, void *) {
-    ESP_LOGD(TAG, "handleConnect()");
-    auto instance = getSTA();
-    if (instance->onConnect) instance->onConnect();
+    CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &handleScanDone, data,
+                                              &scanDoneHandler));
+
+    CHECK(esp_wifi_scan_start(nullptr, false));
 }
 
-void wifi::STA::handleDisconnect(void *, esp_event_base_t, int32_t, void *) {
-    ESP_LOGD(TAG, "handleDisconnect()");
-    auto instance = getSTA();
+namespace {
+    void registerEventHandlers() {
+        ESP_LOGI(TAG, "%s()", __func__);
 
-    if (instance->retryNum++ < instance->retryMax) {
-        if (instance->onReconnecting && !instance->onReconnecting(instance->retryNum, instance->retryMax)) {
-            ESP_LOGI(TAG, "reconnect attempt cancelled");
-            if (instance->onDisconnect) instance->onDisconnect();
-        } else {
-            ESP_LOGI(TAG, "reconnecting, attempt %d / %d", instance->retryNum, instance->retryMax);
-            esp_wifi_connect();
-        }
-    } else {
-        ESP_LOGD(TAG, "disconnected");
-        if (instance->onDisconnect) instance->onDisconnect();
+        CHECK(esp_event_handler_instance_register(
+                WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &handleConnect, nullptr, &connectHandler));
+        CHECK(esp_event_handler_instance_register(
+                WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &handleDisconnect, nullptr, &disconnectHandler));
+        CHECK(esp_event_handler_instance_register(
+                IP_EVENT, IP_EVENT_STA_GOT_IP, &handleIp, nullptr, &ipHandler));
     }
-}
 
-void wifi::STA::handleScanDone(void *data, esp_event_base_t, int32_t, void *) {
-    ESP_LOGD(TAG, "handleScanDone()");
-    auto instance = getSTA();
+    void unregisterEventHandlers() {
+        ESP_LOGI(TAG, "%s()", __func__);
 
-    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &instance->scanDoneHandler));
-    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &instance->handleScanDone));
+        CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &connectHandler));
+        CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disconnectHandler));
+        CHECK(esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &ipHandler));
 
-    if (!instance->onScanDone) {
-        ESP_LOGW(TAG, "handleScanDone: onScanDone not set");
-        return;
+        // TODO these are supposed to be deprecated and should not be necessary
+        // however, handleDisconnect will be called on wifi stop unless unregistered; IDF bug?
+        CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &handleConnect));
+        CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &handleDisconnect));
+        CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &handleIp));
     }
 
-    uint16_t size;
-    esp_wifi_scan_get_ap_num(&size);
-    wifi_ap_record_t apRecords[size];
-    esp_wifi_scan_get_ap_records(&size, apRecords);
+    void handleConnect(void *, esp_event_base_t, int32_t, void *) {
+        ESP_LOGI(TAG, "%s()", __func__);
 
-    vector<APInfo> result;
+        ::connected = true;
+        if (sta::onConnect) sta::onConnect();
+    }
+
+    void handleDisconnect(void *, esp_event_base_t, int32_t, void *) {
+        ESP_LOGI(TAG, "%s()", __func__);
 
-    for (int i = 0; i < size; i++) {
-        APInfo apInfo = {reinterpret_cast<char *>(apRecords[i].ssid), apRecords[i].rssi};
-        result.push_back(apInfo);
+        if (retryNum++ < retryMax) {
+            ESP_LOGI(TAG, "reconnecting, attempt %d / %d", retryNum, retryMax);
+            CHECK(esp_wifi_connect());
+        } else {
+            ESP_LOGI(TAG, "disconnected");
+            ::connected = false;
+            if (sta::onDisconnect) sta::onDisconnect();
+            event.setBit(CONNECT_FINISHED);
+        }
     }
 
-    instance->onScanDone(result, data);
-}
+    void handleIp(void *, esp_event_base_t, int32_t, void *data) {
+        ESP_LOGI(TAG, "%s()", __func__);
 
-void wifi::STA::handleGotIp(void *, esp_event_base_t, int32_t, void *pEventData) {
-    ESP_LOGD(TAG, "handleGotIp()");
-    auto instance  = getSTA();
-    auto eventData = static_cast<ip_event_got_ip_t *>(pEventData);
+        auto eventData = static_cast<ip_event_got_ip_t *>(data);
+        ::ip = net::IP(eventData->ip_info.ip);
 
-    instance->m_ip = net::IP(eventData->ip_info.ip);
-    if (instance->onIp) instance->onIp();
-    instance->retryNum = 0;
-}
+        if (sta::onIp) sta::onIp();
+        retryNum = 0;
 
-void wifi::STA::startScan(void *data) {
-    ESP_LOGD(TAG, "startScan()");
+        event.setBit(CONNECT_FINISHED);
+    }
 
-    if (!onScanDone) { ESP_LOGW(TAG, "startScan: onScanDone not set"); }
+    void handleScanDone(void *data, esp_event_base_t, int32_t, void *) {
+        ESP_LOGI(TAG, "%s()", __func__);
 
-    CHECK(esp_event_handler_instance_register(
-            WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &handleScanDone, data, &scanDoneHandler));
+        CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &scanDoneHandler));
+        CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &::handleScanDone));
 
-    CHECK(esp_wifi_scan_start(nullptr, false));
-}
+        if (!sta::onScanDone) {
+            ESP_LOGW(TAG, "%s: onScanDone not set", __func__);
+            return;
+        }
+
+        uint16_t size;
+        esp_wifi_scan_get_ap_num(&size);
+        wifi_ap_record_t apRecords[size];
+        esp_wifi_scan_get_ap_records(&size, apRecords);
+
+        vector<APInfo> result;
+
+        for (int i = 0; i < size; i++) {
+            APInfo apInfo = {reinterpret_cast<char *>(apRecords[i].ssid), apRecords[i].rssi};
+            result.push_back(apInfo);
+        }
+
+        sta::onScanDone(result, data);
+    }
+}

+ 70 - 0
src/wifi/wifi.cpp

@@ -0,0 +1,70 @@
+#include "kbf/wifi.h"
+
+#include <esp_log.h>
+#include <esp_err.h>
+#include <esp_wifi.h>
+
+#define KBF_INTERNAL
+
+#include "kbf/internal/wifi.h"
+#include "kbf/nvs.h"
+#include "kbf/macros.h"
+
+using namespace kbf;
+using namespace kbf::wifi;
+using std::string;
+
+bool internal::initialized = false;
+
+Mode internal::mode = Mode::OFF;
+
+static constexpr const char *const TAG = "kbf::wifi";
+
+void internal::init() {
+    ESP_LOGI(TAG, "internal::%s()", __func__);
+
+    if (initialized) {
+        ESP_LOGI(TAG, "already initialized");
+        return;
+    }
+
+    CHECK(esp_netif_init());
+    CHECK(esp_event_loop_create_default());
+
+    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
+    auto err = esp_wifi_init(&cfg);
+    if (err == ESP_ERR_NVS_NOT_INITIALIZED) {
+        kbf::nvs::init();
+        CHECK(esp_wifi_init(&cfg));
+    } else {
+        CHECK(err);
+    }
+
+    initialized = true;
+}
+
+void internal::setMode(Mode m) {
+    ESP_LOGI(TAG, "internal::%s(%d)", __func__, (int) m);
+    internal::mode = m;
+}
+
+Mode wifi::mode() { return internal::mode; }
+
+void dual::start(string ssid, std::string password, net::IP ip, net::IP netmask) {
+    ESP_LOGI(TAG, "%s()", __func__);
+
+    sta::start();
+    ap::start(std::move(ssid), std::move(password), ip, netmask);
+}
+
+void wifi::stop() {
+    ESP_LOGI(TAG, "%s()", __func__);
+
+    // TODO stop all at once
+    if (internal::mode == Mode::STA || internal::mode == Mode::DUAL) {
+        sta::stop();
+    }
+    if (internal::mode == Mode::AP) {
+        ap::stop();
+    }
+}

+ 121 - 0
src/wifi_legacy/ap.cpp

@@ -0,0 +1,121 @@
+#include "kbf/wifi_legacy.h"
+
+#include "kbf/macros.h"
+
+using namespace kbf;
+
+wifi_legacy::AP::AP(string ssid, string password, net::IP ip, net::IP netmask) :
+        ssid(std::move(ssid)), password(std::move(password)), ip(ip), netmask(netmask) {
+    ESP_LOGI(TAG, "AP()");
+
+    std::copy(this->ssid.begin(), this->ssid.end() + 1, std::begin(config.ssid));
+    std::copy(this->password.begin(), this->password.end() + 1, std::begin(config.password));
+
+    config.ssid_len       = this->ssid.length();
+    config.max_connection = 5; // TODO use Kconfig
+    config.authmode       = WIFI_AUTH_WPA2_PSK; // TODO use AUTH_OPEN if no password is provided
+}
+
+wifi_legacy::AP::~AP() {
+    ESP_LOGI(TAG, "~AP()");
+    unregisterEventHandlers();
+}
+
+shared_ptr<wifi_legacy::AP>
+wifi_legacy::AP::create(const string &ssid, const string &password, const net::IP &ip, const net::IP &netmask) {
+    if (password.length() < 8) {
+        ABORT("WiFi password needs to be at least 8 characters long.");
+    }
+
+    auto ap = new AP(ssid, password, ip, netmask);
+    return shared_ptr<AP>(ap);
+}
+
+void wifi_legacy::AP::init() {
+    netif = esp_netif_create_default_wifi_ap();
+
+    setupDhcp();
+    registerEventHandlers();
+}
+
+void wifi_legacy::AP::setupDhcp() {
+    ESP_LOGD(TAG, "setupDhcp()");
+
+    // TODO this should only be run once, before bringing up the interface for the first time; if the interface is already up, DHCP seems to fail silently
+
+    esp_netif_ip_info_t ipInfo;
+    IP4_ADDR(&ipInfo.ip, ip[0], ip[1], ip[2], ip[3]);
+    IP4_ADDR(&ipInfo.gw, ip[0], ip[1], ip[2], ip[3]);
+    IP4_ADDR(&ipInfo.netmask, netmask[0], netmask[1], netmask[2], netmask[3]);
+    CHECK(esp_netif_dhcps_stop(netif));
+    CHECK(esp_netif_set_ip_info(netif, &ipInfo));
+    CHECK(esp_netif_dhcps_start(netif));
+}
+
+void wifi_legacy::AP::registerEventHandlers() {
+    ESP_LOGD(TAG, "registerEventHandlers()");
+    CHECK(esp_event_handler_instance_register(
+            WIFI_EVENT, WIFI_EVENT_AP_START, &handleStart, this, &startHandler
+    ));
+    CHECK(esp_event_handler_instance_register(
+            WIFI_EVENT, WIFI_EVENT_AP_STOP, &handleStop, this, &stopHandler
+    ));
+    CHECK(esp_event_handler_instance_register(
+            WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, &handleConnect, this, &connectHandler
+    ));
+    CHECK(esp_event_handler_instance_register(
+            WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, &handleDisconnect, this, &disconnectHandler
+    ));
+}
+
+void wifi_legacy::AP::unregisterEventHandlers() {
+    ESP_LOGD(TAG, "unregisterEventHandlers()");
+
+    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_AP_START, &startHandler));
+    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_AP_STOP, &stopHandler));
+    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, &connectHandler));
+    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, &disconnectHandler));
+
+    // TODO why are these necessary? shouldn't instance unregistration be enough?
+    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_AP_START, &handleStart));
+    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_AP_STOP, &handleStop));
+    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, &handleConnect));
+    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, &handleDisconnect));
+}
+
+void wifi_legacy::AP::handleStart(void *arg, esp_event_base_t, int32_t, void *) {
+    ESP_LOGD(TAG, "handleStart()");
+    auto ap = static_cast<AP *>(arg);
+    if (ap->onStart) ap->onStart();
+}
+
+void wifi_legacy::AP::handleStop(void *arg, esp_event_base_t, int32_t, void *) {
+    ESP_LOGD(TAG, "handleStop()");
+    auto ap = static_cast<AP *>(arg);
+    if (ap->onStop) ap->onStop();
+}
+
+void wifi_legacy::AP::handleConnect(void *arg, esp_event_base_t, int32_t, void *eventData) {
+    ESP_LOGD(TAG, "handleConnect()");
+    auto ap  = static_cast<AP *>(arg);
+    auto sta = STA(*static_cast<wifi_event_ap_staconnected_t *>(eventData));
+
+    if (ap->stations.find(sta.aid) != ap->stations.end()) {
+        ESP_LOGE(TAG, "sta.aid already in use: %d", sta.aid);
+        ABORT("debug me");
+    }
+
+    if (ap->onConnect) ap->onConnect(sta);
+    ap->stations.insert(std::pair<int, STA>(sta.aid, sta));
+}
+
+void wifi_legacy::AP::handleDisconnect(void *arg, esp_event_base_t, int32_t, void *eventData) {
+    ESP_LOGD(TAG, "handleDisconnect()");
+    auto ap  = static_cast<AP *>(arg);
+    auto aid = (static_cast<wifi_event_ap_stadisconnected_t *>(eventData))->aid;
+
+    if (ap->onDisconnect) ap->onDisconnect(ap->stations[aid]);
+    ap->stations.erase(aid);
+}
+
+wifi_legacy::AP::STA::STA(wifi_event_ap_staconnected_t &eventData) : aid(eventData.aid), mac(eventData.mac) {}

+ 10 - 10
src/wifi/driver.cpp → src/wifi_legacy/driver.cpp

@@ -1,4 +1,4 @@
-#include "kbf/wifi.h"
+#include "kbf/wifi_legacy.h"
 
 #include <utility>
 #include <memory>
@@ -17,7 +17,7 @@
 
 using std::shared_ptr;
 
-using namespace kbf::wifi;
+using namespace kbf::wifi_legacy;
 
 static shared_ptr<AP>  s_ap  = nullptr;
 static shared_ptr<STA> s_sta = nullptr;
@@ -43,7 +43,7 @@ static void init() {
     initialized = true;
 }
 
-void kbf::wifi::start(shared_ptr<AP> ap) {
+void kbf::wifi_legacy::start(shared_ptr<AP> ap) {
     ESP_LOGI(TAG, "starting AP mode");
 
     init();
@@ -57,7 +57,7 @@ void kbf::wifi::start(shared_ptr<AP> ap) {
     CHECK(esp_wifi_start());
 }
 
-void kbf::wifi::start(shared_ptr<STA> sta) {
+void kbf::wifi_legacy::start(shared_ptr<STA> sta) {
     ESP_LOGI(TAG, "starting STA mode");
 
     init();
@@ -75,7 +75,7 @@ void kbf::wifi::start(shared_ptr<STA> sta) {
     CHECK(esp_wifi_start());
 }
 
-void kbf::wifi::start(shared_ptr<AP> ap, shared_ptr<STA> sta) {
+void kbf::wifi_legacy::start(shared_ptr<AP> ap, shared_ptr<STA> sta) {
     ESP_LOGI(TAG, "starting dual mode");
 
     init();
@@ -94,7 +94,7 @@ void kbf::wifi::start(shared_ptr<AP> ap, shared_ptr<STA> sta) {
     CHECK(esp_wifi_start());
 }
 
-void kbf::wifi::stop() {
+void kbf::wifi_legacy::stop() {
     auto err = esp_wifi_stop();
     if (err == ESP_ERR_WIFI_NOT_INIT) {
         ESP_LOGW(TAG, "stop(): wifi not initialized");
@@ -113,7 +113,7 @@ void kbf::wifi::stop() {
     }
 }
 
-void kbf::wifi::stopAP() {
+void kbf::wifi_legacy::stopAP() {
     if (s_ap) {
         esp_netif_destroy(s_ap->netif);
         s_ap = nullptr;
@@ -126,14 +126,14 @@ void kbf::wifi::stopAP() {
     }
 }
 
-shared_ptr<kbf::wifi::AP> kbf::wifi::getAP() {
+shared_ptr<kbf::wifi_legacy::AP> kbf::wifi_legacy::getAP() {
     return s_ap;
 }
 
-shared_ptr<kbf::wifi::STA> kbf::wifi::getSTA() {
+shared_ptr<kbf::wifi_legacy::STA> kbf::wifi_legacy::getSTA() {
     return s_sta;
 }
 
-bool kbf::wifi::isRunning() {
+bool kbf::wifi_legacy::isRunning() {
     return s_sta != nullptr || s_ap != nullptr;
 }

+ 167 - 0
src/wifi_legacy/sta.cpp

@@ -0,0 +1,167 @@
+#include "kbf/wifi_legacy.h"
+
+#include "kbf/macros.h"
+#include "kbf/nvs.h"
+
+using namespace kbf;
+
+wifi_legacy::STA::STA() {
+    ESP_LOGI(TAG, "STA()");
+    config.pmf_cfg = {true, false};
+}
+
+wifi_legacy::STA::~STA() {
+    ESP_LOGI(TAG, "~STA()");
+    unregisterEventHandlers();
+}
+
+shared_ptr<wifi_legacy::STA> wifi_legacy::STA::create() {
+    return shared_ptr<STA>(new STA());
+}
+
+void wifi_legacy::STA::init() {
+    netif = esp_netif_create_default_wifi_sta();
+    registerEventHandlers();
+}
+
+
+void wifi_legacy::STA::registerEventHandlers() {
+    ESP_LOGD(TAG, "registerEventHandlers");
+
+    CHECK(esp_event_handler_instance_register(
+            WIFI_EVENT, WIFI_EVENT_STA_START, &handleStart, this, &startHandler));
+    CHECK(esp_event_handler_instance_register(
+            WIFI_EVENT, WIFI_EVENT_STA_STOP, &handleStop, this, &stopHandler));
+    CHECK(esp_event_handler_instance_register(
+            WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &handleConnect, this, &connectHandler));
+    CHECK(esp_event_handler_instance_register(
+            WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &handleDisconnect, this, &disconnectHandler));
+
+    CHECK(esp_event_handler_instance_register(
+            IP_EVENT, IP_EVENT_STA_GOT_IP, &handleGotIp, this, &gotIpHandler));
+}
+
+void wifi_legacy::STA::unregisterEventHandlers() {
+    ESP_LOGD(TAG, "unregisterEventHandlers");
+
+    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_STA_START, &startHandler));
+    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_STA_STOP, &stopHandler));
+    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &connectHandler));
+    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disconnectHandler));
+    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disconnectHandler));
+    CHECK(esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &gotIpHandler));
+
+    // TODO why are these necessary? shouldn't instance unregistration be enough?
+    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_START, &handleStart));
+    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_STOP, &handleStop));
+    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &handleConnect));
+    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &handleDisconnect));
+    CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &handleGotIp));
+}
+
+void wifi_legacy::STA::connect(const string &ssid, const string &password, int maxRetryAttempts) {
+    ESP_LOGI(TAG, "connect()");
+
+    wifi_config_t wifiConfig = {.sta = config};
+    std::copy(ssid.begin(), ssid.end() + 1, std::begin(wifiConfig.sta.ssid));
+    std::copy(password.begin(), password.end() + 1, std::begin(wifiConfig.sta.password));
+    retryNum = 0;
+    retryMax = maxRetryAttempts;
+
+    ESP_LOGD(TAG, "SSID: \"%s\"; pass: \"%s\"; maxRetry = %d", wifiConfig.sta.ssid, wifiConfig.sta.password,
+             maxRetryAttempts);
+
+    // TODO check station_example_main.c; they use ESP_IF_WIFI_STA which doesn't compile in C++
+    CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifiConfig));
+    CHECK(esp_wifi_connect());
+}
+
+void wifi_legacy::STA::disconnect() {
+    ESP_LOGI(TAG, "disconnect()");
+    retryMax = 0;
+    CHECK(esp_wifi_disconnect());
+}
+
+void wifi_legacy::STA::handleStart(void *, esp_event_base_t, int32_t, void *) {
+    ESP_LOGD(TAG, "handleStart()");
+    auto instance = getSTA();
+    if (instance->onStart) instance->onStart();
+}
+
+void wifi_legacy::STA::handleStop(void *, esp_event_base_t, int32_t, void *) {
+    ESP_LOGD(TAG, "handleStop()");
+    auto instance = getSTA();
+    if (instance->onStop) instance->onStop();
+}
+
+void wifi_legacy::STA::handleConnect(void *, esp_event_base_t, int32_t, void *) {
+    ESP_LOGD(TAG, "handleConnect()");
+    auto instance = getSTA();
+    if (instance->onConnect) instance->onConnect();
+}
+
+void wifi_legacy::STA::handleDisconnect(void *, esp_event_base_t, int32_t, void *) {
+    ESP_LOGD(TAG, "handleDisconnect()");
+    auto instance = getSTA();
+
+    if (instance->retryNum++ < instance->retryMax) {
+        if (instance->onReconnecting && !instance->onReconnecting(instance->retryNum, instance->retryMax)) {
+            ESP_LOGI(TAG, "reconnect attempt cancelled");
+            if (instance->onDisconnect) instance->onDisconnect();
+        } else {
+            ESP_LOGI(TAG, "reconnecting, attempt %d / %d", instance->retryNum, instance->retryMax);
+            esp_wifi_connect();
+        }
+    } else {
+        ESP_LOGD(TAG, "disconnected");
+        if (instance->onDisconnect) instance->onDisconnect();
+    }
+}
+
+void wifi_legacy::STA::handleScanDone(void *data, esp_event_base_t, int32_t, void *) {
+    ESP_LOGD(TAG, "handleScanDone()");
+    auto instance = getSTA();
+
+    CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &instance->scanDoneHandler));
+    CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &instance->handleScanDone));
+
+    if (!instance->onScanDone) {
+        ESP_LOGW(TAG, "handleScanDone: onScanDone not set");
+        return;
+    }
+
+    uint16_t size;
+    esp_wifi_scan_get_ap_num(&size);
+    wifi_ap_record_t apRecords[size];
+    esp_wifi_scan_get_ap_records(&size, apRecords);
+
+    vector<APInfo> result;
+
+    for (int i = 0; i < size; i++) {
+        APInfo apInfo = {reinterpret_cast<char *>(apRecords[i].ssid), apRecords[i].rssi};
+        result.push_back(apInfo);
+    }
+
+    instance->onScanDone(result, data);
+}
+
+void wifi_legacy::STA::handleGotIp(void *, esp_event_base_t, int32_t, void *pEventData) {
+    ESP_LOGD(TAG, "handleGotIp()");
+    auto instance  = getSTA();
+    auto eventData = static_cast<ip_event_got_ip_t *>(pEventData);
+
+    instance->m_ip = net::IP(eventData->ip_info.ip);
+    if (instance->onIp) instance->onIp();
+    instance->retryNum = 0;
+}
+
+void wifi_legacy::STA::startScan(void *data) {
+    ESP_LOGD(TAG, "startScan()");
+
+    if (!onScanDone) { ESP_LOGW(TAG, "startScan: onScanDone not set"); }
+
+    CHECK(esp_event_handler_instance_register(
+            WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &handleScanDone, data, &scanDoneHandler));
+
+    CHECK(esp_wifi_scan_start(nullptr, false));
+}

+ 1 - 0
test/CMakeLists.txt

@@ -5,6 +5,7 @@ idf_component_register(
         "driver"
         "esp-idf-lib"
         "rtos"
+        "wifi"
 
         INCLUDE_DIRS
         "."

+ 13 - 13
test/test_http.cpp

@@ -3,7 +3,7 @@
 #include <nlohmann_json/json.hpp>
 
 #include "kbf.h"
-#include "kbf/wifi.h"
+#include "kbf/wifi_legacy.h"
 #include "kbf/http/client.h"
 #include "kbf/http/server.h"
 #include "kbf/http/exception.h"
@@ -22,7 +22,7 @@ std::atomic<bool> asyncFinished = {false};
 std::atomic<int> state;
 
 TEST_CASE("HTTP GET, POST, 404, 405", "[kbf_http]") {
-    wifi::start();
+    wifi_legacy::start();
     auto server = http::Server();
     TEST_ASSERT_FALSE(server.isRunning())
 
@@ -82,11 +82,11 @@ TEST_CASE("HTTP GET, POST, 404, 405", "[kbf_http]") {
     TEST_ASSERT_EQUAL_STRING("bar", response->body.c_str());
 
     server.stop();
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 TEST_CASE("HTTP custom headers", "[kbf_http]") {
-    wifi::start();
+    wifi_legacy::start();
     auto server = http::Server();
 
     http::Response (*handleContentTypeTest)(const http::Request &, void *) = {[](const http::Request &request, void *) {
@@ -123,11 +123,11 @@ TEST_CASE("HTTP custom headers", "[kbf_http]") {
     TEST_ASSERT_EQUAL_STRING("OK", response->body.data());
 
     server.stop();
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 TEST_CASE("HTTP JSON request / response", "[kbf_http]") {
-    wifi::start();
+    wifi_legacy::start();
 
     static const string testKey   = "key";
     static const string testValue = "value";
@@ -162,11 +162,11 @@ TEST_CASE("HTTP JSON request / response", "[kbf_http]") {
 //    TEST_ASSERT_NULL(response->json());
 
     server.stop();
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 TEST_CASE("HTTP timeout", "[kbf_http]") {
-    wifi::start();
+    wifi_legacy::start();
 
     auto server = http::Server();
     http::Response (*handler)(const http::Request &, void *) = {[](const http::Request &request, void *) {
@@ -193,11 +193,11 @@ TEST_CASE("HTTP timeout", "[kbf_http]") {
     TEST_ASSERT_TRUE(caught);
     TEST_ASSERT_EQUAL(2, state);
 
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 TEST_CASE("HTTP onResponseSent", "[kbf_http]") {
-    wifi::start();
+    wifi_legacy::start();
     auto server = http::Server();
     state = 0;
 
@@ -220,11 +220,11 @@ TEST_CASE("HTTP onResponseSent", "[kbf_http]") {
     TEST_ASSERT_EQUAL(2, state);
 
     server.stop();
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 TEST_CASE("HTTP Client errors", "[kbf_http]") {
-    wifi::start();
+    wifi_legacy::start();
 
     auto server = http::Server();
     http::Response (*testGet)(const http::Request &, void *) = {[](const http::Request &request, void *) {
@@ -255,7 +255,7 @@ TEST_CASE("HTTP Client errors", "[kbf_http]") {
     }
     TEST_ASSERT_TRUE(caught);
 
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 TEST_CASE("HTTP CORS", "[notimplemented]") {

+ 5 - 5
test/test_https.cpp

@@ -1,7 +1,7 @@
 #include <nlohmann_json/json.hpp>
 
 #include "kbf.h"
-#include "kbf/wifi.h"
+#include "kbf/wifi_legacy.h"
 #include "kbf/http/server.h"
 #include "kbf/http/client.h"
 #include "kbf/rtos.h"
@@ -13,7 +13,7 @@ using namespace std;
 using nlohmann::json;
 
 TEST_CASE("HTTPS GET", "[kbf_http]") {
-    wifi::start();
+    wifi_legacy::start();
     auto server = http::Server();
     TEST_ASSERT_FALSE(server.isRunning())
 
@@ -31,13 +31,13 @@ TEST_CASE("HTTPS GET", "[kbf_http]") {
     TEST_ASSERT_EQUAL_STRING("OK", response->body.c_str());
 
     server.stop();
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 // TODO KBF-22 fix async test
 static rtos::EventGroup eventGroup;
 TEST_CASE("HTTPS async", "[broken]") {
-    wifi::start();
+    wifi_legacy::start();
 
     http::Response (*handleRequest)(const http::Request &, void *) = {[](const http::Request &request, void *) {
         auto response = http::Response("OK");
@@ -61,5 +61,5 @@ TEST_CASE("HTTPS async", "[broken]") {
     TEST_ASSERT_EQUAL(1, eventGroup.getBit(0));
 
     server.stop();
-    wifi::stop();
+    wifi_legacy::stop();
 }

+ 14 - 14
test/test_net.cpp

@@ -1,5 +1,5 @@
 #include <unity.h>
-#include <kbf/wifi.h>
+#include <kbf/wifi_legacy.h>
 #include <kbf/rtos.h>
 #include <kbf/uart.h>
 
@@ -81,7 +81,7 @@ static shared_ptr<kbf::UART> setupUart() {
 }
 
 void mdnsMaster() {
-    wifi::start(wifi::AP::create(KBF_TEST_MDNS_SSID, KBF_TEST_MDNS_PASS));
+    wifi_legacy::start(wifi_legacy::AP::create(KBF_TEST_MDNS_SSID, KBF_TEST_MDNS_PASS));
     auto mdns = new net::MDNS();
     mdns->setHostname(KBF_TEST_MDNS_MASTER);
 
@@ -92,12 +92,12 @@ void mdnsMaster() {
     TEST_ASSERT_EQUAL_STRING("192.168.4.2", ip->str().c_str());
 
     delete mdns;
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 void mdnsSlave() {
-    wifi::start();
-    auto sta = wifi::getSTA();
+    wifi_legacy::start();
+    auto sta = wifi_legacy::getSTA();
     eventGroup.clear();
 
     sta->onIp = {[]() {
@@ -120,15 +120,15 @@ void mdnsSlave() {
     sta->connect(KBF_TEST_MDNS_SSID, KBF_TEST_MDNS_PASS);
 
     eventGroup.waitForBit(0);
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 TEST_CASE_MULTIPLE_DEVICES("mDNS", "[kbf_net]", mdnsMaster, mdnsSlave)
 
 void mdnsServiceMaster() {
     eventGroup.clear();
-    wifi::start(wifi::AP::create(KBF_TEST_MDNS_SSID, KBF_TEST_MDNS_PASS));
-    wifi::getAP()->onDisconnect = {[](wifi::AP::STA &) {
+    wifi_legacy::start(wifi_legacy::AP::create(KBF_TEST_MDNS_SSID, KBF_TEST_MDNS_PASS));
+    wifi_legacy::getAP()->onDisconnect = {[](wifi_legacy::AP::STA &) {
         eventGroup.setBit(0);
     }};
 
@@ -140,17 +140,17 @@ void mdnsServiceMaster() {
 
     TEST_ASSERT_TRUE(eventGroup.waitForBit(0, 20000));
     delete mdns;
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 void mdnsServiceSlave() {
     eventGroup.clear();
-    wifi::start();
-    wifi::getSTA()->connect(KBF_TEST_MDNS_SSID, KBF_TEST_MDNS_PASS);
-    wifi::getSTA()->onIp         = {[]() {
+    wifi_legacy::start();
+    wifi_legacy::getSTA()->connect(KBF_TEST_MDNS_SSID, KBF_TEST_MDNS_PASS);
+    wifi_legacy::getSTA()->onIp         = {[]() {
         eventGroup.setBit(0);
     }};
-    wifi::getSTA()->onDisconnect = {[]() {
+    wifi_legacy::getSTA()->onDisconnect = {[]() {
         TEST_ASSERT_EQUAL_STRING("", "connection error");
     }};
 
@@ -194,7 +194,7 @@ void mdnsServiceSlave() {
     TEST_ASSERT_EQUAL_STRING(KBF_TEST_SERVICE3_INST, results[0].instanceName.c_str());
 
     delete mdns;
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 TEST_CASE_MULTIPLE_DEVICES("mDNS", "[kbf_net]", mdnsServiceMaster, mdnsServiceSlave)

+ 5 - 5
test/test_web_service.cpp

@@ -1,4 +1,4 @@
-#include <kbf/wifi.h>
+#include <kbf/wifi_legacy.h>
 #include <kbf/http/client.h>
 #include <kbf/web_service.h>
 
@@ -37,7 +37,7 @@ public:
 };
 
 TEST_CASE("WebService", "[kbf_web_service]") {
-    wifi::start();
+    wifi_legacy::start();
 
     auto webService = WebService();
     webService.controller<CounterController>();
@@ -66,7 +66,7 @@ TEST_CASE("WebService", "[kbf_web_service]") {
     TEST_ASSERT_EQUAL_STRING("bar", response->body.c_str());
 
     webService.stop();
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 class HeaderToParamMiddleware : public WebService::Middleware {
@@ -117,7 +117,7 @@ public:
 };
 
 TEST_CASE("WebService Middleware", "[kbf_web_service]") {
-    wifi::start();
+    wifi_legacy::start();
 
     auto webService = WebService();
     webService.controller<EchoController>();
@@ -136,5 +136,5 @@ TEST_CASE("WebService Middleware", "[kbf_web_service]") {
     TEST_ASSERT_EQUAL_STRING("oof", response->headers.at("X-Response").c_str());
 
     webService.stop();
-    wifi::stop();
+    wifi_legacy::stop();
 }

+ 217 - 0
test/wifi/test_wifi.cpp

@@ -0,0 +1,217 @@
+#include <atomic>
+#include <vector>
+
+#include "kbf.h"
+#include "kbf/wifi.h"
+#include "kbf/rtos.h"
+
+#include <unity.h>
+
+#define TEST_SSID   "kbf_test_wifi_ap"
+#define MASTER_SSID "kbf_test_wifi_master"
+#define SLAVE_SSID  "kbf_test_wifi_slave"
+#define WIFI_PASS   "Pas$w0Rd1337"
+
+using namespace kbf;
+using std::vector;
+
+static std::atomic<int> state{0};
+static rtos::EventGroup event; // NOLINT(cert-err58-cpp)
+
+static void singleMaster() {
+    state = 0;
+    event.clear();
+
+    wifi::ap::onConnect    = {[](wifi::STA &) { TEST_ASSERT_EQUAL(0, state++); }};
+    wifi::ap::onDisconnect = {[](wifi::STA &) {
+        TEST_ASSERT_EQUAL(1, state++);
+        event.setBit(0);
+    }};
+    wifi::ap::start(TEST_SSID, WIFI_PASS);
+
+    TEST_ASSERT_EQUAL_STRING(TEST_SSID, wifi::ap::ssid().c_str());
+    TEST_ASSERT_EQUAL_STRING(WIFI_PASS, wifi::ap::password().c_str());
+
+    event.waitForBit(0);
+    TEST_ASSERT_EQUAL(2, state);
+
+    kbf::sleep(100); // wait for slave tests to finish
+    wifi::stop();
+}
+
+static void singleSlave() {
+    state = 0;
+    event.clear();
+
+    wifi::sta::onConnect    = {[]() { TEST_ASSERT_EQUAL(0, state++); }};
+    wifi::sta::onIp         = {[]() {
+        TEST_ASSERT_EQUAL(1, state++);
+        event.setBit(0);
+    }};
+    wifi::sta::onDisconnect = {[]() { TEST_FAIL(); }};
+    wifi::sta::start();
+
+    wifi::sta::connect(TEST_SSID, WIFI_PASS);
+    TEST_ASSERT_EQUAL(2, state);
+    TEST_ASSERT_TRUE(wifi::sta::connected());
+    TEST_ASSERT_EQUAL_STRING(TEST_SSID, wifi::sta::ssid().c_str());
+    TEST_ASSERT_EQUAL_STRING(WIFI_PASS, wifi::sta::password().c_str());
+
+    event.waitForBit(0);
+    TEST_ASSERT_EQUAL(2, state);
+    wifi::stop();
+}
+
+TEST_CASE_MULTIPLE_DEVICES("WiFi AP <--> STA", "[kbf_wifi]", singleMaster, singleSlave);
+
+static void asyncMaster() {
+    state = 0;
+    event.clear();
+
+    wifi::ap::onConnect    = {[](wifi::STA &) { TEST_ASSERT_EQUAL(0, state++); }};
+    wifi::ap::onDisconnect = {[](wifi::STA &) {
+        TEST_ASSERT_EQUAL(1, state++);
+        event.setBit(0);
+    }};
+    wifi::ap::start(TEST_SSID, WIFI_PASS);
+
+    event.waitForBit(0);
+    TEST_ASSERT_EQUAL(2, state);
+
+    kbf::sleep(100); // wait for slave tests to finish
+    wifi::stop();
+}
+
+static void asyncSlave() {
+    state = 0;
+    event.clear();
+
+    wifi::sta::onConnect    = {[]() { TEST_ASSERT_EQUAL(0, state++); }};
+    wifi::sta::onIp         = {[]() {
+        TEST_ASSERT_EQUAL(1, state++);
+        event.setBit(0);
+    }};
+    wifi::sta::onDisconnect = {[]() { TEST_FAIL(); }};
+    wifi::sta::start();
+
+    wifi::sta::connect(TEST_SSID, WIFI_PASS, true);
+    TEST_ASSERT_EQUAL(0, state);
+
+    event.waitForBit(0);
+    TEST_ASSERT_EQUAL(2, state);
+    wifi::stop();
+}
+
+TEST_CASE_MULTIPLE_DEVICES("WiFi AP <--> STA async", "[kbf_wifi]", asyncMaster, asyncSlave);
+
+static void dualMaster() {
+    state = 0;
+    event.clear();
+
+    wifi::ap::onConnect     = {[](wifi::STA &) {
+        TEST_ASSERT_EQUAL(0, state++);
+        wifi::sta::connect(SLAVE_SSID, WIFI_PASS, true);
+    }};
+    wifi::sta::onConnect    = {[]() {
+        TEST_ASSERT_EQUAL(1, state++);
+        wifi::sta::disconnect();
+    }};
+    wifi::sta::onDisconnect = {[]() {
+        TEST_ASSERT_EQUAL(2, state++);
+    }};
+    wifi::ap::onDisconnect  = {[](wifi::STA &) {
+        TEST_ASSERT_EQUAL(3, state++);
+        event.setBit(0);
+    }};
+    wifi::dual::start(MASTER_SSID, WIFI_PASS);
+
+    event.waitForBit(0);
+    TEST_ASSERT_EQUAL(4, state);
+    wifi::stop();
+}
+
+static void dualSlave() {
+    state = 0;
+    event.clear();
+
+    wifi::sta::onConnect    = {[]() { TEST_ASSERT_EQUAL(0, state++); }};
+    wifi::ap::onConnect     = {[](wifi::STA &) { TEST_ASSERT_EQUAL(1, state++); }};
+    wifi::ap::onDisconnect  = {[](wifi::STA &) {
+        TEST_ASSERT_EQUAL(2, state++);
+        wifi::sta::disconnect();
+    }};
+    wifi::sta::onDisconnect = {[]() {
+        TEST_ASSERT_EQUAL(3, state++);
+        event.setBit(0);
+    }};
+    wifi::dual::start(SLAVE_SSID, WIFI_PASS);
+    wifi::sta::connect(MASTER_SSID, WIFI_PASS);
+
+    event.waitForBit(0);
+    TEST_ASSERT_EQUAL(4, state);
+    wifi::stop();
+}
+
+TEST_CASE_MULTIPLE_DEVICES("WiFi dual <--> dual", "[kbf_wifi]", dualMaster, dualSlave);
+
+static void scanMaster() {
+    event.clear();
+
+    wifi::ap::start(TEST_SSID, WIFI_PASS);
+    wifi::ap::onDisconnect = {[](wifi::STA &){ event.setBit(0); }};
+
+    event.waitForBit(0);
+    wifi::stop();
+}
+
+static void scanSlave() {
+    state = 0;
+    event.clear();
+
+    wifi::sta::onScanDone = {[](vector<wifi::APInfo> &apList, void* data) {
+        TEST_ASSERT_EQUAL(0, state++);
+        TEST_ASSERT_EQUAL_STRING("test", static_cast<char *>(data));
+
+        bool found = false;
+        for (const auto &ap : apList) {
+            if (ap.ssid == TEST_SSID) {
+                found = true;
+            }
+        }
+        TEST_ASSERT_TRUE(found);
+
+        wifi::sta::connect(TEST_SSID, WIFI_PASS);
+    }};
+    wifi::sta::onConnect = {[]() { event.setBit(0); }};
+
+    wifi::sta::start();
+    wifi::sta::startScan((void *) "test");
+
+    event.waitForBit(0);
+    TEST_ASSERT_EQUAL(1, state);
+    wifi::stop();
+}
+
+TEST_CASE_MULTIPLE_DEVICES("WiFi scan", "[kbf_wifi]", scanMaster, scanSlave);
+
+static void ipMaster() {
+    event.clear();
+
+    wifi::ap::start(TEST_SSID, WIFI_PASS, net::IP("192.168.100.1"));
+    wifi::ap::onDisconnect = {[](wifi::STA &){ event.setBit(0); }};
+
+    event.waitForBit(0);
+    wifi::stop();
+}
+
+static void ipSlave() {
+    wifi::sta::start();
+    wifi::sta::connect(TEST_SSID, WIFI_PASS);
+
+    auto ip = wifi::sta::ip().str();
+    TEST_ASSERT_EQUAL_STRING("192.168.100.2", ip.c_str());
+
+    wifi::stop();
+}
+
+TEST_CASE_MULTIPLE_DEVICES("WiFi custom IP address", "[kbf_wifi]", ipMaster, ipSlave);

+ 75 - 75
test/test_wifi.cpp → test/wifi/test_wifi_legacy.cpp

@@ -4,7 +4,7 @@
 #include <vector>
 
 #include <kbf.h>
-#include <kbf/wifi.h>
+#include <kbf/wifi_legacy.h>
 #include <kbf/rtos.h>
 #include <kbf/http/client.h>
 #include <kbf/web_service.h>
@@ -40,11 +40,11 @@ static void waitForState(int expectedState, int timeoutSeconds) {
     }
 }
 
-TEST_CASE("WiFi STA mode reconnect", "[kbf_wifi]") {
+TEST_CASE("WiFi legacy STA mode reconnect", "[kbf_wifi_legacy]") {
     using namespace kbf;
 
     state = 0;
-    auto sta = wifi::STA::create();
+    auto sta = wifi_legacy::STA::create();
     sta->onStart        = {[]() { TEST_ASSERT_EQUAL(0, state++); }};
     sta->onConnect      = fail;
     sta->onIp           = fail;
@@ -54,7 +54,7 @@ TEST_CASE("WiFi STA mode reconnect", "[kbf_wifi]") {
         return true;
     }};
     sta->onDisconnect   = {[]() { TEST_ASSERT_EQUAL(3, state++); }};
-    wifi::start(sta);
+    wifi_legacy::start(sta);
 
     waitForState(1, 1);
     TEST_ASSERT_EQUAL(1, state);
@@ -65,14 +65,14 @@ TEST_CASE("WiFi STA mode reconnect", "[kbf_wifi]") {
     kbf::sleep(5000);
     TEST_ASSERT_EQUAL(4, state);
 
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
-TEST_CASE("WiFi STA mode reconnect cancel", "[kbf_wifi]") {
+TEST_CASE("WiFi legacy STA mode reconnect cancel", "[kbf_wifi_legacy]") {
     using namespace kbf;
 
-    wifi::start(wifi::STA::create());
-    auto sta = wifi::getSTA();
+    wifi_legacy::start(wifi_legacy::STA::create());
+    auto sta = wifi_legacy::getSTA();
 
     state = 0;
     sta->onStart        = {[]() { TEST_ASSERT_EQUAL(0, state++); }};
@@ -95,25 +95,25 @@ TEST_CASE("WiFi STA mode reconnect cancel", "[kbf_wifi]") {
     kbf::sleep(5000);
     TEST_ASSERT_EQUAL(3, state);
 
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 void wifiSingleMaster() {
     using namespace kbf;
 
-    wifi::start(wifi::AP::create(KBF_TEST_WIFI_SSID, KBF_TEST_WIFI_PASS));
-    auto ap = wifi::getAP();
+    wifi_legacy::start(wifi_legacy::AP::create(KBF_TEST_WIFI_SSID, KBF_TEST_WIFI_PASS));
+    auto ap = wifi_legacy::getAP();
     TEST_ASSERT_EQUAL_STRING(KBF_TEST_WIFI_SSID, ap->ssid.c_str());
     TEST_ASSERT_EQUAL_STRING(KBF_TEST_WIFI_PASS, ap->password.c_str());
 
     state = 0;
     ap->onStart      = {[]() { TEST_ASSERT_EQUAL(0, state++); }};
-    ap->onConnect    = {[](wifi::AP::STA &) { TEST_ASSERT_EQUAL(1, state++); }};
-    ap->onDisconnect = {[](wifi::AP::STA &) { TEST_ASSERT_EQUAL(2, state++); }};
+    ap->onConnect    = {[](wifi_legacy::AP::STA &) { TEST_ASSERT_EQUAL(1, state++); }};
+    ap->onDisconnect = {[](wifi_legacy::AP::STA &) { TEST_ASSERT_EQUAL(2, state++); }};
     ap->onStop       = {[]() { TEST_ASSERT_EQUAL(3, state++); }};
 
     waitForState(3, 30);
-    wifi::stop();
+    wifi_legacy::stop();
 
     waitForState(4, 1);
     TEST_ASSERT_EQUAL(4, state);
@@ -122,8 +122,8 @@ void wifiSingleMaster() {
 void wifiSingleSlave() {
     using namespace kbf;
 
-    wifi::start(wifi::STA::create());
-    auto sta = wifi::getSTA();
+    wifi_legacy::start(wifi_legacy::STA::create());
+    auto sta = wifi_legacy::getSTA();
 
     state = 0;
     sta->onStart      = {[]() { TEST_ASSERT_EQUAL(0, state++); }};
@@ -144,91 +144,91 @@ void wifiSingleSlave() {
 
     waitForState(4, 1);
     TEST_ASSERT_EQUAL(4, state);
-    wifi::stop();
+    wifi_legacy::stop();
 
     waitForState(5, 1);
     TEST_ASSERT_EQUAL(5, state);
 }
 
-TEST_CASE_MULTIPLE_DEVICES("WiFi AP <--> STA", "[kbf_wifi]", wifiSingleMaster, wifiSingleSlave)
+TEST_CASE_MULTIPLE_DEVICES("WiFi legacy AP <--> STA", "[kbf_wifi_legacy]", wifiSingleMaster, wifiSingleSlave)
 
 void wifiDualMaster() {
     using namespace kbf;
 
-    auto ap  = wifi::AP::create(KBF_TEST_WIFI_DUAL_MASTER_SSID, KBF_TEST_WIFI_PASS);
-    auto sta = wifi::STA::create();
+    auto ap  = wifi_legacy::AP::create(KBF_TEST_WIFI_DUAL_MASTER_SSID, KBF_TEST_WIFI_PASS);
+    auto sta = wifi_legacy::STA::create();
 
     state = 0;
     ap->onStart       = {[]() {
         TEST_ASSERT_EQUAL(0, state++);
     }};
-    ap->onConnect     = {[](wifi::AP::STA &) {
+    ap->onConnect     = {[](wifi_legacy::AP::STA &) {
         TEST_ASSERT_EQUAL(1, state++);
-        auto sta = wifi::getSTA();
+        auto sta = wifi_legacy::getSTA();
         sta->connect(KBF_TEST_WIFI_DUAL_SLAVE_SSID, KBF_TEST_WIFI_PASS);
     }};
     sta->onConnect    = {[]() {
         TEST_ASSERT_EQUAL(2, state++);
-        auto sta = wifi::getSTA();
+        auto sta = wifi_legacy::getSTA();
         sta->disconnect();
     }};
     sta->onDisconnect = {[]() {
         TEST_ASSERT_EQUAL(3, state++);
     }};
-    ap->onDisconnect  = {[](wifi::AP::STA &) {
+    ap->onDisconnect  = {[](wifi_legacy::AP::STA &) {
         TEST_ASSERT_EQUAL(4, state++);
     }};
 
-    wifi::start(ap, sta);
+    wifi_legacy::start(ap, sta);
     waitForState(5, 30);
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 void wifiDualSlave() {
     using namespace kbf;
 
-    auto ap  = wifi::AP::create(KBF_TEST_WIFI_DUAL_SLAVE_SSID, KBF_TEST_WIFI_PASS);
-    auto sta = wifi::STA::create();
+    auto ap  = wifi_legacy::AP::create(KBF_TEST_WIFI_DUAL_SLAVE_SSID, KBF_TEST_WIFI_PASS);
+    auto sta = wifi_legacy::STA::create();
 
     state = 0;
     ap->onStart       = {[]() {
         TEST_ASSERT_EQUAL(0, state++);
-        auto sta = wifi::getSTA();
+        auto sta = wifi_legacy::getSTA();
         sta->connect(KBF_TEST_WIFI_DUAL_MASTER_SSID, KBF_TEST_WIFI_PASS);
     }};
     sta->onConnect    = {[]() {
         TEST_ASSERT_EQUAL(1, state++);
     }};
-    ap->onConnect     = {[](wifi::AP::STA &) {
+    ap->onConnect     = {[](wifi_legacy::AP::STA &) {
         TEST_ASSERT_EQUAL(2, state++);
     }};
-    ap->onDisconnect  = {[](wifi::AP::STA &) {
+    ap->onDisconnect  = {[](wifi_legacy::AP::STA &) {
         TEST_ASSERT_EQUAL(3, state++);
-        auto sta = wifi::getSTA();
+        auto sta = wifi_legacy::getSTA();
         sta->disconnect();
     }};
     sta->onDisconnect = {[]() {
         TEST_ASSERT_EQUAL(4, state++);
     }};
 
-    wifi::start(ap, sta);
+    wifi_legacy::start(ap, sta);
     waitForState(5, 30);
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
-TEST_CASE_MULTIPLE_DEVICES("WiFi AP+STA <--> AP+STA", "[kbf_wifi]", wifiDualMaster, wifiDualSlave)
+TEST_CASE_MULTIPLE_DEVICES("WiFi legacy AP+STA <--> AP+STA", "[kbf_wifi_legacy]", wifiDualMaster, wifiDualSlave)
 
 void wifiScanMaster() {
     using namespace kbf;
-    auto ap = wifi::AP::create(KBF_TEST_WIFI_SSID, KBF_TEST_WIFI_PASS);
+    auto ap = wifi_legacy::AP::create(KBF_TEST_WIFI_SSID, KBF_TEST_WIFI_PASS);
     state = 0;
-    ap->onDisconnect = {[](wifi::AP::STA &) {
+    ap->onDisconnect = {[](wifi_legacy::AP::STA &) {
         TEST_ASSERT_EQUAL(0, state++);
     }};
 
-    wifi::start(ap);
+    wifi_legacy::start(ap);
     waitForState(1, 30);
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 void wifiScanSlave() {
@@ -236,9 +236,9 @@ void wifiScanSlave() {
     using std::vector;
 
     state = 0;
-    wifi::start();
-    auto sta = wifi::getSTA();
-    sta->onScanDone = {[](vector<wifi::APInfo> &apList, void *data) {
+    wifi_legacy::start();
+    auto sta = wifi_legacy::getSTA();
+    sta->onScanDone = {[](vector<wifi_legacy::APInfo> &apList, void *data) {
         TEST_ASSERT_EQUAL(0, state++);
 
         TEST_ASSERT_EQUAL_STRING("foobar!", (char *) data);
@@ -252,7 +252,7 @@ void wifiScanSlave() {
             }
         }
         TEST_ASSERT_TRUE(found)
-        wifi::getSTA()->connect(KBF_TEST_WIFI_SSID, KBF_TEST_WIFI_PASS);
+        wifi_legacy::getSTA()->connect(KBF_TEST_WIFI_SSID, KBF_TEST_WIFI_PASS);
     }};
     sta->onConnect  = {[]() {
         TEST_ASSERT_EQUAL(1, state++);
@@ -260,10 +260,10 @@ void wifiScanSlave() {
 
     sta->startScan((void *) "foobar!");
     waitForState(2, 30);
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
-TEST_CASE_MULTIPLE_DEVICES("WiFi scan", "[kbf_wifi]", wifiScanMaster, wifiScanSlave)
+TEST_CASE_MULTIPLE_DEVICES("WiFi legacy scan", "[kbf_wifi_legacy]", wifiScanMaster, wifiScanSlave)
 
 static kbf::rtos::EventGroup eventGroup;
 
@@ -271,14 +271,14 @@ void wifiIpMaster() {
     using namespace kbf;
     eventGroup = rtos::EventGroup();
 
-    auto ap = wifi::AP::create(KBF_TEST_WIFI_SSID, KBF_TEST_WIFI_PASS, net::IP("192.168.1.1"));
-    ap->onDisconnect = {[](wifi::AP::STA &) {
+    auto ap = wifi_legacy::AP::create(KBF_TEST_WIFI_SSID, KBF_TEST_WIFI_PASS, net::IP("192.168.1.1"));
+    ap->onDisconnect = {[](wifi_legacy::AP::STA &) {
         eventGroup.setBit(0);
     }};
-    wifi::start(ap);
+    wifi_legacy::start(ap);
 
     eventGroup.waitForBit(0);
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 void wifiIpSlave() {
@@ -286,10 +286,10 @@ void wifiIpSlave() {
     using namespace std;
     eventGroup = rtos::EventGroup();
 
-    wifi::start();
-    auto sta = wifi::getSTA();
+    wifi_legacy::start();
+    auto sta = wifi_legacy::getSTA();
     sta->onIp = {[]() {
-        auto ip = wifi::getSTA()->ip();
+        auto ip = wifi_legacy::getSTA()->ip();
         eventGroup.setBit(0);
         TEST_ASSERT_EQUAL(192, ip[0]);
         TEST_ASSERT_EQUAL(168, ip[1]);
@@ -299,10 +299,10 @@ void wifiIpSlave() {
     sta->connect(KBF_TEST_WIFI_SSID, KBF_TEST_WIFI_PASS);
 
     eventGroup.waitForBit(0);
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
-TEST_CASE_MULTIPLE_DEVICES("WiFi custom IP address", "[kbf_wifi]", wifiIpMaster, wifiIpSlave)
+TEST_CASE_MULTIPLE_DEVICES("WiFi legacy custom IP address", "[kbf_wifi_legacy]", wifiIpMaster, wifiIpSlave)
 
 static kbf::http::Client *client;
 
@@ -310,30 +310,30 @@ void wifiModeSwitchMaster() {
     using namespace kbf;
     eventGroup = rtos::EventGroup();
 
-    auto ap = wifi::AP::create(
+    auto ap = wifi_legacy::AP::create(
             KBF_TEST_WIFI_DUAL_MASTER_SSID,
             KBF_TEST_WIFI_PASS,
             net::IP("192.168.1.1")
     );
-    ap->onConnect = {[](wifi::AP::STA &) {
-        wifi::getSTA()->connect(KBF_TEST_WIFI_DUAL_SLAVE_SSID, KBF_TEST_WIFI_PASS);
+    ap->onConnect = {[](wifi_legacy::AP::STA &) {
+        wifi_legacy::getSTA()->connect(KBF_TEST_WIFI_DUAL_SLAVE_SSID, KBF_TEST_WIFI_PASS);
     }};
 
     delete client;
     client = new http::Client();
 
-    auto sta = wifi::STA::create();
+    auto sta = wifi_legacy::STA::create();
     sta->onIp = {[]() {
-        wifi::stopAP();
+        wifi_legacy::stopAP();
         kbf::sleep(1000);
         auto response = client->get("http://192.168.2.1/");
         TEST_ASSERT_EQUAL(200, response->status);
         eventGroup.setBit(0);
     }};
 
-    wifi::start(ap, sta);
+    wifi_legacy::start(ap, sta);
     eventGroup.waitForBit(0);
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
 class TestController : public kbf::WebService::Controller {
@@ -353,23 +353,23 @@ void wifiModeSwitchSlave() {
     using namespace std;
     eventGroup = rtos::EventGroup();
 
-    auto ap = wifi::AP::create(
+    auto ap = wifi_legacy::AP::create(
             KBF_TEST_WIFI_DUAL_SLAVE_SSID,
             KBF_TEST_WIFI_PASS,
             net::IP("192.168.2.1")
     );
-    ap->onDisconnect = {[](wifi::AP::STA &) {
+    ap->onDisconnect = {[](wifi_legacy::AP::STA &) {
         eventGroup.setBit(1);
     }};
 
-    auto sta = wifi::STA::create();
+    auto sta = wifi_legacy::STA::create();
     sta->onReconnecting = {[](int, int) {
         return false;
     }};
     sta->onDisconnect   = {[]() {
         eventGroup.setBit(2);
     }};
-    wifi::start(ap, sta);
+    wifi_legacy::start(ap, sta);
 
     auto webService = WebService();
     webService.controller<TestController>();
@@ -379,22 +379,22 @@ void wifiModeSwitchSlave() {
 
     eventGroup.waitForBit(0);
     kbf::sleep(1000);
-    wifi::stop();
+    wifi_legacy::stop();
 }
 
-TEST_CASE_MULTIPLE_DEVICES("WiFi mode switching", "[kbf_wifi]", wifiModeSwitchMaster, wifiModeSwitchSlave)
+TEST_CASE_MULTIPLE_DEVICES("WiFi legacy mode switching", "[kbf_wifi_legacy]", wifiModeSwitchMaster, wifiModeSwitchSlave)
 
-TEST_CASE("WiFi state handling", "[kbf_wifi]") {
+TEST_CASE("WiFi legacy state handling", "[kbf_wifi_legacy]") {
     using namespace kbf;
 
-    wifi::stop();
-    TEST_ASSERT_FALSE(wifi::isRunning())
-    wifi::start();
+    wifi_legacy::stop();
+    TEST_ASSERT_FALSE(wifi_legacy::isRunning())
+    wifi_legacy::start();
     kbf::sleep(1000);
 
-    TEST_ASSERT_TRUE(wifi::isRunning())
-    wifi::stop();
+    TEST_ASSERT_TRUE(wifi_legacy::isRunning())
+    wifi_legacy::stop();
     kbf::sleep(1000);
-    TEST_ASSERT_FALSE(wifi::isRunning())
-    wifi::stop();
+    TEST_ASSERT_FALSE(wifi_legacy::isRunning())
+    wifi_legacy::stop();
 }

+ 190 - 0
test/wifi/test_wifi_modeswitch.cpp

@@ -0,0 +1,190 @@
+#include <vector>
+
+#include <esp_log.h>
+
+#include "kbf.h"
+#include "kbf/wifi.h"
+#include "kbf/rtos.h"
+#include "kbf/web_service.h"
+#include "kbf/http/client.h"
+#include "kbf/http/exception.h"
+
+#include <unity.h>
+
+#define MASTER_SSID   "kbf_test_wifi_master"
+#define SLAVE_SSID    "kbf_test_wifi_slave"
+#define WIFI_PASS     "Pas$w0Rd1337"
+#define MASTER_IP     "192.168.1.1"
+#define MASTER_AP     "http://192.168.1.1/ping"
+#define MASTER_STA    "http://192.168.2.2/ping"
+#define SLAVE_IP      "192.168.2.1"
+#define SLAVE_AP      "http://192.168.2.1/ping"
+#define SLAVE_STA     "http://192.168.1.2/ping"
+#define PING_RESPONSE "P0NG"
+
+#define PING 0
+
+using namespace kbf;
+using std::vector;
+
+static rtos::EventGroup event; // NOLINT(cert-err58-cpp)
+static string           lastPingID;
+
+class PingController : public WebService::Controller {
+public:
+    PingController() : Controller("/ping") {}
+
+protected:
+    http::Response get(const http::Request &request) override {
+        lastPingID = request.query.at("id");
+        ESP_LOGI("PingController", "%s", lastPingID.c_str());
+        event.setBit(PING);
+        return http::Response(PING_RESPONSE);
+    }
+};
+
+static auto client = new http::Client(); // NOLINT(cert-err58-cpp)
+
+static void pingOK(const string &url, int id) {
+    ESP_LOGI(__func__, "%d", id);
+
+    const auto response = client->get(url + "?id=" + std::to_string(id));
+    TEST_ASSERT_EQUAL(200, response->status);
+    TEST_ASSERT_EQUAL_STRING(PING_RESPONSE, response->body.c_str());
+
+    ESP_LOGI(__func__, "success");
+}
+
+static void pingFail(const string &url) {
+    ESP_LOGI(__func__, "start");
+    try {
+        client->get(url);
+    } catch (http::exception::ConnectionError &) {
+        ESP_LOGI(__func__, "success");
+        return;
+    }
+    TEST_FAIL();
+}
+
+static void waitForPing(int id) {
+    ESP_LOGI(__func__, "%d", id);
+    event.waitForBit(PING);
+    event.clear();
+    TEST_ASSERT_EQUAL_STRING(std::to_string(id).c_str(), lastPingID.c_str());
+}
+
+static void fail() {
+    TEST_FAIL();
+}
+
+static void fail(wifi::STA &) {
+    TEST_FAIL();
+}
+
+void master() {
+    constexpr const char *const TAG = __func__;
+    auto pingService = WebService();
+    pingService.controller<PingController>();
+
+    ESP_LOGI(TAG, "0 - starting AP");
+    wifi::ap::onDisconnect  = fail;
+    wifi::sta::onDisconnect = fail;
+    wifi::ap::start(MASTER_SSID, WIFI_PASS, net::IP(MASTER_IP));
+    TEST_ASSERT_EQUAL(wifi::Mode::AP, wifi::mode());
+    pingService.start();
+
+    ESP_LOGI(TAG, "1 - master is AP, slave is STA");
+    waitForPing(0);
+    pingOK(SLAVE_STA, 1);
+    pingFail(SLAVE_AP);
+
+    ESP_LOGI(TAG, "2 - master is AP, slave switches to DUAL");
+    waitForPing(2);
+    pingOK(SLAVE_STA, 3);
+    pingFail(SLAVE_AP);
+
+    ESP_LOGI(TAG, "3 - master switches to DUAL, slave is DUAL");
+    wifi::sta::start();
+    TEST_ASSERT_EQUAL(wifi::Mode::DUAL, wifi::mode());
+    wifi::sta::connect(SLAVE_SSID, WIFI_PASS);
+    pingOK(SLAVE_STA, 4);
+    pingOK(SLAVE_AP, 5);
+    waitForPing(6);
+    waitForPing(7);
+
+    ESP_LOGI(TAG, "4 - master switches to STA, slave is DUAL");
+    kbf::sleep(1000); // wait for slave to disable onDisconnect
+    wifi::ap::stop();
+    TEST_ASSERT_EQUAL(wifi::Mode::STA, wifi::mode());
+    pingOK(SLAVE_AP, 8);
+    waitForPing(9);
+    pingFail(SLAVE_STA);
+
+    ESP_LOGI(TAG, "5 - master is STA, slave switches to AP");
+    waitForPing(10);
+    pingOK(SLAVE_AP,  11);
+    pingFail(SLAVE_STA);
+
+    ESP_LOGI(TAG, "finished");
+    wifi::sta::onDisconnect = nullptr;
+    kbf::sleep(100); // wait for slave to disable onDisconnect
+
+    pingService.stop();
+    wifi::stop();
+    TEST_ASSERT_EQUAL(wifi::Mode::OFF, wifi::mode());
+}
+
+void slave() {
+    constexpr const char *const TAG = __func__;
+    auto pingService = WebService();
+    pingService.controller<PingController>();
+
+    ESP_LOGI(TAG, "0 - starting STA");
+    wifi::ap::onDisconnect  = fail;
+    wifi::sta::onDisconnect = fail;
+    wifi::sta::start();
+    TEST_ASSERT_EQUAL(wifi::Mode::STA, wifi::mode());
+    pingService.start();
+
+    ESP_LOGI(TAG, "1 - master: AP, slave is STA");
+    wifi::sta::connect(MASTER_SSID, WIFI_PASS);
+    pingOK(MASTER_AP, 0);
+    waitForPing(1);
+    pingFail(MASTER_STA);
+
+    ESP_LOGI(TAG, "2 - master is AP, slave switches to DUAL");
+    wifi::ap::start(SLAVE_SSID, WIFI_PASS, net::IP(SLAVE_IP));
+    TEST_ASSERT_EQUAL(wifi::Mode::DUAL, wifi::mode());
+    pingOK(MASTER_AP, 2);
+    waitForPing(3);
+    pingFail(MASTER_STA);
+
+    ESP_LOGI(TAG, "3 - master switches to DUAL, slave is DUAL");
+    waitForPing(4);
+    waitForPing(5);
+    pingOK(MASTER_STA, 6);
+    pingOK(MASTER_AP, 7);
+
+    ESP_LOGI(TAG, "4 - master switches to STA, slave is DUAL");
+    wifi::sta::onDisconnect = nullptr;
+    waitForPing(8);
+    pingOK(MASTER_STA, 9);
+    pingFail(MASTER_AP);
+
+    ESP_LOGI(TAG, "5 - master is STA, slave switches to AP");
+    wifi::sta::stop();
+    TEST_ASSERT_EQUAL(wifi::Mode::AP, wifi::mode());
+    pingOK(MASTER_STA, 10);
+    waitForPing(11);
+    pingFail(MASTER_AP);
+
+    ESP_LOGI(TAG, "finished");
+    wifi::ap::onDisconnect = nullptr;
+    kbf::sleep(1000); // wait for master to disable onDisconnect
+
+    pingService.stop();
+    wifi::stop();
+    TEST_ASSERT_EQUAL(wifi::Mode::OFF, wifi::mode());
+}
+
+TEST_CASE_MULTIPLE_DEVICES("WiFi mode switching", "[kbf_wifi]", master, slave);