Konfigurering av mikrokontroller med WLAN (Raspberry Pico W, ESP32)

 469 total views,  1 views today

En mikrokontroller med WLAN-kretser kan betjenes via nettet, og trenger derfor mindre knapper og lamper for betjening og konfigurasjon. Det mest nærliggende er å lage et web-grensesnitt, selv om andre protokoller også kan brukes. Men det å kople seg til et nytt WLAN-nettverk uten å legge navn og passord inn i programkoden krever noen knep som jeg skal beskrive her. Programmeringsspråket som brukes er MicroPython.

Anders Fongen, juni 2023

Innledning

Problemstillingen er som følger: Du har programmert en slik mikrokontroller til å bruke WLAN-forbindelsen for brukerbetjening, hente data fra Internet, eller kommunisere med andre maskiner. WLAN-adapteret må programmeres til å kople seg til et trådløst nettverk innen rekkevidde, og må kjenne navnet og passordet til det.

Det enkleste er å skrive nettverksnavnet (SSID) og passordet inn som verdier i programkoden, Men om du gir fra deg programkoden til andre er disse opplysningene til liten hjelp. Dessuten er det sjelden lurt å skrive passord direkte inn i programkoden.

Denne artikkelen løser dette problemet: Hvordan konfiguere en slik mikrokontroller for å kople seg til det stedlige trådløse nettverket?

For en raskere innføring, gå til slutten av artikkelen for å se et demonstrasjonsvideo.

Overordnet metode

  1. Kontrolleren har en liste over kjente nettverk, med navn og passord, lagret på en fil. Når kontrolleren starter, vil den gå gjennom listen og forsøke å kople seg til ett av nettverkene på denne listen.
  2. Dersom intet nettverk lar seg kople til (kanskje fordi listen er tom) vil kontrolleren sette opp sitt eget WLAN-nettverk med kjent navn og passord. Andre maskiner kan nå kople seg til dette nettverket.
  3. I tilfelle punkt 2, vil kontrolleren også starte opp en web-tjener for å bli konfigurert gjennom en web-leser. Eieren kan så skrive inn navn og passord på WLAN-nettverk som kontrolleren skal kunne kople seg til.
  4. Denne listen av kjente nettverk bli så lagret til en fil og blir hentet frem neste gang kontrolleren startes (punkt 1).

I resten av denne artikkelen vil jeg gå gjennom enkeltdelene i programmet som bruker denne metoden. Den fulle programkoden kan lastes ned og kjøres i kontrolleren for demonstrasjonsformål, men du vil trolig ønske å gjøre dine egne endringer.

Metoden del for del

I de følgende avsnittene vises ikke programmet i sin helhet, men kun de enkeltsetningene som knytter seg til hver enkelt delfunksjon. Du må selv finne ut hvordan disse delene settes sammen, men du kan også laste inn den komplette programkoden og studere den.

Starte WLAN-adaperet og observere eksisterende WLAN

import network
net = network.WLAN(network.STA_IF)
net.deinit() # Found to be useful
net.active(True)
observed_networks = net.scan()

Denne koden setter WLAN-adapteret i “station”-modus, som lar den kople seg til eksisterende nettverk. Den siste setningen scanner omgivelsene og bygger opp en liste med nettverk innen rekkevidde. Denne listen inneholder navn, mac-addresse, beskyttelse, signalstyrke, radiokanal m.m. for hvert av dem.

Lage en liste over aktuelle WLAN

Programkoden vil så finne ut hvilke av de observerte nettverkene den har navn og passord til (kalt kjente nettverk), og sortere dem etter synkende signalstyrke, slik at den siden kan velge det nettverket med best radiosignal. Listen over kjente nettverk ligger på såkalt JSON-format, og leses inn i en dictionary-variabel med disse setningene:

import json
def load_json(filename):
    f = open(filename,"r")
    d = json.load(f)
    f.close()
    return d

Listen over nettverk som både er observert og kjent, sortert etter synkende signalstyrke, lages med disse programsetningene:

# Merge the list of observed network and known networks,
# the resulting list will contain the known networks which
# are observed at the moment, sorted by descending rssi
def merge(known_list,observed_list):
    new_list = list()
    for (ssid,pw) in known_list:
        for obs in observed_list:
            if ssid == obs[0].decode(): # Network name
                rssi = obs[3] # Signal strength (RSSI)
                new_list.append((rssi,ssid,pw))
                break # Out of inner for loop
    new_list.sort(reverse=True)
    return new_list

Tilkoplingsprosedyre

Resultatet av funksjonen merge() er en liste over nettverk som det er aktuelt å kople seg til. Programmet vil forsøke det sterkeste nettverket først. Programkoden ser slik ut:

import network, time
# Merge the two lists
interesting_networks = merge(known_networks,observed_networks)
for (rssi,ssid,pw) in interesting_networks:
    net.deinit() # Form of reset
    net.active(True)
    attempts = 1
    net.connect(ssid,pw)
    status = net.status()
    if status in [network.STAT_NO_AP_FOUND, \
                  network.STAT_WRONG_PASSWORD]:
        print("Rejected from ", ssid, "reason=",status)
        break
    else:
        while status != network.STAT_GOT_IP:
            time.sleep_ms(500)
            attempts += 1
            if attempts > 20: # More than 10 seconds?
                print("Give up ", ssid, "reason=",status)
                break # Out of while loop
            status = net.status()
        if status == network.STAT_GOT_IP:
            print("Successful connection to ",ssid)
            return True # Successful connect
    # else iterate in the for loop            
return False

Denne koden er litt omstendelig. Fordi det tar litt tid å kople seg til et WLAN, bør du sette en tidsgrense for hvor lenge du vil vente før du gir opp og forsøker neste nettverk på listen. Denne koden sjekker to ganger i sekundet om forbindelsen er opprettet, og gir opp etter 10 sekunder.En tilkopling vil også kreve at nettverket tildeler en IP-adresse, noe som vises med verdien network.STAT_GOT_IP. Kodesetningene er en del av en funksjon som returnerer True dersom en forbindelse ble opprettet, False ellers.

Dersom forbindelsen ble opprettet, er alt bra. Programmet kan nå fortsette sin utføring av anvenderprogrammet, sette opp socket og forbindelser etc. Dette ligger bak horisonten for denne artikkelen.

Sette opp en konfigurasjonstjener

Dersom det ikke lot seg gjøre å kople seg til et WLAN, kan programmet nå starte en konfigurasjonstjener som gjøre det mulig å redigere listen over kjente nettverk. Det krever at vi lar mikrokontrolleren sette opp sitt eget WLAN med kjent navn og passord, og starter en web-tjener med en brukerdialog for dette formålet.

Opprette et WLAN er enkelt, det skjer med disse programsetningene:

import network
def make_ap(ssid="RPico-config",pw="123456789"):
    net = network.WLAN(network.AP_IF)
    net.deinit() # Form of reset
    net.config(essid=ssid,password=pw)
    net.active(True)
    while not self.sta_if.active():
        time.sleep_ms(500)
    # Access point now active
    print("Access point active")

Når dette er utført, kan programmet sette opp en socket som tillater at en web-leser kopler seg til. Dette gjøres i det web-tjeneren startes. Her vises programsetningene som setter opp en tjener-socket (sock.bind() og sock.listen()), og lar en web-leser kople seg til (sock.accept()). Deretter leser programmet inn de dataene som web-leseren sender, og nå er vi inne i HTTP-protokollen.

def start_web_server(self,port=80):
    sock = socket.socket()
    sock.bind(('0.0.0.0',port))
    sock.listen() # Accepting at most one pending connection
    print('listening on port', port)
    # Now enter main loop. Accept connections and process http requests
    while True:
        try:
            print("Waiting for incoming connection")
            client_socket, cl_addr = sock.accept()
            print('Client connection from', cl_addr)

            client_socket.settimeout(1) # Set socket timeout 1 sec
            request = ''
            try:
                while True:
                    buf = client_socket.recv(1024).decode()
                    request = request + buf
            except OSError:
                pass
            if request.startswith("GET / "):
                response = self.get_response()
                response_code = "200 OK"
            elif request.startswith("POST / "):
                print(request) # Just for debugging
                # Find where the html body starts (two newline chars
                ix = request.find('\r\n\r\n')
                if ix==-1: return # Garbage, quit
                post_parameters = self._parse_parameters(request[ix+4:])
                # Send the post parameters to a HANDLER, which returns
                # a response message
                response = self.post_handler(post_parameters)
                response_code = "200 OK"
            else:
                response_code = "404 Not Found"
                response = '<h1>404 Not Found</h1>'
        except Exception as e:
            response_code = "500 Internal Server Error"
            response = "<h1>500 Internal Server Error</h1>" 
        finally:
            client_socket.send('HTTP/1.0 %s \r\nContent-type:
                                text/html\r\n\r\n'%response_code)
            client_socket.send(response)
            client_socket.close()

HTTP-protokollen er et kapittel for seg, i korthet starter en forespørsel med ordet GET eller POST, etterfulgt av et stinavn til den ressursen som adresseres. I dette tilfellet tillater vi kun adressen “/”, alt annet ignoreres. På de påfølgende linjene i forespørselen kommer mer opplysninger om web-tjeneren og hva den ønsker. De skal vi også se bort fra. Denne delen av forespørselen slutter med en blank linje, uttrykt som '\r\n\r\n'. Etter dette kan det komme opplysninger knyttet til selve web-tjenesten, f.eks. data som legges inn i et skjema, men dette begrenser seg til POST-forespørsel.

Programkoden over viser hvordan den ved en POST-forespørsel trekker ut dataene som knytter seg til web-tjenesten, og leverer dem til en annen funksjon for behandling.

En web-tjener vil behandle forespørselen og sende en respons til web-leseren ved å skrive dataene til en socket. Dette er hva som vises på web-leserens skjerm etter at forespørselen er behandlet, og vil normalt være et resultat av noe databehandling. I dette tilfellet gir GET-operasjonen en respons som er gitt i variabelen get_response, og POST-operasjonen det som returneres fra metoden post_handler(..). Det vil fremgå av den komplette programkoden at get_response inneholder et HTML-skjema som lar brukeren skrive inn data for de kjente WLAN-nettverkene, og post_handler(..) vil produsere en form for bekreftelse på at dataene er blitt lagret på en fil.

Slik vises resultatet av en GET-operasjon: et skjema som skal fylles ut med WLAN-opplysninger

Når brukeren fyller inn det skjemaet som blir vist og sender det med SAVE-knappen vil innholdet sendes til web-tjeneren i form av en POST-operasjon, og dataene vil hentes ut av datastrømmen med funksjonen parse_parameters(). Koden til denne funksjonen, og hjelpefunksjonen unescape() er vist nedenfor:

# Parse a http query string and return named values as a dictionary
def parse_parameters(par_string):
    params = dict()
    for element in par_string.split('&'):
        nv = element.split('=')
        # Unescape %hex coded characters
        unesc = unescape(nv[1])
        params[nv[0]] = unesc
    return params

# Replace e.g. %2f with /
def unescape(s):
    b = bytes(s,'utf-8')
    ix = 0
    ix = b.find(b'%',ix)
    while (ix != -1):
        hx = b[ix+1:ix+3]
        bt = bytes.fromhex(hx)
        b = b.replace(b[ix:ix+3],bt)
        ix = b.find(b'%',ix+1)
    return b.decode()
Resultatet av å trykke “Save” er en POST-operasjon som har dette bildet som resultat.

Jeg tror ikke det er nødvendig med en full beskrivelse av hvordan data fra HTML-skjemaer blir overført, men forsøk gjerne å sette inn noen print-setninger for å studere koden mens den kjører.

Slik programkoden er skrevet vil dataene fra det utfylte skjemaet lagres på en fil i JSON-format. Siden, når mikrokontrolleren starter på nytt, vil den lese innholdet av denne filen og bygge opp en tabell over kjente nettverk som brukes slik det ble beskrevet innledningsvis i denne artikkelen.

Hvilken adresse skal brukes for å kople seg til konfigurasjonstjeneren?

Når din maskin kopler seg til kontrollerens WLAN, bruk disse adressene:

  • Nettverkets navn er “RPico-config” og passordet er “123456789”. Med mindre du skriver noe annet inn i programkoden.
  • Maskinen din får tildelt en IP-adresse ved tilkoplingen, den kan du se i Windows med ipconfig-kommandoen (se bildet nedenfor). Se etter IPv4-adressen som er tildelt ditt trådløse adapter. Noter også IPv4-adressen til “Default Gateway”. Den er sannsynligvis 192.168.4.1, så eksemplet som følger vil vise denne adressen.
  • I web-leseren får du nå kontakt med konfigurasjonstjeneren med URL’en http://192.168.4.1/
Utskriften av “ipconfig” viser at kontrollerens IP-adresse er 192.168.4.1

Single-threaded server

Slik denne programkoden er laget ser du lett at den tar i mot en forbindelse og forespørsel med sock.accept(), og behandler denne forespørselen helt og fullt før neste forbindelse blir akseptert. Dette prinsippet kalles single-threading, og medfører at web-tjeneren gir dårlig ytelse når flere web-lesere bruker tjeneren samtidig. Det har liten betydning i dette tilfellet, fordi det WLAN’et som skapes av mikrokontrolleren kun tillater et par samtidige forbindelser.

Noen kommentarer til det fulle programmet

I denne artikkelen har jeg vist forenklede utdrag av et fullt program, som kan lastes ned herfra. Det kan kjøres slik som beskrevet, men koplingen til et anvenderprogram som vil bruke WLAN-adapteret for sin kommunikasjon må settes inn i programmets linje 210.

Ved å studere programmet i sin helhet kan du få bedre inntrykk av hvordan de enkeltdelene som tidligere er beskrevet, blir knyttet sammen i en samlet kontrollflyt. Dessuten er programmet utformet objekt-orientert, som er en nyttig programmeringsteknikk å beherske.

Programkoden er laget for Raspberry Pico W. Dersom du vil bruke den på en ESP32 krever 5 små endringer i koden, disse er angitt i kommentarsetninger. Søk etter “ESP32” og du finner dem. Send meg en melding om du ikke får det til.

Dersom det er aktuelt å konfigurere andre programparametre, f.eks. nettadresser til bestemte tjenester i Internet, kan de gis verdier med et tilsvarende web-grensesnitt. Jeg anbefaler ikke å bruke denne konfigurasjonskoden til andre formål enn for WLAN-opplysninger. Lag heller en separat web-tjener for de andre parametrene, laget på tilsvarende vis. Python-klassen web-config kan brukes til dette uten endringer.

Heller bruke ferdige biblioteker?

Det finnes ferdige Python-moduler på nettet som gjør deler av denne jobben for oss. Når jeg har valgt å presentere en metode som bare bruker de grunnleggende standardbibliotekene i Python, er det for (1) at denne prosessen skal skape læring, ikke bare en løsning, og (2) standardbiblioteker er skrevet for mange slags anvendelser og tar mye mer plass i minnet enn en skreddesydd løsning slik som denne.

Demonstrasjonsvideo

Her vises hvordan denne konfigurasjonsmetoden fungerer i praksis.

Full programlisting

Den fulle programlistingen for dette eksperimentet kan lastes ned fra denne linken.