├── .gitignore ├── README.md ├── boot.py ├── index.html └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | /uasyncio2 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minimal MicroPython Captive Portal 2 | 3 | This code is tested on ESP32. It creates a wifi access point, and once connected to it a captive portal is opened (served from `index.html`). 4 | 5 | * Works with uasyncio v3 (MicroPython 1.13+) 6 | * Fallback for earlier versions of uasyncio/MicroPython 7 | * Code: [main.py](https://github.com/metachris/micropython-captiveportal/blob/master/main.py) 8 | 9 | --- 10 | 11 | Notes 12 | 13 | * License: MIT 14 | * Author: Chris Hager / https://twitter.com/metachris 15 | * Repository: https://github.com/metachris/micropython-captiveportal 16 | 17 | Built upon 18 | 19 | - https://github.com/p-doyle/Micropython-DNSServer-Captive-Portal 20 | 21 | References 22 | 23 | - http://docs.micropython.org/en/latest/library/uasyncio.html 24 | - https://github.com/peterhinch/micropython-async/blob/master/v3/README.md 25 | - https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md 26 | - https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5 27 | -------------------------------------------------------------------------------- /boot.py: -------------------------------------------------------------------------------- 1 | # This file is executed on every boot (including wake-boot from deepsleep) 2 | #import esp 3 | #esp.osdebug(None) 4 | import gc 5 | #import webrepl 6 | #webrepl.start() 7 | gc.collect() 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello World! 5 | 6 | 7 | 8 | 9 |

Hello Title!

10 |
11 | Hello Div! 12 |
13 | 14 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Minimal captive portal, using uasyncio v3 (MicroPython 1.13+) with a fallback for earlier versions of uasyncio/MicroPython. 3 | 4 | * License: MIT 5 | * Repository: https://github.com/metachris/micropython-captiveportal 6 | * Author: Chris Hager / https://twitter.com/metachris 7 | 8 | Built upon: 9 | - https://github.com/p-doyle/Micropython-DNSServer-Captive-Portal 10 | 11 | References: 12 | - http://docs.micropython.org/en/latest/library/uasyncio.html 13 | - https://github.com/peterhinch/micropython-async/blob/master/v3/README.md 14 | - https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md 15 | - https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5 16 | """ 17 | import gc 18 | import sys 19 | import network 20 | import socket 21 | import uasyncio as asyncio 22 | 23 | # Helper to detect uasyncio v3 24 | IS_UASYNCIO_V3 = hasattr(asyncio, "__version__") and asyncio.__version__ >= (3,) 25 | 26 | 27 | # Access point settings 28 | SERVER_SSID = 'myssid' # max 32 characters 29 | SERVER_IP = '10.0.0.1' 30 | SERVER_SUBNET = '255.255.255.0' 31 | 32 | 33 | def wifi_start_access_point(): 34 | """ setup the access point """ 35 | wifi = network.WLAN(network.AP_IF) 36 | wifi.active(True) 37 | wifi.ifconfig((SERVER_IP, SERVER_SUBNET, SERVER_IP, SERVER_IP)) 38 | wifi.config(essid=SERVER_SSID, authmode=network.AUTH_OPEN) 39 | print('Network config:', wifi.ifconfig()) 40 | 41 | 42 | def _handle_exception(loop, context): 43 | """ uasyncio v3 only: global exception handler """ 44 | print('Global exception handler') 45 | sys.print_exception(context["exception"]) 46 | sys.exit() 47 | 48 | 49 | class DNSQuery: 50 | def __init__(self, data): 51 | self.data = data 52 | self.domain = '' 53 | tipo = (data[2] >> 3) & 15 # Opcode bits 54 | if tipo == 0: # Standard query 55 | ini = 12 56 | lon = data[ini] 57 | while lon != 0: 58 | self.domain += data[ini + 1:ini + lon + 1].decode('utf-8') + '.' 59 | ini += lon + 1 60 | lon = data[ini] 61 | print("DNSQuery domain:" + self.domain) 62 | 63 | def response(self, ip): 64 | print("DNSQuery response: {} ==> {}".format(self.domain, ip)) 65 | if self.domain: 66 | packet = self.data[:2] + b'\x81\x80' 67 | packet += self.data[4:6] + self.data[4:6] + b'\x00\x00\x00\x00' # Questions and Answers Counts 68 | packet += self.data[12:] # Original Domain Name Question 69 | packet += b'\xC0\x0C' # Pointer to domain name 70 | packet += b'\x00\x01\x00\x01\x00\x00\x00\x3C\x00\x04' # Response type, ttl and resource data length -> 4 bytes 71 | packet += bytes(map(int, ip.split('.'))) # 4bytes of IP 72 | # print(packet) 73 | return packet 74 | 75 | 76 | class MyApp: 77 | async def start(self): 78 | # Get the event loop 79 | loop = asyncio.get_event_loop() 80 | 81 | # Add global exception handler 82 | if IS_UASYNCIO_V3: 83 | loop.set_exception_handler(_handle_exception) 84 | 85 | # Start the wifi AP 86 | wifi_start_access_point() 87 | 88 | # Create the server and add task to event loop 89 | server = asyncio.start_server(self.handle_http_connection, "0.0.0.0", 80) 90 | loop.create_task(server) 91 | 92 | # Start the DNS server task 93 | loop.create_task(self.run_dns_server()) 94 | 95 | # Start looping forever 96 | print('Looping forever...') 97 | loop.run_forever() 98 | 99 | async def handle_http_connection(self, reader, writer): 100 | gc.collect() 101 | 102 | # Get HTTP request line 103 | data = await reader.readline() 104 | request_line = data.decode() 105 | addr = writer.get_extra_info('peername') 106 | print('Received {} from {}'.format(request_line.strip(), addr)) 107 | 108 | # Read headers, to make client happy (else curl prints an error) 109 | while True: 110 | gc.collect() 111 | line = await reader.readline() 112 | if line == b'\r\n': break 113 | 114 | # Handle the request 115 | if len(request_line) > 0: 116 | response = 'HTTP/1.0 200 OK\r\n\r\n' 117 | with open('index.html') as f: 118 | response += f.read() 119 | await writer.awrite(response) 120 | 121 | # Close the socket 122 | await writer.aclose() 123 | # print("client socket closed") 124 | 125 | async def run_dns_server(self): 126 | """ function to handle incoming dns requests """ 127 | udps = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 128 | udps.setblocking(False) 129 | udps.bind(('0.0.0.0', 53)) 130 | 131 | while True: 132 | try: 133 | # gc.collect() 134 | if IS_UASYNCIO_V3: 135 | yield asyncio.core._io_queue.queue_read(udps) 136 | else: 137 | yield asyncio.IORead(udps) 138 | data, addr = udps.recvfrom(4096) 139 | print("Incoming DNS request...") 140 | 141 | DNS = DNSQuery(data) 142 | udps.sendto(DNS.response(SERVER_IP), addr) 143 | 144 | print("Replying: {:s} -> {:s}".format(DNS.domain, SERVER_IP)) 145 | 146 | except Exception as e: 147 | print("DNS server error:", e) 148 | await asyncio.sleep_ms(3000) 149 | 150 | udps.close() 151 | 152 | 153 | # Main code entrypoint 154 | try: 155 | # Instantiate app and run 156 | myapp = MyApp() 157 | 158 | if IS_UASYNCIO_V3: 159 | asyncio.run(myapp.start()) 160 | else: 161 | loop = asyncio.get_event_loop() 162 | loop.run_until_complete(myapp.start()) 163 | 164 | except KeyboardInterrupt: 165 | print('Bye') 166 | 167 | finally: 168 | if IS_UASYNCIO_V3: 169 | asyncio.new_event_loop() # Clear retained state 170 | 171 | --------------------------------------------------------------------------------