SensESP 3.4.1-alpha
Universal Signal K sensor toolkit ESP32
Loading...
Searching...
No Matches
wifi_provisioner.cpp
Go to the documentation of this file.
1#include "sensesp.h"
2
3#include "wifi_provisioner.h"
4
5#include <esp_wifi.h>
6
7#include "sensesp_app.h"
8
9namespace sensesp {
10
11// Wifi config portal timeout (seconds). The smaller the value, the faster
12// the device will attempt to reconnect. If set too small, it might
13// become impossible to actually configure the Wifi settings in the captive
14// portal.
15#ifndef WIFI_CONFIG_PORTAL_TIMEOUT
16#define WIFI_CONFIG_PORTAL_TIMEOUT 180
17#endif
18
19// Network configuration logic:
20// 1. Use hard-coded hostname and WiFi credentials by default
21// 2. If the hostname or credentials have been changed in WiFiManager or
22// the web UI, use the updated values.
23// 3. If the hard-coded hostname is changed, use that instead of the saved one.
24// (But keep using the saved WiFi credentials!)
25
26WiFiProvisioner::WiFiProvisioner(const String& config_path,
27 const String& client_ssid,
28 const String& client_password,
29 const String& ap_ssid,
30 const String& ap_password)
31 : FileSystemSaveable{config_path}, Resettable(0) {
32 bool config_loaded = load();
33
34 if (!config_loaded) {
35 if (ap_ssid != "" && ap_password != "") {
36 this->ap_settings_.enabled_ = true;
37 this->ap_settings_.ssid_ = ap_ssid;
38 this->ap_settings_.password_ = ap_password;
39 } else {
40 this->ap_settings_.enabled_ = false;
41 }
42 }
43
44 if (!config_loaded && client_ssid != "" && client_password != "") {
45 ClientSSIDConfig preset_client_config = {client_ssid, client_password,
46 true};
47 client_settings_.push_back(preset_client_config);
48 client_enabled_ = true;
49 }
50
51 // Fill in the rest of the client settings array with empty configs
52 int num_fill = kMaxNumClientConfigs - client_settings_.size();
53 for (int i = 0; i < num_fill; i++) {
55 }
56
57 ESP_LOGD(__FILENAME__, "Enabling WiFi provisioner");
58
59 // Hate to do this, but Raspberry Pi AP setup is going to be much more
60 // complicated with enforced WPA2. BAD Raspberry Pi! BAD!
61 WiFi.setMinSecurity(WIFI_AUTH_WPA_PSK);
62
63 // Try setting hostname already here.
64 String hostname = SensESPBaseApp::get_hostname();
65 WiFi.setHostname(hostname.c_str());
66
67 // Start WiFi with a bogus SSID to initialize the network stack but
68 // don't connect to any network.
69 WiFi.begin("0", "0", 0, nullptr, false);
70
71 // If both saved AP settings and saved client settings
72 // are available, start in STA+AP mode.
73
74 if (this->ap_settings_.enabled_ && this->client_enabled_ == true) {
75 WiFi.mode(WIFI_AP_STA);
78 }
79
80 // If saved AP settings are available, use them.
81
82 else if (this->ap_settings_.enabled_) {
83 WiFi.mode(WIFI_AP);
85 }
86
87 // If saved client settings are available, use them.
88
89 else if (this->client_enabled_) {
90 WiFi.mode(WIFI_STA);
92 }
93
94 if (this->ap_settings_.enabled_ &&
95 this->ap_settings_.captive_portal_enabled_) {
96 dns_server_ = std::unique_ptr<DNSServer>(new DNSServer());
97
98 dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
99 dns_server_->start(53, "*", WiFi.softAPIP());
100
101 event_loop()->onRepeat(1, [this]() { dns_server_->processNextRequest(); });
102 }
103}
104
106 if (dns_server_) {
107 dns_server_->stop();
108 }
109
110 // Stop WiFi
111 WiFi.disconnect(true);
112}
113
115 String hostname = SensESPBaseApp::get_hostname();
116 WiFi.setHostname(hostname.c_str());
117
118 ESP_LOGI(__FILENAME__, "Starting access point %s",
119 ap_settings_.ssid_.c_str());
120
121 bool result =
122 WiFi.softAP(ap_settings_.ssid_.c_str(), ap_settings_.password_.c_str(),
124
125 if (!result) {
126 ESP_LOGE(__FILENAME__, "Failed to start access point.");
127 return;
128 }
129}
130
132 String hostname = SensESPBaseApp::get_hostname();
133 WiFi.setHostname(hostname.c_str());
134
135 // set up WiFi in regular STA (client) mode
136 auto reconnect_cb = [this]() {
137 static uint32_t attempt_num = 0;
138 static uint32_t current_config_idx = 0;
139 static int last_applied_idx = -1;
140
141 int num_configs = client_settings_.size();
142
143 if (WiFi.status() == WL_CONNECTED) {
144 attempt_num = 0;
145 current_config_idx = 0;
146 return;
147 }
148
149 // First check if any of the client settings are defined
150 if (num_configs == 0) {
151 ESP_LOGW(__FILENAME__,
152 "No client settings defined. Leaving WiFi client disconnected.");
153 return;
154 }
155
156 uint32_t prev_config_idx = current_config_idx;
157
158 ClientSSIDConfig config;
159
160 // Get next valid client config
161 for (current_config_idx = current_config_idx;
162 current_config_idx < prev_config_idx + num_configs;
163 current_config_idx++) {
164 config = client_settings_[current_config_idx % num_configs];
165 if (config.ssid_ != "" && config.password_ != "") {
166 break;
167 }
168 }
169
170 ESP_LOGD(__FILENAME__, "Current client config index: %d",
171 current_config_idx);
172 ESP_LOGD(__FILENAME__, "Attempt number: %d", attempt_num);
173 ESP_LOGD(__FILENAME__, "Config SSID: %s", config.ssid_.c_str());
174
175 // If no valid client config found, leave WiFi client disconnected
176 if (config.ssid_ == "" || config.password_ == "") {
177 ESP_LOGW(
178 __FILENAME__,
179 "No valid client settings found. Leaving WiFi client disconnected.");
180 return;
181 }
182
183 ESP_LOGI(__FILENAME__,
184 "Connecting to wifi SSID %s (connection attempt #%d).",
185 config.ssid_.c_str(), attempt_num);
186
187 if (!config.use_dhcp_) {
188 ESP_LOGI(__FILENAME__, "Using static IP address: %s",
189 config.ip_.toString().c_str());
190 // Arduino-ESP32 signature:
191 // WiFi.config(local_ip, gateway, subnet, dns1, dns2).
192 // The pre-refactor code passed (ip, dns, gateway, netmask) which
193 // silently set gateway=dns, subnet=gateway and dns1=netmask — a
194 // long-standing bug that broke every saved static-IP config.
195 WiFi.config(config.ip_, config.gateway_, config.netmask_,
196 config.dns_server_);
197 }
198 // The ESP32 STA rejects a new config (ESP_ERR_WIFI_STATE, "sta is
199 // connecting, cannot set config") while it is still connecting or
200 // auto-reconnecting to the previous SSID, so switching networks needs the
201 // in-progress attempt dropped first. Only disconnect when the target config
202 // actually changes; re-applying the same config (a single network, or a
203 // list with one valid slot) must leave a slow association running rather
204 // than restart it every cycle.
205 int applied_idx = static_cast<int>(current_config_idx % num_configs);
206 if (applied_idx != last_applied_idx) {
207 WiFi.disconnect(false);
208 }
209 WiFi.begin(config.ssid_.c_str(), config.password_.c_str());
210 last_applied_idx = applied_idx;
211 attempt_num++;
212 current_config_idx++; // Move to the next config for the next attempt
213 };
214
215 // Perform an initial connection without a delay.
216 reconnect_cb();
217
218 // Launch a separate onRepeat event to (re-)establish WiFi connection.
219 // Attempts are spaced 20 s apart: when staying on the same network this lets
220 // a slow association finish; when failing over it bounds how long a missing
221 // network is tried before the next configured AP.
222 event_loop()->onRepeat(20000, reconnect_cb);
223}
224
225bool WiFiProvisioner::to_json(JsonObject& root) {
226 JsonObject apSettingsJson = root["apSettings"].to<JsonObject>();
227 ap_settings_.as_json(apSettingsJson);
228
229 JsonObject clientSettingsJson = root["clientSettings"].to<JsonObject>();
230 clientSettingsJson["enabled"] = client_enabled_;
231 JsonArray clientConfigsJson = clientSettingsJson["settings"].to<JsonArray>();
232 int num_serialized = 0;
233 for (auto& config : client_settings_) {
234 if (num_serialized++ >= kMaxNumClientConfigs) {
235 break;
236 }
237 JsonObject clientConfigJson = clientConfigsJson.add<JsonObject>();
238 config.as_json(clientConfigJson);
239 }
240 return true;
241}
242
243bool WiFiProvisioner::from_json(const JsonObject& config) {
244 if (config["hostname"].is<String>()) {
245 // deal with the legacy Json format
246 String hostname = config["hostname"].as<String>();
247 SensESPBaseApp::get()->get_hostname_observable()->set(hostname);
248
249 if (config["ssid"].is<String>()) {
250 String ssid = config["ssid"].as<String>();
251 String password = config["password"].as<String>();
252
253 if (config["ap_mode"].is<String>()) {
254 if (config["ap_mode"].as<String>() == "Access Point" ||
255 config["ap_mode"].as<String>() == "Hotspot") {
256 ap_settings_ = {true, ssid, password};
257 } else {
258 ClientSSIDConfig client_settings = {ssid, password};
259 client_settings_.clear();
260 client_settings_.push_back(client_settings);
261 client_enabled_ = true;
262 }
263 }
264 }
265 } else {
266 // Either an empty config or a new-style config
267 if (config["apSettings"].is<JsonVariant>()) {
268 ap_settings_ = AccessPointSettings::from_json(config["apSettings"]);
269 } else {
271 }
272 if (config["clientSettings"].is<JsonVariant>()) {
273 const JsonObject& client_settings_json = config["clientSettings"];
274 client_enabled_ = client_settings_json["enabled"] | false;
275 client_settings_.clear();
276 const JsonArray& client_settings_json_array =
277 client_settings_json["settings"];
278 for (const JsonObject& cfg_json : client_settings_json_array) {
280 }
281 if (client_settings_.size() == 0) {
282 client_enabled_ = false;
283 }
284 }
285 }
286 // Fill in the rest of the client settings array with empty configs
287 while (client_settings_.size() < kMaxNumClientConfigs) {
289 }
290
291 return true;
292}
293
295 ESP_LOGI(__FILENAME__, "Resetting WiFi SSID settings");
296
297 clear();
298 WiFi.disconnect(true);
299 // On ESP32, disconnect does not erase previous credentials. Let's connect
300 // to a bogus network instead
301 WiFi.begin("0", "0", 0, nullptr, false);
302}
303
305 // Scan fails if WiFi is connecting. Disconnect to allow scanning.
306 if (WiFi.status() != WL_CONNECTED) {
307 ESP_LOGD(__FILENAME__,
308 "WiFi is not connected. Disconnecting to allow scanning.");
309 WiFi.disconnect();
310 }
311 ESP_LOGI(__FILENAME__, "Starting WiFi network scan");
312 int result = WiFi.scanNetworks(true);
313 if (result == WIFI_SCAN_FAILED) {
314 ESP_LOGE(__FILENAME__, "WiFi scan failed to start");
315 }
316}
317
319 std::vector<WiFiNetworkInfo>& ssid_list) {
320 int num_networks = WiFi.scanComplete();
321 if (num_networks == WIFI_SCAN_RUNNING) {
322 return WIFI_SCAN_RUNNING;
323 }
324 if (num_networks == WIFI_SCAN_FAILED) {
325 return WIFI_SCAN_FAILED;
326 }
327 ssid_list.clear();
328 for (int i = 0; i < num_networks; i++) {
329 WiFiNetworkInfo info(WiFi.SSID(i), WiFi.RSSI(i), WiFi.encryptionType(i),
330 WiFi.BSSID(i), WiFi.channel(i));
331 ssid_list.push_back(info);
332 }
333
334 return num_networks;
335}
336
337} // namespace sensesp
Storage object for WiFi access point settings.
static AccessPointSettings from_json(const JsonObject &json)
void as_json(JsonObject &doc)
Storage object for WiFi client settings.
static ClientSSIDConfig from_json(const JsonObject &json)
virtual bool clear() override
Delete the data from a persistent storage.
Definition saveable.cpp:71
virtual bool load() override
Load and populate the object from a persistent storage.
Definition saveable.cpp:8
Automatic calling of the reset() method when the device needs to be reset.
Definition resettable.h:20
static String get_hostname()
Get the current hostname.
static const std::shared_ptr< SensESPBaseApp > & get()
Get the singleton instance of the SensESPBaseApp.
WiFi network info storage class returned by scan results.
bool from_json(const JsonObject &config) override
AccessPointSettings ap_settings_
std::vector< ClientSSIDConfig > client_settings_
int16_t get_wifi_scan_results(std::vector< WiFiNetworkInfo > &ssid_list)
WiFiProvisioner(const String &config_path, const String &client_ssid="", const String &client_password="", const String &ap_ssid="", const String &ap_password="")
bool to_json(JsonObject &doc) override
std::unique_ptr< DNSServer > dns_server_
std::shared_ptr< reactesp::EventLoop > event_loop()
Definition sensesp.cpp:9
constexpr int kMaxNumClientConfigs