Open Model Railroad Network (OpenMRN)
Loading...
Searching...
No Matches
Esp32WiFiManager.cxx
Go to the documentation of this file.
1
35// Ensure we only compile this code for ESP32 MCUs
36#ifdef ESP_PLATFORM
37
38#include "Esp32WiFiManager.hxx"
40#include "openlcb/TcpDefs.hxx"
41#include "os/MDNS.hxx"
42#include "utils/FdUtils.hxx"
46
47#include <esp_log.h>
48#include <esp_sntp.h>
49#include <esp_system.h>
50#include <esp_wifi.h>
51#include <lwip/dns.h>
52#include <mdns.h>
53
54// ESP-IDF v4+ has a slightly different directory structure to previous
55// versions.
56#include <dhcpserver/dhcpserver.h>
57#include <esp_event.h>
58#include <esp_private/wifi.h>
59
60#if defined(CONFIG_IDF_TARGET_ESP32S2)
61#include <esp32s2/rom/crc.h>
62#elif defined(CONFIG_IDF_TARGET_ESP32C3)
63#include <esp32c3/rom/crc.h>
64#elif defined(CONFIG_IDF_TARGET_ESP32S3)
65#include <esp32s3/rom/crc.h>
66#else // default to ESP32
67#include <esp32/rom/crc.h>
68#endif // CONFIG_IDF_TARGET
69#include <stdint.h>
70
71using openlcb::NodeID;
73using openlcb::TcpAutoAddress;
74using openlcb::TcpClientConfig;
77using openlcb::TcpManualAddress;
78using std::string;
79using std::unique_ptr;
80
81#ifndef ESP32_WIFIMGR_SOCKETPARAMS_LOG_LEVEL
84#define ESP32_WIFIMGR_SOCKETPARAMS_LOG_LEVEL INFO
85#endif
86
87#ifndef ESP32_WIFIMGR_MDNS_LOOKUP_LOG_LEVEL
90#define ESP32_WIFIMGR_MDNS_LOOKUP_LOG_LEVEL INFO
91#endif
92
93// Start of global namespace block.
94
95// These must be declared *OUTSIDE* the openmrn_arduino namespace in order to
96// be visible in the MDNS.cxx code.
97
100void mdns_publish(const char *name, const char *service, uint16_t port);
101
103void mdns_unpublish(const char *name, const char *service);
104
112void split_mdns_service_name(string *service_name, string *protocol_name);
113
114// End of global namespace block.
115
116namespace openmrn_arduino
117{
118// When running on a unicore (ESP32-S2 or ESP32-C3) use a lower priority so the
119// task will run in co-op mode with loop. When running on a multi-core SoC we
120// can use a higher priority for the background task.
121#if CONFIG_FREERTOS_UNICORE
124static constexpr UBaseType_t EXECUTOR_TASK_PRIORITY = 1;
125#else // multi-core
128static constexpr UBaseType_t EXECUTOR_TASK_PRIORITY = 3;
129#endif // CONFIG_FREERTOS_UNICORE
130
132static constexpr uint32_t EXECUTOR_TASK_STACK_SIZE = 5120L;
133
135static constexpr uint32_t WIFI_CONNECT_CHECK_INTERVAL_SEC = 5;
136
138static constexpr uint32_t HUB_STARTUP_DELAY_USEC = MSEC_TO_USEC(50);
139
141static constexpr uint32_t TASK_SHUTDOWN_DELAY_USEC = MSEC_TO_USEC(1);
142
145static constexpr int WIFI_CONNECTED_BIT = BIT0;
146
149static constexpr int WIFI_GOTIP_BIT = BIT1;
150
155static constexpr uint8_t MAX_CONNECTION_CHECK_ATTEMPTS = 36;
156
158class Esp32SocketParams : public DefaultSocketClientParams
159{
160public:
161 Esp32SocketParams(
162 int fd, const TcpClientConfig<TcpClientDefaultParams> &cfg)
163 : configFd_(fd)
164 , cfg_(cfg)
165 {
166 // set the parameters on the parent class, all others are loaded
167 // on-demand.
168 mdnsService_ = cfg_.auto_address().service_name().read(configFd_);
169 staticHost_ = cfg_.manual_address().ip_address().read(configFd_);
170 staticPort_ = CDI_READ_TRIMMED(cfg_.manual_address().port, configFd_);
171 }
172
174 SocketClientParams::SearchMode search_mode() override
175 {
176 return (SocketClientParams::SearchMode)CDI_READ_TRIMMED(cfg_.search_mode, configFd_);
177 }
178
182 string mdns_host_name() override
183 {
184 return cfg_.auto_address().host_name().read(configFd_);
185 }
186
189 bool enable_last() override
190 {
191 return CDI_READ_TRIMMED(cfg_.reconnect, configFd_);
192 }
193
197 string last_host_name() override
198 {
199 return cfg_.last_address().ip_address().read(configFd_);
200 }
201
203 int last_port() override
204 {
205 return CDI_READ_TRIMMED(cfg_.last_address().port, configFd_);
206 }
207
212 void set_last(const char *hostname, int port) override
213 {
214 cfg_.last_address().ip_address().write(configFd_, hostname);
215 cfg_.last_address().port().write(configFd_, port);
216 }
217
218 void log_message(LogMessage id, const string &arg) override
219 {
220 switch (id)
221 {
222 case CONNECT_RE:
223 LOG(INFO, "[Uplink] Reconnecting to %s.", arg.c_str());
224 break;
225 case MDNS_SEARCH:
226 LOG(ESP32_WIFIMGR_SOCKETPARAMS_LOG_LEVEL,
227 "[Uplink] Starting mDNS searching for %s.",
228 arg.c_str());
229 break;
230 case MDNS_NOT_FOUND:
231 LOG(ESP32_WIFIMGR_SOCKETPARAMS_LOG_LEVEL,
232 "[Uplink] mDNS search failed.");
233 break;
234 case MDNS_FOUND:
235 LOG(ESP32_WIFIMGR_SOCKETPARAMS_LOG_LEVEL,
236 "[Uplink] mDNS search succeeded.");
237 break;
238 case CONNECT_MDNS:
239 LOG(INFO, "[Uplink] mDNS connecting to %s.", arg.c_str());
240 break;
241 case CONNECT_MANUAL:
242 LOG(INFO, "[Uplink] Connecting to %s.", arg.c_str());
243 break;
244 case CONNECT_FAILED_SELF:
245 LOG(ESP32_WIFIMGR_SOCKETPARAMS_LOG_LEVEL,
246 "[Uplink] Rejecting attempt to connect to localhost.");
247 break;
248 case CONNECTION_LOST:
249 LOG(INFO, "[Uplink] Connection lost.");
250 break;
251 default:
252 // ignore the message
253 break;
254 }
255 }
256
259 bool disallow_local() override
260 {
261 return true;
262 }
263
264private:
265 const int configFd_;
266 const TcpClientConfig<TcpClientDefaultParams> cfg_;
267};
268
269// With this constructor being used the Esp32WiFiManager will manage the
270// WiFi connection, mDNS system and the hostname of the ESP32.
271Esp32WiFiManager::Esp32WiFiManager(const char *station_ssid
272 , const char *station_password, openlcb::SimpleStackBase *stack
273 , const WiFiConfiguration &cfg, wifi_mode_t wifi_mode
274 , uint8_t connection_mode, const char *hostname_prefix
275 , const char *sntp_server, const char *timezone, bool sntp_enabled
276 , uint8_t softap_channel, wifi_auth_mode_t softap_auth_mode
277 , const char *softap_ssid, const char *softap_password)
278 : DefaultConfigUpdateListener(), Service(&executor_)
279 , hostname_(hostname_prefix)
280 , ssid_(station_ssid), password_(station_password), cfg_(cfg)
281 , stack_(stack), wifiMode_(wifi_mode), softAPChannel_(softap_channel)
282 , softAPAuthMode_(softap_auth_mode), softAPName_(softap_ssid)
283 , softAPPassword_(softap_password), sntpEnabled_(sntp_enabled)
284 , sntpServer_(sntp_server), timeZone_(timezone)
285 , connectionMode_(connection_mode)
286{
287 // Extend the capacity of the hostname to make space for the node-id and
288 // underscore.
289 hostname_.reserve(MAX_HOSTNAME_LENGTH);
290
291 // Generate the hostname for the ESP32 based on the provided node id.
292 // node_id : 0x050101011425
293 // hostname_ : esp32_050101011425
294 NodeID node_id = stack_->node()->node_id();
295 hostname_.append(uint64_to_string_hex(node_id, 0));
296
297 // The maximum length hostname for the ESP32 is 32 characters so truncate
298 // when necessary. Reference to length limitation:
299 // https://github.com/espressif/esp-idf/blob/master/components/tcpip_adapter/include/tcpip_adapter.h#L611
300 if (hostname_.length() > MAX_HOSTNAME_LENGTH)
301 {
302 LOG(WARNING, "ESP32 hostname is too long, original hostname:%s",
303 hostname_.c_str());
304 hostname_.resize(MAX_HOSTNAME_LENGTH);
305 LOG(WARNING, "truncated hostname:%s", hostname_.c_str());
306 }
307
308 // Release any extra capacity allocated for the hostname.
309 hostname_.shrink_to_fit();
310}
311
312Esp32WiFiManager::~Esp32WiFiManager()
313{
314 // Remove our event listeners from the event loop, note that we do not stop
315 // the event loop.
316 esp_event_handler_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID
317 , &Esp32WiFiManager::process_idf_event);
318 esp_event_handler_unregister(IP_EVENT, ESP_EVENT_ANY_ID
319 , &Esp32WiFiManager::process_idf_event);
320
321 // shutdown the background task executor.
322 executor_.shutdown();
323
324 // cleanup event group
325 vEventGroupDelete(wifiStatusEventGroup_);
326
327 // cleanup internal vectors/maps
328 ssidScanResults_.clear();
329 mdnsDeferredPublish_.clear();
330}
331
332void Esp32WiFiManager::display_configuration()
333{
334 static const char * const wifiModes[] =
335 {
336 "Off",
337 "Station Only",
338 "SoftAP Only",
339 "Station and SoftAP",
340 "Unknown"
341 };
342
343 LOG(INFO, "[WiFi] Configuration Settings:");
344 LOG(INFO, "[WiFi] Mode:%s (%d)", wifiModes[wifiMode_], wifiMode_);
345 LOG(INFO, "[WiFi] Station SSID:'%s'", ssid_.c_str());
346 LOG(INFO, "[WiFi] SoftAP SSID:'%s'", softAPName_.c_str());
347 LOG(INFO, "[WiFi] Hostname:%s", hostname_.c_str());
348 LOG(INFO, "[WiFi] SNTP Enabled:%s, Server:%s, TimeZone:%s",
349 sntpEnabled_ ? "true" : "false", sntpServer_.c_str(),
350 timeZone_.c_str());
351}
352
353#ifndef CONFIG_FREERTOS_UNICORE
358static void thread_entry(void *param)
359{
360 // donate our task to the executor.
361 static_cast<Executor<1> *>(param)->thread_body();
362 vTaskDelete(nullptr);
363}
364#endif // !CONFIG_FREERTOS_UNICORE
365
366ConfigUpdateListener::UpdateAction Esp32WiFiManager::apply_configuration(
367 int fd, bool initial_load, BarrierNotifiable *done)
368{
369 AutoNotify n(done);
370 LOG(VERBOSE, "Esp32WiFiManager::apply_configuration(%d, %d)", fd,
371 initial_load);
372
373 // Cache the fd for later use by the wifi background task.
374 configFd_ = fd;
375
376 // always load the connection mode.
377 connectionMode_ = CDI_READ_TRIMMED(cfg_.connection_mode, fd);
378
379 // Load the CDI entry into memory to do an CRC32 check against our last
380 // loaded configuration so we can avoid reloading configuration when there
381 // are no interesting changes.
382 unique_ptr<uint8_t[]> crcbuf(new uint8_t[cfg_.size()]);
383
384 // If we are unable to seek to the right position in the persistent storage
385 // give up and request a reboot.
386 if (lseek(fd, cfg_.offset(), SEEK_SET) != cfg_.offset())
387 {
388 LOG_ERROR("lseek failed to reset fd offset, REBOOT_NEEDED");
390 }
391
392 // Read the full configuration to the buffer for crc check.
393 FdUtils::repeated_read(fd, crcbuf.get(), cfg_.size());
394
395 // Calculate CRC32 from the loaded buffer.
396 uint32_t configCrc32 = crc32_le(0, crcbuf.get(), cfg_.size());
397 LOG(VERBOSE, "existing config CRC32:%" PRIu32 ", new CRC32:%" PRIu32,
398 configCrc32_, configCrc32);
399
400 if (initial_load)
401 {
402 // If we have more than one core start the Executor on the APP CPU (1)
403 // since the main OpenMRN Executor normally will run on the PRO CPU (0)
404#ifndef CONFIG_FREERTOS_UNICORE
405 xTaskCreatePinnedToCore(&thread_entry, // entry point
406 "Esp32WiFiConn", // task name
407 EXECUTOR_TASK_STACK_SIZE, // stack size
408 &executor_, // entry point arg
409 EXECUTOR_TASK_PRIORITY, // priority
410 nullptr, // task handle
411 APP_CPU_NUM); // cpu core
412#else
413 // start the background task executor since it will be used for any
414 // callback notifications that arise from starting the network stack.
415 executor_.start_thread(
416 "Esp32WiFiConn", EXECUTOR_TASK_PRIORITY, EXECUTOR_TASK_STACK_SIZE);
417#endif // !CONFIG_FREERTOS_UNICORE
418 }
419 else if (configCrc32 != configCrc32_)
420 {
421 // If a configuration change has been detected, wake up the wifi stack
422 // so it can consume the change.
423 wifiStackFlow_.notify();
424 }
425
426 // Store the calculated CRC-32 for future use when the apply_configuration
427 // method is called to detect any configuration changes.
428 configCrc32_ = configCrc32;
429
430 // Inform the caller that the configuration has been updated as the wifi
431 // task will reload the configuration as part of it's next wake up cycle.
433}
434
435// Factory reset handler for the WiFiConfiguration CDI entry.
436void Esp32WiFiManager::factory_reset(int fd)
437{
438 LOG(VERBOSE, "Esp32WiFiManager::factory_reset(%d)", fd);
439
440 // General WiFi configuration settings.
441 CDI_FACTORY_RESET(cfg_.sleep);
442 // NOTE: this is not using CDI_FACTORY_RESET so consumers can provide a
443 // default value as part of constructing the Esp32WiFiManager instance.
444 cfg_.connection_mode().write(fd, connectionMode_);
445
446 // Hub specific configuration settings.
447 CDI_FACTORY_RESET(cfg_.hub().port);
448 cfg_.hub().service_name().write(
449 fd, TcpDefs::MDNS_SERVICE_NAME_GRIDCONNECT_CAN_TCP);
450
451 // Node link configuration settings.
452 CDI_FACTORY_RESET(cfg_.uplink().search_mode);
453 CDI_FACTORY_RESET(cfg_.uplink().reconnect);
454
455 // Node link manual configuration settings.
456 cfg_.uplink().manual_address().ip_address().write(fd, "");
457 CDI_FACTORY_RESET(cfg_.uplink().manual_address().port);
458
459 // Node link automatic configuration settings.
460 cfg_.uplink().auto_address().service_name().write(
461 fd, TcpDefs::MDNS_SERVICE_NAME_GRIDCONNECT_CAN_TCP);
462 cfg_.uplink().auto_address().host_name().write(fd, "");
463
464 // Node link automatic last connected node address.
465 cfg_.uplink().last_address().ip_address().write(fd, "");
466 CDI_FACTORY_RESET(cfg_.uplink().last_address().port);
467
468 // Reconnect to last connected node.
469 CDI_FACTORY_RESET(cfg_.uplink().reconnect);
470}
471
472void Esp32WiFiManager::process_idf_event(void *arg, esp_event_base_t event_base
473 , int32_t event_id, void *event_data)
474{
475 LOG(VERBOSE, "Esp32WiFiManager::process_idf_event(%s, %" PRIi32 ", %p)",
476 event_base, event_id, event_data);
477 if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
478 {
479 Singleton<Esp32WiFiManager>::instance()->on_station_started();
480 }
481 else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED)
482 {
483 Singleton<Esp32WiFiManager>::instance()->on_station_connected();
484 }
485 else if (event_base == WIFI_EVENT &&
486 event_id == WIFI_EVENT_STA_DISCONNECTED)
487 {
488 Singleton<Esp32WiFiManager>::instance()->on_station_disconnected(
489 static_cast<wifi_event_sta_disconnected_t *>(event_data)->reason);
490 }
491 else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_START)
492 {
493 Singleton<Esp32WiFiManager>::instance()->on_softap_start();
494 }
495 else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STOP)
496 {
497 Singleton<Esp32WiFiManager>::instance()->on_softap_stop();
498 }
499 else if (event_base == WIFI_EVENT &&
500 event_id == WIFI_EVENT_AP_STACONNECTED)
501 {
502 auto sta_data = static_cast<wifi_event_ap_staconnected_t *>(event_data);
503 Singleton<Esp32WiFiManager>::instance()->on_softap_station_connected(
504 sta_data->mac, sta_data->aid);
505 }
506 else if (event_base == WIFI_EVENT &&
507 event_id == WIFI_EVENT_AP_STADISCONNECTED)
508 {
509 auto sta_data = static_cast<wifi_event_ap_staconnected_t *>(event_data);
510 Singleton<Esp32WiFiManager>::instance()->on_softap_station_disconnected(
511 sta_data->mac, sta_data->aid);
512 }
513 else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE)
514 {
515 auto scan_data = static_cast<wifi_event_sta_scan_done_t *>(event_data);
516 Singleton<Esp32WiFiManager>::instance()->on_wifi_scan_completed(
517 scan_data->status, scan_data->number);
518 }
519 else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
520 {
521 ip_event_got_ip_t *data = static_cast<ip_event_got_ip_t *>(event_data);
522 Singleton<Esp32WiFiManager>::instance()->on_station_ip_assigned(
523 htonl(data->ip_info.ip.addr));
524 }
525 else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP)
526 {
527 Singleton<Esp32WiFiManager>::instance()->on_station_ip_lost();
528 }
529}
530
531// Adds a callback which will be called when the network is up.
532void Esp32WiFiManager::register_network_up_callback(
533 esp_network_up_callback_t callback)
534{
535 OSMutexLock l(&networkCallbacksLock_);
536 networkUpCallbacks_.push_back(callback);
537}
538
539// Adds a callback which will be called when the network is down.
540void Esp32WiFiManager::register_network_down_callback(
541 esp_network_down_callback_t callback)
542{
543 OSMutexLock l(&networkCallbacksLock_);
544 networkDownCallbacks_.push_back(callback);
545}
546
547// Adds a callback which will be called when the network is initializing.
548void Esp32WiFiManager::register_network_init_callback(
549 esp_network_init_callback_t callback)
550{
551 OSMutexLock l(&networkCallbacksLock_);
552 networkInitCallbacks_.push_back(callback);
553}
554
555// Adds a callback which will be called when SNTP packets are processed.
556void Esp32WiFiManager::register_network_time_callback(
557 esp_network_time_callback_t callback)
558{
559 OSMutexLock l(&networkCallbacksLock_);
560 networkTimeCallbacks_.push_back(callback);
561}
562
563// Shuts down the hub listener (if enabled and running) for this node.
564void Esp32WiFiManager::stop_hub()
565{
566 auto stack = static_cast<openlcb::SimpleCanStackBase *>(stack_);
567 stack->shutdown_tcp_hub_server();
568}
569
570// Creates a hub listener for this node after loading configuration details.
571void Esp32WiFiManager::start_hub()
572{
573 auto stack = static_cast<openlcb::SimpleCanStackBase *>(stack_);
574
575 // Check if we already have a GC Hub running or not, if we do then we can
576 // skip creating one. It would be best to validate the port in use but it
577 // is not exposed by GcHub at this time.
578 if (!stack->get_tcp_hub_server())
579 {
580 hubServiceName_ = cfg_.hub().service_name().read(configFd_);
581 uint16_t hub_port = CDI_READ_TRIMMED(cfg_.hub().port, configFd_);
582 LOG(INFO, "[HUB] Starting TCP/IP listener on port %" PRIu16, hub_port);
583 stack->start_tcp_hub_server(hub_port);
584 auto hub = stack->get_tcp_hub_server();
585
586 // wait for the hub to complete it's startup tasks
587 while (!hub->is_started())
588 {
589 usleep(HUB_STARTUP_DELAY_USEC);
590 }
591
592 mdns_publish(hubServiceName_, hub_port);
593 }
594}
595
596// Disconnects and shuts down the uplink connector socket if running.
597void Esp32WiFiManager::stop_uplink()
598{
599 if (uplink_)
600 {
601 LOG(INFO, "[Uplink] Disconnecting from uplink.");
602 uplink_->shutdown();
603 uplink_.reset(nullptr);
604
605 // Mark our cached notifiable as invalid
606 uplinkNotifiable_ = nullptr;
607
608 // force close the socket to ensure it is disconnected and cleaned up.
609 ::close(uplinkFd_);
610 }
611}
612
613// Creates an uplink connector socket that will automatically add the uplink to
614// the node's hub.
615void Esp32WiFiManager::start_uplink()
616{
617 unique_ptr<SocketClientParams> params(
618 new Esp32SocketParams(configFd_, cfg_.uplink()));
619
620 if (uplink_)
621 {
622 // If we already have an uplink, update the parameters it is using.
623 uplink_->reset_params(std::move(params));
624 }
625 else
626 {
627 // create a new uplink and pass in the parameters.
628 uplink_.reset(new SocketClient(stack_->service(), &executor_,
629 &executor_, std::move(params),
630 std::bind(&Esp32WiFiManager::on_uplink_created, this,
631 std::placeholders::_1, std::placeholders::_2)));
632 }
633}
634
635// Converts the passed fd into a GridConnect port and adds it to the stack.
636void Esp32WiFiManager::on_uplink_created(int fd, Notifiable *on_exit)
637{
638 LOG(INFO, "[Uplink] Connected to hub, configuring GridConnect HubPort.");
639
640 // stash the socket handle and notifiable for future use if we need to
641 // clean up or re-establish the connection.
642 uplinkFd_ = fd;
643 uplinkNotifiable_ = on_exit;
644
645 const bool use_select =
646 (config_gridconnect_tcp_use_select() == CONSTANT_TRUE);
647
648 // create the GridConnect port from the provided socket fd.
649 // NOTE: this uses a local notifiable object instead of the provided
650 // on_exit since it will be invalidated upon calling stop_uplink() which
651 // may result in a crash or other undefined behavior.
653 static_cast<openlcb::SimpleCanStackBase *>(stack_)->can_hub(),
654 uplinkFd_, &uplinkNotifiableProxy_, use_select);
655
656 // restart the stack to kick off alias allocation and send node init
657 // packets.
658 stack_->restart_stack();
659}
660
661// Enables the ESP-IDF wifi module logging at verbose level, will also set the
662// sub-modules to verbose if they are available.
663void Esp32WiFiManager::enable_esp_wifi_logging()
664{
665 esp_log_level_set("wifi", ESP_LOG_VERBOSE);
666
667// arduino-esp32 1.0.2 uses ESP-IDF 3.2 which does not have these two methods
668// in the headers, they are only available in ESP-IDF 3.3.
669#if defined(WIFI_LOG_SUBMODULE_ALL)
670 esp_wifi_internal_set_log_level(WIFI_LOG_VERBOSE);
671 esp_wifi_internal_set_log_mod(
672 WIFI_LOG_MODULE_ALL, WIFI_LOG_SUBMODULE_ALL, true);
673#endif // WIFI_LOG_SUBMODULE_ALL
674}
675
676// Starts a background scan of SSIDs that can be seen by the ESP32.
677void Esp32WiFiManager::start_ssid_scan(Notifiable *n)
678{
679 clear_ssid_scan_results();
680 std::swap(ssidCompleteNotifiable_, n);
681 // If there was a previous notifiable notify it now, there will be no
682 // results but that should be fine since a new scan will be started.
683 if (n)
684 {
685 n->notify();
686 }
687 // Start an active scan all channels, 120ms per channel (defaults)
688 wifi_scan_config_t cfg;
689 bzero(&cfg, sizeof(wifi_scan_config_t));
690 // The boolean flag when set to false triggers an async scan.
691 ESP_ERROR_CHECK(esp_wifi_scan_start(&cfg, false));
692}
693
694// Returns the number of SSIDs found in the last scan.
695size_t Esp32WiFiManager::get_ssid_scan_result_count()
696{
697 OSMutexLock l(&ssidScanResultsLock_);
698 return ssidScanResults_.size();
699}
700
701// Returns one SSID record from the last scan.
702wifi_ap_record_t Esp32WiFiManager::get_ssid_scan_result(size_t index)
703{
704 OSMutexLock l(&ssidScanResultsLock_);
705 wifi_ap_record_t record = wifi_ap_record_t();
706 if (index < ssidScanResults_.size())
707 {
708 record = ssidScanResults_[index];
709 }
710 return record;
711}
712
713// Clears all cached SSID scan results.
714void Esp32WiFiManager::clear_ssid_scan_results()
715{
716 OSMutexLock l(&ssidScanResultsLock_);
717 ssidScanResults_.clear();
718}
719
720// Advertises a service via mDNS.
721//
722// If mDNS has not yet been initialized the data will be cached and replayed
723// after mDNS has been initialized.
724void Esp32WiFiManager::mdns_publish(string service, const uint16_t port)
725{
726 {
727 OSMutexLock l(&mdnsInitLock_);
728 if (!mdnsInitialized_)
729 {
730 // since mDNS has not been initialized, store this publish until
731 // it has been initialized.
732 mdnsDeferredPublish_[service] = port;
733 return;
734 }
735 }
736
737 // Schedule the publish to be done through the Executor since we may need
738 // to retry it.
739 executor_.add(new CallbackExecutable([service, port]()
740 {
741 string service_name = service;
742 string protocol_name;
743 split_mdns_service_name(&service_name, &protocol_name);
744 esp_err_t res = mdns_service_add(
745 NULL, service_name.c_str(), protocol_name.c_str(), port, NULL, 0);
746 LOG(VERBOSE, "[mDNS] mdns_service_add(%s.%s:%" PRIu16 "):%s.",
747 service_name.c_str(), protocol_name.c_str(), port,
748 esp_err_to_name(res));
749 // ESP_FAIL will be triggered if there is a timeout during publish of
750 // the new mDNS entry. The mDNS task runs at a very low priority on the
751 // PRO_CPU which is also where the OpenMRN Executor runs from which can
752 // cause a race condition.
753 if (res == ESP_FAIL)
754 {
755 // Send it back onto the scheduler to be retried
757 service, port);
758 }
759 else if (res == ESP_ERR_INVALID_ARG)
760 {
761 // ESP_ERR_INVALID_ARG can be returned if the mDNS server is not UP
762 // which we have previously checked via mdnsInitialized_, this can
763 // also be returned if the service name has already been published
764 // since the server came up. If we see this error code returned we
765 // should try unpublish and then publish again.
766 Singleton<Esp32WiFiManager>::instance()->mdns_unpublish(service);
768 service, port);
769 }
770 else if (res == ESP_OK)
771 {
772 LOG(INFO, "[mDNS] Advertising %s.%s:%" PRIu16 ".",
773 service_name.c_str(), protocol_name.c_str(), port);
774 }
775 else
776 {
777 LOG_ERROR("[mDNS] Failed to publish:%s.%s:%" PRIu16,
778 service_name.c_str(), protocol_name.c_str(), port);
780 service, port);
781 }
782 }));
783}
784
785// Removes advertisement of a service from mDNS.
786void Esp32WiFiManager::mdns_unpublish(string service)
787{
788 {
789 OSMutexLock l(&mdnsInitLock_);
790 if (!mdnsInitialized_)
791 {
792 // Since mDNS is not in an initialized state we can discard the
793 // unpublish event.
794 return;
795 }
796 }
797 string service_name = service;
798 string protocol_name;
799 split_mdns_service_name(&service_name, &protocol_name);
800 LOG(INFO, "[mDNS] Removing advertisement of %s.%s."
801 , service_name.c_str(), protocol_name.c_str());
802 esp_err_t res =
803 mdns_service_remove(service_name.c_str(), protocol_name.c_str());
804 LOG(VERBOSE, "[mDNS] mdns_service_remove:%s.", esp_err_to_name(res));
805 // TODO: should we queue up unpublish requests for future retries?
806}
807
808// Initializes the mDNS system on the ESP32.
809//
810// After initialization, if any services are pending publish they will be
811// published at this time.
812void Esp32WiFiManager::start_mdns_system()
813{
814 {
815 OSMutexLock l(&mdnsInitLock_);
816 // If we have already initialized mDNS we can exit early.
817 if (mdnsInitialized_)
818 {
819 return;
820 }
821
822 // Initialize the mDNS system.
823 LOG(INFO, "[mDNS] Initializing mDNS system");
824 ESP_ERROR_CHECK(mdns_init());
825
826 // Set the mDNS hostname based on our generated hostname so it can be
827 // found by other nodes.
828 LOG(INFO, "[mDNS] Setting hostname to \"%s\"", hostname_.c_str());
829 ESP_ERROR_CHECK(mdns_hostname_set(hostname_.c_str()));
830
831 // Set the default mDNS instance name to the generated hostname.
832 ESP_ERROR_CHECK(mdns_instance_name_set(hostname_.c_str()));
833
834 // Set flag to indicate we have initialized mDNS.
835 mdnsInitialized_ = true;
836 }
837
838 // Publish any deferred mDNS entries
839 for (auto & entry : mdnsDeferredPublish_)
840 {
841 mdns_publish(entry.first, entry.second);
842 }
843 mdnsDeferredPublish_.clear();
844}
845
846void Esp32WiFiManager::on_station_started()
847{
848 // Set the generated hostname prior to connecting to the SSID
849 // so that it shows up with the generated hostname instead of
850 // the default "Espressif".
851 LOG(INFO, "[Station] Setting hostname to \"%s\".", hostname_.c_str());
852 esp_netif_set_hostname(espNetIfaces_[STATION_INTERFACE], hostname_.c_str());
853 uint8_t mac[6];
854 esp_wifi_get_mac(WIFI_IF_STA, mac);
855 LOG(INFO, "[Station] MAC Address:%s", mac_to_string(mac).c_str());
856
857 // Start the DHCP service before connecting so it hooks into
858 // the flow early and provisions the IP automatically.
859 LOG(INFO, "[Station] Starting DHCP Client.");
860 ESP_ERROR_CHECK(esp_netif_dhcpc_start(espNetIfaces_[STATION_INTERFACE]));
861
862 LOG(INFO, "[Station] Connecting to SSID:%s.", ssid_.c_str());
863 // Start the SSID connection process.
864 esp_wifi_connect();
865
866 // Schedule callbacks via the executor rather than call directly here.
867 {
868 OSMutexLock l(&networkCallbacksLock_);
869 for (esp_network_init_callback_t cb : networkInitCallbacks_)
870 {
871 executor_.add(new CallbackExecutable([cb]
872 {
873 cb(STATION_INTERFACE);
874 }));
875 }
876 }
877}
878
879void Esp32WiFiManager::on_station_connected()
880{
881 LOG(INFO, "[Station] Connected to SSID:%s", ssid_.c_str());
882 // Set the flag that indictes we are connected to the SSID.
883 xEventGroupSetBits(wifiStatusEventGroup_, WIFI_CONNECTED_BIT);
884}
885
886void Esp32WiFiManager::on_station_disconnected(uint8_t reason)
887{
888 // flag to indicate that we should print the reconnecting log message.
889 bool was_previously_connected = false;
890
891 // capture the current state so we can check if we were already connected
892 // with an IP address or still in the connecting phase.
893 EventBits_t event_bits = xEventGroupGetBits(wifiStatusEventGroup_);
894
895 // Check if we have already connected, this event can be raised
896 // even before we have successfully connected during the SSID
897 // connect process.
898 if (event_bits & WIFI_CONNECTED_BIT)
899 {
900 // If we were previously connected and had an IP address we should
901 // count that as previously connected, otherwise we will just reconnect
902 // and not wake up the state flow since it may be waiting for an event
903 // and will wakeup on it's own.
904 was_previously_connected = event_bits & WIFI_GOTIP_BIT;
905
906 LOG(INFO, "[Station] Lost connection to SSID:%s (reason:%d)",
907 ssid_.c_str(), reason);
908 // Clear the flag that indicates we are connected to the SSID.
909 xEventGroupClearBits(wifiStatusEventGroup_, WIFI_CONNECTED_BIT);
910 // Clear the flag that indicates we have an IPv4 address.
911 xEventGroupClearBits(wifiStatusEventGroup_, WIFI_GOTIP_BIT);
912 }
913
914 // If we are managing the WiFi and MDNS systems we need to
915 // trigger the reconnection process at this point.
916 if (was_previously_connected)
917 {
918 LOG(INFO, "[Station] Reconnecting to SSID:%s.",
919 ssid_.c_str());
920 wifiStackFlow_.notify();
921 }
922 else
923 {
924 LOG(INFO,
925 "[Station] Connection to SSID:%s (reason:%d) failed, retrying.",
926 ssid_.c_str(), reason);
927 }
928 esp_wifi_connect();
929
930 // Schedule callbacks via the executor rather than call directly here.
931 {
932 OSMutexLock l(&networkCallbacksLock_);
933 for (esp_network_init_callback_t cb : networkInitCallbacks_)
934 {
935 executor_.add(new CallbackExecutable([cb]
936 {
937 cb(STATION_INTERFACE);
938 }));
939 }
940 }
941}
942
943void Esp32WiFiManager::on_station_ip_assigned(uint32_t ip_address)
944{
945 LOG(INFO, "[Station] IP address:%s", ipv4_to_string(ip_address).c_str());
946
947 // Start the mDNS system since we have an IP address, the mDNS system
948 // on the ESP32 requires that the IP address be assigned otherwise it
949 // will not start the UDP listener.
950 start_mdns_system();
951
952 // Set the flag that indictes we have an IPv4 address.
953 xEventGroupSetBits(wifiStatusEventGroup_, WIFI_GOTIP_BIT);
954
955 // wake up the stack to process the event
956 wifiStackFlow_.notify();
957
958 // Schedule callbacks via the executor rather than call directly here.
959 {
960 OSMutexLock l(&networkCallbacksLock_);
961 for (esp_network_up_callback_t cb : networkUpCallbacks_)
962 {
963 executor_.add(new CallbackExecutable([cb, ip_address]
964 {
965 cb(STATION_INTERFACE, ip_address);
966 }));
967 }
968 }
969
970 configure_sntp();
971 reconfigure_wifi_tx_power();
972 if (statusLed_)
973 {
974 statusLed_->write(true);
975 }
976}
977
978void Esp32WiFiManager::on_station_ip_lost()
979{
980 // Clear the flag that indicates we are connected and have an
981 // IPv4 address.
982 xEventGroupClearBits(wifiStatusEventGroup_, WIFI_GOTIP_BIT);
983
984 // wake up the stack to process the event
985 wifiStackFlow_.notify();
986
987 // Schedule callbacks via the executor rather than call directly here.
988 {
989 OSMutexLock l(&networkCallbacksLock_);
990 for (esp_network_down_callback_t cb : networkDownCallbacks_)
991 {
992 executor_.add(new CallbackExecutable([cb]
993 {
994 cb(STATION_INTERFACE);
995 }));
996 }
997 }
998}
999
1000void Esp32WiFiManager::on_softap_start()
1001{
1002 uint32_t ip_address = 0;
1003 uint8_t mac[6];
1004 esp_wifi_get_mac(WIFI_IF_AP, mac);
1005 LOG(INFO, "[SoftAP] MAC Address:%s", mac_to_string(mac).c_str());
1006
1007 // Set the generated hostname prior to connecting to the SSID
1008 // so that it shows up with the generated hostname instead of
1009 // the default "Espressif".
1010 LOG(INFO, "[SoftAP] Setting hostname to \"%s\".", hostname_.c_str());
1011 ESP_ERROR_CHECK(esp_netif_set_hostname(
1012 espNetIfaces_[SOFTAP_INTERFACE], hostname_.c_str()));
1013
1014 // fetch the IP address from the adapter since it defaults to
1015 // 192.168.4.1 but can be altered via sdkconfig.
1016 esp_netif_ip_info_t ip_info;
1017 ESP_ERROR_CHECK(
1018 esp_netif_get_ip_info(espNetIfaces_[SOFTAP_INTERFACE], &ip_info));
1019 ip_address = ntohl(ip4_addr_get_u32(&ip_info.ip));
1020
1021 LOG(INFO, "[SoftAP] IP address:%s", ipv4_to_string(ip_address).c_str());
1022
1023 // If we are operating only in SoftAP mode we can start any background
1024 // services that would also be started when the station interface is ready
1025 // and has an IP address.
1026 if (wifiMode_ == WIFI_MODE_AP)
1027 {
1028 start_mdns_system();
1029 reconfigure_wifi_tx_power();
1030 if (connectionMode_ & CONN_MODE_HUB_BIT)
1031 {
1032 start_hub();
1033 }
1034 }
1035
1036 // Schedule callbacks via the executor rather than call directly here.
1037 {
1038 OSMutexLock l(&networkCallbacksLock_);
1039 for (esp_network_up_callback_t cb : networkUpCallbacks_)
1040 {
1041 executor_.add(new CallbackExecutable([cb, ip_address]
1042 {
1043 cb(SOFTAP_INTERFACE, htonl(ip_address));
1044 }));
1045 }
1046 }
1047}
1048
1049void Esp32WiFiManager::on_softap_stop()
1050{
1051 if (wifiMode_ == WIFI_MODE_AP)
1052 {
1053 stop_uplink();
1054 stop_hub();
1055 }
1056
1057 // Schedule callbacks via the executor rather than call directly here.
1058 {
1059 OSMutexLock l(&networkCallbacksLock_);
1060 for (esp_network_down_callback_t cb : networkDownCallbacks_)
1061 {
1062 executor_.add(new CallbackExecutable([cb]
1063 {
1064 cb(SOFTAP_INTERFACE);
1065 }));
1066 }
1067 }
1068}
1069
1070void Esp32WiFiManager::on_softap_station_connected(uint8_t mac[6], uint8_t aid)
1071{
1072 LOG(INFO, "[SoftAP aid:%d] %s connected.", aid,
1073 mac_to_string(mac).c_str());
1074}
1075
1076void Esp32WiFiManager::on_softap_station_disconnected(uint8_t mac[6],
1077 uint8_t aid)
1078{
1079 LOG(INFO, "[SoftAP aid:%d] %s disconnected.", aid,
1080 mac_to_string(mac).c_str());
1081}
1082
1083void Esp32WiFiManager::on_wifi_scan_completed(uint32_t status, uint8_t count)
1084{
1085 OSMutexLock l(&ssidScanResultsLock_);
1086 if (status)
1087 {
1088 LOG_ERROR("[WiFi] SSID scan failed!");
1089 }
1090 else
1091 {
1092 uint16_t num_found = count;
1093 esp_wifi_scan_get_ap_num(&num_found);
1094 LOG(VERBOSE, "[WiFi] %" PRIu16 " SSIDs found via scan", num_found);
1095 ssidScanResults_.resize(num_found);
1096 esp_wifi_scan_get_ap_records(&num_found, ssidScanResults_.data());
1097#if LOGLEVEL >= VERBOSE
1098 for (int i = 0; i < num_found; i++)
1099 {
1100 LOG(VERBOSE, "SSID:%s, RSSI:%d, channel:%d"
1101 , ssidScanResults_[i].ssid
1102 , ssidScanResults_[i].rssi, ssidScanResults_[i].primary);
1103 }
1104#endif
1105 }
1106 if (ssidCompleteNotifiable_)
1107 {
1108 ssidCompleteNotifiable_->notify();
1109 ssidCompleteNotifiable_ = nullptr;
1110 }
1111}
1112
1113// SNTP callback hook to schedule callbacks.
1114void Esp32WiFiManager::sync_time(time_t now)
1115{
1116 OSMutexLock l(&networkCallbacksLock_);
1117 for (esp_network_time_callback_t cb : networkTimeCallbacks_)
1118 {
1119 executor_.add(new CallbackExecutable([cb,now]
1120 {
1121 cb(now);
1122 }));
1123 }
1124}
1125
1126static void sntp_update_received(struct timeval *tv)
1127{
1128 time_t new_time = tv->tv_sec;
1129 LOG(INFO, "[SNTP] Received time update, new localtime:%s"
1130 , ctime(&new_time));
1131 Singleton<Esp32WiFiManager>::instance()->sync_time(new_time);
1132}
1133
1134void Esp32WiFiManager::configure_sntp()
1135{
1136 if (sntpEnabled_ && !sntpConfigured_)
1137 {
1138 sntpConfigured_ = true;
1139 LOG(INFO, "[SNTP] Polling %s for time updates", sntpServer_.c_str());
1140#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5,1,0)
1141 esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
1142 esp_sntp_setservername(0, sntpServer_.c_str());
1143 sntp_set_time_sync_notification_cb(sntp_update_received);
1144 esp_sntp_init();
1145#else
1146 sntp_setoperatingmode(SNTP_OPMODE_POLL);
1147 sntp_setservername(0, sntpServer_.c_str());
1148 sntp_set_time_sync_notification_cb(sntp_update_received);
1149 sntp_init();
1150#endif // IDF v5.1+
1151
1152 if (!timeZone_.empty())
1153 {
1154 LOG(INFO, "[TimeZone] %s", timeZone_.c_str());
1155 setenv("TZ", timeZone_.c_str(), 1);
1156 tzset();
1157 }
1158 }
1159}
1160
1161void Esp32WiFiManager::reconfigure_wifi_radio_sleep()
1162{
1163 wifi_ps_type_t current_mode = WIFI_PS_NONE;
1164 ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_get_ps(&current_mode));
1165 uint8_t sleepEnabled = CDI_READ_TRIMMED(cfg_.sleep, configFd_);
1166
1167 if (sleepEnabled && current_mode != WIFI_PS_MIN_MODEM)
1168 {
1169 LOG(INFO, "[WiFi] Enabling radio power saving mode");
1170 // When sleep is enabled this will trigger the WiFi system to
1171 // only wake up every DTIM period to receive beacon updates.
1172 // no data loss is expected for this setting but it does delay
1173 // receiption until the DTIM period.
1174 ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM));
1175 }
1176 else if (!sleepEnabled && current_mode != WIFI_PS_NONE)
1177 {
1178 LOG(INFO, "[WiFi] Disabling radio power saving mode");
1179 // When sleep is disabled the WiFi radio will always be active.
1180 // This will increase power consumption of the ESP32 but it
1181 // will result in a more reliable behavior when the ESP32 is
1182 // connected to an always-on power supply (ie: not a battery).
1183 ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
1184 }
1185}
1186
1187void Esp32WiFiManager::reconfigure_wifi_tx_power()
1188{
1189 ESP_ERROR_CHECK(esp_wifi_set_max_tx_power(wifiTXPower_));
1190}
1191
1192Esp32WiFiManager::WiFiStackFlow::WiFiStackFlow(Esp32WiFiManager * parent)
1193 : StateFlowBase(parent), parent_(parent),
1194 wifiConnectBitMask_(WIFI_CONNECTED_BIT)
1195{
1196 start_flow(STATE(startup));
1197}
1198
1199StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::startup()
1200{
1201 if (parent_->wifiMode_ != WIFI_MODE_NULL)
1202 {
1203 return yield_and_call(STATE(init_interface));
1204 }
1205 return yield_and_call(STATE(noop));
1206}
1207
1208StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::noop()
1209{
1210 return wait_and_call(STATE(noop));
1211}
1212
1213StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::init_interface()
1214{
1215 // create default interfaces for station and SoftAP, ethernet is not used
1216 // today.
1217 ESP_ERROR_CHECK(esp_netif_init());
1218
1219 // create the event loop.
1220 esp_err_t err = esp_event_loop_create_default();
1221
1222 // The esp_event_loop_create_default() method will return either ESP_OK if
1223 // the event loop was created or ESP_ERR_INVALID_STATE if one already
1224 // exists.
1225 if (err != ESP_OK && err != ESP_ERR_INVALID_STATE)
1226 {
1227 LOG(FATAL, "[WiFi] Failed to initialize the default event loop:%s",
1228 esp_err_to_name(err));
1229 }
1230
1231 parent_->espNetIfaces_[STATION_INTERFACE] =
1232 esp_netif_create_default_wifi_sta();
1233 parent_->espNetIfaces_[SOFTAP_INTERFACE] =
1234 esp_netif_create_default_wifi_ap();
1235
1236 // Connect our event listeners.
1237 esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
1238 &Esp32WiFiManager::process_idf_event, nullptr);
1239 esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID,
1240 &Esp32WiFiManager::process_idf_event, nullptr);
1241
1242 return yield_and_call(STATE(init_wifi));
1243}
1244
1245StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::init_wifi()
1246{
1247 // Start the WiFi adapter.
1248 wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
1249 LOG(INFO, "[WiFi] Initializing WiFi stack");
1250
1251 // Disable NVS storage for the WiFi driver
1252 cfg.nvs_enable = false;
1253
1254 // override the defaults coming from arduino-esp32, the ones below improve
1255 // throughput and stability of TCP/IP, for more info on these values, see:
1256 // https://github.com/espressif/arduino-esp32/issues/2899 and
1257 // https://github.com/espressif/arduino-esp32/pull/2912
1258 //
1259 // Note: these numbers are slightly higher to allow compatibility with the
1260 // WROVER chip and WROOM-32 chip. The increase results in ~2kb less heap
1261 // at runtime.
1262 //
1263 // These do not require recompilation of arduino-esp32 code as these are
1264 // used in the WIFI_INIT_CONFIG_DEFAULT macro, they simply need to be redefined.
1265 if (cfg.static_rx_buf_num < 16)
1266 {
1267 cfg.static_rx_buf_num = 16;
1268 }
1269 if (cfg.dynamic_rx_buf_num < 32)
1270 {
1271 cfg.dynamic_rx_buf_num = 32;
1272 }
1273 if (cfg.rx_ba_win < 16)
1274 {
1275 cfg.rx_ba_win = 16;
1276 }
1277
1278 ESP_ERROR_CHECK(esp_wifi_init(&cfg));
1279
1280 if (parent_->verboseLogging_)
1281 {
1282 parent_->enable_esp_wifi_logging();
1283 }
1284
1285 // Create the event group used for tracking connected/disconnected status.
1286 // This is used internally regardless of if we manage the rest of the WiFi
1287 // or mDNS systems.
1288 parent_->wifiStatusEventGroup_ = xEventGroupCreate();
1289
1290 wifi_mode_t requested_wifi_mode = parent_->wifiMode_;
1291 if (parent_->wifiMode_ == WIFI_MODE_AP)
1292 {
1293 // override the wifi mode from AP only to AP+STA so we can perform wifi
1294 // scans on demand.
1295 requested_wifi_mode = WIFI_MODE_APSTA;
1296 }
1297 // Set the requested WiFi mode.
1298 ESP_ERROR_CHECK(esp_wifi_set_mode(requested_wifi_mode));
1299
1300 // This disables storage of SSID details in NVS which has been shown to be
1301 // problematic at times for the ESP32, it is safer to always pass fresh
1302 // config and have the ESP32 resolve the details at runtime rather than
1303 // use a cached set from NVS.
1304 esp_wifi_set_storage(WIFI_STORAGE_RAM);
1305
1306 if (parent_->wifiMode_ == WIFI_MODE_APSTA ||
1307 parent_->wifiMode_ == WIFI_MODE_AP)
1308 {
1309 return yield_and_call(STATE(configure_softap));
1310 }
1311 return yield_and_call(STATE(configure_station));
1312}
1313
1314StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::configure_station()
1315{
1316 // Configure the SSID details for the station based on the SSID and
1317 // password provided to the Esp32WiFiManager constructor.
1318 wifi_config_t conf;
1319 memset(&conf, 0, sizeof(wifi_config_t));
1320 strcpy(reinterpret_cast<char *>(conf.sta.ssid), parent_->ssid_.c_str());
1321 if (!parent_->password_.empty())
1322 {
1323 strcpy(reinterpret_cast<char *>(conf.sta.password),
1324 parent_->password_.c_str());
1325 }
1326
1327 LOG(INFO, "[WiFi] Configuring Station (SSID:%s)", conf.sta.ssid);
1328 ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &conf));
1329
1330 return yield_and_call(STATE(start_wifi));
1331}
1332
1333StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::configure_softap()
1334{
1335 wifi_config_t conf;
1336 memset(&conf, 0, sizeof(wifi_config_t));
1337 conf.ap.authmode = parent_->softAPAuthMode_;
1338 conf.ap.beacon_interval = 100;
1339 conf.ap.channel = parent_->softAPChannel_;
1340 conf.ap.max_connection = 4;
1341
1342 if (!parent_->softAPName_.empty())
1343 {
1344 // Configure the SSID for the Soft AP based on the SSID passed
1345 // to the Esp32WiFiManager constructor.
1346 strcpy(reinterpret_cast<char *>(conf.ap.ssid),
1347 parent_->softAPName_.c_str());
1348 }
1349 else if (parent_->wifiMode_ == WIFI_MODE_AP && !parent_->ssid_.empty())
1350 {
1351 strcpy(reinterpret_cast<char *>(conf.ap.ssid),
1352 parent_->ssid_.c_str());
1353 }
1354 else
1355 {
1356 // Configure the SSID for the Soft AP based on the generated
1357 // hostname when operating in WIFI_MODE_APSTA mode.
1358 strcpy(reinterpret_cast<char *>(conf.ap.ssid),
1359 parent_->hostname_.c_str());
1360 }
1361
1362 if (parent_->softAPAuthMode_ != WIFI_AUTH_OPEN)
1363 {
1364 if (!parent_->softAPPassword_.empty())
1365 {
1366 strcpy(reinterpret_cast<char *>(conf.ap.password),
1367 parent_->softAPPassword_.c_str());
1368 }
1369 else if (!parent_->password_.empty())
1370 {
1371 strcpy(reinterpret_cast<char *>(conf.ap.password),
1372 parent_->password_.c_str());
1373 }
1374 else
1375 {
1376 LOG(WARNING,
1377 "[WiFi] SoftAP password is blank, using OPEN auth mode.");
1378 parent_->softAPAuthMode_ = WIFI_AUTH_OPEN;
1379 }
1380 }
1381
1382 LOG(INFO, "[WiFi] Configuring SoftAP (SSID:%s)", conf.ap.ssid);
1383 ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &conf));
1384
1385 // If we are only enabling the SoftAP we can transition to starting the
1386 // WiFi stack.
1387 if (parent_->wifiMode_ == WIFI_MODE_AP)
1388 {
1389 return yield_and_call(STATE(start_wifi));
1390 }
1391 // Configure the station interface
1392 return yield_and_call(STATE(configure_station));
1393}
1394
1395StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::start_wifi()
1396{
1397 // Start the WiFi stack. This will start the SoftAP and/or connect to the
1398 // SSID based on the configuration set above.
1399 LOG(INFO, "[WiFi] Starting WiFi stack");
1400 ESP_ERROR_CHECK(esp_wifi_start());
1401
1402 // force WiFi power to maximum during startup.
1403 ESP_ERROR_CHECK(esp_wifi_set_max_tx_power(84));
1404
1405 if (parent_->wifiMode_ != WIFI_MODE_AP && parent_->waitForStationConnect_)
1406 {
1407 return sleep_and_call(&timer_,
1408 SEC_TO_NSEC(WIFI_CONNECT_CHECK_INTERVAL_SEC),
1409 STATE(wait_for_connect));
1410 }
1411 return wait_and_call(STATE(reload));
1412}
1413
1414StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::wait_for_connect()
1415{
1416 EventBits_t bits =
1417 xEventGroupWaitBits(parent_->wifiStatusEventGroup_,
1418 wifiConnectBitMask_, pdFALSE, pdTRUE, 0);
1419 // If we need the STATION interface *AND* configured to wait until
1420 // successfully connected to the SSID this code block will wait for up to
1421 // approximately three minutes for an IP address to be assigned. In most
1422 // cases this completes in under thirty seconds. If there is a connection
1423 // failure the esp32 will be restarted via a FATAL error being logged.
1424 if (++wifiConnectAttempts_ <= MAX_CONNECTION_CHECK_ATTEMPTS)
1425 {
1426 // If we have connected to the SSID we then are waiting for IP
1427 // address.
1428 if (bits & WIFI_CONNECTED_BIT)
1429 {
1430 LOG(INFO, "[IPv4] [%d/%d] Waiting for IP address assignment.",
1431 wifiConnectAttempts_, MAX_CONNECTION_CHECK_ATTEMPTS);
1432 }
1433 else
1434 {
1435 // Waiting for SSID connection
1436 LOG(INFO, "[WiFi] [%d/%d] Waiting for SSID connection.",
1437 wifiConnectAttempts_, MAX_CONNECTION_CHECK_ATTEMPTS);
1438 }
1439 // Check if have connected to the SSID
1440 if (bits & WIFI_CONNECTED_BIT)
1441 {
1442 // Since we have connected to the SSID we now need to track
1443 // that we get an IP.
1444 wifiConnectBitMask_ |= WIFI_GOTIP_BIT;
1445 }
1446 // Check if we have received an IP.
1447 if (bits & WIFI_GOTIP_BIT)
1448 {
1449 return yield_and_call(STATE(reload));
1450 }
1451 return sleep_and_call(&timer_,
1452 SEC_TO_NSEC(WIFI_CONNECT_CHECK_INTERVAL_SEC),
1453 STATE(wait_for_connect));
1454 }
1455 // Check if we successfully connected or not. If not, force a reboot.
1456 if ((bits & WIFI_CONNECTED_BIT) != WIFI_CONNECTED_BIT)
1457 {
1458 LOG(FATAL, "[WiFi] Failed to connect to SSID:%s.",
1459 parent_->ssid_.c_str());
1460 }
1461
1462 // Check if we successfully connected or not. If not, force a reboot.
1463 if ((bits & WIFI_GOTIP_BIT) != WIFI_GOTIP_BIT)
1464 {
1465 LOG(FATAL, "[IPv4] Timeout waiting for an IP.");
1466 }
1467
1468 return yield_and_call(STATE(reload));
1469}
1470
1471StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::reload()
1472{
1473 if (parent_->wifiMode_ == WIFI_MODE_STA ||
1474 parent_->wifiMode_ == WIFI_MODE_APSTA)
1475 {
1476 EventBits_t bits = xEventGroupGetBits(parent_->wifiStatusEventGroup_);
1477 if (!(bits & WIFI_GOTIP_BIT))
1478 {
1479 // Since we do not have an IP address we need to shutdown any
1480 // active connections since they will be invalid until a new IP
1481 // has been provisioned.
1482 parent_->stop_hub();
1483 parent_->stop_uplink();
1484 return wait_and_call(STATE(reload));
1485 }
1486 }
1487
1488 parent_->reconfigure_wifi_radio_sleep();
1489 if (parent_->connectionMode_ & CONN_MODE_HUB_BIT)
1490 {
1491 parent_->start_hub();
1492 }
1493 else
1494 {
1495 parent_->stop_hub();
1496 }
1497
1498 if (parent_->connectionMode_ & CONN_MODE_UPLINK_BIT)
1499 {
1500 parent_->start_uplink();
1501 }
1502 else
1503 {
1504 parent_->stop_uplink();
1505 }
1506 return wait_and_call(STATE(reload));
1507}
1508
1509} // namespace openmrn_arduino
1510
1512static constexpr uint32_t MDNS_QUERY_TIMEOUT = 2000;
1513
1515static constexpr size_t MDNS_MAX_RESULTS = 10;
1516
1517// Advertises an mDNS service name.
1518void mdns_publish(const char *name, const char *service, uint16_t port)
1519{
1520 // The name parameter is unused today.
1521 Singleton<Esp32WiFiManager>::instance()->mdns_publish(service, port);
1522}
1523
1524// Removes advertisement of an mDNS service name.
1525void mdns_unpublish(const char *name, const char *service)
1526{
1527 Singleton<Esp32WiFiManager>::instance()->mdns_unpublish(service);
1528}
1529
1530// Splits an mDNS service name.
1531void split_mdns_service_name(string *service_name, string *protocol_name)
1532{
1533 HASSERT(service_name != nullptr);
1534 HASSERT(protocol_name != nullptr);
1535
1536 // if the string is not blank and contains a period split it on the period.
1537 if (service_name->length() && service_name->find('.', 0) != string::npos)
1538 {
1539 string::size_type split_loc = service_name->find('.', 0);
1540 protocol_name->assign(service_name->substr(split_loc + 1));
1541 service_name->resize(split_loc);
1542 }
1543}
1544
1545// EAI_AGAIN may not be defined on the ESP32
1546#ifndef EAI_AGAIN
1547#ifdef TRY_AGAIN
1548#define EAI_AGAIN TRY_AGAIN
1549#else
1550#define EAI_AGAIN -3
1551#endif
1552#endif // EAI_AGAIN
1553
1554// Looks for an mDNS service name and converts the results of the query to an
1555// addrinfo struct.
1556int mdns_lookup(
1557 const char *service, struct addrinfo *hints, struct addrinfo **addr)
1558{
1559 unique_ptr<struct addrinfo> ai(new struct addrinfo);
1560 if (ai.get() == nullptr)
1561 {
1562 LOG_ERROR("[mDNS] Allocation failed for addrinfo.");
1563 return EAI_MEMORY;
1564 }
1565 bzero(ai.get(), sizeof(struct addrinfo));
1566
1567 unique_ptr<struct sockaddr> sa(new struct sockaddr);
1568 if (sa.get() == nullptr)
1569 {
1570 LOG_ERROR("[mDNS] Allocation failed for sockaddr.");
1571 return EAI_MEMORY;
1572 }
1573 bzero(sa.get(), sizeof(struct sockaddr));
1574
1575 struct sockaddr_in *sa_in = (struct sockaddr_in *)sa.get();
1576 ai->ai_flags = 0;
1577 ai->ai_family = hints->ai_family;
1578 ai->ai_socktype = hints->ai_socktype;
1579 ai->ai_protocol = hints->ai_protocol;
1580 ai->ai_addrlen = sizeof(struct sockaddr_in);
1581 sa_in->sin_len = sizeof(struct sockaddr_in);
1582 sa_in->sin_family = hints->ai_family;
1583
1584 string service_name = service;
1585 string protocol_name;
1586 split_mdns_service_name(&service_name, &protocol_name);
1587
1588 mdns_result_t *results = NULL;
1589 if (ESP_ERROR_CHECK_WITHOUT_ABORT(
1590 mdns_query_ptr(service_name.c_str(),
1591 protocol_name.c_str(),
1592 MDNS_QUERY_TIMEOUT,
1593 MDNS_MAX_RESULTS,
1594 &results)))
1595 {
1596 // failed to find any matches
1597 return EAI_FAIL;
1598 }
1599
1600 if (!results)
1601 {
1602 // failed to find any matches
1603 LOG(ESP32_WIFIMGR_MDNS_LOOKUP_LOG_LEVEL,
1604 "[mDNS] No matches found for service:%s.",
1605 service);
1606 return EAI_AGAIN;
1607 }
1608
1609 // make a copy of the results to preserve the original list for cleanup.
1610 mdns_result_t *res = results;
1611 // scan the mdns query results linked list, the first match with an IPv4
1612 // address will be returned.
1613 bool match_found = false;
1614 while (res && !match_found)
1615 {
1616 mdns_ip_addr_t *ipaddr = res->addr;
1617 while (ipaddr && !match_found)
1618 {
1619 // if this result has an IPv4 address process it
1620 if (ipaddr->addr.type == IPADDR_TYPE_V4)
1621 {
1622 LOG(ESP32_WIFIMGR_MDNS_LOOKUP_LOG_LEVEL,
1623 "[mDNS] Found %s providing service:%s on port %" PRIu16,
1624 res->hostname, service, res->port);
1625 inet_addr_from_ip4addr(
1626 &sa_in->sin_addr, &ipaddr->addr.u_addr.ip4);
1627 sa_in->sin_port = htons(res->port);
1628 match_found = true;
1629 }
1630 ipaddr = ipaddr->next;
1631 }
1632 res = res->next;
1633 }
1634
1635 // free up the query results linked list.
1636 mdns_query_results_free(results);
1637
1638 if (!match_found)
1639 {
1640 LOG(ESP32_WIFIMGR_MDNS_LOOKUP_LOG_LEVEL,
1641 "[mDNS] No matches found for service:%s.", service);
1642 return EAI_AGAIN;
1643 }
1644
1645 // return the resolved data to the caller
1646 *addr = ai.release();
1647 (*addr)->ai_addr = sa.release();
1648
1649 // successfully resolved an address, inform the caller
1650 return 0;
1651}
1652
1653// The functions below are not available via the standard ESP-IDF provided
1654// API.
1655
1661int getifaddrs(struct ifaddrs **ifap)
1662{
1663 esp_netif_ip_info_t ip_info;
1664
1665 /* start with something "safe" in case we bail out early */
1666 *ifap = nullptr;
1667
1668 // Lookup the interface by it's internal name assigned by ESP-IDF.
1669 esp_netif_t *iface = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
1670 if (iface == nullptr)
1671 {
1672 // Station interface was not found.
1673 errno = ENODEV;
1674 return -1;
1675 }
1676
1677 // retrieve TCP/IP address from the interface
1678 if (esp_netif_get_ip_info(iface, &ip_info) != ESP_OK)
1679 {
1680 // Failed to retrieve IP address.
1681 errno = EADDRNOTAVAIL;
1682 return -1;
1683 }
1684
1685 // allocate memory for various pieces of ifaddrs
1686 std::unique_ptr<struct ifaddrs> ia(new struct ifaddrs);
1687 if (ia.get() == nullptr)
1688 {
1689 errno = ENOMEM;
1690 return -1;
1691 }
1692 bzero(ia.get(), sizeof(struct ifaddrs));
1693 std::unique_ptr<char[]> ifa_name(new char[6]);
1694 if (ifa_name.get() == nullptr)
1695 {
1696 errno = ENOMEM;
1697 return -1;
1698 }
1699 strcpy(ifa_name.get(), "wlan0");
1700 std::unique_ptr<struct sockaddr> ifa_addr(new struct sockaddr);
1701 if (ifa_addr == nullptr)
1702 {
1703 errno = ENOMEM;
1704 return -1;
1705 }
1706 bzero(ifa_addr.get(), sizeof(struct sockaddr));
1707
1708 // copy address into ifaddrs structure
1709 struct sockaddr_in *addr_in = (struct sockaddr_in *)ifa_addr.get();
1710 addr_in->sin_family = AF_INET;
1711 addr_in->sin_addr.s_addr = ip_info.ip.addr;
1712 ia.get()->ifa_next = nullptr;
1713 ia.get()->ifa_name = ifa_name.release();
1714 ia.get()->ifa_flags = 0;
1715 ia.get()->ifa_addr = ifa_addr.release();
1716 ia.get()->ifa_netmask = nullptr;
1717 ia.get()->ifa_ifu.ifu_broadaddr = nullptr;
1718 ia.get()->ifa_data = nullptr;
1719
1720 // report results
1721 *ifap = ia.release();
1722 return 0;
1723}
1724
1728void freeifaddrs(struct ifaddrs *ifa)
1729{
1730 while (ifa)
1731 {
1732 struct ifaddrs *next = ifa->ifa_next;
1733
1734 HASSERT(ifa->ifa_data == nullptr);
1735 HASSERT(ifa->ifa_ifu.ifu_broadaddr == nullptr);
1736 HASSERT(ifa->ifa_netmask == nullptr);
1737
1738 delete ifa->ifa_addr;
1739 delete[] ifa->ifa_name;
1740 delete ifa;
1741
1742 ifa = next;
1743 }
1744}
1745
1747const char *gai_strerror(int __ecode)
1748{
1749 switch (__ecode)
1750 {
1751 default:
1752 return "gai_strerror unknown";
1753 case EAI_AGAIN:
1754 return "temporary failure";
1755 case EAI_FAIL:
1756 return "non-recoverable failure";
1757 case EAI_MEMORY:
1758 return "memory allocation failure";
1759 }
1760}
1761
1762#endif // ESP32
#define CDI_READ_TRIMMED(PATH, fd)
Requests a readout of a numeric variable with trimming.
#define CDI_FACTORY_RESET(PATH)
Performs factory reset on a CDI variable.
@ STATION_INTERFACE
This is used for the Station WiFi interface.
@ SOFTAP_INTERFACE
This is used for the SoftAP WiFi interface.
void create_gc_port_for_can_hub(CanHubFlow *can_hub, int fd, Notifiable *on_exit, bool use_select)
Creates a new port on a CAN hub in gridconnect format for a select-compatible file descriptor.
int mdns_lookup(const char *service, struct addrinfo *hints, struct addrinfo **addr)
Lookup an mDNS name.
Definition MDNS.cxx:73
#define STATE(_fn)
Turns a function name into an argument to be supplied to functions expecting a state.
Definition StateFlow.hxx:61
This class sends a notification in its destructor.
A BarrierNotifiable allows to create a number of child Notifiable and wait for all of them to finish.
A notifiable class that calls a particular function object once when it is invoked,...
UpdateAction
Specifies what additional steps are needed to apply the new configuration.
@ UPDATED
No additional step is necessary.
@ REBOOT_NEEDED
Need to reboot the hardware.
Implementation of ConfigUpdateListener that registers itself in the constructor and unregisters itsel...
Default implementation that supplies parametrized values for static and mdns connection methods.
Implementation the ExecutorBase with a specific number of priority bands.
Definition Executor.hxx:266
An object that can schedule itself on an executor to run.
virtual void notify()=0
Generic callback.
Class to allow convenient locking and unlocking of mutexes in a C context.
Definition OS.hxx:494
Collection of related state machines that pend on incoming messages.
static T * instance()
Definition Singleton.hxx:77
SearchMode
Parameter that determines what order we will attempt to connect.
Return type for a state flow callback.
Base class for state machines.
SimpleStack with a CAN-bus based interface and IO functions for CAN-bus.
This structure shows what parameters are configurable in the TcpClientConfig CDI group definition.
Static class for constants and utilities related to the TCP transport protocol.
Definition IfTcpImpl.hxx:48
Esp32WiFiManager()
Default constructor.
#define CONSTANT_TRUE
We cannot compare constants to zero, so we use 1 and 2 as constant values for booleans.
#define htons(x)
Converts a host endian short value to network endian.
Definition in.h:93
#define ntohl(x)
Converts a network endian long value to host endian.
Definition in.h:87
#define htonl(x)
Converts a host endian long value to network endian.
Definition in.h:91
#define LOG(level, message...)
Conditionally write a message to the logging output.
Definition logging.h:99
static const int VERBOSE
Loglevel that is usually not printed, reporting debugging information.
Definition logging.h:59
static const int WARNING
Loglevel that is always printed, reporting a warning or a retryable error.
Definition logging.h:55
static const int INFO
Loglevel that is printed by default, reporting some status information.
Definition logging.h:57
static const int FATAL
Loglevel that kills the current process.
Definition logging.h:51
#define LOG_ERROR(message...)
Shorthand for LOG(LEVEL_ERROR, message...). See LOG.
Definition logging.h:124
#define HASSERT(x)
Checks that the value of expression x is true, else terminates the current process.
Definition macros.h:138
uint64_t NodeID
48-bit NMRAnet Node ID type
const char * gai_strerror(int __ecode)
see 'man gai_strerror'
int getifaddrs(struct ifaddrs **ifap)
Create a linked list of structures describing the network interfaces of the local system.
void freeifaddrs(struct ifaddrs *ifa)
Free a previously generated linked list of structures describing the network interfaces of the local ...
void mdns_publish(const char *name, const char *service, uint16_t port)
Publish an mDNS name.
void mdns_unpublish(const char *name, const char *service)
Unpublish an mDNS name.
#define EAI_MEMORY
Memory allocation failure.
Definition netdb.h:69
#define EAI_AGAIN
Temporary failure in name resolution.
Definition netdb.h:67
#define EAI_FAIL
, Non-recoverable failure in name res
Definition netdb.h:68
#define MSEC_TO_USEC(_msec)
Convert a millisecond value to a microsecond value.
Definition os.h:274
#define SEC_TO_NSEC(_sec)
Convert a second value to a nanosecond value.
Definition os.h:286
#define AF_INET
IPv4 Socket (UDP, TCP, etc...)
Definition socket.h:54
static void repeated_read(int fd, void *buf, size_t size)
Performs a reliable read from the given FD.
Definition FdUtils.hxx:50
Structure to contain information about address of a service provider.
Definition netdb.h:48
struct sockaddr * ai_addr
Socket address for socket.
Definition netdb.h:54
int ai_socktype
Socket type.
Definition netdb.h:51
int ai_protocol
Protocol for socket.
Definition netdb.h:52
int ai_family
Protocol family for socket.
Definition netdb.h:50
network interface address list member
char * ifa_name
name of interface.
struct sockaddr * ifa_netmask
netmask of interface
void * ifa_data
address-specific data
struct ifaddrs * ifa_next
next item in list
struct sockaddr * ifu_broadaddr
broadcast address of interface
struct sockaddr * ifa_addr
address of interface
in_addr_t s_addr
Address.
Definition in.h:51
Structure describing an Internet socket address.
Definition in.h:56
uint16_t sin_family
protocol family (AF_INET)
Definition in.h:57
uint16_t sin_port
port number
Definition in.h:58
struct in_addr sin_addr
internet address
Definition in.h:59
IPv4 socket address.
Definition socket.h:83