Einer der größten Vorteile des ESP32 ist sein eingebautes WiFi-Modul. Nie war es einfacher, seinem IoT-Gerät Zugang zum Netzwerk oder zum Internet zu verschaffen. Der ESP32 kann aber auch selbst ein WiFi-Netz aufmachen, über welches man ihn konfigurieren kann.
Wenn man ein neues IoT-Gerät im Netzwerk anmelden will, kann den Namen des Netzwerks (SSID) und das Passwort direkt in den Quellcode schreiben. Meiner Meinung nach ist diese Methode aber manchmal unpraktisch. Wenn das Gerät nicht in dem Netzwerk genutzt wird, in dem es entwickelt wird, oder wenn mehrere Geräte in unterschiedlichen Netzwerken genutzt werden sollen, muss man jedes mal den Quellcode erneut anfassen.
Der ESP32 kann aber auch selbst ein WiFi-Netz aufmachen. Dazu kann man sich dann mit z.B. dem Smartphone verbinden, eine Website aufrufen und den ESP32 konfigurieren. Zunächst das WiFi-Netz:
import network wlan = network.WLAN(network.AP_IF) wlan.active(True)
network.AP_IF
Jetzt kann man sich schon mit dem Netzwerk verbinden. Es hat kein Passwort und heißt ESP_XXXXXX. Das war es allerdings auch schon. Mehr geht noch nicht. Um das zu ändern, müssen wir auf eingehende Verbindungen antworten. Das übernimmt für uns das Modul usocket
. Wir wollen auf alle eingehenden Verbindungen, die auf Port 80 (Standardport für HTTP-Anfragen) ankommen, antworten:s = usocket.socket() s.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1) s.bind(('0.0.0.0', 80)) s.listen()
Jetzt horchen wir am Port 80. Wir müssen mit den eingehenden Verbindungen aber auch etwas tun. Dazu begeben wir uns in eine Endlosschleife und waren, bis sich ein Client mit unserem WiFi-Netz verbindet und eine Verbindung auf Port 80 herstellt.
while True: client_sock, client_addr = s.accept() print("Client address:", client_addr)
Wir können das tun, indem die IP des ESP32 (192.168.4.1) in die Adresszeile unseres Browsers eingeben:
Der ESP32 sieht auch, dass sich ein neuer Client verbunden hat, allerdings sieht der Client noch nichts, weil wir noch nichts zurücksenden.
MicroPython v1.13 on 2020-09-02; ESP32 module with ESP32 Type "help()" for more information. >>> MPY: soft reboot Client address: ('192.168.4.2', 50552) Client address: ('192.168.4.2', 50554)
Bevor wir etwas zurücksenden, schauen wir uns aber erst einmal an, was uns der Client sendet. In MicroPython können wir den Socket direkt wie einen Stream benutzen, also Daten direkt lesen und schreiben:
while True: client_sock, client_addr = s.accept() print("Client address:", client_addr) print("Request:") while True: h = client_sock.readline() if h == b"" or h == b"\r\n": break print(h)
Als Ausgabe erhalten wir den Request:
MicroPython v1.13 on 2020-09-02; ESP32 module with ESP32 Type "help()" for more information. >>> MPY: soft reboot Client address: ('192.168.4.2', 50678) Request: b'GET / HTTP/1.1\r\n' b'Host: 192.168.4.1\r\n' b'Connection: keep-alive\r\n' b'Cache-Control: max-age=0\r\n' b'Save-Data: on\r\n' b'Upgrade-Insecure-Requests: 1\r\n' b'User-Agent: Mozilla/5.0 (Linux; Android 10; ELE-L29) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Mobile Safari/537.36\r\n' b'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\n' b'Accept-Encoding: gzip, deflate\r\n' b'Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7,lb;q=0.6\r\n'
Hier siehen wir jetzt folgendes:
Wir senden aber noch nichts zurück. Das können wir tun, indem wir einfach in den Stream schreiben (und ihn am Ende schließen). Also z.B.:
while True: client_sock, client_addr = s.accept() print("Client address:", client_addr) print("Request:") while True: h = client_sock.readline() if h == b"" or h == b"\r\n": break print(h) client_sock.write("<html><head><title>Hello World</title></head>") client_sock.write("<body><h1>Hello World</h1></body></html>") client_sock.close()
Gut. Wir können jetzt also schon mal etwas an den Browser zurücksenden. Wir wollen aber nicht die komplette Seite in den Quellcode schreiben, sondern z.B. eine Datei zurücksenden. Dazu erstellen wir uns eine Datei config.html, in die wir folgendes schreiben:
<html> <head> <title>WiFi-Einstellungen</title> </head> <script> </script> <body> <h1>WiFi-Einstellungen</h1> <form method="get"> <table> <tr> <td><label for="ssid">SSID:</label></td> <td><input type="text" name="ssid" /></td> </tr> <tr> <td><label for="pass">Passwort:</label></td> <td><input type="text" name="pass" /></td> </tr> <tr> <td></td> <td><input type="submit" /></td> </tr> </table> </form> </body> </html>
Die kopieren wir dann auf den ESP32. Ich habe mir dazu ein extra Verzeichnis mit dem Namen html angelegt:
> mkdir /pyboard/test > cp config.html /pyboard/test
Jetzt können wir einfach diese Datei öffnen und zurücksenden:
while True: client_sock, client_addr = s.accept() print("Client address:", client_addr) print("Request:") while True: h = client_sock.readline() if h == b"" or h == b"\r\n": break print(h) with open("html/config.html", "r") as f: client_sock.write(f.read()) client_sock.close()
Super, jetzt bekommen wir schon mal eine Website angezeigt, in die wir unsere Daten eintragen können:
Wenn wir das tun und auf “Senden” drücken, bekommen wir folgende Ausgabe:
MPY: soft reboot Client address: ('192.168.4.2', 50766) Request: b'GET /?ssid=FRITZ%21Box+7490&pass=Geheim HTTP/1.1\r\n' b'Host: 192.168.4.1\r\n' b'Connection: keep-alive\r\n' b'Cache-Control: max-age=0\r\n' b'Save-Data: on\r\n' b'Upgrade-Insecure-Requests: 1\r\n' b'User-Agent: Mozilla/5.0 (Linux; Android 10; ELE-L29) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Mobile Safari/537.36\r\n' b'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\n' b'Referer: http://192.168.4.1/\r\n' b'Accept-Encoding: gzip, deflate\r\n' b'Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7,lb;q=0.6\r\n' Client address: ('192.168.4.2', 50768)
GET
: Methode, hier könnte auch z.B. POST
stehen. Wir wollen alle GET
-Anfragen mit einer Funktion beantworten./?ssid=FRITZ%21Box+7490&pass=Geheim
: Alles vor dem ? ist die angeforderte Seite, also z.B. config.html oder wie hier eine Default-Seite, wie index.html. Wir wollen config.html auch als Default-Seite zurückgeben. Hinter dem ? folgen die Wertepaare in %-KodierungHTTP/1.1
: die HTTP-VersionWir wollen alle GET-Anfragen in einer Funktion abarbeiten, also:
while True: client_sock, client_addr = s.accept() print("Client address:", client_addr) print("Request:") req = client_sock.readline().decode("ascii") if req.startswith("GET"): process_GET_req(req, client_sock) with open("html/config.html", "r") as f: client_sock.write(f.read()) client_sock.close()
process_GET_req
nehmen wir erste Zeile wie oben beschrieben auseinander. Anschließend nehmen wir die folgenden Header-Zeilen auseinander und schreiben sie in ein Python-Dictionary-Objekt. Vielleicht brauchen wir sie später ja noch einmal. Danach nehmen wir die Ressource auseinander. Wir trennen sie am ? in file und query. Das query nehmen wir noch einmal auseinander und trennen es am & in die einzelnen Felder. Diese sind Key-Value-Paare, die wir wiederum in ein Dict-speichern können. Diese können %-Kodiert sein. Wir müssen sie also wieder dekodieren:def urlencode(line): d = { r"+": " ", r"%20": "!", r"%21": "!", r"%22": '"', r"%23": "#", r"%24": "$", r"%25": "%", r"%26": "&", r"%27": "'", r"%28": "(", r"%29": ")", r"%2A": "*", r"%2B": "+", r"%2C": ",", r"%2D": "-", r"%2E": ".", r"%2F": "/", r"%3A": ":", r"%3B": ";", r"%3C": "<", r"%3D": "=", r"%3E": ">", r"%3F": "?", r"%4A": "@", r"%5B": "[", r"%5C": "\\", r"%5D": "]", r"%7B": "{", r"%7C": "|", r"%7D": "}", } for key in d: line = line.replace(key, d[key]) return line def process_GET_req(req, sock): method, ressource, version = req.split(" ") version = version[:-2] header = {} while True: h = sock.readline().decode("ascii") if h == "" or h == "\r\n": break if ": " in h: key, val = h.split(": ") header[key] = val[:-2] file = "" query = "" if "?" in ressource: file, query = ressource.split("?") else: file = ressource if file in ["", "/"]: file = "config.html" fields = {} if "=" in query: for field in query.split("&"): key, val = field.split("=") fields[urlencode(key)] = urlencode(val)
Die Dictionaries siehen dann also so aus:
{ 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; ELE-L29) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Mobile Safari/537.36', 'Save-Data': 'on', 'Connection': 'keep-alive', 'Cache-Control': 'max-age=0', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'Accept-Encoding': 'gzip, deflate', 'Host': '192.168.4.1', 'Upgrade-Insecure-Requests': '1', 'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7,lb;q=0.6' } { 'ssid': 'FRITZ!Box 7490', 'pass': 'Geheim' }
SSID und Passwort können wir in eine Datei schreiben und sie beim nächsten Start abrufen.
def process_query(file, fields): if file == "config.html": if ("ssid" in fields) & ("pass" in fields): if "credentials.json" in uos.listdir(): with open("credentials.json", "w") as f: try: credentials = ujson.load(f) except: credentials = {"wifi":{}} credentials["wifi"][fields["ssid"]] = fields["pass"] ujson.dump(credentials, f)
Dem Client können wir noch eine Datei zurückgeben. Wir geben ihm aber nicht pauschal config.html zurück, sondern die Datei, die er angefragt hat. So können wir in Zukunft noch weitere Seiten zur Verfügung stellen:
if file in uos.listdir("html"): with open("html/"+file, "r") as f: sock.write(f.read())
Wir wollen das alles natürlich nur machen, wenn wir nicht schon eine gültige credentials.json haben. Unser gesamtes Skript sieht dann so aus:
import network import usocket import ujson import uos import utime import machine if "credentials.json" in uos.listdir(): wlan = network.WLAN(network.STA_IF) wlan.active(True) nets = wlan.scan() try: with open("credentials.json", "r") as f: credentials = ujson.load(f) for ssid in credentials["wifi"]: if ssid in [net[0].decode("ascii") for net in nets]: wlan.connect(ssid, credentials["wifi"][ssid]) i = 0 while not wlan.isconnected() and i < 5: utime.sleep(1) i += 1 if wlan.isconnected(): break except: pass if wlan.isconnected(): print("Hurra") print(wlan.ifconfig()) while True: pass # # # Your Code here # # else: print(":(") wlan.active(False) wlan = network.WLAN(network.AP_IF) wlan.active(True) s = usocket.socket() s.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1) s.bind(('0.0.0.0', 80)) s.listen(5) def urlencode(line): d = { r"+": " ", r"%20": "!", r"%21": "!", r"%22": '"', r"%23": "#", r"%24": "$", r"%25": "%", r"%26": "&", r"%27": "'", r"%28": "(", r"%29": ")", r"%2A": "*", r"%2B": "+", r"%2C": ",", r"%2D": "-", r"%2E": ".", r"%2F": "/", r"%3A": ":", r"%3B": ";", r"%3C": "<", r"%3D": "=", r"%3E": ">", r"%3F": "?", r"%4A": "@", r"%5B": "[", r"%5C": "\\", r"%5D": "]", r"%7B": "{", r"%7C": "|", r"%7D": "}", } for key in d: line = line.replace(key, d[key]) return line def process_GET_req(req, sock): method, ressource, version = req.split(" ") version = version[:-2] header = {} while True: h = sock.readline().decode("ascii") if h == "" or h == "\r\n": break if ": " in h: key, val = h.split(": ") header[key] = val[:-2] file = "" query = "" if "?" in ressource: file, query = ressource.split("?") else: file = ressource if file in ["", "/"]: file = "config.html" fields = {} if "=" in query: for field in query.split("&"): key, val = field.split("=") fields[urlencode(key)] = urlencode(val) if file in uos.listdir("html"): with open("html/"+file, "r") as f: sock.write(f.read()) process_query(file, fields) def process_query(file, fields): if file == "config.html": if ("ssid" in fields) & ("pass" in fields): if "credentials.json" in uos.listdir(): with open("credentials.json", "w") as f: try: credentials = ujson.load(f) except: credentials = {"wifi":{}} credentials["wifi"][fields["ssid"]] = fields["pass"] ujson.dump(credentials, f) utime.sleep(1) machine.reset() while True: client_sock, client_addr = s.accept() req = client_sock.readline().decode("ascii") if req.startswith("GET"): process_GET_req(req, client_sock) with open("html/config.html", "r") as f: client_sock.write(f.read()) client_sock.close()
Das ist natürlich noch nicht 100%ig endverbrauchertauglich und auch nicht wirklich sicher, aber vielleicht hilft es euch ja, eure Projekte zu verwirklichen.
Eydam-Prototyping
Saccasner Straße 19
03096 Schmogrow-Fehrow
Germany