├── LICENSE ├── README.md ├── boot.py ├── configserver.py ├── html ├── index.html ├── main.css └── templates │ ├── wifi-add.tpl │ ├── wifi-saved.tpl │ ├── wifi-scan.tpl │ └── wifi-status.tpl ├── main.py ├── screenshot.png └── wifi_database.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Carsten B. L. Tschense 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 | # micropython-configserver 2 | Captive portal for micropython including a dumb DNS server and a webserver to configure wifi networks. 3 | -------------------------------------------------------------------------------- /boot.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import webrepl 3 | import network 4 | import wifi_database 5 | import time 6 | gc.collect() 7 | 8 | sta_if = network.WLAN(network.STA_IF) 9 | sta_if.active(True) 10 | 11 | for wifi in wifi_database.iter_wifis(): 12 | sta_if.connect(wifi[0], wifi[1]) 13 | for i in range(50): 14 | time.sleep_ms(100) 15 | if sta_if.isconnected(): 16 | wifi_database.active_wifi = wifi[0] 17 | break 18 | if wifi_database.active_wifi is not None: 19 | break 20 | if wifi_database.active_wifi is None: 21 | sta_if.active(False) 22 | 23 | webrepl.start() 24 | gc.collect() -------------------------------------------------------------------------------- /configserver.py: -------------------------------------------------------------------------------- 1 | import picoweb 2 | import uasyncio as asyncio 3 | import gc 4 | import network 5 | import socket 6 | import ustruct 7 | import ure as re 8 | import wifi_database 9 | from picoweb.utils import parse_qs 10 | import utime 11 | gc.collect() 12 | 13 | class HTTPRequest: 14 | 15 | def __init__(self): 16 | pass 17 | 18 | def read_form_data(self): 19 | size = int(self.headers["Content-Length"]) 20 | data = yield from self.reader.readline() 21 | line = data.decode() 22 | if line.startswith('------'): 23 | boundary = line 24 | chars_read = len(line) 25 | self.form = {} 26 | while True: 27 | data = yield from self.reader.readline() 28 | line = data.decode() 29 | chars_read += len(line) 30 | if line == boundary: 31 | continue 32 | if chars_read >= size or line == boundary.strip() + '--\r\n': 33 | break 34 | elif line.startswith('Content-Disposition: form-data;'): 35 | field = re.compile('.*=\"(.*)\"').match(line).group(1) 36 | elif line != '\r\n': 37 | self.form[field] = line.strip() 38 | else: 39 | form = parse_qs(line) 40 | self.form = form 41 | 42 | class MyWebApp(picoweb.WebApp): 43 | def get_task(self, host='0.0.0.0', port=80, debug=False): 44 | gc.collect() 45 | self.debug = int(debug) 46 | self.init() 47 | if debug: 48 | print("* Running on http://%s:%s/" % (host, port)) 49 | return asyncio.start_server(self._handle, host, port) 50 | 51 | 52 | def handle_template(req, resp): 53 | if req.method == 'POST': 54 | yield from req.read_form_data() 55 | yield from picoweb.start_response(resp) 56 | yield from app.render_template(resp, req.url_match.group(1), (req, )) 57 | 58 | 59 | async def dns_server(): 60 | myip = network.WLAN(network.AP_IF).ifconfig()[0] 61 | print('binding to', myip) 62 | mysocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 63 | mysocket.bind(socket.getaddrinfo(myip, 53)[0][-1]) 64 | while True: 65 | yield uasyncio.IORead(mysocket) 66 | mysocket.setblocking(False) 67 | packet, address = mysocket.recvfrom(256) 68 | # is query? opcode 0? 1 question? 69 | if (packet[2] & 0x80 == 0x0 and packet[2] & 0xf0 == 0x0 and packet[4:6] == b'\x00\x01'): 70 | # we take the request, change a few bits and append the answer 71 | response = bytearray(packet) 72 | response[2] |= 0x80 # change from query to response 73 | response[3] = 0 # recursion not available and responsecode stays 0 74 | response[7] = 1 # number of answers 75 | response += b'\xc0\x0c' # stuff 76 | response += b'\x00\x01' # A entry 77 | response += b'\x00\x01' # class IN 78 | response += b'\x00\x00\x00\x00' # TTL 0 79 | response += b'\x00\x04' # length of address 80 | response += ustruct.pack('BBBB', *[int(x) for x in myip.split('.')]) 81 | mysocket.sendto(response, address) 82 | 83 | ROUTES = [ 84 | ('/', lambda req, resp: (yield from app.sendfile(resp, 'index.html'))), 85 | (re.compile("^/(.*\.htm[l]?)$"), lambda req, resp: (yield from app.handle_static(req, resp))), 86 | (re.compile("^/(.*\.css)$"), lambda req, resp: (yield from app.handle_static(req, resp))), 87 | (re.compile("^/(.+\.tpl)($|\?.*)"), lambda req, resp: (yield from handle_template(req, resp))), 88 | ] 89 | app = MyWebApp(pkg='html', serve_static=False, routes=ROUTES) 90 | gc.collect() 91 | app._load_template('wifi-scan.tpl') 92 | app._load_template('wifi-add.tpl') 93 | app._load_template('wifi-status.tpl') 94 | app._load_template('wifi-saved.tpl') 95 | gc.collect() 96 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Config 4 | 5 | 6 | 79 | 80 | 81 |
82 | Current network 83 |
84 | Available networks 85 |
86 | loading ... 87 |
88 | Saved networks 89 |
90 | loading ... 91 |
92 |
93 | 94 | -------------------------------------------------------------------------------- /html/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | font-size: 14px; 4 | margin: 0; 5 | padding: 0; 6 | text-align: center; /* old IE fix */ 7 | } 8 | 9 | #content { 10 | width: 500px; 11 | height: 500px; 12 | position: relative; 13 | margin-left: auto; 14 | margin-right: auto; 15 | margin-top: 10px; 16 | text-align: left; 17 | } 18 | 19 | .empty_bars { 20 | color: #aaa; 21 | } 22 | 23 | #current_network { 24 | width: 490px; 25 | height: 40px; 26 | border: 1px solid black; 27 | margin-top: 5px; 28 | margin-bottom: 20px; 29 | padding: 5px; 30 | background-color: #dddddd; 31 | line-height: 45px; 32 | vertical-align: middle; 33 | } 34 | 35 | #current_name { 36 | font-weight: bold; 37 | display: inline-block; 38 | width: 400px; 39 | font-size: 1.5em; 40 | margin-left: 20px; 41 | line-height: normal; 42 | } 43 | 44 | #current_strength { 45 | width: 50px; 46 | display: inline-block; 47 | font-weight: bold; 48 | font-size: 1.5em; 49 | line-height: normal; 50 | } 51 | 52 | #saved_networks { 53 | width: 490px; 54 | height: 100px; 55 | border: 1px solid black; 56 | margin-top: 10px; 57 | padding: 5px; 58 | overflow: scroll; 59 | } 60 | 61 | #networks { 62 | width: 490px; 63 | height: 200px; 64 | border: 1px solid black; 65 | margin-top: 10px; 66 | margin-bottom: 20px; 67 | padding: 5px; 68 | overflow: scroll; 69 | } 70 | 71 | .network { 72 | height: 40px; 73 | background-color: #ccddee; 74 | padding-left: 10px; 75 | margin-bottom: 5px; 76 | line-height: 45px; 77 | vertical-align: middle; 78 | } 79 | 80 | .network:hover { 81 | background-color: #aabbcc; 82 | } 83 | 84 | .network:nth-child(odd) { 85 | background-color: #dddddd; 86 | } 87 | 88 | .network:nth-child(odd):hover { 89 | background-color: #aabbcc; 90 | } 91 | 92 | .network_name { 93 | font-weight: bold; 94 | display: inline-block; 95 | width: 300px; 96 | font-size: 1.5em; 97 | line-height: normal; 98 | } 99 | 100 | .network_encryption { 101 | width: 100px; 102 | display: inline-block; 103 | line-height: normal; 104 | } 105 | 106 | .network_strength { 107 | width: 30px; 108 | display: inline-block; 109 | font-weight: bold; 110 | line-height: normal; 111 | } 112 | 113 | .network_connect { 114 | display: none; 115 | width: 400px; 116 | margin-left: 30px; 117 | } -------------------------------------------------------------------------------- /html/templates/wifi-add.tpl: -------------------------------------------------------------------------------- 1 | {% args req %} 2 | {% set import wifi_database %} 3 | {% if wifi_database.connect_and_add_wifi(req.form['essid'], req.form['password']) is True %} 4 | OK 5 | {% else %} 6 | ERROR 7 | {% endif %} -------------------------------------------------------------------------------- /html/templates/wifi-saved.tpl: -------------------------------------------------------------------------------- 1 | {% set import wifi_database %} 2 | {% for mynetwork in wifi_database.iter_wifis() %} 3 |
4 |
{{mynetwork[0]}}
5 |
6 | {% endfor %} -------------------------------------------------------------------------------- /html/templates/wifi-scan.tpl: -------------------------------------------------------------------------------- 1 | {% set import network %} 2 | {% set authmodes = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2', 'WPA/WPA2'] %} 3 | {% set network.WLAN(network.STA_IF).active(True) %} 4 | {% for mynetwork in sorted(network.WLAN(network.STA_IF).scan(), key=lambda x: x[3], reverse=True) %} 5 | {% set quality_bars = round(5 * (0 if mynetwork[3] < -100 else (1 if mynetwork[3] > -50 else 2e-2 * (mynetwork[3] + 100)))) %} 6 |
7 |
{{mynetwork[0].decode('utf-8')}}
8 |
{{authmodes[mynetwork[4]]}}
9 |
{% for i in range(quality_bars) %}I{% endfor %}{% for i in range(5-quality_bars) %}I{% endfor %}
10 |
11 | 12 | Password:
13 |
14 | {% endfor %} -------------------------------------------------------------------------------- /html/templates/wifi-status.tpl: -------------------------------------------------------------------------------- 1 | {% set import network %} 2 | {% set import wifi_database %} 3 | {% if network.WLAN(network.STA_IF).isconnected() is True and wifi_database.active_wifi is not None %} 4 | {% set quality_bars = 0 %} 5 | {% for mynetwork in network.WLAN(network.STA_IF).scan() %} 6 | {% if mynetwork[0].decode('utf-8') == wifi_database.active_wifi %} 7 | {% set quality_bars = round(5 * (0 if mynetwork[3] < -100 else (1 if mynetwork[3] > -50 else 2e-2 * (mynetwork[3] + 100)))) %} 8 | {% endif %} 9 | {% endfor %} 10 |
{{wifi_database.active_wifi}}
11 |
{% for i in range(quality_bars) %}I{% endfor %}{% for i in range(5-quality_bars) %}I{% endfor %}
12 | {% else %} 13 | ERROR 14 | {% endif %} -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import uasyncio as asyncio 2 | import configserver 3 | 4 | 5 | loop = asyncio.get_event_loop(5) 6 | loop.create_task(configserver.dns_server()) 7 | loop.create_task(app.get_task(host='0.0.0.0')) 8 | loop.run_forever() -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carstenblt/micropython-configserver/b45bd33e7057b5115d8cc77039b241474efe4182/screenshot.png -------------------------------------------------------------------------------- /wifi_database.py: -------------------------------------------------------------------------------- 1 | import network 2 | import time 3 | 4 | active_wifi = None 5 | database_file = "wifi-database" 6 | 7 | def get_wifi(essid): 8 | try: 9 | myfile = open(database_file, "r") 10 | except OSError: 11 | return None 12 | 13 | line = myfile.readline() 14 | password = None 15 | while line != '': 16 | if line.strip() == essid: 17 | password = myfile.readline().strip() 18 | break 19 | myfile.readline() 20 | line = myfile.readline() 21 | myfile.close() 22 | return password 23 | 24 | def add_wifi(essid, password): 25 | try: 26 | myfile = open(database_file, "r+") 27 | except OSError: 28 | myfile = open(database_file, "w+") 29 | 30 | line = myfile.readline() 31 | content = None 32 | while line != '': 33 | if line.strip() == essid: 34 | position = myfile.tell() 35 | content = myfile.readlines() 36 | content[0] = password + '\n' 37 | myfile.seek(position) 38 | myfile.writelines(content) 39 | break 40 | myfile.readline() 41 | line = myfile.readline() 42 | if content == None: 43 | myfile.write(essid + '\n' + password + '\n') 44 | myfile.close() 45 | 46 | def remove_wifi(essid): 47 | try: 48 | myfile = open(database_file, "r+") 49 | except OSError: 50 | return 51 | 52 | line = myfile.readline() 53 | while line != '': 54 | if line.strip() == essid: 55 | position = myfile.tell() - len(line) 56 | myfile.readline() 57 | content = myfile.readlines() 58 | myfile.seek(position) 59 | myfile.writelines(content) 60 | myfile.truncate() 61 | break 62 | myfile.readline() 63 | line = myfile.readline() 64 | if content == None: 65 | myfile.write(essid + '\n' + password + '\n') 66 | myfile.close() 67 | 68 | def iter_wifis(): 69 | try: 70 | myfile = open(database_file, "r") 71 | except OSError: 72 | return None 73 | 74 | line = myfile.readline() 75 | while line != '': 76 | yield (line.strip(), myfile.readline().strip()) 77 | line = myfile.readline() 78 | myfile.close() 79 | 80 | def connect_and_add_wifi(essid, password): 81 | sta = network.WLAN(network.STA_IF) 82 | sta.active(True) 83 | sta.connect(essid, password) 84 | for i in range(50): 85 | time.sleep_ms(100) 86 | if sta.isconnected(): 87 | add_wifi(essid, password) 88 | active_wifi = essid 89 | return True 90 | sta.active(False) 91 | return False 92 | 93 | --------------------------------------------------------------------------------