├── LICENSE.md ├── README.md ├── boot.py ├── code.py └── wifimgr.py /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2023 Robert Klebe, dotpointer 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 | # CircuitPython WiFi Manager 2 | 3 | A WiFi manager for CircuitPython. Opens an access point to allow the user to 4 | configure the device to configure the device to connect to available WiFi 5 | networks. When the device is configured it then connects to the first 6 | available matching network and hands over the control to your code. 7 | 8 | Based upon the original version in MicroPython, see the Original MicroPython 9 | version authors section. 10 | 11 | ## Description 12 | 13 | WiFi manager for CircuitPython 8+. 14 | 15 | ## Compatibility 16 | 17 | Tested on WeMos/LOLIN S2 Mini/Adafruit Feather ESP32-S2 with CircuitPython 8.0.2. 18 | 19 | Should be compatible with other CircuitPython 8+ devices too, please try it 20 | out to find out. 21 | 22 | The device must have CircuitPython WiFi hardware support. 23 | 24 | ## Main Features 25 | 26 | - Brings up an access point with a web based connection manager 27 | located at http://192.168.4.1/ if no network as been configured 28 | - Saves WiFi SSID:s and passwords in "wifi.dat" in CSV format 29 | - Connects automatically to the first available matching network 30 | - Easy to apply 31 | - Reset settings GPIO button support to let the user reset WiFi settings 32 | - USB write protected GPIO button support to try it out while connected to the computer 33 | 34 | ## Usage 35 | 36 | Install CircuitPython 8+ on your device. 37 | 38 | Upload boot.py, code.py and wifimgr.py to the file system of the device. 39 | 40 | Write your code below the connection prodedure in code.py or import it from 41 | code.py. 42 | 43 | Setup a GPIO to GND jumper wire or button in boot.py to control when 44 | CircuitPython or the connected computer can write to the file system. 45 | 46 | Connect to the WifiManager_AABBCC network, the password is "password". 47 | Visit http://192.168.4.1/ to configure. 48 | 49 | ## How it works 50 | 51 | It scans for available networks and then checks "wifi.dat" for matching 52 | network SSID:s and then it tries to connect the matching networks. 53 | 54 | If that did not succeed then it opens an access point with a web page 55 | to allow the user to configure WiFi client settings. Then it tries to 56 | connect and saves the settings to "wifi.dat". 57 | 58 | Then when a connection to an access point has been established it 59 | hands over the control to your code. 60 | 61 | ## Important to know 62 | 63 | Only the device or the USB host (like a computer) are allowed write-access 64 | to the file system in CircuitPython - not both at the same time. Therefore 65 | there are GPIO options in boot.py to setup a button to choose which one should 66 | have write access. Connect the GPIO and GND (push the button) to give 67 | CircuitPython write permission. 68 | 69 | When the code.py has completed the connection stops. This is by CircuitPython 70 | design. It is maybe obvious, but to keep the device responding to ping for 71 | instance you need to have a simple loop preferably with a sleep timeout 72 | running. 73 | 74 | ## How to regain write permission 75 | 76 | If you loose write permission to the file system of the device then you can 77 | do this to remove the file that locks it through a serial connection: 78 | 79 | 1. Make a backup of your files on the file system to a folder on your computer. 80 | 81 | 2. Open a serial connection. For example in Debian Linux you can use screen: 82 | apt install screen, then ls /dev/ttyACM* to find the current serial port number - 83 | note the number it is ending in, usually 0, then screen /dev/ttyACM0 - replace 84 | 0 with your number, then press Enter. 85 | 86 | 2. Press Ctrl+C, then Enter to get the REPL prompt. 87 | 88 | 3. Type import os, then os.listdir() to list files or os.unlink('boot.py') to 89 | remove boot.py or another file that is doing the locking, then hard reset the 90 | device by removing USB or power or connecting the EN pin to GND if available. 91 | 92 | ## Notable behaviour differences from the MicroPython version 93 | 94 | It does not automatically connect to the first open network it finds, 95 | because that did not seem secure nor usable. 96 | 97 | It does not open an access point if none of the configured networks 98 | are connectable because of security reasons. 99 | 100 | ## Issues and contributions 101 | 102 | If you find an issue please also supply a possible solution if possible, maybe 103 | as a pull request. 104 | 105 | ## Authors 106 | 107 | * **Robert Klebe** - *Development* - [dotpointer](https://github.com/dotpointer) 108 | 109 | ## Original MicroPython version authors 110 | 111 | * **Tayfun ULU** - *Original MicroPython version* - [tayfunulu](https://github.com/tayfunulu/WiFiManager/) 112 | 113 | * **CPOPP** - *Original MicroPython version - web server* - [CPOPP](https://github.com/cpopp/MicroPythonSamples) 114 | 115 | ## License 116 | 117 | This project is licensed under the MIT License - see the 118 | [LICENSE.md](LICENSE.md) file for details. 119 | -------------------------------------------------------------------------------- /boot.py: -------------------------------------------------------------------------------- 1 | import digitalio 2 | import microcontroller 3 | import storage 4 | 5 | # make the USB read only button, to allow CircuitPython to write to the file 6 | # system 7 | # usage: change the GPIO below, then connect a jumper wire or button between 8 | # the GPIO and the GND pin 9 | button_usb_readonly = digitalio.DigitalInOut(microcontroller.pin.GPIO38) 10 | button_usb_readonly.direction = digitalio.Direction.INPUT 11 | button_usb_readonly.pull = digitalio.Pull.UP 12 | if not button_usb_readonly.value: 13 | storage.remount("/", False) 14 | 15 | # reset settings button, to reset the wifi settings, useful to wire to a reset 16 | # button on the physical hardware to allow the user to reset the wifi settings 17 | # usage: change the GPIO below, then connect a jumper wire or button between 18 | # the GPIO and the GND pin to empty the wifi.dat file, please note that 19 | # CircuitPython needs write access, see the note above about this 20 | button_reset_settings = digitalio.DigitalInOut(microcontroller.pin.GPIO40) 21 | button_reset_settings.direction = digitalio.Direction.INPUT 22 | button_reset_settings.pull = digitalio.Pull.UP 23 | if not button_reset_settings.value: 24 | print('Emptying wifi.dat') 25 | try: 26 | with open('wifi.dat', 'w') as f: 27 | f.write('') 28 | except OSError as e: 29 | print("Exception", str(e)) 30 | -------------------------------------------------------------------------------- /code.py: -------------------------------------------------------------------------------- 1 | import supervisor 2 | import time 3 | import wifimgr 4 | 5 | # disable auto reload to stop unexpected reloads 6 | supervisor.set_next_code_file(filename = 'code.py', reload_on_error = False, reload_on_success = False) 7 | supervisor.runtime.autoreload = False 8 | 9 | wlan = wifimgr.get_connection() 10 | if wlan is None: 11 | print("Could not initialize the network connection.") 12 | while True: 13 | pass 14 | 15 | # put your code below this line, example below to keep it running, 16 | # otherwise it will not respond to ping 17 | print("WifiManager routines has ended") 18 | while True: 19 | time.sleep(1) 20 | -------------------------------------------------------------------------------- /wifimgr.py: -------------------------------------------------------------------------------- 1 | import re 2 | import socketpool 3 | import storage 4 | import time 5 | import wifi 6 | 7 | # extract access point mac address 8 | mac_ap = ' '.join([hex(i) for i in wifi.radio.mac_address_ap]) 9 | mac_ap = mac_ap.replace('0x', '').replace(' ', '').upper() 10 | 11 | # access point settings 12 | AP_SSID = "WifiManager_" + mac_ap[5:10] + mac_ap[1:2] 13 | AP_PASSWORD = "password" 14 | # AP_AUTHMODES = [wifi.AuthMode.OPEN] 15 | AP_AUTHMODES = [wifi.AuthMode.WPA2, wifi.AuthMode.PSK] 16 | 17 | FILE_NETWORK_PROFILES = 'wifi.dat' 18 | ap_enabled = False 19 | server_socket = None 20 | 21 | def do_connect(ssid, password): 22 | wifi.radio.enabled = True 23 | if wifi.radio.ap_info is not None: 24 | return None 25 | print('Trying to connect to "%s"...' % ssid) 26 | try: 27 | wifi.radio.connect(ssid, password) 28 | except Exception as e: 29 | print("Exception", str(e)) 30 | for retry in range(200): 31 | connected = wifi.radio.ap_info is not None 32 | if connected: 33 | break 34 | time.sleep(0.1) 35 | print('.', end='') 36 | if connected: 37 | t = [] 38 | t.append(str(wifi.radio.ipv4_address)) 39 | t.append(str(wifi.radio.ipv4_subnet)) 40 | t.append(str(wifi.radio.ipv4_gateway)) 41 | t.append(str(wifi.radio.ipv4_dns)) 42 | print('\nConnected. Network config: IP: ', end='') 43 | print('%s, subnet: %s, gateway: %s, DNS: %s' % tuple(t)) 44 | 45 | else: 46 | print('\nFailed. Not Connected to: ' + ssid) 47 | return connected 48 | 49 | 50 | def get_connection(): 51 | """return a working wifi.radio connection or None""" 52 | 53 | while wifi.radio.ap_info is None: 54 | # first check if there already is any connection: 55 | if wifi.radio.ap_info is not None: 56 | print('WiFi connection detected') 57 | return wifi.radio 58 | 59 | connected = False 60 | # connecting takes time, wait and retry 61 | time.sleep(3) 62 | if wifi.radio.ap_info is not None: 63 | print('WiFi connection detected') 64 | return wifi.radio 65 | 66 | # read known network profiles from file 67 | profiles = read_profiles() 68 | 69 | # search networks in range 70 | wifi.radio.enabled = True 71 | 72 | # networks are configured 73 | if (len(profiles)): 74 | networks = [] 75 | for n in wifi.radio.start_scanning_networks(): 76 | networks.append([n.ssid, n.bssid, n.channel, n.rssi, n.authmode]) 77 | wifi.radio.stop_scanning_networks() 78 | 79 | for ssid, bssid, channel, rssi, authmodes in sorted(networks, key=lambda x: x[3], reverse=True): 80 | encrypted = 0 81 | authmodes_text = [] 82 | for authmode in authmodes: 83 | if (authmode == wifi.AuthMode.OPEN): 84 | authmodes_text.append('Open') 85 | elif (authmode == wifi.AuthMode.WEP): 86 | authmodes_text.append('WEP') 87 | encrypted = 1 88 | elif (authmode == wifi.AuthMode.WEP): 89 | authmodes_text.append('WPA') 90 | encrypted = 1 91 | elif (authmode == wifi.AuthMode.WPA): 92 | authmodes_text.append('WPA') 93 | encrypted = 1 94 | elif (authmode == wifi.AuthMode.WPA2): 95 | authmodes_text.append('WPA2') 96 | encrypted = 1 97 | elif (authmode == wifi.AuthMode.WPA3): 98 | authmodes_text.append('WPA3') 99 | encrypted = 1 100 | elif (authmode == wifi.AuthMode.PSK): 101 | authmodes_text.append('PSK') 102 | encrypted = 1 103 | elif (authmode == wifi.AuthMode.ENTERPRISE): 104 | authmodes_text.append('ENTERPRISE') 105 | encrypted = 1 106 | authmodes_text = ', '.join(authmodes_text) 107 | print("Found \"%s\", #%d, %d dB, %s" % (ssid, channel, rssi, authmodes_text), end='') 108 | if ssid in profiles: 109 | print(", known") 110 | if encrypted: 111 | password = profiles[ssid] 112 | connected = do_connect(ssid, password) 113 | else: # open 114 | print(", open") 115 | connected = do_connect(ssid, None) 116 | else: 117 | print(", unknown") 118 | if connected: 119 | break 120 | # no networks configured 121 | else: 122 | connected = start_ap() 123 | 124 | if connected: 125 | return wifi.radio 126 | 127 | def handle_configure(client, request): 128 | global ap_enabled 129 | print('Handle configure start') 130 | print("Request:", request.strip()) 131 | match = re.search("ssid=([^&]*)&password=(.*)", request) 132 | 133 | if match is None: 134 | send_response(client, "Parameters not found", status_code=400) 135 | print('Handle configure aborted, missing parameters') 136 | return False 137 | ssid = match.group(1).replace("%3F", "?").replace("%21", "!") 138 | password = match.group(2).replace("%3F", "?").replace("%21", "!") 139 | 140 | if len(ssid) == 0: 141 | send_response(client, "SSID must be provided", status_code=400) 142 | print('Handling configure aborted, no SSID provided') 143 | return False 144 | 145 | if do_connect(ssid, password): 146 | try: 147 | profiles = read_profiles() 148 | except OSError: 149 | profiles = {} 150 | profiles[ssid] = password 151 | write_result = write_profiles(profiles) 152 | response = get_html_head() + """\ 153 |
154 | """
155 | response = response + """\
156 | Successfully connected to the WiFi network "%(ssid)s".
157 | """ % dict(ssid=ssid)
158 |
159 | if write_result is False:
160 | print('Failed to write changes')
161 | response = response + """\
162 |
163 | Failed to save changes.
164 | """
165 | response = response + """\
166 |