SensESP 3.5.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 init_wifi();
52}
53
55 const String& config_path,
56 const std::vector<ClientSSIDConfig>& client_configs, const String& ap_ssid,
57 const String& ap_password)
58 : FileSystemSaveable{config_path}, Resettable(0) {
59 bool config_loaded = load();
60
61 if (!config_loaded) {
62 if (ap_ssid != "" && ap_password != "") {
63 this->ap_settings_.enabled_ = true;
64 this->ap_settings_.ssid_ = ap_ssid;
65 this->ap_settings_.password_ = ap_password;
66 } else {
67 this->ap_settings_.enabled_ = false;
68 }
69 }
70
71 if (!config_loaded) {
72 // Seed the preset client list, skipping incomplete entries and honoring
73 // the kMaxNumClientConfigs cap.
74 for (const ClientSSIDConfig& config : client_configs) {
76 break;
77 }
78 if (config.ssid_ == "" || config.password_ == "") {
79 continue;
80 }
81 client_settings_.push_back(config);
82 }
84 }
85
86 init_wifi();
87}
88
90 // Fill in the rest of the client settings array with empty configs
91 int num_fill = kMaxNumClientConfigs - client_settings_.size();
92 for (int i = 0; i < num_fill; i++) {
94 }
95
96 ESP_LOGD(__FILENAME__, "Enabling WiFi provisioner");
97
98 // Hate to do this, but Raspberry Pi AP setup is going to be much more
99 // complicated with enforced WPA2. BAD Raspberry Pi! BAD!
100 WiFi.setMinSecurity(WIFI_AUTH_WPA_PSK);
101
102 // Try setting hostname already here.
103 String hostname = SensESPBaseApp::get_hostname();
104 WiFi.setHostname(hostname.c_str());
105
106 // Start WiFi with a bogus SSID to initialize the network stack but
107 // don't connect to any network.
108 WiFi.begin("0", "0", 0, nullptr, false);
109
110 // If both saved AP settings and saved client settings
111 // are available, start in STA+AP mode.
112
113 if (this->ap_settings_.enabled_ && this->client_enabled_ == true) {
114 WiFi.mode(WIFI_AP_STA);
117 }
118
119 // If saved AP settings are available, use them.
120
121 else if (this->ap_settings_.enabled_) {
122 WiFi.mode(WIFI_AP);
124 }
125
126 // If saved client settings are available, use them.
127
128 else if (this->client_enabled_) {
129 WiFi.mode(WIFI_STA);
131 }
132
133 if (this->ap_settings_.enabled_ &&
134 this->ap_settings_.captive_portal_enabled_) {
135 dns_server_ = std::unique_ptr<DNSServer>(new DNSServer());
136
137 dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
138 dns_server_->start(53, "*", WiFi.softAPIP());
139
140 event_loop()->onRepeat(1, [this]() { dns_server_->processNextRequest(); });
141 }
142}
143
145 if (dns_server_) {
146 dns_server_->stop();
147 }
148
149 // Stop WiFi
150 WiFi.disconnect(true);
151}
152
154 String hostname = SensESPBaseApp::get_hostname();
155 WiFi.setHostname(hostname.c_str());
156
157 ESP_LOGI(__FILENAME__, "Starting access point %s",
158 ap_settings_.ssid_.c_str());
159
160 bool result =
161 WiFi.softAP(ap_settings_.ssid_.c_str(), ap_settings_.password_.c_str(),
163
164 if (!result) {
165 ESP_LOGE(__FILENAME__, "Failed to start access point.");
166 return;
167 }
168}
169
171 String hostname = SensESPBaseApp::get_hostname();
172 WiFi.setHostname(hostname.c_str());
173
174 // set up WiFi in regular STA (client) mode
175 auto reconnect_cb = [this]() {
176 static uint32_t attempt_num = 0;
177 static uint32_t current_config_idx = 0;
178 static int last_applied_idx = -1;
179
180 int num_configs = client_settings_.size();
181
182 if (WiFi.status() == WL_CONNECTED) {
183 attempt_num = 0;
184 current_config_idx = 0;
185 return;
186 }
187
188 // First check if any of the client settings are defined
189 if (num_configs == 0) {
190 ESP_LOGW(__FILENAME__,
191 "No client settings defined. Leaving WiFi client disconnected.");
192 return;
193 }
194
195 uint32_t prev_config_idx = current_config_idx;
196
197 ClientSSIDConfig config;
198
199 // Get next valid client config
200 for (current_config_idx = current_config_idx;
201 current_config_idx < prev_config_idx + num_configs;
202 current_config_idx++) {
203 config = client_settings_[current_config_idx % num_configs];
204 if (config.ssid_ != "" && config.password_ != "") {
205 break;
206 }
207 }
208
209 ESP_LOGD(__FILENAME__, "Current client config index: %d",
210 current_config_idx);
211 ESP_LOGD(__FILENAME__, "Attempt number: %d", attempt_num);
212 ESP_LOGD(__FILENAME__, "Config SSID: %s", config.ssid_.c_str());
213
214 // If no valid client config found, leave WiFi client disconnected
215 if (config.ssid_ == "" || config.password_ == "") {
216 ESP_LOGW(
217 __FILENAME__,
218 "No valid client settings found. Leaving WiFi client disconnected.");
219 return;
220 }
221
222 ESP_LOGI(__FILENAME__,
223 "Connecting to wifi SSID %s (connection attempt #%d).",
224 config.ssid_.c_str(), attempt_num);
225
226 if (!config.use_dhcp_) {
227 ESP_LOGI(__FILENAME__, "Using static IP address: %s",
228 config.ip_.toString().c_str());
229 // Arduino-ESP32 signature:
230 // WiFi.config(local_ip, gateway, subnet, dns1, dns2).
231 // The pre-refactor code passed (ip, dns, gateway, netmask) which
232 // silently set gateway=dns, subnet=gateway and dns1=netmask — a
233 // long-standing bug that broke every saved static-IP config.
234 WiFi.config(config.ip_, config.gateway_, config.netmask_,
235 config.dns_server_);
236 }
237 // The ESP32 STA rejects a new config (ESP_ERR_WIFI_STATE, "sta is
238 // connecting, cannot set config") while it is still connecting or
239 // auto-reconnecting to the previous SSID, so switching networks needs the
240 // in-progress attempt dropped first. Only disconnect when the target config
241 // actually changes; re-applying the same config (a single network, or a
242 // list with one valid slot) must leave a slow association running rather
243 // than restart it every cycle.
244 int applied_idx = static_cast<int>(current_config_idx % num_configs);
245 if (applied_idx != last_applied_idx) {
246 WiFi.disconnect(false);
247 }
248 WiFi.begin(config.ssid_.c_str(), config.password_.c_str());
249 last_applied_idx = applied_idx;
250 attempt_num++;
251 current_config_idx++; // Move to the next config for the next attempt
252 };
253
254 // Perform an initial connection without a delay.
255 reconnect_cb();
256
257 // Launch a separate onRepeat event to (re-)establish WiFi connection.
258 // Attempts are spaced 20 s apart: when staying on the same network this lets
259 // a slow association finish; when failing over it bounds how long a missing
260 // network is tried before the next configured AP.
261 event_loop()->onRepeat(20000, reconnect_cb);
262}
263
264bool WiFiProvisioner::to_json(JsonObject& root) {
265 JsonObject apSettingsJson = root["apSettings"].to<JsonObject>();
266 ap_settings_.as_json(apSettingsJson);
267
268 JsonObject clientSettingsJson = root["clientSettings"].to<JsonObject>();
269 clientSettingsJson["enabled"] = client_enabled_;
270 JsonArray clientConfigsJson = clientSettingsJson["settings"].to<JsonArray>();
271 int num_serialized = 0;
272 for (auto& config : client_settings_) {
273 if (num_serialized++ >= kMaxNumClientConfigs) {
274 break;
275 }
276 JsonObject clientConfigJson = clientConfigsJson.add<JsonObject>();
277 config.as_json(clientConfigJson);
278 }
279 return true;
280}
281
282bool WiFiProvisioner::from_json(const JsonObject& config) {
283 if (config["hostname"].is<String>()) {
284 // deal with the legacy Json format
285 String hostname = config["hostname"].as<String>();
286 SensESPBaseApp::get()->get_hostname_observable()->set(hostname);
287
288 if (config["ssid"].is<String>()) {
289 String ssid = config["ssid"].as<String>();
290 String password = config["password"].as<String>();
291
292 if (config["ap_mode"].is<String>()) {
293 if (config["ap_mode"].as<String>() == "Access Point" ||
294 config["ap_mode"].as<String>() == "Hotspot") {
295 ap_settings_ = {true, ssid, password};
296 } else {
297 ClientSSIDConfig client_settings = {ssid, password};
298 client_settings_.clear();
299 client_settings_.push_back(client_settings);
300 client_enabled_ = true;
301 }
302 }
303 }
304 } else {
305 // Either an empty config or a new-style config
306 if (config["apSettings"].is<JsonVariant>()) {
307 ap_settings_ = AccessPointSettings::from_json(config["apSettings"]);
308 } else {
310 }
311 if (config["clientSettings"].is<JsonVariant>()) {
312 const JsonObject& client_settings_json = config["clientSettings"];
313 client_enabled_ = client_settings_json["enabled"] | false;
314 client_settings_.clear();
315 const JsonArray& client_settings_json_array =
316 client_settings_json["settings"];
317 for (const JsonObject& cfg_json : client_settings_json_array) {
319 }
320 if (client_settings_.size() == 0) {
321 client_enabled_ = false;
322 }
323 }
324 }
325 // Fill in the rest of the client settings array with empty configs
326 while (client_settings_.size() < kMaxNumClientConfigs) {
328 }
329
330 return true;
331}
332
334 ESP_LOGI(__FILENAME__, "Resetting WiFi SSID settings");
335
336 clear();
337 WiFi.disconnect(true);
338 // On ESP32, disconnect does not erase previous credentials. Let's connect
339 // to a bogus network instead
340 WiFi.begin("0", "0", 0, nullptr, false);
341}
342
344 // Scan fails if WiFi is connecting. Disconnect to allow scanning.
345 if (WiFi.status() != WL_CONNECTED) {
346 ESP_LOGD(__FILENAME__,
347 "WiFi is not connected. Disconnecting to allow scanning.");
348 WiFi.disconnect();
349 }
350 ESP_LOGI(__FILENAME__, "Starting WiFi network scan");
351 int result = WiFi.scanNetworks(true);
352 if (result == WIFI_SCAN_FAILED) {
353 ESP_LOGE(__FILENAME__, "WiFi scan failed to start");
354 }
355}
356
358 std::vector<WiFiNetworkInfo>& ssid_list) {
359 int num_networks = WiFi.scanComplete();
360 if (num_networks == WIFI_SCAN_RUNNING) {
361 return WIFI_SCAN_RUNNING;
362 }
363 if (num_networks == WIFI_SCAN_FAILED) {
364 return WIFI_SCAN_FAILED;
365 }
366 ssid_list.clear();
367 for (int i = 0; i < num_networks; i++) {
368 WiFiNetworkInfo info(WiFi.SSID(i), WiFi.RSSI(i), WiFi.encryptionType(i),
369 WiFi.BSSID(i), WiFi.channel(i));
370 ssid_list.push_back(info);
371 }
372
373 return num_networks;
374}
375
376} // 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_
void init_wifi()
Bring the WiFi interface up from the current ap_settings_ / client_settings_ state....
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