diff --git a/library.json b/library.json index 8ece952ee..de08388a2 100644 --- a/library.json +++ b/library.json @@ -40,7 +40,8 @@ }, { "owner": "alanswx", - "name": "ESPAsyncWiFiManager" + "name": "ESPAsyncWiFiManager", + "version": "^0.31" }, { "owner": "bblanchon", diff --git a/src/sensesp/controllers/system_status_controller.cpp b/src/sensesp/controllers/system_status_controller.cpp index 2165ab86d..9e5305906 100644 --- a/src/sensesp/controllers/system_status_controller.cpp +++ b/src/sensesp/controllers/system_status_controller.cpp @@ -2,21 +2,21 @@ namespace sensesp { -void SystemStatusController::set_input(WifiState new_value, +void SystemStatusController::set_input(WiFiState new_value, uint8_t input_channel) { // FIXME: If pointers to member functions would be held in an array, // this would be a simple array dereferencing switch (new_value) { - case WifiState::kWifiNoAP: + case WiFiState::kWifiNoAP: this->update_state(SystemStatus::kWifiNoAP); break; - case WifiState::kWifiDisconnected: + case WiFiState::kWifiDisconnected: this->update_state(SystemStatus::kWifiDisconnected); break; - case WifiState::kWifiConnectedToAP: + case WiFiState::kWifiConnectedToAP: this->update_state(SystemStatus::kWSDisconnected); break; - case WifiState::kWifiManagerActivated: + case WiFiState::kWifiManagerActivated: this->update_state(SystemStatus::kWifiManagerActivated); break; } diff --git a/src/sensesp/controllers/system_status_controller.h b/src/sensesp/controllers/system_status_controller.h index ba0e99186..7e4de9ad3 100644 --- a/src/sensesp/controllers/system_status_controller.h +++ b/src/sensesp/controllers/system_status_controller.h @@ -24,15 +24,15 @@ enum class SystemStatus { * set_wifi_* and set_ws_* methods to take the relevant action when such * an event occurs. */ -class SystemStatusController : public ValueConsumer, +class SystemStatusController : public ValueConsumer, public ValueConsumer, public ValueProducer { public: SystemStatusController() {} - /// ValueConsumer interface for ValueConsumer (Networking object + /// ValueConsumer interface for ValueConsumer (Networking object /// state updates) - virtual void set_input(WifiState new_value, + virtual void set_input(WiFiState new_value, uint8_t input_channel = 0) override; /// ValueConsumer interface for ValueConsumer /// (WSClient object state updates) diff --git a/src/sensesp/net/http_server.cpp b/src/sensesp/net/http_server.cpp index 115d4234f..282fc4fc5 100644 --- a/src/sensesp/net/http_server.cpp +++ b/src/sensesp/net/http_server.cpp @@ -136,6 +136,24 @@ HTTPServer::HTTPServer() : Startable(50) { server->on("/info", HTTP_GET, std::bind(&HTTPServer::handle_info, this, _1)); } + + +void HTTPServer::start() { + // only start the server if WiFi is connected + if (WiFi.status() == WL_CONNECTED) { + server->begin(); + debugI("HTTP server started"); + } else { + debugW("HTTP server not started, WiFi not connected"); + } + WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { + if (event == SYSTEM_EVENT_STA_GOT_IP) { + server->begin(); + debugI("HTTP server started"); + } + }); +} + void HTTPServer::handle_not_found(AsyncWebServerRequest* request) { debugD("NOT_FOUND: "); if (request->method() == HTTP_GET) { diff --git a/src/sensesp/net/http_server.h b/src/sensesp/net/http_server.h index 65dd9c4d3..6bd898516 100644 --- a/src/sensesp/net/http_server.h +++ b/src/sensesp/net/http_server.h @@ -16,7 +16,7 @@ class HTTPServer : public Startable { public: HTTPServer(); ~HTTPServer() { delete server; } - virtual void start() override { server->begin(); } + virtual void start() override; void handle_not_found(AsyncWebServerRequest* request); void handle_config(AsyncWebServerRequest* request); void handle_device_reset(AsyncWebServerRequest* request); diff --git a/src/sensesp/net/networking.cpp b/src/sensesp/net/networking.cpp index 09279cf2d..2b712a560 100644 --- a/src/sensesp/net/networking.cpp +++ b/src/sensesp/net/networking.cpp @@ -14,9 +14,12 @@ namespace sensesp { #define WIFI_CONFIG_PORTAL_TIMEOUT 180 #endif -bool should_save_config = false; - -void save_config_callback() { should_save_config = true; } +// Network configuration logic: +// 1. Use hard-coded hostname and WiFi credentials by default +// 2. If the hostname or credentials have been changed in WiFiManager or +// the web UI, use the updated values. +// 3. If the hard-coded hostname is changed, use that instead of the saved one. +// (But keep using the saved WiFi credentials!) Networking::Networking(String config_path, String ssid, String password, String hostname, const char* wifi_manager_password) @@ -24,30 +27,46 @@ Networking::Networking(String config_path, String ssid, String password, wifi_manager_password_{wifi_manager_password}, Startable(80), Resettable(0) { - this->output = WifiState::kWifiNoAP; + this->output = WiFiState::kWifiNoAP; preset_ssid = ssid; preset_password = password; preset_hostname = hostname; + default_hostname = hostname; + + load_configuration(); - if (!ssid.isEmpty()) { - debugI("Using hard-coded SSID %s and password", ssid.c_str()); - this->ap_ssid = ssid; - this->ap_password = password; - } else { - load_configuration(); + if (default_hostname != preset_hostname) { + // if the preset hostname has changed, use it instead of the loaded one + SensESPBaseApp::get()->get_hostname_observable()->set(preset_hostname); + default_hostname = preset_hostname; } + server = new AsyncWebServer(80); dns = new DNSServer(); - wifi_manager = new AsyncWiFiManager(server, dns); } void Networking::start() { debugD("Enabling Networking object"); + + // If we have preset or saved WiFi config, always use it. Otherwise, + // start WiFiManager. WiFiManager always starts the configuration portal + // instead of trying to connect. + if (ap_ssid != "" && ap_password != "") { + debugI("Using SSID %s", ap_ssid.c_str()); setup_saved_ssid(); + } else if (ap_ssid == "" && WiFi.status() != WL_CONNECTED && + wifi_manager_enabled_) { + debugI("Starting WiFiManager"); + setup_wifi_manager(); } - if (ap_ssid == "" && WiFi.status() != WL_CONNECTED) { + // otherwise, fall through and WiFi will remain disconnected +} + +void Networking::activate_wifi_manager() { + debugD("Activating WiFiManager"); + if (WiFi.status() != WL_CONNECTED) { setup_wifi_manager(); } } @@ -63,42 +82,69 @@ void Networking::setup_wifi_callbacks() { WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED); } +/** + * @brief Start WiFi using preset SSID and password. + */ void Networking::setup_saved_ssid() { - this->emit(WifiState::kWifiDisconnected); + this->emit(WiFiState::kWifiDisconnected); setup_wifi_callbacks(); - const char* hostname = SensESPBaseApp::get_hostname().c_str(); - - WiFi.setHostname(hostname); - - WiFi.begin(ap_ssid.c_str(), ap_password.c_str()); - - debugI("Connecting to wifi %s.", ap_ssid.c_str()); + String hostname = SensESPBaseApp::get_hostname(); + WiFi.setHostname(hostname.c_str()); + + auto reconnect_cb = [this]() { + if (WiFi.status() != WL_CONNECTED) { + debugI("Connecting to wifi SSID %s.", ap_ssid.c_str()); + WiFi.begin(ap_ssid.c_str(), ap_password.c_str()); + } + }; + + // Perform an initial connection without a delay. + reconnect_cb(); + + // Launch a separate onRepeat reaction to (re-)establish WiFi connection. + // Connecting is attempted only every 20 s to allow the previous connection + // attempt to complete even if the network is slow. + ReactESP::app->onRepeat(20000, reconnect_cb); } +/** + * This method gets called when WiFi is connected to the AP and has + * received an IP address. + */ void Networking::wifi_station_connected() { debugI("Connected to wifi, SSID: %s (signal: %d)", WiFi.SSID().c_str(), WiFi.RSSI()); debugI("IP address of Device: %s", WiFi.localIP().toString().c_str()); debugI("Default route: %s", WiFi.gatewayIP().toString().c_str()); debugI("DNS server: %s", WiFi.dnsIP().toString().c_str()); - this->emit(WifiState::kWifiConnectedToAP); + this->emit(WiFiState::kWifiConnectedToAP); } +/** + * This method gets called when WiFi is disconnected from the AP. + */ void Networking::wifi_station_disconnected() { debugI("Disconnected from wifi."); - this->emit(WifiState::kWifiDisconnected); + this->emit(WiFiState::kWifiDisconnected); } +/** + * @brief Start WiFi using WiFi Manager. + * + * If the setup process has been completed before, this method will start + * the WiFi connection using the saved SSID and password. Otherwise, it will + * start the WiFi Manager. + */ void Networking::setup_wifi_manager() { - should_save_config = false; + wifi_manager = new AsyncWiFiManager(server, dns); String hostname = SensESPBaseApp::get_hostname(); setup_wifi_callbacks(); // set config save notify callback - wifi_manager->setSaveConfigCallback(save_config_callback); + wifi_manager->setBreakAfterConfig(true); wifi_manager->setConfigPortalTimeout(WIFI_CONFIG_PORTAL_TIMEOUT); @@ -106,89 +152,91 @@ void Networking::setup_wifi_manager() { wifi_manager->setDebugOutput(false); #endif AsyncWiFiManagerParameter custom_hostname( - "hostname", "Set ESP Device custom hostname", hostname.c_str(), 20); + "hostname", "Set ESP32 device custom hostname", hostname.c_str(), 20); wifi_manager->addParameter(&custom_hostname); + wifi_manager->setTryConnectDuringConfigPortal(false); // Create a unique SSID for configuring each SensESP Device String config_ssid = SensESPBaseApp::get_hostname(); config_ssid = "Configure " + config_ssid; const char* pconfig_ssid = config_ssid.c_str(); - this->emit(WifiState::kWifiManagerActivated); + this->emit(WiFiState::kWifiManagerActivated); WiFi.setHostname(SensESPBaseApp::get_hostname().c_str()); - if (!wifi_manager->autoConnect(pconfig_ssid, wifi_manager_password_)) { - debugE("Failed to connect to wifi and config timed out. Restarting..."); - - this->emit(WifiState::kWifiDisconnected); - - ESP.restart(); + wifi_manager->startConfigPortal(pconfig_ssid, wifi_manager_password_); + + // WiFiManager attempts to connect to the new SSID, but that doesn't seem to + // work reliably. Instead, we'll just attempt to connect manually. + + bool connected = false; + this->ap_ssid = wifi_manager->getConfiguredSTASSID(); + this->ap_password = wifi_manager->getConfiguredSTAPassword(); + + // attempt to connect with the new SSID and password + if (this->ap_ssid != "" && this->ap_password != "") { + debugD("Attempting to connect to acquired SSID %s and password", + this->ap_ssid.c_str()); + WiFi.begin(this->ap_ssid.c_str(), this->ap_password.c_str()); + for (int i = 0; i < 20; i++) { + if (WiFi.status() == WL_CONNECTED) { + connected = true; + break; + } + delay(1000); + } } - debugI("Connected to wifi,"); - debugI("IP address of Device: %s", WiFi.localIP().toString().c_str()); - this->emit(WifiState::kWifiConnectedToAP); + // Only save the new configuration if we were able to connect to the new SSID. - if (should_save_config) { + if (connected) { String new_hostname = custom_hostname.getValue(); debugI("Got new custom hostname: %s", new_hostname.c_str()); SensESPBaseApp::get()->get_hostname_observable()->set(new_hostname); - this->ap_ssid = WiFi.SSID(); debugI("Got new SSID and password: %s", ap_ssid.c_str()); - this->ap_password = WiFi.psk(); save_configuration(); - debugW("Restarting in 500ms"); - ReactESP::app->onDelay(500, []() { ESP.restart(); }); - } -} - -static const char SCHEMA_PREFIX[] PROGMEM = R"({ -"type": "object", -"properties": { -)"; - -String get_property_row(String key, String title, bool readonly) { - String readonly_title = ""; - String readonly_property = ""; - - if (readonly) { - readonly_title = " (readonly)"; - readonly_property = ",\"readOnly\":true"; } - - return "\"" + key + "\":{\"title\":\"" + title + readonly_title + "\"," + - "\"type\":\"string\"" + readonly_property + "}"; + debugW("Restarting..."); + ESP.restart(); } String Networking::get_config_schema() { - String schema; - // If hostname is not set by SensESPAppBuilder::set_hostname() in main.cpp, - // then preset_hostname will be "SensESP", and should not be read-only in the - // Config UI. If preset_hostname is not "SensESP", then it was set in - // main.cpp, so it should be read-only. - bool hostname_preset = preset_hostname != "SensESP"; - return String(FPSTR(SCHEMA_PREFIX)) + - get_property_row("hostname", "ESP device hostname", hostname_preset) + - "}}"; + static const char kSchema[] = R"###({ + "type": "object", + "properties": { + "ssid": { "title": "WiFi SSID", "type": "string" }, + "password": { "title": "WiFi password", "type": "string", "format": "password" }, + "hostname": { "title": "Device hostname", "type": "string" } + } + })###"; + + return String(kSchema); } // FIXME: hostname should be saved in SensESPApp void Networking::get_configuration(JsonObject& root) { - // root["hostname"] = SensESPBaseApp::get_hostname(); + String hostname = SensESPBaseApp::get_hostname(); + root["hostname"] = hostname; + root["default_hostname"] = default_hostname; + root["ssid"] = ap_ssid; + root["password"] = ap_password; } bool Networking::set_configuration(const JsonObject& config) { - debugD("%s\n", __func__); - // if (!config.containsKey("hostname")) { - // return false; - //} - // - // if (preset_hostname == "SensESP") { - // SensESPBaseApp::get()->get_hostname_observable()->set( - // config["hostname"].as()); - //} + if (!config.containsKey("hostname")) { + return false; + } + + SensESPBaseApp::get()->get_hostname_observable()->set( + config["hostname"].as()); + + if (config.containsKey("default_hostname")) { + default_hostname = config["default_hostname"].as(); + } + ap_ssid = config["ssid"].as(); + ap_password = config["password"].as(); return true; } diff --git a/src/sensesp/net/networking.h b/src/sensesp/net/networking.h index 686510cce..305902dcc 100644 --- a/src/sensesp/net/networking.h +++ b/src/sensesp/net/networking.h @@ -7,6 +7,7 @@ #include #include +#include "sensesp/net/wifi_state.h" #include "sensesp/system/configurable.h" #include "sensesp/system/observablevalue.h" #include "sensesp/system/resettable.h" @@ -15,20 +16,13 @@ namespace sensesp { -enum class WifiState { - kWifiNoAP = 0, - kWifiDisconnected, - kWifiConnectedToAP, - kWifiManagerActivated -}; - /** * @brief Manages the ESP's connection to the Wifi network. */ class Networking : public Configurable, public Startable, public Resettable, - public ValueProducer { + public ValueProducer { public: Networking(String config_path, String ssid, String password, String hostname, const char* wifi_manager_password); @@ -39,6 +33,11 @@ class Networking : public Configurable, virtual bool set_configuration(const JsonObject& config) override final; virtual String get_config_schema() override; + void enable_wifi_manager(bool state) { + wifi_manager_enabled_ = state; + } + + void activate_wifi_manager(); protected: void setup_saved_ssid(); void setup_wifi_callbacks(); @@ -54,13 +53,22 @@ class Networking : public Configurable, // FIXME: DNSServer and AsyncWiFiManager could be instantiated in // respective methods to save some runtime memory DNSServer* dns; - AsyncWiFiManager* wifi_manager; + AsyncWiFiManager* wifi_manager = nullptr; + + bool wifi_manager_enabled_ = true; String ap_ssid = ""; String ap_password = ""; + + /// hardcoded values provided as constructor parameters String preset_ssid = ""; String preset_password = ""; String preset_hostname = ""; + + // original value of hardcoded hostname; used to detect changes + // in the hardcoded value + String default_hostname = ""; + const char* wifi_manager_password_; }; diff --git a/src/sensesp/net/wifi_state.h b/src/sensesp/net/wifi_state.h new file mode 100644 index 000000000..d23691214 --- /dev/null +++ b/src/sensesp/net/wifi_state.h @@ -0,0 +1,18 @@ +#ifndef SENSESP_NET_WIFI_STATE_H_ +#define SENSESP_NET_WIFI_STATE_H_ + +namespace sensesp { + +enum class WiFiState { + kWifiNoAP = 0, + kWifiDisconnected, + kWifiConnectedToAP, + kWifiManagerActivated +}; + +// alias WiFiState for backward compatibility +using WifiState = WiFiState; + +} // namespace sensesp + +#endif // SENSESP_NET_WIFI_STATE_H_ diff --git a/src/sensesp_app.cpp b/src/sensesp_app.cpp index 02a455f94..a14ed44d9 100644 --- a/src/sensesp_app.cpp +++ b/src/sensesp_app.cpp @@ -28,7 +28,7 @@ void SensESPApp::setup() { // create the networking object networking_ = - new Networking("/system/networking", ssid_, wifi_password_, + new Networking("/System/WiFi Settings", ssid_, wifi_password_, SensESPBaseApp::get_hostname(), wifi_manager_password_); if (ota_password_ != nullptr) { @@ -43,7 +43,7 @@ void SensESPApp::setup() { sk_delta_queue_ = new SKDeltaQueue(); // create the websocket client - this->ws_client_ = new WSClient("/system/sk", sk_delta_queue_, + this->ws_client_ = new WSClient("/System/Signal K Settings", sk_delta_queue_, sk_server_address_, sk_server_port_); // connect the system status controller