├── LICENSE ├── README.md ├── main.py └── wifi_manager.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Igor Ferreira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WiFi Manager 2 | 3 | WiFi Manager for ESP8266 and ESP32 using MicroPython. It might work in any other board since it only uses standard MicroPython libraries, but that's not tested. 4 | 5 | ![ESP8266](https://img.shields.io/badge/ESP-8266-000000.svg?longCache=true&style=flat&colorA=CC101F) 6 | ![ESP32](https://img.shields.io/badge/ESP-32-000000.svg?longCache=true&style=flat&colorA=CC101F) 7 | 8 | ## What's new? 9 | 10 | Version 2.0 comes with some improvements: 11 | - Better documentation (I hope); 12 | - Some aesthetical changes in the code; 13 | - Removal of unnecessary messages; 14 | - Removal of the ability to set the ip address (to avoid unexpected problems); 15 | - Option to reboot after network configuration (needs improvement); 16 | 17 | ## Wishlist 18 | 19 | - [ ] Allow user to customize CSS; 20 | - [ ] Custom fields for extra configuration (like mqtt server, etc) 21 | - [ ] Turn this into a real python library with the option to be installed using pip; 22 | 23 | ## How It Works 24 | 25 | - When your device starts up, it will try to connect to a previously saved wifi. 26 | - If there is no saved network or if it fails to connect, it will start an access point; 27 | - By connecting to the access point and going to the address 192.168.4.1 you be able to find your network and input the credentials; 28 | - It will try to connect to the desired network, and if it's successful, it will save the credentials for future usage; 29 | - Be aware that the wifi credentials will be saved in a plain text file, and this can be a security fault depending on your application; 30 | 31 | ## Installation and Usage 32 | 33 | ```python 34 | # Download the "wifi_manager.py" file to your device; 35 | 36 | # Import the library: 37 | from wifi_manager import WifiManager 38 | 39 | # Initialize it 40 | wm = WifiManager() 41 | 42 | # By default the SSID is WiFiManager and the password is wifimanager. 43 | # You can customize the SSID and password of the AP for your needs: 44 | wm = WifiManager(ssid="my ssid",password="my password") 45 | 46 | # Start the connection: 47 | wm.connect() 48 | ``` 49 | 50 | ## Methods 51 | 52 | ### .connect() 53 | 54 | Tries to connect to a network and if it doesn't work start the configuration portal. 55 | 56 | ### .disconnect() 57 | 58 | Disconnect from network. 59 | 60 | ### .is_connected() 61 | 62 | Returns True if it's connected and False if it's not. It's the simpler way to test the connection inside your code. 63 | 64 | ### .get_address() 65 | 66 | Returns a tuple with the network interface parameters: IP address, subnet mask, gateway and DNS server. 67 | 68 | ## Notes 69 | 70 | - Do not use this library with other ones that works directly with the network interface, since it might have conflicts; 71 | 72 | ## Thanks To 73 | 74 | https://github.com/tayfunulu/WiFiManager/ 75 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from wifi_manager import WifiManager 2 | import utime 3 | 4 | # Example of usage 5 | 6 | wm = WifiManager() 7 | wm.connect() 8 | 9 | while True: 10 | if wm.is_connected(): 11 | print('Connected!') 12 | else: 13 | print('Disconnected!') 14 | utime.sleep(10) 15 | -------------------------------------------------------------------------------- /wifi_manager.py: -------------------------------------------------------------------------------- 1 | # Author: Igor Ferreira 2 | # License: MIT 3 | # Version: 2.1.0 4 | # Description: WiFi Manager for ESP8266 and ESP32 using MicroPython. 5 | 6 | import machine 7 | import network 8 | import socket 9 | import re 10 | import time 11 | 12 | 13 | class WifiManager: 14 | 15 | def __init__(self, ssid = 'WifiManager', password = 'wifimanager', reboot = True, debug = False): 16 | self.wlan_sta = network.WLAN(network.STA_IF) 17 | self.wlan_sta.active(True) 18 | self.wlan_ap = network.WLAN(network.AP_IF) 19 | 20 | # Avoids simple mistakes with wifi ssid and password lengths, but doesn't check for forbidden or unsupported characters. 21 | if len(ssid) > 32: 22 | raise Exception('The SSID cannot be longer than 32 characters.') 23 | else: 24 | self.ap_ssid = ssid 25 | if len(password) < 8: 26 | raise Exception('The password cannot be less than 8 characters long.') 27 | else: 28 | self.ap_password = password 29 | 30 | # Set the access point authentication mode to WPA2-PSK. 31 | self.ap_authmode = 3 32 | 33 | # The file were the credentials will be stored. 34 | # There is no encryption, it's just a plain text archive. Be aware of this security problem! 35 | self.wifi_credentials = 'wifi.dat' 36 | 37 | # Prevents the device from automatically trying to connect to the last saved network without first going through the steps defined in the code. 38 | self.wlan_sta.disconnect() 39 | 40 | # Change to True if you want the device to reboot after configuration. 41 | # Useful if you're having problems with web server applications after WiFi configuration. 42 | self.reboot = reboot 43 | 44 | self.debug = debug 45 | 46 | 47 | def connect(self): 48 | if self.wlan_sta.isconnected(): 49 | return 50 | profiles = self.read_credentials() 51 | for ssid, *_ in self.wlan_sta.scan(): 52 | ssid = ssid.decode("utf-8") 53 | if ssid in profiles: 54 | password = profiles[ssid] 55 | if self.wifi_connect(ssid, password): 56 | return 57 | print('Could not connect to any WiFi network. Starting the configuration portal...') 58 | self.web_server() 59 | 60 | 61 | def disconnect(self): 62 | if self.wlan_sta.isconnected(): 63 | self.wlan_sta.disconnect() 64 | 65 | 66 | def is_connected(self): 67 | return self.wlan_sta.isconnected() 68 | 69 | 70 | def get_address(self): 71 | return self.wlan_sta.ifconfig() 72 | 73 | 74 | def write_credentials(self, profiles): 75 | lines = [] 76 | for ssid, password in profiles.items(): 77 | lines.append('{0};{1}\n'.format(ssid, password)) 78 | with open(self.wifi_credentials, 'w') as file: 79 | file.write(''.join(lines)) 80 | 81 | 82 | def read_credentials(self): 83 | lines = [] 84 | try: 85 | with open(self.wifi_credentials) as file: 86 | lines = file.readlines() 87 | except Exception as error: 88 | if self.debug: 89 | print(error) 90 | pass 91 | profiles = {} 92 | for line in lines: 93 | ssid, password = line.strip().split(';') 94 | profiles[ssid] = password 95 | return profiles 96 | 97 | 98 | def wifi_connect(self, ssid, password): 99 | print('Trying to connect to:', ssid) 100 | self.wlan_sta.connect(ssid, password) 101 | for _ in range(100): 102 | if self.wlan_sta.isconnected(): 103 | print('\nConnected! Network information:', self.wlan_sta.ifconfig()) 104 | return True 105 | else: 106 | print('.', end='') 107 | time.sleep_ms(100) 108 | print('\nConnection failed!') 109 | self.wlan_sta.disconnect() 110 | return False 111 | 112 | 113 | def web_server(self): 114 | self.wlan_ap.active(True) 115 | self.wlan_ap.config(essid = self.ap_ssid, password = self.ap_password, authmode = self.ap_authmode) 116 | server_socket = socket.socket() 117 | server_socket.close() 118 | server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 119 | server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 120 | server_socket.bind(('', 80)) 121 | server_socket.listen(1) 122 | print('Connect to', self.ap_ssid, 'with the password', self.ap_password, 'and access the captive portal at', self.wlan_ap.ifconfig()[0]) 123 | while True: 124 | if self.wlan_sta.isconnected(): 125 | self.wlan_ap.active(False) 126 | if self.reboot: 127 | print('The device will reboot in 5 seconds.') 128 | time.sleep(5) 129 | machine.reset() 130 | self.client, addr = server_socket.accept() 131 | try: 132 | self.client.settimeout(5.0) 133 | self.request = b'' 134 | try: 135 | while True: 136 | if '\r\n\r\n' in self.request: 137 | # Fix for Safari browser 138 | self.request += self.client.recv(512) 139 | break 140 | self.request += self.client.recv(128) 141 | except Exception as error: 142 | # It's normal to receive timeout errors in this stage, we can safely ignore them. 143 | if self.debug: 144 | print(error) 145 | pass 146 | if self.request: 147 | if self.debug: 148 | print(self.url_decode(self.request)) 149 | url = re.search('(?:GET|POST) /(.*?)(?:\\?.*?)? HTTP', self.request).group(1).decode('utf-8').rstrip('/') 150 | if url == '': 151 | self.handle_root() 152 | elif url == 'configure': 153 | self.handle_configure() 154 | else: 155 | self.handle_not_found() 156 | except Exception as error: 157 | if self.debug: 158 | print(error) 159 | return 160 | finally: 161 | self.client.close() 162 | 163 | 164 | def send_header(self, status_code = 200): 165 | self.client.send("""HTTP/1.1 {0} OK\r\n""".format(status_code)) 166 | self.client.send("""Content-Type: text/html\r\n""") 167 | self.client.send("""Connection: close\r\n""") 168 | 169 | 170 | def send_response(self, payload, status_code = 200): 171 | self.send_header(status_code) 172 | self.client.sendall(""" 173 | 174 | 175 | 176 | WiFi Manager 177 | 178 | 179 | 180 | 181 | 182 | {0} 183 | 184 | 185 | """.format(payload)) 186 | self.client.close() 187 | 188 | 189 | def handle_root(self): 190 | self.send_header() 191 | self.client.sendall(""" 192 | 193 | 194 | 195 | WiFi Manager 196 | 197 | 198 | 199 | 200 | 201 |

WiFi Manager

202 |
203 | """.format(self.ap_ssid)) 204 | for ssid, *_ in self.wlan_sta.scan(): 205 | ssid = ssid.decode("utf-8") 206 | self.client.sendall(""" 207 |

208 | """.format(ssid)) 209 | self.client.sendall(""" 210 |

211 |

212 |
213 | 214 | 215 | """) 216 | self.client.close() 217 | 218 | 219 | def handle_configure(self): 220 | match = re.search('ssid=([^&]*)&password=(.*)', self.url_decode(self.request)) 221 | if match: 222 | ssid = match.group(1).decode('utf-8') 223 | password = match.group(2).decode('utf-8') 224 | if len(ssid) == 0: 225 | self.send_response(""" 226 |

SSID must be providaded!

227 |

Go back and try again!

228 | """, 400) 229 | elif self.wifi_connect(ssid, password): 230 | self.send_response(""" 231 |

Successfully connected to

232 |

{0}

233 |

IP address: {1}

234 | """.format(ssid, self.wlan_sta.ifconfig()[0])) 235 | profiles = self.read_credentials() 236 | profiles[ssid] = password 237 | self.write_credentials(profiles) 238 | time.sleep(5) 239 | else: 240 | self.send_response(""" 241 |

Could not connect to

242 |

{0}

243 |

Go back and try again!

244 | """.format(ssid)) 245 | time.sleep(5) 246 | else: 247 | self.send_response(""" 248 |

Parameters not found!

249 | """, 400) 250 | time.sleep(5) 251 | 252 | 253 | def handle_not_found(self): 254 | self.send_response(""" 255 |

Page not found!

256 | """, 404) 257 | 258 | 259 | def url_decode(self, url_string): 260 | 261 | # Source: https://forum.micropython.org/viewtopic.php?t=3076 262 | # unquote('abc%20def') -> b'abc def' 263 | # Note: strings are encoded as UTF-8. This is only an issue if it contains 264 | # unescaped non-ASCII characters, which URIs should not. 265 | 266 | if not url_string: 267 | return b'' 268 | 269 | if isinstance(url_string, str): 270 | url_string = url_string.encode('utf-8') 271 | 272 | bits = url_string.split(b'%') 273 | 274 | if len(bits) == 1: 275 | return url_string 276 | 277 | res = [bits[0]] 278 | appnd = res.append 279 | hextobyte_cache = {} 280 | 281 | for item in bits[1:]: 282 | try: 283 | code = item[:2] 284 | char = hextobyte_cache.get(code) 285 | if char is None: 286 | char = hextobyte_cache[code] = bytes([int(code, 16)]) 287 | appnd(char) 288 | appnd(item[2:]) 289 | except Exception as error: 290 | if self.debug: 291 | print(error) 292 | appnd(b'%') 293 | appnd(item) 294 | 295 | return b''.join(res) 296 | --------------------------------------------------------------------------------