├── html ├── hero.png ├── lock.png ├── README.md ├── root.html └── wifi.html ├── docs ├── portal-root.png └── portal-wifi.png ├── .gitignore ├── library.json ├── README.md └── src ├── WiFiManager.h ├── WiFiManager.cpp └── WiFiManagerAssets.h /html/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthorley/wifimanager-pico/HEAD/html/hero.png -------------------------------------------------------------------------------- /html/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthorley/wifimanager-pico/HEAD/html/lock.png -------------------------------------------------------------------------------- /docs/portal-root.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthorley/wifimanager-pico/HEAD/docs/portal-root.png -------------------------------------------------------------------------------- /docs/portal-wifi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthorley/wifimanager-pico/HEAD/docs/portal-wifi.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode 3 | .vscode/.browse.c_cpp.db* 4 | .vscode/c_cpp_properties.json 5 | .vscode/launch.json 6 | .vscode/ipch 7 | 8 | **/.DS_Store 9 | -------------------------------------------------------------------------------- /html/README.md: -------------------------------------------------------------------------------- 1 | # Purpose 2 | 3 | HTML and related content included here is to support development of HTML web pages shown as part of the capture portal. 4 | These pages are not deployed but converted into PROGMEM strings within `WiFiManagerAssets.h` and programtically updated in `WiFiManager.cpp`. 5 | 6 | Icons are attributed to https://pictogrammers.com/library/mdi/. 7 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WiFiManager-Pico", 3 | "version": "1.0.0", 4 | "description": "Captive portal for WiFi credentials capture and management.", 5 | "keywords": "WiFiManager Pico", 6 | "authors": 7 | [ 8 | { 9 | "name": "Matt Thorley" 10 | } 11 | ], 12 | "license": "MIT", 13 | "dependencies": { 14 | }, 15 | "frameworks": "*", 16 | "platforms": "*" 17 | } -------------------------------------------------------------------------------- /html/root.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 65 | 66 | 67 | 68 | 69 | 73 | 74 |
70 | 71 | Hero logo 72 |
75 | 76 |
PicoW
77 |

78 |

Configure the WiFi connection for the following device
79 |

80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
NameOXRS-XYZ-PICOW-FW
Short nameOXRS-XYZ
MakerXYZ
Version0.0.1
98 |

99 | 100 | 101 | 104 | 105 |
102 | Configure WiFi 103 |
106 | 107 | 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WiFiManager for RaspberryPi PicoW 2 | 3 | This library provides a captive portal similar to ESP32 WiFiManager implementations to allow WiFi credentials to be entered and stored in (simulated) EEPROM so that devices can be configured to local WiFi networks. 4 | 5 | Features 6 | - Basic customisable HTML content 7 | - Persistent storage of credentials in EEPROM 8 | 9 | It does not support OTA nor device configuration outside of WiFi credentials. 10 | 11 | ## Basic Usage 12 | 13 | To create a captive portal: 14 | 15 | // Connect using saved creds, or start captive portal if none found. 16 | // Blocks until connected or the portal is closed 17 | 18 | WiFiManager wm("ap_SSID", "ap_password"); // Access point credentials for portal 19 | bool success = wm.autoConnect(); 20 | 21 | By default, the portal appears as below. However, some basic customisation is available for the main (hero) image, title and maker attribution: 22 | 23 | const char main_img[] PROGMEM = R"=====( 24 | data:image/png;base64, ..." 25 | )====="; 26 | 27 | String title("Cool device"); 28 | String name("DeviceName"): 29 | String shortname("TLA"); 30 | String maker("Person"); 31 | String version("1.0"); 32 | wm.setContentText(title, name, shortname, maker, version); 33 | wm.setContentMainImage(main_img); 34 | bool success = wm.autoConnect(); 35 | 36 | where `main_img` is a char* to a base64 encoded image in this case. 37 | 38 | ## Captive Portal 39 | 40 | Following a similar approach to the ESP WiFiManager, if no WiFi credentials are found within EEPROM, the PicoW creates an access point with an SSID of and password of chosing. Once connected a captive portal is presented enabling selection of found WiFi networks and password to be entered. Once validated, these credentials are saved in EEPROM and used to connect on next restart. 41 | 42 | The portal home page confirms the device attributes including name, maker and firmware version. 43 | 44 | Whilst the captive portal has been tested on iOS, Android and macOS, windows has not. If the portal does not show, entering `http://picow.local` into the browser will pull it up. 45 | 46 | Captive Portal Home 47 | 48 | The portal WiFi page allows configuration of the target network. 49 | 50 | Captive Portal WiFi setup 51 | 52 | ## Content 53 | 54 | The templates for the captive portal web assets (e.g. HTML, CSS, images) are defined in [WiFiManagerAssets.h](./src/WiFiManagerAssets.h) where web assets or fragments of HTML are stored as `PROGMEM` variables within flash. These variables are then consumed within [WiFiManager.cpp](./src/WiFiManager.cpp) to hydrate the web assets and perform any dynamic text or style substitutiions using `String` to replace `${variable_name}` with content. 55 | 56 | The web assets are derived from web content held under [html](./html). These web assets can be extended and tested in isolation before being encoded into `PROGMEM` variables. 57 | 58 | Note the use of EEPROM (used to store WiFi credentials) is [simulated](https://arduino-pico.readthedocs.io/en/latest/eeprom.html), since the Raspberry Pi Pico RP2040 does not come with an EEPROM onboard, it is simulated by using a single 4K chunk of flash at the end of flash space. 59 | 60 | ## Installation 61 | 62 | For platformio, add the library as a reference to your main project under ```lib_deps```: 63 | 64 | ``` 65 | [env:pico] 66 | platform = https://github.com/maxgerhardt/platform-raspberrypi.git 67 | framework = arduino 68 | board = rpipicow 69 | board_build.core = earlephilhower 70 | board_build.filesystem_size = 0.5m 71 | monitor_filters = time 72 | monitor_speed = 115200 73 | lib_deps = 74 | https://github.com/mthorley/wifimanager-pico.git 75 | CRC@^1.0.1 76 | ``` 77 | 78 | ## Debugging 79 | 80 | The library will output logs to Serial, if debug is enabled in the constructor: 81 | 82 | ``` 83 | bool debug = true; 84 | WiFiManager wm("ap_SSID", "ap_password", debug); // Access point 85 | ``` 86 | 87 | ## Attribution 88 | 89 | Hero image is a nod to the excellent [OXRS ecosystem](https://oxrs.io/). 90 | -------------------------------------------------------------------------------- /src/WiFiManager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include // yes this is correct 8 | #include // simulated EEPROM: https://arduino-pico.readthedocs.io/en/latest/eeprom.html 9 | #include 10 | 11 | /** 12 | * @brief WiFiManager provides captive portal and network configuration management to allow devices 13 | * to connect to a local network SSID and password. 14 | * 15 | * Derived from the excellent https://www.smartlab.at/implement-an-esp32-hot-spot-that-runs-a-captive-portal/ and 16 | * https://stackoverflow.com/questions/54583818/esp-auto-login-accept-message-by-os-with-redirect-to-page-like-public-wifi-port 17 | */ 18 | 19 | #define SSID_OR_PWD_MAXLEN 32 20 | 21 | class WiFiManager 22 | { 23 | public: 24 | WiFiManager(char const *apName, char const *apPassword, bool serialLog = false); 25 | ~WiFiManager(); 26 | 27 | bool autoConnect(); 28 | void setContentText(String& title, String& name, String& shortname, String& maker, String& version); 29 | void setContentMainImage(const char* img); 30 | 31 | inline static const char *HOSTNAME = "picow"; // hostname for mDNS to enable http://picow.local 32 | 33 | private: 34 | // Used to load/save credentials and CRC32 as struct in EEPROM. 35 | typedef struct wifi_credentials_t { 36 | char _ssid[SSID_OR_PWD_MAXLEN + 1]; 37 | char _pwd[SSID_OR_PWD_MAXLEN + 1]; 38 | uint32_t _crc; // CRC32 check 39 | } wifi_credentials_t; 40 | 41 | // state machine states 42 | typedef enum State { // _currentState, _previousState; 43 | START, 44 | LOAD_CREDENTIALS, // -> (check for WiFi credentials in EEPROM) ? CONNECT_TO_WLAN : SHOW_PORTAL // _connectionAttempts reset; setupConfigPortal false; APrunning false; 45 | CONNECT_TO_WLAN, // -> (connection successful) ? STOP : RETRY_CONNECTION // exceeded retries, then _accessPointRunning false to reinitalise AP 46 | RETRY_CONNECTION, // -> (num connection attempts > 3) ? SHOW_PORTAL : CONNECT_TO_WLAN // _connectionAttempts++ 47 | SHOW_PORTAL, // -> (WLAN configured) ? SAVE_CREDS : SHOW_PORTAL 48 | SAVE_CREDENTIALS, // -> (save credentials) then CONNECT_TO_WLAN // _connectionAttempts reset 49 | STOP 50 | } State_t; 51 | 52 | // heart of the machine 53 | void cycleStateMachine(); 54 | 55 | // wifi connection methods 56 | int8_t connectWifi() const; 57 | const char* getWLStatus(const int8_t wlStatus) const; 58 | 59 | // captive portal methods 60 | bool startConfigPortal(); 61 | bool redirectToPortal() const; 62 | boolean isIp(const String& str) const; 63 | 64 | // credential EEPROM storage 65 | bool loadCredentials(); 66 | void saveCredentials(); 67 | 68 | // webserver handler callbacks 69 | void handleRoot() const; 70 | void handleWifi() const; 71 | void handleWifiSave(); 72 | void handleNotFound() const; 73 | 74 | // html helpers 75 | void sendStandardHeaders() const; 76 | void getSignalStrength(String& cssStyle, const int32_t rssi) const; 77 | 78 | // members 79 | State_t _currentState; // current state of network configuration state machine 80 | State_t _previousState; // previous state of network configuration state machine 81 | uint8_t _connectionAttempts; // count of connection attempts 82 | bool _setupConfigPortal; // true if web/dns servers have already been setup for captive portal 83 | bool _accessPointRunning; // true if the AP WiFi is running to support captive portal 84 | 85 | bool _serialLog; // true if Serial log output is required for debugging 86 | 87 | // Portal content related 88 | const char* _hero_img; // base64 image reference 89 | String _title; 90 | String _name; 91 | String _shortname; 92 | String _maker; 93 | String _version; 94 | 95 | // RSSI thresholds for displaying strength indicators 96 | inline static const int32_t STRONG_RSSI_THRESHOLD = -55; 97 | inline static const int32_t MEDIUM_RSSI_THRESHOLD = -80; 98 | 99 | // Access Point network parameters 100 | static const IPAddress AP_IP; 101 | 102 | // DNS related 103 | inline static const byte DNS_PORT = 53; 104 | 105 | // WiFi connection consts 106 | inline static const uint8_t MAX_CONNECTION_ATTEMPTS = 3; // max number of attempts to connect to WiFi 107 | inline static const uint16_t MAX_CONNECTION_TIMEOUT_MS = 10000; // num of ms to wait for successful WiFi connection 108 | 109 | // Target WLAN SSID and password 110 | char _wlanSSID[SSID_OR_PWD_MAXLEN + 1]; 111 | char _wlanPassword[SSID_OR_PWD_MAXLEN + 1]; 112 | 113 | // Access point SSID and password 114 | char _apSSID[SSID_OR_PWD_MAXLEN + 1]; 115 | char _apPassword[SSID_OR_PWD_MAXLEN + 1]; 116 | }; 117 | -------------------------------------------------------------------------------- /html/wifi.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 127 | 133 | 134 | 135 | 136 | 137 | 141 | 142 |
138 | 139 | Hero logo 140 |
143 | 144 |

PicoW
145 |

146 |

Available WiFi Networks
147 |

148 | 149 | 150 | 151 | 158 | 159 | 162 | 163 | 164 | 165 | 172 | 173 | 176 | 177 | 178 | 185 | 186 | 189 | 190 | 191 | 198 | 199 | 202 | 203 |
152 |
153 |
154 |
155 |
156 |
157 |
NETGEAR 160 | 161 |
166 |
167 |
168 |
169 |
170 |
171 |
Not Your Wifi 174 | 175 |
179 |
180 |
181 |
182 |
183 |
184 |
halflife 187 | 188 |
192 |
193 |
194 |
195 |
196 |
197 |
Open network on 2.4GHz 200 | 201 |
204 |

205 |
206 | 207 | 208 | 211 | 212 |
209 |
WiFi Settings
210 |
213 | 214 | 215 | 222 | 223 |
216 |
217 |
218 |
219 | 220 |
221 |
224 | 225 | 226 | 229 | 230 |
227 |

Back to home

228 |
231 | 232 | 233 | -------------------------------------------------------------------------------- /src/WiFiManager.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "WiFiManager.h" 3 | #include "WiFiManagerAssets.h" 4 | 5 | /* 6 | Tasks 7 | - unit tests for EEPROM creds 8 | - validation of ssid, pwd for type and length 9 | */ 10 | 11 | // Logging macros 12 | #define LOGF_ERROR(fmt, ...) if (_serialLog) { Serial.print("[E] "); Serial.printf(fmt, __VA_ARGS__); Serial.println(); } 13 | #define LOGF_DEBUG(fmt, ...) if (_serialLog) { Serial.print("[D] "); Serial.printf(fmt, __VA_ARGS__); Serial.println(); } 14 | #define LOG_DEBUG(s) if (_serialLog) { Serial.print("[D] "); Serial.println(s); } 15 | 16 | // DNS server 17 | DNSServer dnsServer; 18 | 19 | // Web server 20 | WebServer server(80); 21 | 22 | static const char *_LOG_PREFIX = "[WiFiManager] "; 23 | 24 | // AccessPoint network config 25 | const IPAddress WiFiManager::AP_IP = IPAddress(192,168,42,1); 26 | 27 | WiFiManager::WiFiManager(const char *apName, const char *apPassword, bool serialLog) : 28 | _wlanSSID(""), 29 | _wlanPassword(""), 30 | _serialLog(serialLog), 31 | _setupConfigPortal(false), 32 | _accessPointRunning(false), 33 | _connectionAttempts(0), 34 | _currentState(State_t::START), 35 | _previousState(State_t::STOP) 36 | { 37 | strncpy(_apSSID, apName, 32); 38 | strncpy(_apPassword, apPassword, 32); 39 | 40 | _hero_img = ::hero_img; 41 | _title = "PicoW"; 42 | 43 | // FIXME: Is this in the correct location... 44 | EEPROM.begin(512); 45 | } 46 | 47 | WiFiManager::~WiFiManager() 48 | { 49 | // stop WiFiManager servers so other servers downstream (e.g. aWOT) can use the same ports 50 | dnsServer.stop(); 51 | server.stop(); 52 | } 53 | 54 | bool WiFiManager::autoConnect() 55 | { 56 | // iterate through state machine until we have configured WiFi 57 | while (_currentState != State_t::STOP) 58 | cycleStateMachine(); 59 | 60 | return true; 61 | } 62 | 63 | // update html main image and title content 64 | void WiFiManager::setContentText(String& title, String& name, String& shortname, String& maker, String& version) 65 | { 66 | _title = title; 67 | _name = name; 68 | _shortname = shortname; 69 | _maker = maker; 70 | _version = version; 71 | } 72 | 73 | void WiFiManager::setContentMainImage(const char* img) 74 | { 75 | _hero_img = img; 76 | } 77 | 78 | void WiFiManager::cycleStateMachine() 79 | { 80 | if (_previousState != _currentState) 81 | LOGF_DEBUG("Current state %d", _currentState); 82 | 83 | switch(_currentState) { 84 | case START: 85 | _currentState = LOAD_CREDENTIALS; // move to LOAD_CREDENTIALS, initialise state variables 86 | _accessPointRunning = false; 87 | _setupConfigPortal = false; 88 | _connectionAttempts = 0; 89 | break; 90 | 91 | case LOAD_CREDENTIALS: // attempt to load WiFi credentials from EEPROM 92 | if (loadCredentials()) { 93 | LOG_DEBUG(F("Credentials exist")); 94 | _currentState = CONNECT_TO_WLAN; 95 | } 96 | else { 97 | LOG_DEBUG(F("Credential CRC match fail")); 98 | _currentState = SHOW_PORTAL; // CRC match failure so show captive portal 99 | } 100 | break; 101 | 102 | case SHOW_PORTAL: // spin up captive portal 103 | startConfigPortal(); // portal wifi page form submit will move the state to SAVE_CREDENTIALS 104 | break; 105 | 106 | case SAVE_CREDENTIALS: 107 | saveCredentials(); 108 | _connectionAttempts = 0; 109 | _currentState = CONNECT_TO_WLAN; 110 | break; 111 | 112 | case CONNECT_TO_WLAN: // attempt to connect and retry 113 | if (connectWifi() != WL_CONNECTED) { 114 | _connectionAttempts++; 115 | delay(500); // give it some time 116 | } 117 | else { 118 | _currentState = STOP; 119 | } 120 | 121 | if (_connectionAttempts > MAX_CONNECTION_ATTEMPTS-1) { 122 | _accessPointRunning = false; // reinitialise AP 123 | _currentState = SHOW_PORTAL; 124 | } 125 | break; 126 | 127 | case STOP: // done: WiFi creds configured and connected to 128 | break; 129 | 130 | default: 131 | LOGF_ERROR("Invalid state %d", _currentState); 132 | break; 133 | } 134 | 135 | // if the config portal has been setup, handle dns and web requests 136 | if (_setupConfigPortal) { 137 | dnsServer.processNextRequest(); 138 | server.handleClient(); 139 | } 140 | 141 | if (_previousState != _currentState) 142 | LOGF_DEBUG("Transitioned to state %d", _currentState); 143 | 144 | _previousState = _currentState; 145 | } 146 | 147 | // Is str an IP address 148 | boolean WiFiManager::isIp(const String& str) const 149 | { 150 | for (size_t i = 0; i < str.length(); i++) 151 | { 152 | int c = str.charAt(i); 153 | if (c != '.' && (c < '0' || c > '9')) 154 | { 155 | return false; 156 | } 157 | } 158 | return true; 159 | } 160 | 161 | /** 162 | * Convert WL_STATUS into String for logging. 163 | */ 164 | const char* WiFiManager::getWLStatus(const int8_t wlStatus) const 165 | { 166 | switch (wlStatus) 167 | { 168 | case WL_IDLE_STATUS: 169 | return "WL_IDLE_STATUS"; 170 | case WL_NO_SSID_AVAIL: 171 | return "WL_NO_SSID_AVAIL"; 172 | case WL_SCAN_COMPLETED: 173 | return "WL_SCAN_COMPLETED"; 174 | case WL_CONNECTED: 175 | return "WL_CONNECTED"; 176 | case WL_CONNECT_FAILED: 177 | return "WL_CONNECT_FAILED"; 178 | case WL_CONNECTION_LOST: 179 | return "WL_CONNECTION_LOST"; 180 | case WL_DISCONNECTED: 181 | return "WL_DISCONNECTED"; 182 | default: 183 | return "Unknown"; 184 | } 185 | } 186 | 187 | /** 188 | * Connect to configured WLAN WiFi 189 | * 190 | * @return int8_t connection result 191 | */ 192 | int8_t WiFiManager::connectWifi() const 193 | { 194 | LOGF_DEBUG("Connecting as wifi client to SSID %s", _wlanSSID); 195 | 196 | WiFi.disconnect(); 197 | WiFi.begin(_wlanSSID, _wlanPassword); 198 | int8_t res = WiFi.waitForConnectResult(MAX_CONNECTION_TIMEOUT_MS); 199 | 200 | LOGF_DEBUG("Connection result %s", getWLStatus(res)); 201 | return res; 202 | } 203 | 204 | /** 205 | * Create access point and register and serve web content for 206 | * the captive portal. 207 | */ 208 | bool WiFiManager::startConfigPortal() 209 | { 210 | if (!_accessPointRunning) { 211 | // Go into AP mode, configure IP (so DNS can use it) 212 | WiFi.config(AP_IP); 213 | WiFi.beginAP(_apSSID, _apPassword); 214 | while (WiFi.status() != WL_CONNECTED) 215 | { 216 | delay(500); 217 | LOG_DEBUG("."); // waiting 218 | } 219 | 220 | delay(500); // wait for AP to stabalise 221 | 222 | String ip = WiFi.localIP().toString(); 223 | LOGF_DEBUG("AccessPoint IP address %s", ip.c_str()); 224 | _accessPointRunning = true; 225 | } 226 | 227 | // only setup config portal once 228 | if (!_setupConfigPortal) 229 | { 230 | LOG_DEBUG(F("Starting config portal")); 231 | 232 | // Setup the DNS server redirecting all domains to the AP_IP 233 | dnsServer.setErrorReplyCode(DNSReplyCode::NoError); 234 | dnsServer.start(DNS_PORT, "*", AP_IP); 235 | 236 | // Setup web pages: root, wifi config and not found 237 | server.on("/", std::bind(&WiFiManager::handleRoot, this)); 238 | server.on("/wifi", std::bind(&WiFiManager::handleWifi, this)); 239 | server.on("/wifisave", std::bind(&WiFiManager::handleWifiSave, this)); 240 | server.onNotFound(std::bind(&WiFiManager::handleNotFound, this)); 241 | server.begin(); // Web server start 242 | 243 | LOG_DEBUG(F("HTTP server started")); 244 | _setupConfigPortal = true; 245 | } 246 | 247 | return _setupConfigPortal; 248 | } 249 | 250 | /** 251 | * Redirect to captive portal if we got a request for another domain. 252 | * Return true in that case so the page handler does not try to handle the request again. 253 | */ 254 | bool WiFiManager::redirectToPortal() const 255 | { 256 | if (!isIp(server.hostHeader()) && server.hostHeader() != (String(HOSTNAME) + ".local")) 257 | { 258 | LOG_DEBUG(F("Request redirected to captive portal")); 259 | 260 | IPAddress ip = server.client().localIP(); 261 | server.sendHeader("Location", String("http://") + ip.toString(), true); 262 | server.send(302, "text/plain", ""); // Empty content inhibits Content-length header so we have to close the socket ourselves. 263 | server.client().stop(); // Stop is needed because we sent no content length 264 | 265 | return true; 266 | } 267 | return false; 268 | } 269 | 270 | /** 271 | * Send standard HTTP headers 272 | */ 273 | void WiFiManager::sendStandardHeaders() const 274 | { 275 | server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); 276 | server.sendHeader("Pragma", "no-cache"); 277 | server.sendHeader("Expires", "-1"); 278 | } 279 | 280 | /** 281 | * Handle root or redirect to captive portal 282 | */ 283 | void WiFiManager::handleRoot() const 284 | { 285 | // If captive portal then redirect instead of displaying the page 286 | if (redirectToPortal()) { 287 | return; 288 | } 289 | 290 | sendStandardHeaders(); 291 | String html(root_template); 292 | html.replace("${hero.img}", hero_img); 293 | html.replace("${firmware.name}", _name); 294 | html.replace("${firmware.shortname}", _shortname); 295 | html.replace("${firmware.maker}", _maker); 296 | html.replace("${firmware.version}", _version); 297 | server.send(200, "text/html", html); 298 | } 299 | 300 | /** 301 | * Converts RSSI into a CSS style to indicate WiFi network signal strength. 302 | */ 303 | void WiFiManager::getSignalStrength(String& cssStyle, const int32_t rssi) const 304 | { 305 | if (rssi < STRONG_RSSI_THRESHOLD) 306 | cssStyle = "strong"; 307 | else if(rssi < MEDIUM_RSSI_THRESHOLD) 308 | cssStyle = "medium"; 309 | else 310 | cssStyle = "weak"; 311 | } 312 | 313 | /** 314 | * Show available wifi networks. 315 | */ 316 | void WiFiManager::handleWifi() const 317 | { 318 | sendStandardHeaders(); 319 | String html(wifi_template); 320 | html.replace("${hero.img}", hero_img); 321 | html.replace("${img.lock}", lock_img); 322 | 323 | String htmlNetworks; 324 | LOG_DEBUG(F("Network scan start")); 325 | int n = WiFi.scanNetworks(); 326 | LOG_DEBUG(F("Network scan complete")); 327 | 328 | if (n > 0) { 329 | for (int i = 0; i < n; i++) { 330 | if ( strlen(WiFi.SSID(i)) > 0 ) // ignore hidden SSIDs 331 | { 332 | String htmlNetwork(wifi_network_template); 333 | htmlNetwork.replace("${name}", WiFi.SSID(i)); 334 | 335 | String signal; 336 | getSignalStrength(signal, WiFi.RSSI(i)); 337 | htmlNetwork.replace("${signal}", signal); 338 | 339 | htmlNetwork.replace("${network.security}", WiFi.encryptionType(i) == ENC_TYPE_NONE ? "unlock" : "lock"); 340 | htmlNetworks += htmlNetwork; 341 | } 342 | } 343 | } 344 | else { 345 | htmlNetworks = F("No networks found"); 346 | } 347 | 348 | WiFi.scanDelete(); // clean-up 349 | 350 | html.replace("${networks}", htmlNetworks); 351 | server.send(200, "text/html", html); 352 | server.client().stop(); // Stop is needed because we sent no content length 353 | } 354 | 355 | /** 356 | * Process the WiFi save form, save credentials and redirect to WiFi config page again. 357 | */ 358 | void WiFiManager::handleWifiSave() 359 | { 360 | LOG_DEBUG(F("Handle WiFi save")); 361 | 362 | server.arg("n").toCharArray(_wlanSSID, SSID_OR_PWD_MAXLEN); 363 | server.arg("p").toCharArray(_wlanPassword, SSID_OR_PWD_MAXLEN); 364 | server.sendHeader("Location", "wifi", true); 365 | sendStandardHeaders(); 366 | server.send(302, "text/plain", ""); // Empty content inhibits Content-length header so we have to close the socket ourselves. 367 | server.client().stop(); // Stop is needed because we sent no content length 368 | 369 | _currentState = State_t::SAVE_CREDENTIALS; // force next step in state machine 370 | } 371 | 372 | void WiFiManager::handleNotFound() const 373 | { 374 | // If captive portal then redirect instead of displaying the error page 375 | if (redirectToPortal()) 376 | { 377 | return; 378 | } 379 | 380 | String message = F("File Not Found\n\n"); 381 | message += F("URI: "); 382 | message += server.uri(); 383 | message += F("\nMethod: "); 384 | message += (server.method() == HTTP_GET) ? "GET" : "POST"; 385 | message += F("\nArguments: "); 386 | message += server.args(); 387 | message += F("\n"); 388 | 389 | for (uint8_t i = 0; i < server.args(); i++) 390 | { 391 | message += String(F(" ")) + server.argName(i) + F(": ") + server.arg(i) + F("\n"); 392 | } 393 | sendStandardHeaders(); 394 | server.send(404, "text/plain", message); 395 | //FIXME: button to get back home? 396 | } 397 | 398 | bool WiFiManager::loadCredentials() 399 | { 400 | // load credentials from EEPROM (emulated) 401 | wifi_credentials_t temp; 402 | EEPROM.get(0, temp); 403 | 404 | CRC32 crc32; 405 | crc32.add((const uint8_t*)temp._ssid, strlen(temp._ssid)); 406 | crc32.add((const uint8_t*)temp._pwd, strlen(temp._pwd)); 407 | uint32_t tempCRC = crc32.calc(); 408 | 409 | LOGF_DEBUG("Retrieved credentials for SSID: %s and CRC %d", temp._ssid, temp._crc); 410 | 411 | // compare CRC 412 | if (tempCRC != temp._crc) 413 | { 414 | return false; 415 | } 416 | else 417 | { 418 | // copy temp to this 419 | strncpy(_wlanSSID, temp._ssid, sizeof(temp._ssid)); 420 | strncpy(_wlanPassword, temp._pwd, sizeof(temp._pwd)); 421 | return true; 422 | } 423 | } 424 | 425 | void WiFiManager::saveCredentials() 426 | { 427 | // save to EEPROM (emulated) 428 | wifi_credentials_t temp; 429 | strncpy(temp._ssid, _wlanSSID, sizeof(_wlanSSID)); 430 | strncpy(temp._pwd, _wlanPassword, sizeof(_wlanPassword)); 431 | 432 | CRC32 crc32; 433 | crc32.add((const uint8_t*)_wlanSSID, strlen(_wlanSSID)); 434 | crc32.add((const uint8_t*)_wlanPassword, strlen(_wlanPassword)); 435 | temp._crc = crc32.calc(); 436 | 437 | EEPROM.put(0, temp); 438 | EEPROM.commit(); 439 | 440 | LOGF_DEBUG("Stored credentials for SSID: %s and CRC: %d", temp._ssid, temp._crc); 441 | } 442 | -------------------------------------------------------------------------------- /src/WiFiManagerAssets.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /** 4 | * @brief HTML assets for captive portal used by WiFiManager classes. 5 | */ 6 | 7 | const char hero_img[] PROGMEM = R"=====( 8 | data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOAAAADZCAYAAAA5WDyEAAAACXBIWXMAAAsSAAALEgHS3X78AAAgAElEQVR4nOy9CXwkZ3nn/3vr6PvQLY3m9jW+PT6xsYlxIDiEIwYHG7IJJEuyS2D9D8cCgQRjYPMBQjjWAeM4DgGSLIZA4hBIjA04HCZgfM14Dttzz2hGGo2kVt9XHf/P81RVd/Xdkrp7JI0eGKu7qrqquqq+/T73K0zTxJqsLLnsxrtHC8n52/ikTes/kupNKz7fkR0/fucP1m7nypE1AJe5XHD1/7nDNPVXGaZ+CQyjD5IISIoCIcsQXg8DyO8lCXDupYGcMHAEBvbCwPd3/uRdXzjTr+NylTUAl5lceO3HN5mG/ieGqd2sa4WzhBCQvT5IXg9kr7cCNGvwM+0XKANY9Z7usdDEo0LHfTsfe/cDZ/QFXmayBuAykYuu+/j1ulH8oq4VLjFJpVRUKMGABZ2QrJMsAbZwAJ3lQhfPCw3v2Pmzd6+pqstA1gA8zXLRiz95vW4WGDy25WQVaiQMyeOxTqwuYIsH0FlujYjiTTv/690nV/P1Xe6yBuBpkotu+OQmw9Du043iy0zDUIQA1EgEis8PGgErIOoCgCxkK+ri688+9p7fW23Xd6XIGoCnQS684eP36EbhLaahB+jyq4EAlGDQtu/QOwBtDyoMEZM08aGd//WeNWdNj2UNwB7KRS/55B26WfygYWhjBIGkqvBEouzRdEPRcwDtt2QfQhO3PfuL9+xc0Rd6BckagD2Qi2/81Cbd1L6jG4VL2P6SZHjCEUgetS4UpwtA59iSJj0IA2979hf/e80+7LKsAdhlufCln3xINwovg2kqgIBC6mYgUOfhXz4AkgjLPrz32Z+/910r5mKvQFkDsEty0U2f+qiOwntMwwjQAy/7fFCCYQjncMscQP5Do7UhJoQu3vvs4+9dix92QdYA7LBc/LJPX2+YxW/qpmXnyaoHSiDI2Sqm2ezhX54AOq+FIZ4Qunjrs79875p92EFZA7BDcvHLP7PJhP6Abhauo0sqSRKDJ3u8LhBWLoDOC6FJXxGGeP+zT7x3zT7sgKwB2AG5+BWffkBH4VbTMBUIAcXjc9l5WFUAWsvJPpQ+vuuX7/voabzsq0LWAFyCXPyKz95hSIVPmKYRMA1A9nig+AMQQq4L1yoCkLcThvS8ZIi7nn3ifWv24SJlDcBFyCU3f+56Q9K/akA7i+N5sgLZF+C/LA3gWm0AOoskQ3pUGOKdzz75vjX7cIGyBuAC5NLfuHuTAe0+XWg3Ww+ngOoLsJ3XDlyrFUDwg8Rq6ddhivfveup9a/Zhm7IGYJtyyav+7z2GrP2hYRjszlQ8fi4TcsIK3QRQz+VQzGah53MwikVo2Ux5F+5tAc4l5YRuvx8qqcOS3BMAS9tSWpshfWjXU+9fS2trQ9YAbCGXvvruO3RZ/4gJox+GVa1AD3l1iVCnATQKReTm51BMJWEUNWcjBopsTforkYfV3oehFRlOvZCHqRul7dVACN5wlM+5JwDa+yT7UBjSO3Y9/f61sqcmsgZgA7n0NZ+/3pT1LxpCv4QLWoUMxeuHJCmLhqudbQi8zNwpFOJxfk8J2gSRGgpD9fkYvFYqKI+SuSxy8RgDCU74DsLfP8R1hr0AsKSakn1oSm/a9fT719TSOrIGYJVc9trPbzJk8z5D0l8G0+D0MVn1QVa9roe98wCauobs7AxysRgvV3wBTtT2hiNLsgEJxGxslv+S+KIDPCI6lRfdBtBelhOGdO/upz+wltZWJWsAuuTS13/hHkPW30JhBc5iUTxQVB8pVNZGXQIwF5tDduYUTMPgEco/PAxPMFx1zMUBaP0xUUglWKU1NA3U5oJGQ08w0iMAra2Fyfbh23c984G1sIUtawDSqPf6e95gKMbdpjA4fYzVTY8fwgbPdD9kHQSwmEkjPXmC7TeyKX39A/APDHXNC2rqOvKJOPKJGMNOKrUv2s9/nc91E0C3fSiZ8m27dvzJGR+2OKMB3P76L27SVfM7pkwdx0yGQFF8diC9ChygYwCSjZY5OYVCKsnLvZEoAsMjtn2HrochjGIB2dgM/wCQeAJhBpEcTL0A0FknmfKDwpDetmvnn5yx9uEZC+Blb/ziQ4ZsvMyEqQgTkCUVsuK1no/qB71DABqajtzcLNt6YMeIH/7hUShe32mJA2pZsg9PQS8U2Cb0hqLwRvvL23YZQHv7nGTIH9+984NnZFrbGQfg9tv/+g7Dq3/ChBngsIKkQpG9lXZQFwDMx+eRPnmS1UC28waH4I1G6z7AvQ7E00hMIyKppVSdT/ah6gv2CkBLLTXFhDDl9+7e+cEzyj48YwDcfvt91xte85umZNt5kKFIHiueVwVPJwEkNS976hQ0UvckGb6+frb1WN2ss5/TASD/3zCQT8wjm5jjVWQD+/sGbO+vcxW7B6CzTJjSE5IpvXXXsx88I+zDVQ/g5W+8b5PhwQOmalwHik+bArKkQBJV8bAOA6gX88hOO/E8E55oH/yDw1wXWDrmMgKwdN5ageOQTtjCEwjBHx2ywhY9ANDZj2QqX6G0tt27Priq7cNVDeD2N//NA6bHvNWx8yTY4KEWuk4BSCpmbnaWbT3KSJF9XgRGRjkQ3o6deLoBdMAiADOz0zB0zfLQRvrhDUV6BqD1WsQkyJ/b/eyfrlr7cFUCePnv3n+H6cdHTJj9dEMlyJCgcljBdN/wDgNYSMSRmZpiLyeNGIHRMXijfQty1CwXAJ1rkk/GkbPDFpIiI9A3zOppLwB0XkqmeF6Y8l27d//pqrMPVxWAl//ul643/fiqqRhnwWqjAJlqZCFXwdRZALVcDunJSWhpy63vHxqGb8Cx82pHydIxVwCA/FI3GMJ8ap7fs31IaW1UftUDAJ31kik/KiC9c/fuP1019uGqAPCKN39pk+kR95k+82aTwTMhmSokU6rzMKJjAJK6SeDl5+0HMxBAcHycM2iaqamlY64QAJ1FejGHbGwOWt62D0MR+EL9lTMzdRFAOGVPkL++Z/eHVkU37xUP4BV/8OV7jAD+EDAVDisYChWIcq1evYezUwBmTk6zrcdhBVVFcHw9d7gu7W8VAuhsy57d+GzZPowOwOMP1d8nOgugc15CSDHJlD+0e8+freiypxUL4BV/8JU7TB8+CAVj9B0kXUDoipU+5n5QOwxgMZVG6vgEDA5eywgMD8M3ONgWXKsFQGc7Ukspx9Q0DciKCl9kkPvh9AJARwSk5yXI79i9589WZNnTigPwyrd+5XrTL75oesFdpqELyLpE+YWVIxXqP5yLBZCyRdITEwwgbeQbGERgZMR2z7cH12oDEJzdozGIxYyVVqd4A/BHB+yyrfLxugWg83kJyoMC0tt27/2zFRW2WFEAXvG//v4h+IWVPmYAoihB0svxqW4AyHYeqZszM7yIwgmBdWNW+lj1fs9AAJ0/XH+YinHVPok32AdvMFKyD7sNIP8VIidBvnfP3jtXTNnTigDwyrf//UfNkPweCJPLhERRQNLkCpusGwDm5+aQPjEJg+w8jwfBsTGe02GxcK1mAJ1tC+kU8qkYj4zkJfWG++DxBXsDoP1WgOKHytv3PPehZR+2WNYAXvlH//AGhOS7oZpjdJpCA6SCoL4j1gZdArCQTLJ3U89m2c7zDg4iMDpac7ya/a4BaK0zdORTiVLYgppWeUN9XFvZCwCd/0iQn5eEfNvu5z60bMMWyxLAq97+j5vMgPQd+MUlfDF1EyIvaFbX2pvVQQDZzjt+AoV4ghd7+/sQXLeOczjrHa9mv2sAVuyTvKSUW6rl0rzY4wvBF+4v99Npdh06AKCzQBbKV4SQ3r/7uQ8tO/tw2QF41bsfeAAh6VZTmIrQSacBpHzVhe0wgKamc95m5qR1f6ilfGB0BGowWLG7NQAXBqAjFDckEPWiVfbk8YfhC/U1vw4dBBBWUConCfXje56/c1mltS0bAK/64wfuQJ/8CcgImFQcS+DleBrlxkZ8BwDMzc0hO3mSRz/qNuYfGeWRr/YzawAuFkDn/hUyKeSSc1Zam6zwaKh6Aj0B0NlGCGlCEsp79zx/57KwD087gFf/8QPXIyJ/0/RKXCaEogmRpdlaXQ9XFwAsJlPITE5xWEHIEtfn+YeHqvporgFY8bklAoiSfTiPvBO2UL0Moix7KvfTJQAdEZCfkIT81j0v3Hla7cPTBuDV7/r6JgTlBxCSryMbj/9laeSr8/B1EEAtX0B2copHPlrniUas9DG1XCWxBmD3AHTWsX0Yn4FGbRNhwuuPcEW+KM+g2FUA7dc5SShfJ/twzwt3nhb78LQAeM2ffPMe9Cl/aAoolLeJjAHkTEvdrPfwdQhAGvEy0zPcAlDx+xEcXwc1FKp52NYA7D6Ajmj5HLKJmVJamzcQhTcQrrzuXQPQWiAgxSShfG7Pvg/33D7sKYDXvO+bd4h+9SOmKrhMCDkDIm0AWgvAlghgfj7OWSzU9JY8msH167haoeKhWQPwtADoLMul5lHIJG37UIY/PMj9WCuOW31edfa5GACdP0Jw2OKuPfs+3DP7sCcAXvPeb16PPvWrCMg8mxAKBkTK4L8sRncA1NIZpI8dRzGV4ve+4SEExsbY5qt5wNYAPK0AWtedyp7mUMhZZV3UDoNAJCB7AaDzNSRJflQSypv27Ou+WtpVAF/0/m9tMoPyfaJPvZk9m2TnJQ0gq1sbOMfuMICUhUHg5WdneTmpmaHNG9nLabr3sQbgsgLQ2ZbaYlBam2UfAt5AhEMXNf176ny+EwCCWzELtg/3HvhIV8ueugbgiz784D3o97wFCrj7GJI6kNKrbmznAcyenEbmxJRVJuRREd60CWo4VAXTGoD1z2F5AOhIIZdhEE1DIzsN3lA/PN5gxTbdAtBZLoSISZL6ob377+pK2VPHAXzRnQ/eIfrUD8Ivj7FTJa8DMQ2mZjR8kDoBYDGRQuLQkXKZ0PgYhxXqQrAGYINzWF4AmrZaWsgkLPvQJPvQA18waqe1dR9ARyQhPS9J6m179n+4o2GLjgF47Z89uAl96ndEROUu02bRYPAYQOeh7gKAeq6A5OHDDCCJd3AAwQ3rbbuhAQRrADY4h+UHoLMRmRX5TBxFsg/JrPAFOXRRPVtVtwB0Pi9L6oNCyG/bc+DDHbEPOwLgtX/x3YdEn+dlEFBMsvPmC2V1E4562FkAqUKBMlhI3aQFZOcFN2yAEvBX7n8NwFUBoLNeK+SQT8e57SNNMuPxhbkan1RU9+66BaAlIidLyr17D350yWVPSwLwuj//zkcx4nuPkAWXCZnxIsy4BjgTRHYJwNypWaSOTnAOJzlWghvXw9sXrQTF/XnXsjUAVzaAznmRpzSfjvF344oVf4Ttw94AaAmXPcnq2/ce+MiiwxaLAvC6j33nDWLIe7cIKmMmB9I1mLEizFyVd7PDABbjSaSOHoOWyVot1EeGOZhePkad/a8BuCoBtNYbbBvms3FeTmELb6DPTmvrPoDOPiRJeUKSlLfuOXDXgu3DBQF43ce+s0n0eR4Q/d7r+KIWDJizOSBtgccwov7NWAqANEd66sgE8jGryzT1YAmsXwfZo1Ydo87+1wBctQA67w29iFx6Hlohy6tVT5AdNaWwRfU+3Met+H6LA9ARSfJ8RaK0toN3tW0ftg3giz/38ANi2HsrhFCgGTDnCzDnChUn1mkAaW50svOyU9Ns87Gdt34Marh68so1AGuv+5kDoLNeK+aRy8xzHNGxD72+SO0+3J+r+H5LA5BfCrIP1Y/vPfTRttLaWgJ43aceukMeD3wCqsTxPCNegDmTB0phhe4ASHYeBdONfIHbQdCI5xsarL1ZawC2BNB39gDkkDXJip7KI7tvpsE5rGwAnbeFrKWWcrc8IcMX6LO6tfUCQFu47ElW37v3YHP7sCGA133yoeulMf83pZA6xoCkizBmcjAzesOL3QkAC4kEMsdO2GEFE4HxdfCPjbDNV/dmrQFYdx9yyIPh11+M4VsvgRz0VNxbPV3AqX/aicm/e2JVAmjdQx35bAKFrBWeUlQPfIH+cre2LgPovJUl5VEhKe/ce7C+fVgXwOvv/eFD0ljgZgajoMM8lbU8nGadB7dDAOqajvThozzy0Xae/j6ENq/nfiINb+AagA0BPOczr0bo0nU199YtqWdOYN8f/+uqBNBZQFUWuXQMWtHq1ubxhthjaoUtug8guImwyEmS+nUh5PfvPVRpH1YA+OJPPbxJGg/8Qgqr7N00ZnMwp3OlsEK3AEwfOY7M1EkOK1AcL7h5IzyRcPnhWgOw7nVrBKD/rEFs++vXox15+sYvrmoAnT9aIYNcJg7DsMuefFbYovL7dQdAR6ibt0xhi0MfLamlpe4413/64U3S5tAzUsQzRsF041gK5sksj07dEj2fx9yO3UhPnOAjUMJ0/6UXWvCtyaKlcDLJamYraWeb1SI0oUwoOlZyypCzJp2Yhq4Xe/YNTdPo17T8187f8qE7nWUM4PWfeWSTtDn8jBRU+2kmHONwEmayuydGpUKxHXugpbPwDvRh4PJL4V832tVjnimipwo4+hc/agoYrTvy8UfPuGtD6ieBSL1oCD6CsFjI9PQcdK3wEQdCVkFf8rWfHZD6PGeRCqm/MG8F1OuoZ51SQWn+vLknd8LQdETO2cLezaYq5poKWvu9mqigzns5qGLg5m0IXbbO8oLS6DiVRPLp44j/9DD0ZL5GvVutKmi942paDtnULN/vQHAAiurvqgrqXkF2oax4t4gX/9UP7lDOjd7NjpDjaRinspXfvgsAxvfuQ34uxipnYHy0awW5qxFAJeJloNThELzrIzDyGoqn0iicSiP11HFoiVyd47YJyxkGIP3HGQUlSUYoMtYzAElkWX1QkYZ97+RlBZ3DDN0WUj0JPrLzGL41aVuCF46g/+XnVtxUyavAuyHK/8LbxzH3yAtI71rV06p3VGRZhdcXRj6XhFbMQlb8PTu2Yeg3SiLs2URvzERvDPL8bIz/+tfgW5DQyMfwtZC+G89aId9o+YisWOp5Lx0ygOWUkYRiRyb1OkNlF4Q8nyTegf4eftWVL8KrtPUdJE97263J8hCp12chKTbvNohr0p6QnZfZO91yW3KwrMnChJoFw4rT9fjK0VTqRfvgEU/LzTshktc6Tm56pifHW00y98g+xL6/D8WZWrd5fiLO9t/8jw6e6ZdpwZLPWZPxlNpc9EgIfEU/kYKyKQLhVyCCCsxUd/Vg38gQMkePI3viJM/BoAQDPf3Sy1XImSL5FJ68hHyGHI/NajBylfcjvWca6d2Wk6XkZavjQVyT9iSbjpEzhFPUKE+0l5cwl5uHpD1XHomkzSGay6mrByUVNLR1E5cXze9+nr2iZ7IQeL6NUXg3RqEOB6EM+PmfOhTkZRRqEFJ378mZKpSaRkF4xxPaS6EaRl3LQ6Jf2MITk3xoalgrbwl3HUIaBSPnbOVA/NyOPUgdPsavzzQhsLzjEYgmjhPJr3KIYU06J1RJn0nNopBPMXz+4ECP7T8TmfQpfsV3Xjue4KHXc/koq6LyuVEYhxLlFhNdEIKQ6vyS+w8hQ0W3p2YQXD8O/7qRM+ZRY89mG6Ob8Mg9OZ8zQbhEKZ/iFviK4oE/NFhq6NQbMZFNl51p8vjVt95FL4xEHkYsD3kkAKFIEH1WASfcaWmtZIH6s+z1WMF4mpUslUYhNo/C3Dxkvw+y17vqHweu/gj7WqqYlCShxbufJLGahdpV0KhHwXaqlvf5o1yoS697JoaBbPoU25wsQkBe/9I33uXM0WCmC9COJCCKJqRBv+WY6fNY1e95o/VpLtKApawY3+gIzGKRp4fOn5rlJG01FOCJHFezkKNFUmUItf6vsJEtonA8sehre6YLlR9RvidlupDqSc6WQGgQitK7H3jBCdh5hq+i/lYCFBrtKu6tpqNwYA7SRALK+YOQRwMQYwGYYVdFfBdEUmSEz97K1e9kE9JoOBeLVVbEr0KhXM78iQQkj8yqJl0H9oIWdOjZIjhMtAbfgoVgq6yI99apiO/FeejIZa0+NdVCmo8igiowXxsUN/JFFJ6ZgtTnh+fiISCgQNoQrOwJ0wWhsETfhdtKPWEyxye5KVNw0wa7J8zqFAIReY0Tksx6ib9r0rYUcq6eMFKDnjBdFm6ZmE+hWEg1Pi45PTe96a13GZPpxgTnNGhH4kDBZLtQ+GSIsGKNq9mq0XCxX7DO5whEAo50dC2V5gTuYiLJduOZYB+uycKFuqJlEqc4tEDPDdX+BUJDkGS1p1fTqr6fbZ5bKgtIBODm3/6Du2g0M1okY+vxPLRjCXbXSjaICCnWSFisV76xAGnwORqiyT6kgl3qjlaIJ5GbmeXXSiBgzf+wJme88HTXqTmeO4JGHuoLGogM9TyzxdALyGYsR09TEeDQk3DCEPKmCPRYHmaqRUWEbqC4fxb6sTjbhxI5aEZ8pc7Y6FLYgka8yHlnlzpj52ZnkZ+f587Y/tER/iVZkzNP2uqM3QOhOth8NgZdL7SOGNjwkQeUsp3kja95811ClSEPB2ASSFmt5RnTB/WpFLetEBEvhFeGCMlWAJ+8pQvN52lzcwLRPzLE80GQt5SALMzFOJ6o+Hr7a7cmp1dobohs4hSrnUJS4Av2wR/s53kieilFtjdjMMw2Bh8HPifspOuQ11/3hrukPh8vlIf9nA1jJAttNWMys0XopJaaEgSpo14JCMrWZwsLgHCBvJL66RsZZj2/MB9HPhZDMZmC4g9AUnur769Jb4XaC2YSMzxNmWPn+cMDPQ0rkNDsTLnUDKeUtSWSYC+3E/PlFN6iBnnswtfcRaMfFEuNk8IeyGPWLKRtFelSb/54DvpkGkKRIYVVCALRL1m2YTt1hovQFihp2RMO8TwRWjbDAOZmZqAXCjwjLq1fk9UjTn/PfHqes1hofkCaP56aK/UymM7nkZmDVmw/h5k0TIm0RNd5UpiJBip5/MrX32XE85CHg9bQKCznh9Tv4xggVUeQJ7ShOPBQT5m5LDBfZJUUfvufh9RSs/mIugR1nRwxvqEBeMJhFJIpaKkUhzAIQHWt0mLFizNDbi45x7E0miGXRjwvzRnfyx9ZmhItPc+hhbZNLFmCoAoX9lFYbPF/aG4VO4xnpaIVDOixHKehwflSQnBKmjwahBT1giCtG/urbkRDsaxTGQ5RUHkTPBIQkKyDN1JLO2Avk13oH7EC9jQakmqam52D4vdz6GJNVp4U8xlk4qc4jUxAwBcegD80UA6m90j0vDMXYZtORlI3vQqPfNWDs6nrcGpw4c4FpanGqDaQszGClXYUjWjyeJgZMtLFytGsEVOZIvRTWUi6sOoMPQKC1FJiWKvXPqozooaC8A0PsmeqmEggPzvHQJJauha2WBlCI102McMeThptvIEo/BxW6G36mKkV2c6rl8XS6EOWuqlYtp4oL+cUtKJmTd3ukhKALNyOnuaByEMKqRCqUt6JCY7/KWNBJrhUuNsMHtpfMs8pbJz57ydHjWC1VOiuPjQd9hjz1FTRCDzRKPRcHkVWS2esKc6CwbX6umUqpG7mEnPIJmN8r6ibdSA6AtXr5xGwZ6IbyKXnoBUaJ6hUC492PhVCdoEHGz7dZM2QkrHdovj8RXn9tbe9G4ZZ8dNCZUj6ZArI65CiPlfJjLC8pYN+SAM+IKs1tw8doeruuRwQL0IK2GopgSgDgkbD7mS1QVIV+AYHIPv90NJpHgmzM7NcFEyq6ZosH6EJNrPxGejFAmsqgegwj3w9tfNIa8omUMwl0O68mWznBVSrgsix8RzhSWw1y+FSR/q3n7NP3va7f3Rz4VRqS70NaJQj7yaRS3agW0hVJfuQRjaqoqhRK+vtj3qPkn2YMyCFFZiKgOkT1giody92SjFC//Awvy6mMyjMz3MckeKKZDuuyekTrZBDOnYSWt4uEwr2IRAd6nkVjF7IWXae3saAAtvO86usbjJzbg8nPcdF3crvbeB8lGTFfPHej9wj3/DvX9g6+9D+K8yqUbAk1LV6Pg/9ZIZzQGmYrdhRUIU0GuQTqLEPGwgF+43pLIQprPghmWaqBaDoYmE82YHcBr+ooZBMcn6pls2upbWdBiF3fmZ+Gvm0lT7GeZt9wz2182BPk5BPzUMvtllvKSyfCHUqqGfKUI0nq5stihXW33r9T4Z+85LD8qV3ffClkixPp587dVGzNoXkNjVOpi37sM/HQ27pnChsEfVCGvbzdjSZZ0uh4TlVgDGTh3DUUpryXbZGQ9GtTCIhwdMX4emujUKhFD8k+Ekt7X1rujNLKIaXT8WQic9wYari8TJ4Hl+op/E88kYW0wkU86n2VS+y84Ie69l3WWXCzg7jDhKF1uVjA1ee/9wl//yH36HXDKD/nIGYbyz8QmrnyfMajoTOiZN9eDzFhFPQHm5vD4UtyD6Mehkus9DauOOp0GJ5IKlZo6EqYKqm9cW6qJaS6ukb6IfkUaFnsjwzL4UuKJVJ8a+ltXVD8umEHVbIs4rpjw7CF+rnkqGeCU3Kkkuxref0A20pim3neeuEFUjfzGnWqNeG3UjwXf7DP/66854BpBfe8XA6fMnortz+WFBL5sda7chM5KFPWelAUlVPUbILOZvGJ3P9YFtqKenMszkIDVyJbyomp4rTJ7upltKo5xsYsNLaCMJEgttjyB51zT7skJCdl4lNo5hNWeljgQiCfSOQld6lDXJVej6HQiq2sPSxgGq3ixRlT6z9x8zrnI7pTGDb9PhCwvo33PDjS75ljXyl5b9jJu+q3jj5yxOjU/9vx2u1dH7cOpJZOasQXD0oDRMipEI5qw8i6q2c0cg2RmnE1I46bRUqZyaqndHIUoQ5Pa7fJlA3IfICQhO15+Da51JnR6I0tvTxE+ygIaG+pRTcl1RP3ePV7HeZTU92umdH4jKhxBy0XJoXk5rpC/dXqvmNrkPFy6XNjsStTtIJVyC96lq7xbS0L3it3GZWi6vuDcXyTOrXqpdPqnyqrvO3/+vr6y32LoMAACAASURBVEtd/A9/8OXoDWfNokrqAujIqa/vvnjuRwd/zShqkWYAOkcTgz4LRJ+VkO0Gggt7D8xznLElgPZxOClgzA/4hHVdaHQsUgxRlD/TQQCd1+SgSU9OQs9mWSX1Dg4iMDq6BmCbAJJql08l2LkBVve98Ib67Pq83k1PJgxKH4vD0IpVYYXGAAryRficioXy9+Ut6ZnOFO00MrPiutQDUPb5i6OvvuK/zr//txvOhNoUQEeOfuInv546MHMlTFNpBqDz4CibI5DXBWE6dXquUZFS2rQX5kplT80AdJZJQao79FreUpr0viggaXLXAHRuVn5uDukTkxwUppKn4NgYPOHIGoBNACykU+xkMTSN7TxvuA8eX7ByZOoygMIAtEyKK9PL+2oBIMfzFAhFVB4X9ryXWcvOM937aAAgjfD9V5z73PYf/H8lW6+RtAUgSf5YPHT8C4+/Oncqua3i6tQB0PpCgkdDifJLqx90UveOJ6EdTZZTc5oA6LyVKfjfp8AUJl9kUZQg6VLXAOSRW9eRPjlteUrJERYIIrBuDIrXV7vfMxhALZdFLhVjO4seQE8gAm8wYgXSezhBp5HPs7pZeZAmAJKzL6iUe6/a+3OeE3I6sp3nvubV5+QCMLhx7OT59/z2P9VTN+tJ2wA6knz8+OiJLz/1Bj1XsDokNQLQtgHJIypvCluB/KqHnxwv2pEk56C2AyD/upBHasALhKwbC11A1iUIQ+oKgM4ftg8nJthBQxv5BgYRoORvO1PjTAWQRrpcIoYi5W2SU8sbgD9alTDdAwApyaNI6qapw3Uw175qAWRnn9eu0av6ruwUzNgOFtf+GgGoBIO5s+/8zW+N/48X78cCZMEAOjL5N09cM/+LYzeZmu4rn08tgM5rypohELlUyXURWa9OFy37kCouWgDo7I88U+j3cEobd78yyEmjWJ6qLgDovCAAU8cnOIZI9mFgeJhrEs9EAAm8QirBgXTyaPoig1b3sRrIugggtUlJp2AUrfnuK45TcQqu9x4J1A2QwKtYblqT4phpza5YqLpOdQCUVFUfe+3Vj227/00N7bxmsmgAHTn0oR/ckp2YvwgGBQ4aA8giS1DWByFvjFQA6Gyjz2ahH4jbMZXmADrHIQ8s+lRAtmoOJUOBZEhOCnrHAXRuBjWHykxPs4pKVfjB8fVQA4Hy/lYxgMVMGtn4LHs5Sd30RQfg8Yfq7xNdApC0kmwGWjZdsbOmAMqUeeWBUIVrt7b5YtjxvEzR9ZUbA0jO0dC5G49c9fP3fhlLkCUDSJI/Gg8d+8xjt+Zj6S1NAbS/CMUJla1RSIO+8kPpuhEUsuBgv1M31QRAfmHHIs2IpYaS90syVUim1DUA+f9kH05OcoMo2K0yguPjkBXPqgSQ0rWysTnO2yQhB4s3GC3ZeXX36TwLDdc1uA4NACTbn7SPQipeu69GABItAZlzN10PU+n7sp2Xtu0897VtAKB/ZDB2wf2/+4/t2nnNpCMAOjL37/vOOvXgnt/QyD5sAqBzc9k+PCvK6T2lB8l6Stk5w2rpdKY1gM5r8mT1e2D6rGXCEJBNUkvlrgDoLNNyOQaRKi5I/EPDVnDfyfBY4QCSWpadt+vz6IfG44e/306YbrVPdBDAoo5i0mpJYaLOvuoAyGmOAbWirK4EX0GHmSlU1ug1AVD2+opnf+SWbyzUzmsmHQXQkYlP/+zG5LOT1+ol+7A+gM4qan1BIyIB5ADobGNQIvjRJPclbQmgDQ/bmRTEl6xtCUAZKtuH3QDQ+VNIxJGZmuIEXxoVAqNj8Eb7VjSA+WScbT166KltPidMe/xtBeJLslQAqQtfJgM9l61CowmAZOdFPK4mSK7rq+tWK00KLVTuoC6AQsjm2Ouu/sm2v1mcnddMugIgWC2dDx2/94mXZ4/FLjINw3KJNQCQhcIWmyKQx0NlVcC1vXYyA/1InNN/WgHo7JgqNVgtJbWXDGYokIRa2rbTADqvs9PTyM3NWn0ffV4ERkY5fLGSAKSwQmZ2umznRfrhDUXsa4WeAEh1opQMQTG9im2bAejYeV65tKx0T8nhl7XtPNOs/B6uc3X2T5qrf8PoyWue+cC96JJ0DUBHEr+YGD35Dzt+PT+X2tIUQPsFXTjlvH5Ikaq0NlgqiHY8ZYUtSmpDYwCtHQouozID1g2lwI8s2SB2CUBaR23rstOnUIhbtooaCiMwMsbFwKVjLkMAqf1CZu4UA0jiCYTgjw7Z4RbnfnQfQEr10tLJ8lRe7m3rAch5mwpATjn3vbC3McjBQo2ndbN80k0AVKOR1EVf/b266WOdlK4D6Mj0P+68ePaRfb+mF4qRZgA6F5kAVM7ts1pZODfQcG6OBu3gPCdvtwTQ4ZBUkT4FhmKW1FJF8lh5iV0A0NmGPYanTkHLpKkKE76+fvj6Hftw+QBIKmZufg45O32M7by+Ae42XX5GewAg/cgmk5w+Zq0xa7etApDieYh6UErbdH8vKo9LFizPeuWO6gIoezzF9f/9xoe3/p/feAI9kJ4B6MixTz12Y+KZiRtM3VJLGwHoQCGvD1lhC9kVVrD/clrbofmK/jSNAHTUWhphWS0VdthCUqHIroeswwA6K/PxeaRPnrTCFooK/+AQvNHosgCwkEoiG5thCKmrHDlYqO9mzf66CaBmQk+nOZPGvU0zAIVt56FUm2qWATQN7mtbTnmsOWjF9xCSbA685IKnL/7WW/8NPZSeA0iSPzIfOv7Fx1+eOTx7WSsAWWgWmbOiVtvEKgjpP1Strx2Ks1raCkBnEakrph8l+1CWVMiKt2sAgrNGdLYNs7NWAbAaoFKoYSt+eBoApG4A2dgpzvIhFdMbisIb7S9v2wsAKayQzkLPpFyLWwBIzrqIao181d+V/pcosnfTXQbXCEDyjvrXj5w87/O3tZ0+1kk5LQA6kvj5xOjk3z7x2kIiO94UQLMMjXJWtGwfovwAkqpRKntCg4ex+sEVVksMw2NYZVVC4tFQlCZx7CyAzjbkJc2cnOKRh8QbiSIwPGKppT0A0CgWeMQj9Rhs54Xhi/bzNF6dLEcqSSMAyZSgTBrdqEKtAYD2/QK1zayXPpbXYMzn7P5E7hOoD6AnEkltfv/N/7ruf1zXsbDCQuW0AujI1N8+dePsD/f9ilHUpWYAOu+pIxuFLay0NtfNMi0vl7Z/3roRaAGg84ZUmKCAKdlqqpDZBnIKMDsNoLMNAZCZPllKYCbb0D8w1DUASf3NJ+LI22EFxetn8Ohv+Tp3H0CaAl1LUJmQVvGZZgDydOmUT2ybIqZ7O7LzaHavglYJesUplN9LimIOv+ryn2+7/40P4zTLaQcw9l9HfkcvalvpuiR+cBiZ56b54WgGoPPQUdhCGg9avRidB8M2Aih+qO2LWX06Sp9pAKD9njLiDVZLrRmeKKOFathM1K8/hGs/iwHQ2SYXm0N25pQda1O5g5snGK465tIApJxNcrI4ZUIEnidYXVrVZQCp/UjCZee5dtsQQFVwP1oukK36ThxWSOTLPoBWqWhCIHLhZgzfejmp3Jp3Y/8Tg6+94Hs4jXLaAEzumLypkMxeR6YQX1QutiVDvIj4fx5E7vh8SwDBYQuJnTQUzC8B6Hp4WC09lrTS2loAyK9JzfEJ6B52DbKRIKs+2xvYHQD5FVWPz84gF4vxcsUXYLWUilmXAiCBl43NwhltKG/TG3bSx6rPs0sAciFrHhqp3KVVLQCk+9DvBYJKxfmV7Ol0EWY8VzX5T2MAfePDGHnd5VAG3Pm6oAmFcuErNnw3fO3GXTgN0nMAMwfmLsxOxV8JgRBnrdAARc2b3G01TM4vRfynh6Clck0BdFZyWtvGsOUVcz88dPM1g5001NUNrQB0XtMD6hcwZN3KriH70Buwy2w6D2DpwSoUkTp5AlrGKialnjWeUBQqTb2mVI0Cdc6b3PfkXCnSv0yypE14QmGGj0bYxufQBQBzOqubfB728lYACgopUMMv4drWPi7ZjWReWOljVaDXAZDyc0dedyV8Zw0ClZtWiBL2nYi+9Oxv+7b2n0QPpWcAavFcNLl3+rcM09gAW33gyVp0182ruTgm0s9MIfnUBHTXCFYPQOelNOK309rKaqmjfhrUaPhQHHoi3xpAW0i9NXyAISxHDTkqFJ+rfWGHAXQe7mImg9zsDE+9Vvq1liQeEQlEAsl5OAk42E2HSuq7ZesweKTOWuDVHqtrABZM6BTPK7jnVWgOIDXxEoN+QBGl45e2Ldp2XraqWqEBgHStBn7lIkRfcrb7IFXHrBXvuuiOvpef84jS72+/L/0SpCcAxp88/ltaoXgBBCQe6Yr2vPJ1HnjrveuF/Tr2yH5kD8/ZD1hjAFkFVSQo66z6w2oI6Q9No6YfnLfT2lznUOd8HOOdYk6GqsOw90NOGtnrK+f4dhhAZzn3Ls1kGETynmq5MpDVDyH9MBBoqs9vvba7S/e0JwzNg5DMcu5mTZZJIwBlCdKQjwGsHtW5Po8C6e5a0WYACoHglnUY/Z1ras+96lTqisk/upr/nOGf9v/6eT9qsFXHpKsApp87dU0+nnl5yc4r2qNeowfCkTpw0R8aweYffgG56UTdbUoA2vvkWZ22RiEP+CsAdNZrxxLQT6StLsYtACzdfI8EXXF6QAqovgCPSt0CsFNOmLrXu5MAkkaTKbK6WbMvVH6+dPaUPtbntcwG9zr7O3HjZve0eC0A9A71YeT2q8t2XvW5V51KXXEdQ/KqidD28UfC127qmn3YFQDzk8mN6aOx2yDZdp5mWk16S9lAiwOQ75lPRjGew9x3n0dxOtkUQGcZ24dbI5D8SnmV9R/2klJvGmM63RpA57UkwfSYMIQFIo00si9Qns/gDANQ5DToiZTt6DFr94XKz5uOnUdOFiHK5+Gsp7xNUjdzVdUKDQCUAz5s+fgtMGeytVwtAUBHlP7g4egNWx7ynTXQcfuwowBqiXw0tW/mNYZpnM1fguw7UvOKZtX3WhyAvvMHIYb9JX/N/Dd2I/G9fdAzhfLmdQB03suOWuqUPbkeMqvsKQGDpuVuBaDzXhEwFJ1ycdhhSo18aZ56iiOeEQDSfU1koOezde9tPQBpxlgM+lild3PHW3M8L2clTZd20xhAsvOG3ngNxj/267yMzIHi7mlkHp+o+kzFi7pv6x3DLUISmmcsurvT9mHHAEw8O/UqrVC8gtvq0vnnDcu7aZZ/9RYLoEphhk0RGHWmbaCZfWP/sAOpx49ZDVObAAjbPiRvKcFY85BTW4zpDIPI9mErAJ0vpgjossa9UegXnfqi0JRowh0/XE0Akj8qkYWRdrzKVTZ59b5IVAliyMedpt3fo3QKsZylbhpmxTnVBVAIBC7cgHO+/VbUE0kzkX5kP4pTyY4B6KyjsIX/7MGf979yW0fswyUDmN43e00hkb0JEnx8XcnBktVdYYXFA0gNfr2XjsDwtJ4wxTiawPQXH0fm4GzlPqsBdH49PRKUcxuXPZFtqE8k2wPQtNrbQaWxsMibSZIEJRC043irCEDyJCfs8EZpaRMAHTtvwNWsya1h8AQ9lD6mux77xgB6x/px9tfeDGVTH1qJmM8j9cg+6MlCxwB0RAp4ZiPXbf5u8JKxQy1PpIksGsD8VGpj9kT8daaEfr5eug2eVuuZWgyAvu2jMENKw+vkiExJxDSPg90EeP7Rg5j80hMozKVrj1H94NKFpLKnc/oqurWVUrcoV5Hih3O51gC6Hx/VgG5a9qGsehhECgmsaADzOvT5JE/tVv2ANgKQE6aHAxBS7ffiJrdks2W10nk0A5DsvA1/dCM2/O+b+L1Os9jmC5ZXuonwtOoTCSQf3le50RIBdETpDxwefO2F31ysWrpgALVkPpo+MPcaQxhbea4vrjLWy+pmzYkuDEDP1j5OL2s13QW37/d6oCr1Z9aZ/vtncPJbO2EU2ktFc9RSYduHpYeWptlOUNlTwrJNKr5CE68gTYcvaXafShOyzwclGC7Pn7pCAKTsJCOehpHLue5hcwC5wzSNeO5qBdheaEpHI/AoFlt1LeoBSPb60K9fivPuvx31pFAoolDUqs6oVui6F355HNlnpyqvRbUsEEBeSGltY9HdQ7dd8mCL06g9r4UAmNwz/SqtqF0GIayoLncN1hs/AM6bNgBURoJQzu6ra+dVi0dV4PGoLWcNL0wmcOyvfobUM5MtAeT31DaRwhY0z6ELQGc1eUq1wwk7bFH+bnUBdD4vky+qYO9PsG1I89QvewA5npeHkUiVVrYEUJW40x15OOuN9gY5WGZzditK1z7qAQiBwLljOO/e2xG4YLTO3UXF1vl8AUWt9TRaUl5H+gcHUJxMNt7ZQgG0RchyLnjxuh9Gbzrrly1PxPlMOwCmD8xdU0zlXgJZ4rACZyWktao8vMUBSGEFzwVDMPyt54gjNdPv9bScyJG8adnjM8gcnub3xZMZxP5lP4qxbPnodW1E6yWXPVG1hTutzfGwUtnTRNKKH7YDoPNHNqCZloeVyo5ojgmam3BZApgqwoinrOZF5avaFEDKYKGO5aUu067vQT1YjJMZOx/X+XRjAOWQD+vuuB6BS0fJ6QH/hkH41w9VTApbTwyq6i8UWT1tKdMZpP/zIPRkvnLLJQDovCT7sP/XzvundsIWTQFkdfNQ7LdNRYw46gPdnPJ8DtXnsjAAfRcOwaRfyxYnKdHc4V5Pyc5rJvmpeaQOTPJUwdWSfXYW8w8fhl7QmgJYsg8HfJC3REptMeAK5JN9qO+PWfMftgTQCX4J6ILUUqspEDX09USiXIW+LADM0xyNSZTLhNx3pj6AIqxCUCK84k7utvZpFHWYU2mYGa20j2YAQlEweMvFGLj1wpp7J/k8CJ29Dp6hcIsngPw5OvL5Ylv2ob5/DqkfufwoHQDQEc9o5Ln+V277TjP7sCGAiV0nf8eQsNVKHzOtdt25qr77iwTQsz4EaVO4PTvPo0JVW0/YX5xPI31gCloq23Lb+ENHkNpx0nIcNXvQeeQSVvxwzCp7Kq1yfrWpLca+eWtCfrTxkNNL2YRuFngaL5Mr4wM0t0CpQqHnAGqAMZeGmc1WbNYUQB9NHRe0OgtUnQOnj83lYcxkUCkNAIRA+JotGHvntQ3vmSNqXxDBs9dBCTWfxZj2XmzTPpQME/knTlj2YQcBNO34oW/z4BODt1xYt+ypBsDcROLCXDz7GsjCxxeLvFRprSJms1gA5YgX6vkDbdl55FwhJ0srO8/IFZE5Mo3cVKz1Tt2fSxUx98/7kTvmpE41ANB+TbWC8oYw5BG/bd+Uvxs4rS0JfTLtSptqruZxj1LJgGbk2SYirZrmrSePac8AJCUhlWfvpvu+NgVQEpDGAlZxLFz7dw4xn7PUTd1sKxfUt3UY6+64DgrNqLwA8Y31M4it1FI6t3yh2JZ9KNJFZP7zEIqTiQY7a7GwDoCOyJTWdvn6R8LXVaa1VQCY3jd7U1HXbxBCSDxpCqlXRaPqhrt2vQAAvZcMwwi2HslIzfR5VI6jNRPHzstOzNZVN9uV/ME45v/jMIqxTO0DWv3wOt28SS11AsouGEtlT6ey7Xsa6cdA6NB1a3IRqrZQI2Gej7B0Lt0AMK3BiCXLdl4bAFIGi0QzF5d7IJXApKJY82SaVfOa7+neh326klfFunfdwHbeYsWxDwObR1rugexCAlE3mutdPC3ndBqpRw/CqGsfVkt7ADriHYk8N/zftpfmDSwBmHx+5lWGhKt4pCMny3y+HExfAoByxAPlgoGW6ibZeV4vdShr7YwpzCTZzjNyhZbbtivpX0wh/qNjlirZBMDS+Q4HoGxx0toqH3aaZ4DKnqy0NjQH0NmnoEGjyPOXm1YbdHiiEVfh7NIBpNpLbkwbS8HUtErcmgDIMwltCLKXs+KcTXsaL7LzEs69cK2rB6CQMPC6SzBw6wUdu3dkH4a3rWf1tJXQSEge01ZqKYctHj+O7M7JitOvlYUBSCL7PbPr3nbt5+EASGpnPld4A1806pc/X9lRarEAKkMBSGdHmn5ZYYcV1DbCCjTCJHYfYXuvWxL75wPI7D1lNwpqDCC/VwTbhqSaVjzs9rYUwNcPJ8qzPaEJgM57YXIQX9ctjynll1LoguKIJe/vAgHkNuw0B0ImZ9u9btCaAEhhhQ0hBhClln/2Ws3gvqzmXLbSlm4EIKePjWH8g7/StXtHAIa3bYDkU5tuZ9r2Yb6oNd2OxCAnzX8eLH+w7t5qXzbc3F6hDgQOj/7eVV9hAOMvzHwQElROhp3NV95ULA5AyizxbB9uOvKxnUfgtQgrwIYvvuNQW06WpUpxKoP4fxxi+7CdVDSey35rBHK/r/zgOdtT2GIybYUt7Ae1KYCl0ZJzODiQb1CPS6PI3lJSTSVZhlCtzmBcJe8Gjsq9SCXP6zBzBW4F0aj7c0MAZQFpNMBz/rvXlS5FLGuHFewfKfcIXwdAz1gfxt93A5TRhdl5ixFSS6OXbW3ppIH9Y5nLF9lr2kyKzkjYQQBJotdt+Za45fnDv9RlS/U05vJAQa/99CIA9F481NDmIzvPq6pthRUcmX/yQE/gc0t25wwS/3kMBcc+bASL/bCV0to8cmVSuGmVPVH80JjOtgkgqhxfqLnuguyZogGZ2glmc65dVe5zIQBSBosYD7oqRsrfg/uwTGVgpitV/0YACp+K4d+9HOGXblnopV+SEIT9V57TciR0pFVam6QZmP/yUx0HUIn6TojXPHcwC0XyEXjGbL4+XIsA0POisZrVrdLHGgk5WtIHJhf0mU5K4tEJpH5+AnquvVQ0DltsCFU+xPZqyvhnEB37cKEAulRZoRuQsuUY21IAZDtvfbCi2a3pVjdPpDiTpeqEnCNXnqMkIXrTORj6/e09vlNlIS9paNv6BX2mWNTYUVMPnCzVn56olz2zeACp8l5h+GhZfvGexGpRx0M1B283fayeFGYbuIV7JJGbNiB09Sjmv3cY6Z2tazJZ5ZzOMITyWMiem856SGmUFBd4YJzKQDuctNTF0ymkPpOdF7HDCgSdcP3GTqU5dxPtZJeQI23LIDZ87FdP73eie7AIBx3FmxVVKeWXukUZDjYAcPFi6qbSOi6wRKF76fN52vJuNpJuOl3aFSmkYuDWcxF+8Tjm/mU/ClMtbgbZfocTME6kLbWUu3w5GTGWF9XT74M+mYI+kVrAmXRIyM4bCUCM+MsJBqY9J5dpJaAbdF6F9n4glIEghv/wKgQuaR0SWM5SSv5QZLYPW4UtliplAKXFjE3NhfYY8HtbxvRaiRLy99z+ayTquiBG334ZMjtO8YioJ3NNtyfNorh7BlK/ndam2j9EwurITR5UaSgA/UjCKnvqgZBzRaJ5GD1SpQpMUtD5XLjZbYtULv4aXhX9r9qG/ted35Nzb1dkn2dJn6dn1u/3IpvNc45pdudUx88xPxGDoqUKUOwJDU0U2/hYa9Fms1A3hnjkWyp8JJT/t1wAdCRw2TD/S/zgGBI/OdoyGYDgon8M3FjAaptoizMnItuHx5NWAkSnxStblQrk3ZRE2ZQTtk1IZUKTKZjTmca2i1skCcGrNmL0jqs7f64dkHaC862ELg1BmDkx35VzTD8/CSX33AxCV43zAyGCijVZ/RKFfvWl+SKU9f6OnChlwlPwfblBSBJ52UaEXjTG1Rbp50+13F4/loQ2mYKyOQJpyG/dZlG2D6WIh9PkuGaOYmxLsM25Lq/PB/R7IPyqy2lUiRjNw284YZI2xLt1EKPvuKYnYYXFSDuxwLaloCP+7b0dP0eKDc89+RzE1e/5hjn85u2QA1ZpDGf357RKF84iA/GRK9ZB6Wsdj2nrhHsQhF+qFCfTmP3aXhRmM22lolE6m7Ip7HKAwOUxtSsKqDMYuf8pmJ7XXHNd2F5QJ/WLmtmSB1OWIAJyxT5rWis61QrJAmfssJ3njgOWLnrFQwAR8mHkj65e1nYewecda92uol2Z+dazyE/YI2Ad1WDreh3nbzXgVYFkGnjukIzj06LR5qX9nPzW44g9ux/iqrf/o6mui2Dw1gs4fmIZ4AWrvUTpA4sDkCR4wTC860IduyDdSEPrtGSeOYXYt/dBzxWbAugARw4Z8phS/LAawFp4XJBSEncpV9c1OSWa74Oq0Y3jaQawOg6IOgCS3Rp9zfnov2Xbsr3mC0lHa0coI2z23/aU4UPt8z7UZ+L2m2ufw6/+m4pkWjQEMP7YAUw+/DjvUB6/+ta7KOk0fzQJ71l9kFTZehCEPWfDEqU4k0YxloPa72+Zud6OyAEvx3jIc8fzRhgNf2dOm6hjQURu3Ah9toAileS0OEcuWCVXvwFIztx3TsCmWZYQjYKGWbmNcP8VFctoTgXjcBLG8Ta9m9Rl+ppNWP/nvwr/+UPL7jrDDroHNg0jctGmJTteHMkfj2PmwV0onmrunSbHPo1+7rB2vijw1F4ZjZJrkk8cweRDPy+fP42ApfCPT0H/K86B76x+61eT2xIUSzdrMSNg6VeYbLmNUfjP6u8IiFhCKVIvhey52f+3F9mDc67LUTkCui8VqZI0HRd5TaV+b9WoVpX3WRoB7aB9dbI2pRbGC1bqGGU56ZWlUhXjXdUIqI5FMPbua5etnYcFlCS1KzTqzf/oIDJ76ns81w8bGB8xMBOTcPyUQKEgEA4aOH+LdV0JvucOSSiU29SWhMyHme/vwtwvLXuSb4EEiGs+8E2TmtK6E2096yKI/OoWqENBazcFwyo30c0lAci/2KqEwNZ++DZFO3LRsMBi3NMl+X3zmHtwH2sEC0lFo7YYFEMkrYSmYuNrGLZbZdCc7twepGhNUsnz5Vk2PI2qvHyhqWg+FSP/6xr4l7Gd125R7kIk+YujSD1zgm3uetbb7a8osMrpCMH29e+pbPeVpEEmTP5YDJMP/hz52bI62xRA52YEt48jePU6yAGP9Qub1a32AnVBaw9AZ5kS9iJ43iCU8hRN7gAAIABJREFU/s54StGiHcVykdRjJzD/8CErU6MNAKuvm9uW7GQqGqmbfb95AfpuOW/ZXruFtKVoV3IH5xD/8QHq6m59ovKCl+Qdt+Vrlv3gcYVHvJJUfUxL5DDzyE7MP2tXU7g2MO3BSFz7l981tQPzdQGErZaGX7QB/otGICmS1dGK2lMU9CUB6PzxjAQR3DYEydeZpJzqhkzLUYxkEbHvHkR65xTX07kvVc8BFNTyfwRj779u2V6vhTRmaleoGVPskReQn4hXfqIBgFvXG7jsXB1D/SZmYoJV0F/uUmquNQnVlCaeOIKp7z9Rec/cANpdFsSLv/qoWfjllNVoqaHr3ITSF0D45VvgHY9Yy6nvR1ovt2DA4gB0Phc4ewC+zX0dtQ9pNCzMnN480mZSPJFG7F/3I1vq5o2eAigNBLDu/S9e9nYeBdU7FdejgSNB6ubTx+uu3zqu4yXbNYSD1jU6fkrCT59WMDMv6nFZc61Tuycx9d3HoWdzZQ2j3n2hcJEiQVz/tR+Z+lQaxedjTQE07WW+rf0Iv2QLlD576uSC3RvUWBqAsH/pgucPwTveORWD7MPk88eXd9ji6WnEHz6Ewql0TwCU+/0Y/O+Xw3/x8vRswrbzCLxOhRVIUjtOsK1n5LUGMAF/cEseXrVy5eO7Ffxyt9wUwNyxeZz63tPITExXrakDILXrVxWrNxABSMu1qTS05+dc29UH0FkWuGwM4RdvZLWUF1OAOF89eSYWBKAjFLLwnzPAfzslVNJEHtPlbB/GHz6C5I+PQs86brQOAkiTxoyGMPDfLlnW4NGPMNl5nQymF44nEPv+C2yTlaQBgFdfqOGai8qVEORsefBRteEIqCWyiP34Bc5qYXG3M3H+674vBJ/HniZP1yGu/fz3THnAZ6UDEoQH5+1K5+YAsofOryJ81XoEt49ZDwB5SXNGVfxw4QA6y73rwwicPVia12+pQvYhQZidmOnI/rolmaemuf6QHhwtW1g8gKrMk1X6t4+i//bO9WDplgS2jHTczov/+KAdAqr/jNUI+SU8Job7rPAOqaD1PsN23i8PY/Zne6BnXfMSNgOQvJ4En13uZRQ0iGs+9M+mSg1yXZXb2uE4jJPplgBaZ2JC6fchevO5UEeCdhsG0xoNNXNJANILNsA398G3pbP2YfL5iWWd1uZI8UQK+QNx5Gk231iO5tovXz5KTZu1e29SZQX14PEr8J47gL7fPPd0n3rb4hmK8KjXSTuPQgqJx4/Wt7/qvK1c3mClvTi9exIzj+5EfjZeWtEKQMomsp5fu4scxdjzRSsQr1wwxJnybhuOsia0/fN2LKk5gM5nvFv6EXnZViuvFHZ/EgLRWPoEnfRgBS8YYa9pp2QlpLWtZqEys+DZYx218zLPTSP+k0O1jZI7AGBhOoXZh3cieeB4jWrZEEDJhs8p97NTA/n8KI7LmTCyBPXSYavXpbNj25bTT6bBYYo6aimLC0CnNUH4mg0IXLEOkmrXm+XtKaqXAKBzXdRBP0IXDEMOextcwYWLpZYurb/omrQvnD62eYRDC52SwokEezcpjazhs1R3Oeosr1xJsMw9sgexJ59HBV7NAKT0QAozyKJyOxqMCpplrpGGV0pFo5mBzopCHg1WNOKB3fuRqra1o/G2AOTj+xRErt8E/wVD1jLDUUuxJACdBb71UQQvHO5cGpJmcN+Z5ZzWthrEv2GI4euknZd8/Cgyey3vYw1XSwQw/thBzD62m8MKlWsaAEhQUdE1fT+7u0Bpc9NSO1kjtI9TkQvKmfmDfihn9fHstKWToQ8a9oSVB+ZhzGZbAuiorZRTGL1xE1TqqEyi2aqpUWc0rXfRGgBIQh7YwLmD8G3pb3A1Fy6U4E0grgT7cCVJuz072xW283acYFvPLGio/k0vySIBzOyfwcxDTyM/F6947poByD8qlN0iXOVITtIDxc1dTZ85E0aSjBoAnU8qm8OQKR6niBKAzkHZPnxhzgo9tADQee3fNozw9RtKaW00wT//MxcPoPNS9isIXbYO6kBn09rSR6bX7MMlSlfSxw7NIf7TQ9BLk3yaHQNQT+Qw/e2nkXLsPKA1gHK1ull2PLJzk2LlnPHk+jQNTqpvSl538S2vFoo0Xn0i3B5hMm01nQ06ZR4W2aReyuMhJp5rB9soCdJmM0g/NQXJo0AZDHAeHJwyjiVWPZHtlj8WR3EuC89g0Nr3EoUSfZd72dNylooyoUBn7PXibAaxh19A6qnjHe3iB9vOm//RCzjxjZ+iGGuz+5mwWOB/darGCDpWOavn0bTF4w19Vlz5xi+/UfSrX2uUC0oiRT2QNkV4YpKyg8b+RaAmPkcTbCM2GwHd+6VYSN/LtsJ7Vn/JPhSslsK1HdoeASvVAcC/tR+B8zoXT1oJaW3LSbpRJkQjHnk4rQU1WyxpBEw+eRQzP9gJPZdvq1QL3MdH4jgrz3JV2s4e+UjdpIoUp5aszjlIkiczEfvboKCNrnrzV/8FQeWWVqlo8mgA8qYINxGqtgHpgMXn50CVFe7PlE6ser+mCc+WPoSvWQ91OFgGsegeERcHIOxM89BFI/BuOLPKnk6ndKVM6IkJtvXMvKtPZ4cApGr3mYd2IHvClZjRCkDK33SPeK5UP9OwwHPyo83yBhXnIIQEr39g28HJz74gnCLOq97y1R/CL99UccCa0cyeR319iFVQ1n2d1DPby6PPZKEfnLd/ASq/VDWAzqrgJWMIXTlmVYOblqNG6K59LwJA54US8SF40QjUwUD1rVm0rISyp14K2XnBzSOdTR87kUDsh/vtKaQb3V/XggUASLbj3CO7kdh1uP1aSXJqUkZWxUzA9montMAVQhVWYg2AFnx9//Pg5P+9DzwXj+sDV/3OV76MkPKWpgDay7iVHrWwGPCVjU4XjNqRODRqfaA1qLJwAQi7NMNKaxvl/QgG0ZpSC0sA0HntGQshdPEoJH+HvHAroOyp29KtMqH5R/cjf9yt7ncGQLLzUk8cxakfPOP6WAsAyc7zKtZ8H9WjgWnPJpYv1g42dQAkr6fXF/0jBz5UA0hy5W1/9zIRkL8Ar7StGYClkpaIBzKBGFRrZhKi4D3lllKOafWJVwPovFCiPoRv3AzvhoiVb2pYIMIUSwLQEbIN/WcPnJFpbZ2UbpQJJZ84htSOenOALB3AzO5JTH/3KcvOc23cDEC280jdrDiMvV7TrSiAZiWomFWfRxWAsuKd93hCLzow+dkX3IerAdCRK2//u3cgJH9MyFJ/O6lo0kgQytlRWy11QUt/UwVoB+O2fdgcQAcy78Y+hG/YaLU1pHWGsEbDdkBrAiDbsgEFgW3D8G3srH243MueOiHdSB/LvnAK8Z8e5uTkWoKwJAALx+YR+/HzSO0/Xrk9mgBIP84BleYSdW1uP6+Uw0nmVVGreM4aASiEZHi94T8/OHX3nfW+WUMASa78rS+NwiN9En7pdgjhawYgr1IElPEQ5M0RV7QfFWlt+pG49cvRAkAH0sAlYwhfu94KLRiWg0YYwrUtFgygs9AzGEDg/OGO2ocroexpMdKVMqHJBOKPHeY+OSXpEIAUz4v/ZD/mn9xnLTfr7KAaQCoVIjtPlSr3y+aVwXMulmsJm6eiUTBeVgM7jkzf03SKqKYAOnLlrV+6FD7pc/BLNzUF0D4p8hIp5/Zx2KImvFHU2TakFuzcSKgFgHySXgXhazcicOFQCWaG0KhwRdVc31YAOn+oQVTw/OGO2ocroeypXel4mVAqz+DlDseamBVuaR9Ag1XZo4j9dK+tbtpbNANQCM78EqW2KGbFc0IxRzNbLBcVVB2zGkBF9U8pauDGAyc+U6Fu1pO2AHTkyt/60hsRkD4FVdrQTioadWdWtg1AeKTy93bKnrIatEPz0Gdype3L16YqQ8Z+Sx7N6Mu3QrXzVQnC2tFw4QDCTmsj25BGxE7JSk9r60r62LOTHFooL0THAMwemOGwQmE2WbNRIwDhtdVN4V5lAchtHQk8pxChYq+1AMqyJ6OogXcdnPzcfWhTFgSgI1e+4Ut3Iih/ABJ87aSiUciCWrDzhJVuuChsMZ9n+5DsRPcFqAeg855st8hNmyEFrPZ8gkfEeoWT7QPo7JtS5UKXjMKzrnOpUyut7KnTXaZh23kEnpas6i7WAQCpge7s93Yjc/CEa3ctAKScTar+cU2SUwqk00iXLZZjj+7RsM45kZ2neoL3H5y6+3+2vhKVsigASa583f2jpkf6pPBLbymfT5NMGEXisIU84q+Ay7QB5mp8mqfAXfbUAEDnQoWuHEfw8jHLPmQQBYQpLQlAZzklj4cuGWOvbKdkuZc9daVMaDLJ3s38ZKI+XEsAkJw2se8/j/gT+0orWgJIkxDRpDVe2bbt3Mc2rX6qOb2cxYLGAFIWjKL4d8iK97b9baib9WTRADpyxS33Xwq/9LfCI13VTioazcDE8cOIlV9qukZQmi2W7cOjibYAhB2PjP7KZvjOHrDXW2qpgLQkAJ0X/nMGETx/pCP5pVjG3bw7XiaUKiD55ASyPGNUdUG2SxYJYOJnhxD72XPQM/mKFQ0BtGeLAtl6QpT26TyznLdJcyLqVVksVftz9qoo3ilZ8f/+gROfeWhRF8iWJQPoyBWvu5/sw3sgC6s2qEUqGgXwlbP7OIu8DJR9Majs6YUYV10AzQF0VnqGQwjdsLFsH9JoCNkK6LtkoQDC6dZ24QjbiJ0SsgsJxNNtH3YjfYySpcnWKydMdw7A/LF5zDz4FPKxZJ0f2PoAsnMlIPOchq6Hyfo/5W2mCnbBuUszc5+Ma39CVjOq4v/0gcnP1Q0rLFQ6BqAjV9x6/2fhl94GAV87Hk7KLS2ntTnLrW2ssqeYpRKgOYDOS+85A4hct8FKazN4PISE8uwZiwGwpL0EVESu2gB1qLNpbaej7KkrZUJHYkj87Ah7ORuDUSVtAqjHc5j5t2eROXDCtVkLAClvk9r6q1LtDFHcYLoII1s1H2YDAGnQVNTggwen7n7doi5OA+k4gCRXvPZvRuER95o+6RZe0CIVjVzA8sYw5JHaany++MdTKB5Ncgijch1QDaDzfcLXbkCAKuZt1VEyqVBSXhKAziLPcIBBlAIrL63NSR/rxAyyjmhzGcT/6wjnb5akQwBy97GfHkTssb11VMEGANLPbliF8Jd7ebqf89K8GRxWqKsiVRxFUQI7ZMVz2/7jn16UnddMugKgI1e85j6yD79hqsKaWK4BgM4LihvSiMj2YbVaSmltVPZ0ItUWgByPifoQvHo9/Gf32ysFJCGX5+taJIDOclJLA+cMddQ+7GbZU1fKhH5+BNkXZqouUmcAzOyawsx/PO3qkwq0AhAh1ZoNuDJ/zC6MNWCQt13TXasaAyjL6ryiBt60//inl2TnNZOuAujIFb953zvglz5mSqK/GYDWX7vsaWO45KmC06DWMGGki1xtUSp7agKgs8y7MYrglevgGQ1aI65w1NIqFzTK+3G/aAQgCc2nSNX41Fa/U9LpsqdudJlO7z6J5FMTZTuvgwAWJuKYfWgX8idmKwFpBiBVpEc9VaaM/TGy8xL5Cpu0/PFaACmsoHiCf37gxGc7Yuc1k54A6MgVr7vvy6aX0trga5mKJkuQ1wV5LvUSWGYZXn2Wyp7idp8NlD5bD0DnPamkoWs3lMIWNBoKTvgTiwbQeUk5q2FqizHcuYd8qfZhN8ArTCV5Dj29Jp63dACpTCj2yF6kdh91MdICQEof6/Nak8pWzRLMdh6pmzS1XjupaNQ9XPb96ODU3S9d2FVZvPQUQJLLX/XXo/CKr8EjuPawlaOG7cOtdtmTC0DnupXUUs1oCaC1P6t/TGD7aClsUQYRlZ9ZAIDOht71EYS3j3fMPoQNYjGeRmE+3RRGcqyQN1ONBuEdinQsgwV2WIG6TDeO5y0eQKdMaO4Hu8oLWwFI4IUUCKddSkl1tFVOBq9QagfRCkBZ8R2QFe9v7J/4y47bec2k5wA6cvmr//qN8Ii7yvZhfQCdZSLqhbI1AuEU7bpsRE5rO5aEcTLTEkBnkRLyIPySzfButEZY8pYKyQZxCQDCnlOdyp4C53bOPnRLvdBFJ0c5t5CdZ6mb1U2KqjdcHIBk580/uhfFuaRrdXMARUgFaJJSUXV/YXcLTxatTmkVp1cfQCEpGUX139pNO6+ZnDYAHbn81ffeCZ/0TlOCHT9EXQCtxSbk9SEoG8OumI4rEZfS2o4muKFUKwCd1971UYRfvMEqe2ItRLZAdN3dhQLoCIUtgheNdrRtYi8lu38GiZ/TbEK1tlONLBBAKhOK/3gfMgemyj+k7g/XAVA4dh6FFdzb0T/K20zmYWS12ntUB0C28xT//ftPfHbB6WOdlNMOIMnlr7x3FAo+Ca+43XSXPTXKhJEFlI0UPwxWekRtFVSfprKnhGV0twDQWU8TzNA/cqqAq5dly2MqFg+gs47swvDl4yXIl7uQnUddpovOvBNVGsFSACQ7L/6TA0g+eaDmmjYEkOJ5Ua/Vq9Y+lhtAmpbbCisYrtvTCEABWfHsODh5d9MyoV7JsgDQkctf+cVL4ZX+1lRxVVMATafsSYZybr8dtigDyLetaLBtqB9Nuj7TGEAOg3gUhK5Zb3Xztn2kQlIYxpIsAkDnhW/LAIPYDbW0E0J2HoGXOxqr+l5VLxYJYPKxw0g89oI141Md1bIGQJpPgeJ5EU95u5Lzzi6MjeVL6WOoDsRXnJ5J1QpTsuK7cd/Ep3pq5zWTZQWgI5e/8otvNL3iU5CxgRe1SEWTqOzp3H4OW5T70tg3isue4hXdvBsB6LymeSciN23hsAXsLlaSrNqtxrFoAGk1jbCBbUOsmi4XoTxIsvPon1GontRk6QDm9s9g7qFdbOeVB7fmAHKlQr8XTkpvaTvTanRLZoZZM9FmfQAlSclIsu9d+4//ZdtlQr2SZQmgI9tf+cU74RMf4LQ2NAawBA6ppesCducql31As9HE89yfhlSVVgBW2Ic3boJse9qEZIPolgUC6AiVPYWvGGev6emU7P5Znq5Zt8vBzHqQLRLA4qk0Yo/sQXb/yYp71QxALowlO8+r1Kia5NE053PlewizKYBk58mK99v7j3+2o+ljnZRlDSDJ5TffM2rK+KTpk97STi6oSfbh1qir7KmsftIfYzqN4uGEK60Ntft0B3FN05rt6cIha9INTshVIMlK5eexMACdh8QzEkLkRQR550IG7UjhZBKpp0+wvVfJwtIBNHIaEo8dQvynz1dv1BhAmgKhz2OHFcpaTOmWJPMw5wts55U/3QhAQermDklWb1tO6mY9WfYAOrL9FfdcCi++YSpiWzupaFR6QrM9kXpaDSHV43E378l0WwCSSNQ28Xoqe7LS2jjJW/VUxQ8XDqAjgfOGuRDYgbxbQupm4hfH2MNZe55LBzD15ATiP9wLPVOozTKpByDF8yhhOmqp+CXtxDlUrghjNleK81Z+uhZAIXmmZNnz+/smPnVawgoLlRUDoCPbX/GFN8Ir7imFLRoA6DwQXPa0NWqltRkuEHiqKCp7moeRyLcE0IGYPZrX2WVPtn0oq57KqahKH6580cqbSpAHLx5jG7HTwnbenmmk95ys7DJdcxqLA5DKhOYe3GHZeaXb0hxAsvMETQwri4r7yJ8rGjBnsjya1rtm1QAKIRdlxfuJfRN/2fX0sU7KigPQke03f+Gzphdvs9La7IWNMmEUK62N7EMhSyUAnRtoxAvQ9tmzPaE5gI74tg4gdO36kurIaqmiomKOjgUC6Ajtk7qF+7cuvf5Qi2V5tMvsn7U6N9ecyNIApO5jc9/eheyBkzXbNASQJvwZ8nLPzZKZYL8wHTsvXmWTAg0ApLCC78H9xz+9bO28ZrJiASTZ/vLPU/zwXtMj7LKn5qlo1GiVHDXSSKAyrc1ez9X4E6myfdgEQB6xvBK3TfRfNFyKHxKEklJtHy4MQOdBlUMedgR5RkP8rx31VE8XoM1l2cbLHZ2vU5vXGQApfSz95ATmv7+nshVJMwCpvfugj1XOSo+0rZEkCzDmcpad5x4N61wz02qCtEOS1NteOPYXy9rOayYrGkBHtr/88y8zPfgCZLGtnVQ0CurKG0JVsz3ZD4FmWGGL6Yz9scYAlkAJ+xC8aqxcMc9qqeqKHy4OwOp1FD9UBwKVD69L2KGC+sBUyBIBzO46idi/P1sqE2oHQE6Y5rCCqPjR41cZjcNEpRG6Wh2tumZCyBlJ8d2679hfrAg7r5msCgAd2f7yz7/D9OBjbB+2kYpGI6GyJVo5jzc7akx2deuH4tDj+fJ+GgDoPGye8QjC122A0u/nhRJl03g85RlTlwhgNVDVdlCr7cvrFwdgYWIe8w/tQf54rOrjjQGkRrdiNMA1iDU9g8jOI/CoF0sbqWjU+06RfX/+wsSnVpSd10xWFYCOXPZrf/VlqLjdrLAPG2TCOGVPlF/qjIIueLns6XBlN2/XTioAdJb7zx9G+EXjJZWR1VK1XvxwZQDIXaYfeQHpXRO1590IQEofG/VbxbEwy9fItNu7x3MwqaazNFV5MwAFJNnzo/0Tn+5ZmVCvZFUCSLL9pr+61FTwOVOFPeVa81Q0ah4sU/yQprl2Q/j/t3cusW1cVxg+994ZypQoO3atSoFVOA3SBgVMqkkE103rqo13WRQBCqTKKuguQLtoltn4UUe2Hn7FQIMsEqC7AlkFBYpuCrQBmkWBAIWBWm9GUqQ6styoepESRfKe4px5cIYih5RNUZQ4vxeihvPyYD7de+55aWtamn+QgvxXG27vtyAAwXZb0EJK6wtW2hMnAZPbwmcfNjaAtFqa+nwB1j9LQn4z47nlAADJzjsZ5Smn/xlbz586KrOd57GzgwAU0kxKFXl18svBA2vnBenQAuio55W7/WjAZVDwfDWhaGQXUpMZyhv0hbWh5WDOU9rTo3RFAJ3zq1gL98anrHxAO5qGpqVSNTSAm/9egtW/TUDu6w3/NwEAUgSL+GYUhBSelWb7Fik/b3kTMJ0D3HnRHQAKUGmpWt6enB9quPCxWurQA+io55W7F9GA34LE40EAOp+pUpv6VsxKe7K/c54VUv98AnEtUxFA5+Ui+/DoT06Diln5jDQSqojtP4Tid3r/AKRe+2ufJmGL3ApFRms5AIEK3RJ4FD4GhRQxdNwKS2nOWChMRaE8gEJoJVs+nJwf3tc0oXqpaQAk9fTd7QSFQzoCb1YCkEX+w+4YqKdjvkgaZx9aKc1xWJuuCKCzndwWbS92FtwWpskj4n4DSG6F1b9OQerzWc/hFQA0JcjOVs5YcP4QeQGklU3LrYCFC5UFkGtu3puav9EQaUL1UlMB6Kin7z2yDz9CA3sDAQRr1ONuwM8dB9luxSmi92XLWvZhbsGuZFYBQHAiXnqf5ho16ETTRDz2YZ0BXP9sDjb+Ydl5/sPLAEhpkieOgOyIus/I+/9mfx5VJ8gWl+AvDaAQxqJUZt/E3OG084LUlAA66ul7rx9NHEEJ3dWEosnjLRzWZlXzBr8NmMnyaMhxixAMoFuHJNYCR396mp3sJKlst4WSdQFwa3oZVv4yCrnlDd/IFgQgJ8Z2RO2cRvQ9I9zMAz5Msb3nm5Z7b8wbikb+PGXenJgbOjRuhd2qqQF01NN356I24R0AtFLWK4SicRHhrjZPCTywR0bkECoCEVMluj2BH0DnnEe+fZybkMqYNRV1pqVih31YGwCp7N/a35OwNb1UtE95AKmnB5CdR3l6RX9cqOyfXtoEpOmm53xlARRCS2n+afLL4QMZPlZLhQDa6jl/h9KePkADX6smFpT8h8YzR61pmOc7ZzpGLov8/IbbQzwIQEfU7enId05wCBqwKyPCYW3C3yv5sQFM31+C9L8WIDO95D8eAgCMSA5YILdCsSOdxHYerQrncMc1dwLIFQbuCWm+PjF7remmm6UUAliknh/fSaCBH6NCrtZWKRSNRgSqXeotm+C+qOQ/nF+3/IdVAOickyqqRZ55CiJdbezMJ9uQQVSKl/irBZAc6Nvzq5CZWYat0a84Rcj6Hv3HQ2kAJY14J6NW+BgUev7zPhvboP+zUViAKgG9LxRNqhUpzTfGZ68f+PCxWioEsIwS52/3o4Hvo8BCtbaASBhahje627lyl/tCOsvx1O0puWItxVcBoHeUiZw6BsaJIzwqGt+IWkWFohEwT7Zx0Zo8FSNKWYsduZUtbmKSXVyD7IMVyH2dKj0SVQAQjkVA0spvRO6M26Q+6QvrVhEk97YDABQyq1RkcHz2etPaeUEKAaygxPlbt9GAt5Dsw0qhaOS26Grj0DZr1HAgdNKeMpBPrlqN/qE6AL1cFE9lRV6D3My5G7AYAoBdAUjNTMSpGNdeRe+9gzWaa8rPe5jeYVSWBlCAMMxPJmYHm97OC1IIYBVK/Oh2J0r8Iyr9M3fvUgDam8htwdkWHa2e/Qr75hbWrWx8zvJuAACVAEnT3Y6oP3rF2W95C/SDdXaqF92Qc2XfPQppJIU0Xx2fGQjtvAoKAdyFEi/fuoAKf8/2YQCA1k/kconq9DFrBdEDID9zSnuaW3PTngrH1hdAtvO6Wv2Fju0v9fo24IMNriwHUMIRX7iytUnItFSRX4zPDIR2XpUKAXwMJV6+9Wtt6KtA9mEAgM7vtFJKK6bsttCeF5Z+pLa5iLC27cN6Ach1WE63cxC6e7sOmJkc6MU0pwp5T1QWQAFaysjA+My10M7bpUIAH1OJH96iaekQSvwl+Q8rhqLZaU9UWt+FwrNQQ0v5lI3v7/ZUewAZPCrNEXOqj2Hhdqm8O90Hjcp59E+PywAolPnp+Mz1Q5cmVC+FAD6hEuduJlDiHe3Yh+UAtH+lVVJF2RZuNW/viihA/lEKNIOYrx2ALRIkZaRTYqzdk98HE8W1/ncLNLlLtvOey5UHUEi1KJTZN5Z8N7TznkAhgDVS/NzNflT6Mgos3+0JCiMIvNfPAAACjElEQVQS24fPPuX2tXMAdIFI5Th9h/Pn1ra9B7sfywLYIq2KY1TWneJXW42iRSNPvc10FvT8eiFbwcd7CQDJzpPG22NfDBzqNKF6KQSwxoqfu3ERlX4H3bA2KAmg81l1tYLqbvd3dvUc40TW0IjIrbeoTF/GXj3VCNIeKSkXjzaJdk9hW+85wA845pBro7p2nmeoLQmgAA3K/HA8OdAUaUL1UgjgHij+gxudIHBIK/1mJQD5JSf/4amYFV/qHQ3dqaIfKPd3jSCyjiujKEujFIBgg/woDXoxZbkVoAjOEgAKZdwb++JaU6UJ1UshgHuo+NkbCZT6I5S6NxBAW2wfkv/wZDQYQGebCyD4y2gUhc/xv+UM6P9tcbHbakPRQMhFqYxfjU5fDd0Ke6QQwDoofnakH6UeQYHdQQB6V0wp9YkXTmixpkQz0kAAwXJvcBOTVbsGi9vCCyoDKGVaSOPm2PTV0K2wxwoBrKPiZ4dva6nf8tmHVUTCcGfYFsX9LgohblSkT4Nwm5AiaCrvR66EtYzvnLuKBTXMT8am3w3Dx+qkEMA6K9470olCf6CVfq1aAJ/YDVENgJwmpF4fnboSuhXqqBDAfVK8dyShRf5jFPp59w72A0ApVoQ03xiduhLaefugEMB9Vvyl4X4t81baUz0BlCILyhwcnbwS2nn7qBDABtGZlwb/oKXmsLY9BZDAE+rPo1O/C+28BlAIYAPpzIvDnSD0kAb9cyvQu0YACm5U8VBIden+xOUwgqWBFALYoDrzwtAFFPo3CPg9BMdOrBJABk6sAog5EOr9+xOXQugaVCGAB0Rnvj94AQA7AOC7iPgs37XWMZHNPQdCPESAfxKU98cvhTbdQREA/B8cLEZXD43gYwAAAABJRU5ErkJggg==" 9 | )====="; 10 | 11 | const char lock_img[] PROGMEM = R"=====( 12 | data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAATZJREFUSEvdlb8uBHEUhb+jl0g0Eh0KrMcQQiIa4UUkGn8rFB6EaIUQj7GLQieikGj0R36bKTazM3N3Z1bBLed37/lyz725I345NIi+7Q3gGJjN8l+BQ0k3UX0IsL0HnJUI7Uq6qIJUAmyvAnfAB5BA95nYCnAOTAHLkh7LIBHgIQkAO5KuekVsbwOXwK2k9bqAL2ACGJf0nQNMAp/Au6TpugCnQkmFndqufO/WVg0oEojeCwG2N7OtmY9WMPf+lK3ude/3vg5svwGlngbQtqSlCND1tW7k51XUwf8FpOEdAanDdB7WimxsYtGCpOckansGSAevL5oAWpI6GWARaI8akCw6AMaAE2Br1ICBNreJRX8HkIaXhlgnOpJa0alIwzsF5oYkvAD7kqqP3ZCiYXr40w8VgoQf8KKEGdyIA9gAAAAASUVORK5CYII= 13 | )====="; 14 | 15 | const char root_template[] PROGMEM = R"=====( 16 | 17 | 18 | 19 | 78 | 79 | 80 | 81 | 82 | 85 | 86 |
83 | Hero logo 84 |
87 | 88 |

PicoW
89 |

90 |

Configure the WiFi connection for the following device
91 |

92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
Name${firmware.name}
Short name${firmware.shortname}
Maker${firmware.maker}
Version${firmware.version}
110 |

111 | 112 | 113 | 116 | 117 |
114 | Configure WiFi 115 |
118 | 119 | 120 | )====="; 121 | 122 | const char wifi_template[] PROGMEM = R"=====( 123 | 124 | 125 | 126 | 127 | 246 | 252 | 253 | 254 | 255 | 256 | 259 | 260 |
257 | Hero logo 258 |
261 | 262 |

PicoW
263 |

264 |

Available WiFi Networks
265 |

266 | 267 | ${networks} 268 |
269 |

270 |
271 | 272 | 273 | 276 | 277 |
274 |
WiFi Settings
275 |
278 | 279 | 280 | 287 | 288 |
281 |
282 |
283 |
284 | 285 |
286 |
289 | 290 | 291 | 294 | 295 |
292 |

Back to home

293 |
296 | 297 | 298 | )====="; 299 | 300 | const char wifi_network_template[] PROGMEM = R"=====( 301 | 302 | 303 |

304 |
305 |
306 |
307 |
308 | 309 | ${name} 310 | 311 | 312 | 313 | 314 | )====="; 315 | --------------------------------------------------------------------------------