Mottak og bruk av ADS-B signaler fra fly

 31 total views

I denne artikkelen beskriver jeg hvordan det er mulig å ta imot radiosignaler fra fly med ADS-B meldinger, og hvilken informasjon disse meldingene inneholder.

(c) Anders Fongen, februar 2024

Fly i normal trafikk sender ut radiosignaler med meldinger om flight-id., posisjon, høyde, fart og retning. Dette skjer i et meldingsformat som kalles ADS-B og er en digital transmisjon på frekvensen 1080 MHz. Dette signalet brukes av luftkontrollen som supplement til radar, og tjenester som Flightradar24 benytter også i stor grad disse radiosignalene.

Kan du ha interesse av å bruke disse signalene? Selv har jeg planer om å bruke fly som reflektorer av radiosignaler, og på den måten utvide rekkevidden av VHF/UHF-signaler over horisonten. Da trenger jeg å vite hvordan antennen skal stilles for å peke mot flyet, og jeg må derfor vite hvor det befinner seg til enhver tid.

Hva du trenger for å motta ADS-B

  • En SDR mottaker av typen RTL-SDR. De finnes på Ebay fra en hundrelapp og oppover. De selges ofte som utstyr for å kunne motta digital TV på PC, men egner seg til mange andre formål også.
  • En Linux-maskin, gjerne en Raspberry Pi. Har du en gammel PC stående som ikke brukes og som har en USB port og nettverksadapter (WLAN eller Ethernet), så bruk gjerne den. Du vil trenge litt Linux-erfaring for å gjennomføre det som blir beskrevet her.
  • Noe gratis programvare som lastes ned fra Internet, bl.a. dump1090.

Oversikt over konfigurasjonen som skal vises her

  1. Radiosignalet fra fly mottas av SDR-dongelen som er koplet til en Linux-maskinens USB-port.
  2. Fra SDR går et “råsignal” (I/Q-signalet) til Linux-maskinen (grønn pil), hvor programmet dump1090 dekoder signalet og henter ut meldingene. Disse meldingene, gjengitt på såkalt SBS-1 format, sendes ut fra Linux-maskinen med TCP-protokoll på port 30003 (rød pil).
  3. Meldingene på SBS-1 format kan leses med et program som man kan skrive selv etter hvilke ønsker man har. Jeg har valgt å lage et Python-program for en Raspberry Pico W mikrokontroller som henter ut flyenes løpende posisjon (lengdegrad, breddegrad, høyde) og regne ut retningen til en direktiv antenne som skal følge flyet.
  4. Raspberry Pico lager elektriske signaler for å styre to motorer på et enkel platform (blå pil) for å stille inn ønsket retning og vinkel (azimuth og elevation).

Konfigurasjon av Raspberry pi (2)

Komponent (2) skal ta imot I/Q-signaler fra SDR-dongelen, dekode ADS-B meldingene som overføres, og presentere data på SBS-1 format gjennom en TCP-forbindelse. For dette trenges en enkel Linux-maskin som må konfigureres for dette formålet. Den følgende beskrivelsen tar utgangspunkt i en Raspberry Pi v.3, men fremgangsmåten blir nokså lik også for andre type Linux. Sørg for at Linux-versjonen er nylig oppdatert.

Følgende kommandoer kan gis for å installere drivere og dump1090-programmet:

$ sudo apt update
$ sudo apt upgrade
$ sudo apt install librtlsdr-dev
$ sudo apt install rtl-sdr
$ git clone https://github.com/antirez/dump1090.git dump1090
$ cd dump1090
$ make

Evt. feilmeldinger følges opp og rettes. På katalogen dump1090 ligger nå programmet dump1090 som startes med kommandoen ./dump1090 --net.

SDR-dongelen kan nå koples til maskinens USB-port og en egnet antenne, slik som vist på figuren. Deretter kan dump1090-programmet startes. Det gir ingen løpende utdata til skjermen, man overvåker utdata på metoden som er vist nedenfor.

Utdata fra dump1090

Som vist på figuren over har vi satt opp dump1090 til å sende ut data i SBS-1 format til TCP-port 30003. Med programmet netcat kan vi ta en titt på hvordan dataene ser ut, og gjøre oss opp en mening om hvordan de kan behandles. Ta en titt på denne videoen:

Det er i hovedsak opplysninger om posisjon og flight-id som er at interesse for denne anvendelsen, men data om retning og fart er også tilgjengelig.

Analyse av SBS-1 data og beregning av antenneretning (3)

Figuren over viser hvordan utdata fra dump1090 overføres via TCP-protokoll til alle som ønsker det (rød pil). Figuren viser derfor at flere ulike anvendelser kan hente disse dataene fra samme dump1090-instans.

I det eksperimentet som beskrives her, skjer denne bearbeidingen i en Raspberry Pico W, som er en mikrokontroller med WLAN-grensesnitt og som kan programmeres i Micropython. Programkoden i denne mikrokontrolleren kan deles i tre deler:

  1. Et hovedprogram som etablerer en TCP-forbindelse til dump1090-noden (2), kaller så på Python-moduler for analyse, beregning og antennestyring
  2. En modul for å analysere SBS-1 melding for posisjon og identifikasjon av fly, og for å beregne antenneretning til flyet.
  3. En modul som mottar opplysninger om antenneretning og styrer servomotorene på antenneplattformen i henhold til disse opplysningene.

1 – Hovedprogram

Hovedprogrammet initialiserer TCP-forbindelsen, WLAN-adapteret, I2C-bussen og oppretter nødvendige objekter for de påfølgende operasjonene. Slik ser programkoden ut:

from wlan_config import wlan_config # WLAN configuration module
from AzElPlatform import AzElPlatform # Az-El antenna control
from AzElCalculator import AzElCalculator
import socket
from machine import Pin, I2C
import ssd1306

# using default address 0x3C
# TODO change GPIO numbers if necessary
i2c = I2C(0,sda=Pin(8), scl=Pin(9))
display = ssd1306.SSD1306_I2C(128, 64, i2c)

# Configure wlan
wlan_config()

# Make network tocket and connect to ads-b listener
sock = socket.socket()

# TODO change IP address to the actual dump1090 node
sock.connect(("192.168.2.11",30003))

# Configure bearing calculator (lat,lon,alt)
# TODO Replace numbers with your own location
azelcalc = AzElCalculator(61.1353,10.4419,253)

# Initialize platform motor control
# TODO change numbers to your own use of GPIO ports
azelplat = AzElPlatform(16,17)

# Start reading ads-b messages
while True:
    message = sock.readline().decode()
    result = azelcalc.analyzeMessage(message)
    if result != None:
        print(result) # For debugging only
        
        # If flightId is present, send to OLED display
        flightId = result['flightid']
        if flightId != None:
            distance = int(result['distance']/1000)
            display.fill(0)
            display.text('%s-%d km'%(flightId,distance),5,8)
        else:
            display.fill(0)
        display.show()
        # Find azimuth and elevation from result
        azimuth = float(result['azimuth'])
        elevation = float(result['elevation'])
        # Point antenna in that direction
        azelplat.setDirection(int(azimuth),int(elevation))

Deler av programkoden styrer et OLED-display for å vise flight-id og avstand til fly som blir fulgt av antennen. Disse kan kommenteres bort om dette er uten interesse.

Modulen wlan_config som importeres her er presentert i en annen blogg-artikkel: WLAN-konfigurasjon i Raspberry Pico W. Der vises hvordan wlan-adapteret kan konfigureres uten å skrive nettverkspassordet inn i koden.

2 – Analyse og behandling av SBS-1 utdata, beregningav retning

Metoden analyzeMessage() i modulen AzElCalculator mottar en SBS-1 melding som parameter og henter ut flight-id og posisjonsdata fra den. Programmet beregner så retningen for en antenne som skal følge dette flyet, i form av horisontal og vertikal vinkel (Azimuth og Elevation). For denne beregningen brukes Python-modulen AltAzRange som kan hentes fra denne adressen. Programkoden for AzElCalculator ser slik ut (merk at noen av linjene er lange og er brukket i flere deler):

# Receive SBS-1 message, find position of aircraft and
# calculate azimuth and elevation for the direction to it
from AltAzRange import AltAzimuthRange
class AzElCalculator:
    def __init__(self,myLatitude,myLongitude,myAltitude):
        AltAzimuthRange.default_observer(myLatitude,myLongitude,myAltitude)
        self.airplane=AltAzimuthRange()
        self.flightId = [(None,None),(None,None),(None,None),(None,None),(None,None)]
        
    def analyzeMessage(self,messageLine):
        elements = messageLine.split(",")
        
        # ES Airborne Position Message
        if (elements[1] == '3'):
            icao = elements[4]
            altitude = elements[11]
            if altitude == '': return None
            latitude = elements[14]
            if latitude == '': return None
            longitude = elements[15]
            if longitude == '': return None
            self.airplane.target(float(latitude),float(longitude),float(altitude)*0.3048)
            # Observe that the position is not returned, only the direction
            result = self.airplane.calculate()
            result['icao'] = icao
            result['flightid'] = self.getFlightId(icao)
            return result
        # ES Identification and Category
        elif (elements[1] == '1'):
            icao = elements[4]
            flightid = elements[10].strip()
            if flightid == '': return None
            self.storeFlightId(icao,flightid)
            return None
        # ES Airborne Velocity Message
        elif (elements[1] == '4'):
            result = dict()
            result['icao'] = elements[4]
            result['groundspeed'] = elements[12]
            result['track'] = elements[13]
            result['flightid'] = self.getFlightId(result['icao'])
            # We can return the "result" object, but choose not to
            # return result # is this data is needed
            return None

    def storeFlightId(self,icao,flightId):
        if self.getFlightId(icao) == None:
            # Add flight id to list
            self.flightId.insert(0,(icao,flightId))
            self.flightId.pop() # Kill the oldest entry
    
    def getFlightId(self,icao):
        for (i,f) in self.flightId:
            if i == icao: return f
        return None

3- Styre en direktiv antenne mot flyet

Oppgaven med programmering av to servomotorer for å styre en antenne i to akser (horisontalt og vertikalt) skjer med modulen AzElPlatform som er presentert i en tidligere blogg-artikkel: Styring av servomotor fra Raspberry Pico og Micropython. For ordens skyld gjengis programkoden er:

# Python class to control a simple Azimuth-Elevation
# platform. It uses two SG90 servo motors for the two
# axes. Since they only rotate 180 degrees, the 180
# degree range of the elevation rotor is used to cover
# the left half (180-359 degrees) of the azimuth

from machine import Pin, PWM
class AzElPlatform:

    def __init__(self,azrotor, elrotor):
        self.azport = PWM(Pin(azrotor))
        self.elport = PWM(Pin(elrotor))

        self.azport.freq(50)
        self.elport.freq(50)

    def setDirection(self,az,el): # Angle in degrees
        # Check parameters: 0-359 and 0-90 allowed
        if not az in range(0,360): return
        if not el in range(0,91): return
        if az>180:
            az = az-180
            el = 180-el # Bend elevation backwards for left half
        # Experimentally established values for
        # Calculation of duty cycles corresponding
        # to rotor angles
        dutyAz = 7800 - az * 6600/180
        dutyEl = el * 7000/180 + 1200

        self.azport.duty_u16(int(dutyAz))
        self.elport.duty_u16(int(dutyEl))
Raspberry Pico W med OLED display viser flight-id NOZ191 med avstand 78 km og følger dette flyet med en direktiv antenne.

Oppsummering

I denne blogg-artikkelen har jeg presentert noen aktiviteter som inkluderer bruk av Software Defined Radio, avansert digital dekoding av radiosignaler fra fly, noe programmering og beregninger på posisjonsdata, og styring av servomotorer for antenner.

Mange av disse emnene vil være av interesse i andre anvendelser enn akkurat den som her er presentert. Bruk gjerne delløsningene i denne artikkelen til dine egne prosjekter, og send meg en melding om resultatene blir interessante.

Styring av servomotor fra Raspberry Pico og Micropython

 39 total views

Her følger en enkel veiledning i hvordan man kan styre en servomotor fra Raspberry Pico. En servomotor lar deg stille rotoren i bestemte posisjoner, og kan brukes til å f.eks. styre et kamera eller en antenne.

(c) Anders Fongen, februar 2024

Innledning

Akslingen på en servomotor skal ikke snurre rundt og rundt, men stille seg i besteme posisjoner. Den egner seg derfor til å stille roret på en modellbåt, eller la et kamera eller en antenne følge et bevegelig objekt. Servomotoren SG90 er billig og lett, bruker lite strøm og er relativt sterk.

Servomotoren SG90

Vi skal i den følgende teksten viser hvordan vi kan styre SG90 fra Micropython på en Raspberry Pico, men løsningen blir veldig lik for andre kontrollere, f.eks. en ESP32.

Pulsbreddemodulasjon

SG90 har tre tilkoplingsledninger. To er for strømtilførsel (spenning+ og jord-), den tredje er for å stille rotoren i ønsket posisjon. Måten dette gjøres på er med såkalt pulsbreddemodulasjon.

En firkantpuls med en bestemt frekvens veksler mellom to spenningsnivåer, f.eks. 5 volt og 0 volt et visst antall ganger i sekundet, men ikke nødvendigvis med like lang tid på hver av spenningsnivåene. Den brøkdelen av tiden hvor spenningen er høy (f.eks. 5 volt) kaller vi duty cycle (av og til kalt arbeidssyklus på norsk), og illustrasjonen nedenfor viser tre firkantpulser med henholdsvis 50%, 75% og 25% duty cycle.

Tre ulike verdier av duty cycle for en firkantpuls. Kilde: wikipedia.org

PWM-signal i Micropython

Pulsbreddemodulasjon, heretter kalt PWM, er innebygget i Micropython og det er lett å sette en GPIO-port til å sende en firkantpuls med varierende duty cycle. Et eksempel på programsetninger som skaper en slik firkantpuls ser slik ut:

# Testkode for WPM-signal
from machine import PWM, Pin
import time
gpioport = 28 # Settes etter behov
pwmpin = PWM(Pin(gpioport, Pin.OUT))
pwmpin.freq(500) 

while True:
    for x in range(0,5):
        pwmpin.duty_u16(16384*x)
        time.sleep(1)

Som det fremgår av programkoden over styres duty cycle med metodekallet duty_u16(verdi), hvor parameterverdien kan variere mellom 0 og 65353 (som er den høyeste verdien for et usignert 16-bits tall). Under vises hvordan det avgitte signalet ser ut på et oscilloskop:

PWM-signal med varierende duty cycle, vist på et oscilloskop.

Oppkopling av SG90

Som tidligere nevnt, SG90 har tre tilkoplingsledninger: Rød ledning for forsyningsspenning koples til pin 40 (VBUS), evt. 39 (VSYS) på Raspberry Pico. Brun ledning koples til jord, f.eks. på pin 38. Orange ledning skal ha PWM-signalet, og du må velge en GPIO-port til dette formålet. I programeksemplet over er GPIO nr. 28 valgt, den finnes på pin 34. En såkalt “pinout” for Raspberry Pico er vist under.

Pinout for Raspberry Pico

Her følger en kort video for å vise effekten av varierende duty-cycle i PWM-signalet:

Her vises hvordan Raspberry Pico koples til en SG90 servomotor

Kalibrering av duty cycle

Når oppkoplingen er gjort kan du teste at motoren reagerer på ulike duty cycle-verdier ved å stille seg i en bestemt posisjon. Nå kan du kalibrere disse verdiene, ved at du noterer hvilke verdier som tilsvarer de posisjonene du ønsker å stille rotoren i. Min erfaring er at forholdet mellom rotorvinkel og duty cycle-verdier er noenlunde lineært, så du kan skrive kode som interpolerer mellom de observerte verdiene. Programmet som kjøres i videoen ovenfor ser slik ut:

from machine import Pin, PWM
import time
gpioport = 28 # Settes etter behov
pwmpin = PWM(Pin(gpioport, Pin.OUT))
pwmpin.freq(50) # Firkanpulsen har 50 Hz
for x in range(2000,7000,100):
    pwmpin.duty_u16(x)
    time.sleep(0.2)

I dette bestemte eksperimentet finner jeg ut at rotoren beveger seg en halv omdreining (180 grader) med duty cycle-verdier mellom 1150 og 7600, altså med et intervall på 7600-1150=6450. Dersom jeg antar at det er et lineært forhold mellom rotorvinkelen og duty cycle-verdier kan jeg lage en funksjon for å stille rotoren i en bestemt vinkel med denne programfunksjonen:

def setRotorAngle(pwmpin,degrees):
    dcvalue = int(degrees*6450/180+1150)
    pwmpin.duty_u16(dcvalue)

En forbedring av programmet ovenfor er derfor slik:

# Testkode for WPM-signal
from machine import Pin, PWM
import time

def setRotorAngle(pwmpin,degrees):
    dcvalue = int(degrees*6450/180+1150)
    # For rotating clockwise, change the previous line to:
    #dcvalue = int(7600-degrees*6450/180)
    pwmpin.duty_u16(dcvalue)

gpioport = 28 # Settes etter behov
pwmpin = PWM(Pin(gpioport, Pin.OUT))
pwmpin.freq(50) # Firkantpulsen har 50 Hz
for x in range(0,181,10):
    setRotorAngle(pwmpin,x)
    time.sleep(0.8)

Her beveger rotoren seg mot klokken med økende gradtall, om du ønsker bevegelsen slik gradtallet brukes på et kompass, altså med klokken må dcvalue kalkuleres slik:

dcvalue = int(7600-degrees*6450/180)

Eksempel på kamerastyring i to akser

En anvendelse av SG90 som jeg har selv har fattet interesse for er å styre et kamera eller en antenne i to akser for å kunne peke på et punkt oppe i luften eller verdensrommet. Et punkt på himmelrommet kan beskrives med to vinkler: Azimuth, som er vinkelen i horisontalplanet relativt til nord, og Elevation, som angir den vertikale vinkelen relativt til horisonten (les her for flere detaljer). En slik innretning kalles en Azimuth-Elevation rotor. Disse finnes i alle størrelser, er bygget for utendørs bruk og er ganske dyre. For eksperimentformål finnes det enkle konstruksjoner som benytter to SG90-motorer for det samme formålet. Søk etter “2 Axis Pan Tilt Mounting Kit” på Ebay: Her vises et bilde av en slik enhet:

Enkel Azimuth Elevation rotor basert på to SG90 motorer

For å styre denne enheten til et punkt på himmelen må du kople begge motorene til Raspberry Pico med felles spenning og jord, men med de orange ledningene til hver sin GPIO-port som du styrer med programsetningene vist ovenfor.

Rekkevidden til en SG90-motor er 180 grader. Skal du rekke over hele himmelkulen trenger du en rekkevidde for Azimuth på 360 grader, og 90 grader for Elevation. Et lite knep kan allikevel gi deg full dekning av himmelkulen over deg:

  1. Dersom Azimuth er 0-179 grader, skal den horisontale rotoren stilles til denne vinkelen, og den vertikale rotoren (Elevation) stilles til den ønskede vinkelen 0-90 grader.
  2. Dersom Azimuth er 180-359 grader, skal den horisontale rotoren stilles til Azimuth-180 grader, og den vertikale skal stilles til 180-Elevation grader. Dvs, at den vertikale rotoren legger seg “bakover” for å dekke den venstre delen av kompassrosen. Dersom det er et kamera på denne plattformen må du ta hensyn til at bildene da blir opp-ned.

Når du skriver kode for en slik innretning må du dessuten ta hensyn til at azimuth-rotoren går “mot klokka” med økende duty cycle-verdi, og du må snu litt om på regnestykket, som vist over.

Python-klasse for styring av en Az-El plattform

Nå setter vi sammen tidligere detaljer, inkludert observerte verdier for 0 og 180 graders rotasjon, til en Python-klasse som styrer begge aksene under ett:

# Python class to control a simple Azimuth-Elevation
# platform. It uses two SG90 servo motors for the two
# axes. Since they only rotate 180 degrees, the 180
# degree range of the elevation rotor is used to cover
# the left half (180-359 degrees) of the azimuth

from machine import Pin, PWM
class AzElPlatform:

    def __init__(self,azrotor, elrotor):
        self.azport = PWM(Pin(azrotor))
        self.elport = PWM(Pin(elrotor))

        self.azport.freq(50)
        self.elport.freq(50)

    def setDirection(self,az,el): # Angle in degrees
        # Check parameters: 0-359 and 0-90 allowed
        if not az in range(0,360): return
        if not el in range(0,91): return
        if az>180:
            az = az-180
            el = 180-el # Bend elevation backwards for left half
        # Experimentally established values for
        # Calculation of duty cycles corresponding
        # to rotor angles
        dutyAz = 7800 - az * 6600/180
        #dutyAz = ((180-az)/180*6600) + 1200
        dutyEl = el * 7000/180 + 1200

        self.azport.duty_u16(int(dutyAz))
        self.elport.duty_u16(int(dutyEl))

Demonstrasjonsvideo

Her følger en video hvor denne Python-klassen blir demonstrert med følgende testprogram:

from AzElPlatform import AzElPlatform
import time
azel = AzElPlatform(16,17) # GPIO-porter for Az og El
el = 20
for az in range(90,271,10):
    azel.setDirection(az,el)
    time.sleep(0.5)

Denne programkoden beveger plattformen fra 90 til 270 grader i horisontalplanet, og må derfor skifte mellom innstilling nr.1 og 2 fra diskusjonen ovenfor. Legg derfor merke til på videoen hvordan Elevation-rotoren snur seg rundt samtidig som Azimuth-rotoren vrir seg en halv omdreining. Videoen demonstrerer altså hvordan vi dekker hele himmelkuppelen med to rotorer med rekkevidde 180 grader.

Demonstrasjon av Azimuth-Elevation platform med to SG90 servomotorer.

SSL-protokoll for web-applikasjoner på web.py

 75 total views

SSL (TLS) -protokoll er svært vanlig i bruk for de fleste web-stedene i Internet. Om du lager en web-applikasjon bør du sette deg inn i hvordan du konfigurerer den for bruk med SSL. Om du bruker web.py for å lage web-applikasjoner i Python, får du en veiledning i denne artikkelen.

(c) 16.01.2024 Anders Fongen

Innledning

Det første leddet i en web-adresse (URL) angir hvilken protokoll som brukes. Oftest står det https:// der, sjeldnere http://. Den første formen forteller web-leseren at forbindelsen til web-tjeneren skal bruke SSL-protokoll (også kalt TLS) for å beskytte mot avlytting og forfalskning.

Jeg gir ingen detaljert beskrivelse av SSL her, fordi det finnes andre steder på Internet, og fordi det ikke er av vesentlig betydning å kjenne detaljene i denne protokollen.

SSL gir kryptert overføring av data, slik at andre ikke kan lese innholdet av dataoverføringen om de lytter til kanalen som brukes. IP-adresser til sender og mottaker er derimot ikke kryptert, og kan oppfattes gjennom avlytting.

SSL gir også autentisering av én eller begge parter i dataoverføringen, slik at det er vanskelig å utgi seg for å hete f.eks. www.vg.no uten å presentere et digitalt nøkkelsertifikat som attesterer at det er web-tjenerens virkelige navn.

Mindre brukt, men allikevel viktig, er at SSL også kan autentisere web-leseren ved hjelp av et digitalt sertifikat, som attesterer at brukeren av web-leseren heter f.eks. Anders Fongen. Denne funksjonen i SSL kan erstatte tradisjonell innlogging med brukernavn og passord.

Bruk av sertifikater og nøkler, og hvordan sertifikater kan validere andre sertifikater, er et noe omfattende tema som jeg heller vil behandle i en egen artikkel. Begrepet kalles Public Key Infrastructure, og det finnes mye informasjon om dette på Internet.

Jeg har i en tidligere artikkel vist en web-applikasjon bygget på web.py-rammeverket og programmert i Python. Denne artikkelen vil videreføre det samme fokus på rammeverk og programmeringsspråk, og vise hvordan SSL-protokoll konfigureres i en web-applikasjon bygget på web.py.

Nøkkelpar og sertifikater

For å konfigurere en web-tjener for SSL-protokoll trenger den et nøkkelpar. Det kan utstedes av en kommersiell leverandør, noe som koster en del (fra 50€/år og oppover). Med et slikt nøkkelpar kan din web-tjener autentisere seg overfor alle klienter i Internet. Det er derimot ikke praktisk mulig å be alle brukerne av web-applikasjonen din om å betale et slikt beløp for å oppnå klient-autentisering.

For web -applikasjoner som skal brukes innenfor en gruppe, firma, forening etc., er det hensiktsmessig å selv stå for utstedelsen av nøkkelpar og sertifikater. Alle som skal bruke den aktuelle web-tjeneren må da installere et sertifikat og et nøkkelpar i sin egen web-leser, og jeg skal vise hvordan dette gjøres i Google Chrome. For Firefox er fremgangsmåten ganske lik.

Bruk helst en Linux-maskin som web-tjener, da fungerer den presenterte metoden best. Her følger et skall-skript som bruker programmet openssl til å lage et rotsertifikat (også kalt CA-sertifikat), og nøkkelpar for web-tjeneren og et nøkkelpar for én klient. Skallskriptet er ment som et eksempel som du modifiseres etter eget ønske.

# Generate keys and certificates for OpenVPN
openssl genrsa -out ca_key.pem 2048
openssl genrsa -out server_key.pem 2048
openssl genrsa -out client_key.pem 2048
#  Generate CA certificate
openssl req -new -x509 -days 1000 -key ca_key.pem -out ca_cert.pem \
    -outform PEM -subj '/C=NO/O=FONGEN/CN=CA'
#  Generate Server certificate
openssl req -new -key server_key.pem -out server_csr.pem \
    -subj '/C=NO/O=FONGEN/CN=SSLSERVER' \
    -addext 'subjectAltName = DNS:ssl.fongen.no'
openssl x509 -req -in server_csr.pem -CA ca_cert.pem -CAkey ca_key.pem \
    -CAcreateserial -addtrust serverAuth -out server_cert.pem \
    -days 1000 -sha256 -copy_extensions copy
openssl verify -CAfile ca_cert.pem server_cert.pem
#  Generate Client certificate
openssl req -new -key client_key.pem -out client_csr.pem \
    -subj '/C=NO/O=FONGEN/CN=AndersFongen'
openssl x509 -req -in client_csr.pem -CA ca_cert.pem -CAkey ca_key.pem \
    -CAcreateserial -addtrust clientAuth -out client_cert.pem \
    -days 1000 -sha256
openssl pkcs12 -export -inkey client_key.pem -in client_cert.pem \
    -out client_keypair.p12
openssl verify -CAfile ca_cert.pem client_cert.pem
#  Remove unneccesary files
rm *_csr.pem

Når dette skallskriptet kjøres (du blir bedt om å skrive et passord underveis, det skal brukes når du installerer klientens nøkkel i web-leseren) skapes det følgende filer på samme katalog:

  • ca_key.pem – Dette er utstederens private nøkkel og skal aldri kopieres eller sendes til andre
  • ca_cert.pem – Dette er utstederens nøkkelsertifikat som skal installeres både i web-tjeneren og i web-leseren
  • server_key.pem, server_cert.pem – Dette er web-tjenerens private nøkkel og nøkkelsertifikat som som skal installeres der, men ikke gis til web-leserne
  • client_keypair.p12 – Dette er nøkkelen og sertifikatet til én klient, beskyttet med det passordet du nettopp skrev inn.

Installasjon av nøkler og sertifikater i Google Chrome

  • I Google Chrome, klikk på knappen med “tre prikker i vertikal ordning” øverst til høyre
  • Velg så “Settings” fra menyen
  • I listen til venstre, klikke “Privacy and Security”
  • Klikk deretter på “Security”, så på “Manage Certificates”
  • Klikk på fanen “Authorities”, og så på knappen “Import”
  • Fra den fildialogen du nå får se, velg filen “ca_cert.pem”
  • Etter at import-jobben er fullført skal du nå se dette rotsertifikatet i listen. Navnet som vises er “org-FONGEN” dersom du ikke har endret dette i skallskriptet (noe du bør gjøre).

Nå skal klientsertifikatet og nøkkelen installeres.

  • Klikk på “Your Certificates” og deretter på knappen “Import”.
  • Fra fildialogen, velg “client_keypair.p12”.
  • Du må nå skrive inn passordet som du valgte da sertifikatet ble laget for litt siden.
  • Nå skal sertifikatet vises i listen med det navnet som du har valgt å bruke.

Installasjon av nøkler og sertifikater i web-tjeneren

Nå er web-leseren klargjort, og vi skal sette opp web-tjeneren med sine nøkler og sertifikater:

from cheroot.server import HTTPServer
from cheroot.ssl.builtin import BuiltinSSLAdapter
import web, ssl

ssl_adapter = BuiltinSSLAdapter(
	certificate = 'server_cert.pem',
	private_key = 'server_key.pem',
	certificate_chain = 'ca_cert.pem')

#ssl_adapter.context.verify_mode = ssl.CERT_REQUIRED
ssl_adapter.context.verify_mode = ssl.CERT_OPTIONAL
HTTPServer.ssl_adapter = ssl_adapter

urls = ("/.*", "hello")
app = web.application(urls, globals())

class hello:
	def GET(self):
		try:
			cn = web.ctx.env['SSL_CLIENT_S_DN_CN']
		except:
			return "No client certificate available"

		return "Hello, " + cn

if __name__ == "__main__":
	app.run()

Jeg har laget en demonstrasjonsvideo som tar deg gjennom disse stegene enkeltvis, og også hvordan web-tjeneren startes og testes. Du kan se den her (beklager lavt lydnivå):

Demonstrasjon av SSL-autentisering i web.py

Takeaway

Avslutningsvis i denne artikkelen vil jeg påpeke at en del av dette stoffet også er til nytte ved bruk av andre web-rammeverk enn web.py. De fleste web-tjenere har støtte for klientautentisering med SSL, og de konfigurerer dette på forskjellige måter. Men laging av sertifikater og nøkler, samt installasjonen av klientnøkler i web-leseren, er de samme operasjonene, derfor kommer det presenterte skallskriptet til nytte.

CMS – Et lettvekts samarbeidssystem for mindre brukergrupper

 62 total views,  1 views today

Visst har vi Teams og andre skybaserte systemer for å samarbeide gjennom nettet, men vi har også behov for noe enklere for små grupper med veldefinerte samarbeidsmodeller. Vi skal vise et slikt system her, og påpeke hvilke fordeler som følger av et kompakt og fokusert system.

CMS-demonstratoren kan kjøres fra adressen http://hos.fongen.no/cms
Kildekoden i arkivform kan lastes ned fra denne lenken

(c) Anders Fongen, 2024

Innledning

Se for deg en gruppe med noen titalls medlemmer, som skal samarbeide om en fokusert og veldefinert oppgave. F.eks planlegging og utforming av et undervisningsfag, en leteoperasjon etter savnede personer, en oppgave knyttet til kartlegging og etterretning, skriving av sakprosa, en chat.

Slike oppgaver er kjennetegnet ved visse egenskaper og behov:

  • Enkle behov for typografisk formatering av tekst
  • Behov for enkelt å opprette nye dokumenter, og endre på eksisterende
  • Høy tillit mellom deltakerne, med felles fokus og nødvendig kompetanse
  • Behov for raskt å gjøre seg kjent med andres bidrag til en aktivitet
  • Enkelt å fjerne utdatert, irrelevant og distraherende innhold

For slike oppgaver er det kanskje fristende å ta frem et generisk samarbeidverktøy med stort behov for styring og konfigurasjon, med høye kapasitetskrav til utstyr og kommunikasjonskanaler. F.eks. Teams, DocuLive, Itslearning, Canvas, Google Disk. Disse dekker muligens behovet, men ikke nødvendigvis på den mest effektive måte, de kan nelig fremstå som en “skyte spurv med kanoner”-løsning. Dessuten trenger de en internett-forbindelse til en skytjeneste, bare så det er sagt.

Vi skal i denne artikkelen presentere et enkelt samarbeidssystem kalt CMS – Content Management System. CMS kjennetegnes ved følgende egenskaper:

  • Minimalt med programkode, < 300 linjer i Python
  • Bruker markdown syntaks for tekstformatering
  • Web brukergrensesnitt (krever kun en web-leser)
  • Dokumentlås som virker
  • Enkel mekanisme for adgangskontroll
  • Dokumenter kan “oppfriskes” automatisk av web-leseren
  • Dokumenter kan opprettes og redigeres gjennom MQTT-protokoll
  • OpenSource, enkel kode for enkel utvidelse og endring

La oss i resten av dette dokumentet presentere disse egenskapene enkeltvis, og forklare hvordan de støtter aktuelle brukstilfeller. En grundigere presentasjon ligger i selve CMS-systemets demonstrasjonstjener på http://hos.fongen.no/cms.

Agil utviklingsmetodikk og Wiki

Behovene som ble listet opp i innledningen minner mye om egenskaper ved moderne system- og programutvikling, kjent som “agile development”. Gå gjerne via denne lenken for å lese litt om “agile” metoder (“agil” har i mellomtiden rukket å bli et norsk ord med norsk uttale).

For å støtte agilt samarbeid utviklet Ward Cunningham i 1995 en dokumentlagringstjeneste kalt Wiki. Med en Wiki kan enhver bruker opprette, redigere og slette dokumenter, altså et fravær av adgangskontroll som kun fungerer i grupper med felles mål og metode. En Wiki kan skrives kompakt med kun noen titalls programlinjer, men uten en redaktør som kan jobbe som “husmor” er de fleste erfaringer den at dokumentsamlingen med tiden blir en uhåndterlig blanding av nyttige, feilaktige, overlappende og utdaterte dokumenter. Wiki-prinsippet finnes også i Wikipedia, men her er det ansatt husmødre som overvåker redigeringen og opprettelsen av nye sider.

Den positive erfaringen med Wiki-prinsippet at en “90%”-løsning, dvs. 90% av behovene til en samarbeidsgruppe, lar seg løse med enkel og kompakt programmering, mens en 99%-løsning er kanskje 100 ganger større og dyrere. Såkalt “feature creep” i systemspesifikasjonene, dvs. at stadig nye funksjonskrav blir lagt til underveis, er en svært dyr uvane, og en fokusert og nøktern “90%”-løsning kan gi mye bedre effekt av innsatsen.

Overordnet beskrivelse av CMS

Dette er en web-tjeneste programmert i Python over rammeverket web.py, og som lar en gruppe av brukere søke, opprette og redigere dokumenter direkte fra sin web-leser. Den skiller mellom “lese”- og “redigere”-brukere slik at kun sistnevnte kan endre innholdet av dokumentsamlingen.

Brukerkontroll

Skillet mellom de to brukergruppene (“lese” og “redigere”) kan gjøres på mange måter, men for å unngå egne tabeller over brukernavn og passord (vi har nok av passord å huske på allerede) har jeg testet ut to metoder: IP-adresser eller klient-sertifikater:

IP-adresser: Bare klienter med IP-adresser som brukes bak brannveggen gir redigeringstillatelse, f.eks. IP-adresser som begynner med 192.168.x.x. Klienter forespørsler fra Internet vil aldri bruke disse adressene. Klienter tilkoplet via Virtuelle Private Nett (VPN) vil derimot kunne bruke adresser som gir redigeringstillatelse.

Klient-sertifikater: Der hvor det benyttes TLS (https:…) protokoll mellom klienten og tjeneneren kan TLS også sørge for at klientmaskinen må fremvise et nøkkelsertifikat og demonstrere sin private nøkkel for å autentisere seg. Kun de klientene som gjennomfører autentisering får redigeringstillatelse, alle andre kun lesetillatelse. Bruk av klient-sertifikater har den fordel at brukerne blir identifisert individuelt og man kan loggføre den enkelte brukers aktiviteter.

Dokumentorganisering

Dokumentene i CMS er hierarkisk organisert omtrent slik som i et filsystem, men istedetfor filkataloger brukes “mor-dokumenter”. Ethvert dokument kan være “mor” til et antall underordnete dokumenter. De underordnete dokumentene med felles mor kalles “søsken”. Når et dokument vises på skjermen vil både de underordnede dokumentene og søsken-dokumentene vises på et sidepanel. CMS inneholder også en enkel søkefunksjon hvor det kan søkes etter ordforekomster i tittel og innhold.

Dokumentene i CMS ligger lagret i en database, ikke som separate filer. Det åpner for en rekke muligheter for å sortere og søke i dokumenter, etablere reasjoner mellom dokumenter, låse dokumenter under redigering osv. Det åpner også for muligheten for andre programmer å skrive direkte til databasen for f.eks. å opprette dokumenter automatisk. Dette er en viktig egenskap ved CMS som vi skal omtale senere.

Redigering med MarkDown

De fleste er vant til å se en ferdig layout når dokumentet redigeres, på den fom det vil bli presentert for en som leser det. Det finnes andre måter å redigere på hvor layout påføres dokumentet som instruksjoner i tekstformat. Hypertext Markup Language (HTML) er det mest kjente eksemplet, hvor f.eks. teksten “<h2>” ikke vises i web-leseren, men indikerer at påfølgende tekst skal vises som en overskrift. Latex er et velkjent system for fremstilling av vitenskapelige artikler som benytter det samme prinsippet.

En av fordelene med markup er at instruksjonene indikerer intensjonen til forfatteren, ikke nøyaktig hvordan utseendet skal bli. “<h2>” kan resultere i ulik utseende, men alltid i form som indikerer en overskrift. En fargeskriver kan gi overskriften en annen uforming enn en svart-hvitt skriver f.eks.

Markdown er et “slenguttrykk” for et markup-språk som produserer HTML-kodet innhold. Markdown er ofte et enkelt sett av instruksjoner i en kort og enkel form, som f.eks. _underlinjert_ eller **uthevet** skrift. Når dokumentet blir presentert vil disse kodene være erstattet av den ønskede layouten. Wiki-systemene har ofte benyttet en form for markdown ved redigering.

Den viktigste egenskapen ved markdown er derimot at et slikt manuskript føles enkelt og naturlig å lese, også uten omforming av instruksjonene. Her er noen eksempler på tekst skrevet med HTML, Latex og Markdown, for å vise at sistnevnte er lettest å lese. Et dokument som ser slik ut:

Kan skrives med henholdsvis HTML, Latex og Markdown på denne måten:

Det er lett å konstatere fra dette eksemplet at Markdown er lettere å skrive inn, og lettere å lese i manuskriptform. I andre enden finner vi Latex, med kraftige mekanismer for å organisere layout og struktur på dokumentet, men som også er tung å skrive inn og vanskelig å lese i manuskriptform.

Markdown kan behandles av programkode for å f.eks. fremstille HTML kode for fremvisning i en web-leser. Med enkle regler kan Markdown behandles med såkalt regulære uttrykk i mange programmeringsspråk, men det finnes også egne programbiblioteker og ferdige programmer som kan gjøre dette. Dette er “kjedelig” programmeringsarbeid og en jobb som er godt å slippe.

Programmet heter pandoc, og finnes i versjoner for Mac, Linux og Windows. Det finnes også et Python-bibliotek kalt pypandoc som tilbyr et utvalg av pandoc’s muligheter gjort tilgjengelig for et Python-program.

Låsing av dokumenter under redigering

Kan flere brukere redigere samme dokument samtidig? I prinsippet ja, og Google Drive tillater nettopp det, men da må de i fellesskap sørge for at dokumentets innhold forblir meningsfylt og sammenhengende. I de fleste tilfellene er det tilstrekkelig løsning å la kun én bruker redigere, og andre som også ønsker dette, må vente på tur. Microsoft Word har lenge hatt en slik “lås”, som åpnes først når dokumentet lukkes etter bruk.

Dette har aldri fungert godt! Alle som har redigert med Word eller Excel har opplevd at en kollega har gått hjem for dagen uten å lukke dokumentet, som da forblir låst inntil neste dag.

I CMS er det valgt en låsemekanisme som automatisk åpner seg etter en viss tid (5 minutter), eller når dokumentet lukkes etter redigering. Dette er et kompromiss: Det løser problemet med gjenglemte låser, men gir brukeren 5 minutter på å fullføre redigeringen (må da gå ut av redigeringen og inn igjen). Dette skjer ved et triks i databasen som også sikrer at flere ikke kan låse et dokument samtidig. Sjekk kildekoden for å se hvilken SQL-syntaks som brukes.

Overvåking av dokumenter

Alle dokumenter i databasen er påført en “refresh rate”, dvs. et tidsrom mellom hver gang dokumentet skal hentes på nytt av web-leseren. Om “refresh rate” er satt til 10 sekunder, vil endringer i dokumentet (gjort av andre) vises frem i alle web-lesere som ser på dette dokumentet senest etter 10 sekunder. Slik kan f.eks. en situasjonsrapport alltid bli vist med de siste oppdateringene uten at brukerne på gjøre en “reload” av dokumentet.

Denne mekanismen introduserer en nødvendig balanse mellom akseptabel forsinkelse i oppdateringen av andre klienter, og den nettverkstrafikken som oppstår knyttet til den stadige hentingen av dokumenttdata. Settes “refresh rate” til 0 skjer ingen slik oppdatering automatisk.

Bruk av bilder

Bilder kan settes inn i dokumentene som redigeres ved å skrive inn en instruksjon i den løpende teksten: <img src=Pictures/filnavn.jpg>. Dette er den vanlige HTML-kommandoen som brukes for dette formålet, og ekstra parametre for å justere størrelse kan også brukes.

Alle bilder legges på samme katalog (Pictures) på CMS-tjenerens filområde, og nye bilder kan lastes opp i PNG eller JPG-format ved å klikke på en knapp i det høyre sidepanelet. Pass på at bildene får unike filnavn, og bilder som ligger der kan inkluderes i mange dokumenter.

Automatiske dokumenter – Internet of things

Automatiske dokumenter betegner dokumenter som legges inn i databasen uten medvirkning fra brukerne av systemet. Disse dokumentene vises frem som andre dokumenter, og egner seg for å vise data fra miljøsensorer, web-kameraer, mashup fra internettkilder (værmelding, trafikkrapporter, børsdata) osv. Et automatisk dokument kombinert med bruk av “refresh rate” kan vise et situasjonsbilde eller en operasjonell status i tilnærmet sann tid, og oppdateres automatisk.

  • Dokumenter som legges inn av brukere kan ikke overskrives eller endres av denne automatiske prosessen.
  • Automatiske dokumenter kan ikke redigeres eller slettes av brukere

Automatiske dokumenter utvider bruksområdet til CMS kraftig, utover det å være en interaktivt dokumentarkiv. CMS kan dermed inkludere og vise tilstanden i omgivelsene. Dette er en kopling til det som kalles Internet-of-things.

I den foreliggende programkoden finnes modulen PubSubAdapter.py som kopler automatiske dokumenter til Publish-Subscribe distribusjon og MQTT-protokoll. På denne måten kan CMS være en del av et større nettverk av noder som samarbeider om å behandle sensor-information. Modulen createCMS doc.py gir et eksempel på hvordan MQTT-protokollen kan brukes for å opprette eller endre et CMS-dokument.

Legg merke til at programkoden i PubSubAdapter.py også tillater at tekst bli lagt til i eksisterende dokumenter, ved å søke etter tegnstrengen <!-- Insert --> i det eksisterende dokumentet. Denne tegnstrengen blir da erstattet med det nye innholdet, og dokumentet forøvrig blir bevart.

Egen kjøring, videre utvidelser og eksperimentering

Linken øverst i artikkelen laster ned et tar-arkiv av kildeteksten. Jeg anbefaler å kjøre CMS-tjeneren på en Linux-maskin, da den ikke er testet under Windows.

Last ned kildekoden i tar-format fra lenken i toppen av artikkelen, og hent frem cms.py i en editor. Du må se over definisjonene i begynnelsen av filen, spesielt om verdien av innerIP svarer til IP-adressene i ditt lokalnett.

Om du vil at CMS skal være tilgjengelig for omverdenen må du konfigurere routeren din med port forwarding, og kanskje satt opp dynamisk dns-navn. Dette omtales ikke her.

Før du starter CMS må disse programmene være installert:

  • Programmene pandoc, sqlite3
  • Python-bibliotekene web.py, pypandoc, paho-mqtt

Start så det hele med kommandoen

python3 cms.py 8080

8080 betegner TCP-porten som CMS skal bruke, om du endrer denne til 80 må du huske på å være root-bruker.

CMS har muligens ingen funksjoner som ikke finnes i andre samarbeidssystemer, men har det fortrinn at det er enkelt og kompakt, med kun noen hundre programsetninger. Systemets konstruksjon er dermed lett å forstå, utvide og endre. Det er derfor mitt ønske at de som laster ned og starter opp CMS vurderer utvidelser og nye funksjoner.

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

 276 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.

Nettradio og MP3-spiller med Raspberry Pico

 213 total views,  2 views today

Ved hjelp av et kretskort som dekoder MP3 til lydsignaler kan vi lage en MP3-spiller eller en Internet-radio. Alt som trengs er litt programmering i Rasberry Pico. Her brukes programmeringsspråket MicroPython.

Anders Fongen, mai 2023

Lyd lagret som MP3 må dekodes før det kan sendes som lydsignaler til en høyttaler. En slik dekoder kan lages i programvare, dersom prosessoren er kraftig nok. Dette går greit i en PC, men en mikrokontroller (f.eks. Arduino, EPS8266, Raspberry Pico) kan ikke påregnes å ha tilstrekkelig datakraft for dette formålet.

Det finnes egne integrerte kretser som utfører MP3-dekoding i maskinvaren, uavhengig av prosessorkraften i kontrolleren, og som lar oss lage en MP3-spiller med billige og strømsparende komponenter. Denne bloggartikkelen vil vise hvordan slike komponenter koples sammen og hvordan programmeringen gjøres.

Komponenter som kan brukes

Mange komponenter kan brukes for et eksperiment likt det som presenteres her. De valgte komponentene er en Raspberry Pico (heretter kalt RPico) og en dekoderkrets kalt VS1053 fra VLSI Solutions.

Oppsett og programmering av RPico blir ikke gjennomgått her, det finnes mange hjelperessurser for dette formålet på nettet. Her vil det vises hvordan RPico og VS1053 koples sammen, og den programmeringskoden som kjører i RPico for å oppnå ønsket virkemåte.

En VS1053 er kun en liten brikke/chip, så for eksperimentformål er det vanlig å bruke et kretskort som også inkluderer støttekretser og tilkoplingspunkter. Som vist på bildet under er det utstyrt med koplingspinner som passer ned i et vanlig prototyp-brett (“breadboard”).

Et eksperimentkort for VS1053

VS1053 er en ganske avansert brikke som i tillegg til å dekode MP3 også dekoder lyd kodet med WAV, WMA, OGG, eller MIDI. Den kan ta opp lyd gjennom en mikrofonport (evt. linjeport) og kode den til OGG m.m. (dog ikke til MP3). Kretskortet som er brukt i denne artikkelen har også en SD-kortleser.

For å ta kretskortet i bruk koples en høyttaler til den ene minijack-porten, mens tilkoplingspinnene koples til pinner på RPico. Gjennom disse ledningene går det signaler for å konfigurere VS1053 for det foreliggende formålet, og for å overføre digitale lydsignaler som skal dekodes.

I ledningene mellom VS1053 og RPico benyttes en overføringsmetode som kalles Serial Peripheral Interface (SPI). Dette er en såkalt seriell metode hvor én og en bit overføres i høyt tempo over en enkel ledning. Fordelen med å benytte en “standardisert” metode er at den kan styres fra maskinvaren slik at det krever mindre programmering, og belaster hovedprosessoren mindre. Dessuten er SPI utformet slik at kontrolleren kan snakke med flere enheter gjennom samme ledning (som vi da kaller en buss). I dette tilfellet kan både SD-kortleseren og dekoderen bruke samme ledningssett.

Ledningsnettet

Tegningen nedenfor viser hvordan RPico og VS1053 kan koples sammen. Her er det mange valgmuligheter, men det må være samsvar mellom hvilke pinner på kontrolleren som brukes, og programvaren som skal styre dem. Det er altså mulig å velge andre pinner på RPico enn hva som foreslås her.

Tilkoplingspunkter på VS1053 og RPico

Bildet over viser navnene som er gitt til pinnene på VS1053. De avviker noe fra de navnene som brukes på databladet, så her kreves litt utprøving for å fortstå hvilke funksjoner de har. Her er fasiten:

VS1053 påtrykkBeskrivelseKoples til
RPico pin
Navn på
RPico
5VDriftsspenning40VBUS
GNDJordforbindelse38GND
CSChip select for SD-kortleseren2GPIO 1
MISOSPI datalinje fra VS10536GPIO 4
SISPI datalinje til VS10535GPIO 3
SCKSPI klokkesignal4GPIO 2
XCSChip select for MP3-dekoderen9GPIO 6
XRESHard reset (aktiv lav)1GPIO 0
XDCSData select (RPico->VS1053)10GPIO 7
DREQData request (VS1053->RPico)11GPIO 8
Sammenkoplingen av VS1053 og RPico

Merk at pinnene CS, XCS, XRES, XDCS er alle “aktiv lav”, dvs. at de skal ha 0 volt for å gi effekt. Dette fremgår også fra programkoden.

Bildet over til høyre viser bruken av en flatkabel med ledninger i forskjellige farger for å gjøre denne sammenkoplingen. Det er lett å gjøre feil i denne prosessen, og fargene var til stor hjelp. Et bilde over pinnene på RPico vises på bildet under. Legg merke til at pinnene MISO, SI og SCK er koplet til RPico på pinnene 4-6, som alle er benevnet SPI0. Om andre pinner brukes til dette formålet er det nødvendig at de koples til samme SPI-kanal (0 eller 1) med betegnelsene RX, TX og SCK.

Kommunikasjon med VS1053

RPico kommuniserer med VS1053 på en av fire måter:

  1. Skriv til et register. Verdiene i registrene konfigurerer VS1053. Programkoden viser hvilke verdier som er nødvendig for eksperimentet i denne artikkelen. Full oversikt over registerverdier finnes i databladet.
  2. Les verdien av et register. Ikke strengt nødvendig i denne artikkelen, men nyttig for å kontrollere at en skriveoperasjon er vellykket.
  3. Sende data. I denne artikkelen vil vi sende MP3-kodet lyd til dekoderen, og det skjer på denne måten.
  4. Motta data. VS1053 kan også kode lyd til digital form, som da kan mottas av RPico. Ikke brukt i denne artikkelen.

Skrive til et register

For å skrive en registerverdi må RPico gjøre følgende:

  • Sette XCS=0, XDCS=1
  • Vente til DREQ==1
  • Bygge en 4 byte streng som starter på x02, deretter fulgt av adressen til registeret, og to bytes med den 16-bits verdien som skal skrives dit.
  • Sende strengen til SPI-kanalen
  • Sette XCS=1

Her ser du Python-koden for funksjonen write_command:

    def write_command(self, address_byte, data_byte1, data_byte2):
        # Set the control pins to indicate a command
        self.xcs.value(0)
        self.xdcs.value(1)

        while not self.dreq.value():
            pass

        # Send the address and data bytes over SPI
        ba = (bytearray([0x02, address_byte, data_byte1, data_byte2]))
        self.spi.write(ba)

        # Turn off XCS signals
        self.xcs.value(1)

Lese en registerverdi

For å lese verdien lagret i et register må RPico gjøre dette:

  • Sette XCS=0, XDCS=1
  • Vente til DREQ==1
  • Bygge en 2 byte streng som starter på x03, deretter følger adressen til registeret.
  • Sende strengen til SPI-kanalen
  • Lese to bytes fra SPI-kanalen, som inneholder verdien av registeret.

Slik:

    def read_register(self, address_byte):
        # Set the control pins to indicate a command
        self.xcs.value(0)
        self.xdcs.value(1)
        while not self.dreq.value():
            pass
        self.spi.write(bytearray([0x03, address_byte]))

        resp = self.spi.read(2)

        self.xcs.value(1)
        return resp

Sende data

Disse stegene er nødvendige:

  • Sette XCS=1, XDCS=0
  • Vente til DREQ==1
  • Sende maksimalt 32 bytes til SPI-kanalen
  • Sette XDCS=1

Slik:

    def write_data(self, data):
        # Set the control pins to indicate data
        self.xcs.value(1)
        self.xdcs.value(0)

        while not self.dreq.value():
            pass
        self.spi.write(data)
        self.xdcs.value(1)

Nødvendige deklarasjoner

Noen programsetninger er nødvendige for å deklarere og initialisere portene, samt importere noen nødvendige biblioteker:

from machine import Pin, SPI
import time
spi_mosi = Pin(3)
spi_miso = Pin(4)
spi_sck = Pin(2)

# Define the pins for the VS1053 module
vs1053_xcs = Pin(6)
vs1053_xdcs = Pin(7)
vs1053_dreq = Pin(8)
vs1053_reset = Pin(0)

# Initialize the SPI bus for the mp3 decoder 
co_spi = SPI(0, baudrate=1000000, polarity=0, phase=0, sck=spi_sck,\\
   mosi=spi_mosi, miso=spi_miso)

Jeg har valgt å legge all håndteringen av VS1053 inn som en klasse i Python, og den blir initialisert med denne koden:

class VS1053:
    def __init__(self, spi, xcs, xdcs, dreq, reset):
        self.xcs = xcs
        self.xdcs = xdcs
        self.dreq = dreq
        self.reset = reset
        self.spi = spi

    def init(self):
        self.xcs.init(Pin.OUT, value=1)
        self.xdcs.init(Pin.OUT, value=1)
        self.dreq.init(Pin.IN)
        self.reset.init(Pin.OUT, value=1)

        # Reset the VS1053 module
        self.reset(vs1053_reset)
        self.write_command(0x0, 0x8, 0x4)
        self.write_command(0x3, 0xe0, 0)    # Clock multiplier
        self.write_command(0xb, 0x30, 0x30) # Volume control, 0-loudest	

    def reset(self,pin_reset): # Hardware reset of VS1053
        self.reset.value(0)
        time.sleep_ms(1)
        self.reset.value(1)

Utover det å opprette instansvariabler, vil koden initialisere GPIO-portene med riktig tilstand, resette VS1053 og så skrive inn noen nødvendige registerverdier. Se seksjon 9.6 i databladet for en full beskrivelse av disse.

Full operasjon av VS1053

Etter konfigurasjon slik som beskrevet over og med de funksjonene for lesing og skriving til VS1053 kan vi nå finne frem MP3-data for å teste at vi får avspilt lyd. MP3-data kan finnes fra mange kilder, men i denne artikkelen vil to muligheter belyses nærmere: (1) Hente MP3-filer fra et SD-minnekort, dvs. filer som du har kjøpt på nettet eller hentet fra CD-plater (kalt “ripping”). (2) Hente MP3-data fra nettet med såkalt “strømming”, som innebærer at lyden spilles av etter hvert som de hentes. Begge alternativer vil bli demonstrert, men metoden med å hente data fra nettet er den enkleste og vil bli vist først.

Internet radio

Raspberry Pico finnes i en “W”-versjon (kalt RPicoW heretter), og den har kretser som kan koples til et trådløst nett og videre til Internet. Med denne versjonen brukt i dette eksperimentet er det relativt enkelt å bruke denne muligheten til å spille av radiostasjoner på Internet.

Her viser vi først programsetningene for å kople RPicoW til et trådløst nett (connect_wifi). Vi trenger som vanlig SSID (nettets navn) og et passord som må legges inn i programmet. Funksjonen connect setter opp en forbindelse til en web-tjener. For det formålet trenger vi en URL som inneholder web -tjenerens navn og angivelse av den MP3-strømmen som ønsket mottatt. MicroPython inneholder ikke ferdiglaget kode for HTTP-protokollen, så funksjonen inneholder også noen kodesetninger som starter den faktiske dataoverføringen.

class netradio:
    def connect_wifi(self,ssid,password):
        self.sta_if = network.WLAN(network.STA_IF)
        self.sta_if.active(True)
        self.sta_if.connect(ssid,password)
        while not self.sta_if.isconnected():
            time.sleep_ms(500)

    def connect(self,radio_IP,radio_URL,tcp_port=80):
        self.sock = socket.socket()
        addr = socket.getaddrinfo(radioIP, tcp_port)[0][-1]
        self.sock.connect(addr)
        request = "GET %s HTTP/1.0\r\nHost: %s\r\n\r\n" % (radioURL,radioIP)
        self.sock.send(request)
        return self.sock
    
    def read_data(self,numbytes):
        return sock.recv(numbytes)

Funksjonen read_data returnerer en såkalt socket, som er en “brønn” som vi kan øse data fra. Den kan brukes som en parameter i VS1053-klassens funksjoner for å hente MP3-data i 32-bytes porsjoner og sende til dekoderen med funksjonen write_data. Funksjonen for å oppnå dette ligger i VS1053-klassen og kalles play_stream. Koden ser slik ut:

    def play_stream(self,socket):
        while True:
            soundpacket = socket.recv(32)
            if soundpacket:
                self.write_data(soundpacket)
            else:
                break
        socket.close()

Hva vi nå trenger for å starte nettradioen er en høyttaler koplet til minijack-utgangen på VS1053, og følgende initialiseringskode i RPicoW:

# Initialize the VS1053 module
vs1053 = VS1053(co_spi, vs1053_xcs, vs1053_xdcs, vs1053_dreq, vs1053_reset)
vs1053.init()

# Start netradio
radioIP = "lyd.nrk.no"
radioURL= "/nrk_radio_klassisk_mp3_h?_hdr=0"

nradio = netradio()
nradio.connect_wifi("WiFi-navn","WiFi-passord")
vs1053.play_stream(nradio.connect(radioIP,radioURL))

Avspilling av MP3 fra SD-kort

Dette kretskortet med VS1053 inneholder også en SD kortleser (ikke alle gjør det), og en tilkoplet RPico (trenger ikke være en RPicoW) kan lese og skrive data til et minnekort. Minnekortet blir en del av filsystemet til RPico, og data leses og skrives gjennom ordinære metoder for filbehandling.

Tilkoplingen av en kortleser bruker også SPI-grensesnittet, og er litt mer omstendelig enn WiFi, så denne delen er spart til slutt, da selve dekoderen nå er testet og kjent.

Kortleseren bruker de samme ledningene til SPI-kommunikasjonen, men trenger sitt eget Chip Select signal. Den finner vi på pinnen CS på kretskortet, og koples til pin 2 (GPIO 1) på RPico. Se illustrasjonen under som viser hvordan SPI-ledningene kan deles av flere tilkoplede enheter.

Deling av SPI-buss på flere enheter. Merk av de har separate SS (Chip select) linjer

Kanaler på samme SPI-buss må initialiseres hver for seg, men en ny deklarasjon trenger ingen parametre når den bruker det samme bussnummeret. Altså slik:

import sdcard, os
# Initialize the SPI bus for the mp3 decoder and the sd card reader
co_spi = SPI(0, baudrate=1000000, polarity=0, phase=0, sck=spi_sck, \\
     mosi=spi_mosi, miso=spi_miso)
sd_spi = SPI(0)

# Chip select for SD card reader
sd_cs = Pin(1, Pin.OUT, value=1)

# Init SD card reader interface
card = sdcard.SDCard(sd_spi, sd_cs)
os.mount(card, '/sd')
print(os.listdir('/sd')) # Prints the files on the root directory

Modulen sdcard blir importert, dette er koden for å styre lesing og skriving på SD-kortet. Kildekoden kan lastes ned via lenken nedenfor, og må plasseres i filsystemet på RPico, f.eks. på /lib-katalogen.

Koden vist ovenfor gjør at filene på SD-kortet er gjort tilgjengelig fra katalogen /sd. Filer med MP3-innhold kan nå spilles av med en ny funksjon som vi legger til VS1053-klassen. Slik:

def play_file(self, filename):
        # Open the file and read the data
        chunk = 8192
        with open(filename, "rb") as f:
            data = f.read(chunk)

            # Loop through the file and send the data to the VS1053
            while data:
                idx = 0
                while True:
                    if idx+32 > len(data):
                        self.write_data(data[idx:])
                        break
                    else:
                        self.write_data(data[idx:idx+32])
                        idx += 32
                data = f.read(chunk)
    

Koden i funksjonen over kan trenge en forklaring: Her leses MP3-dataene i porsjoner på 8192 bytes, som deles opp i pakker på 32 bytes som sendes til VS1053 med write_data-funksjonen. Lesing av 8192 bytes tar mindre tid enn det å spille av 32 bytes, så det oppstår ikke hakking i lyden, noe som skjer dersom dette tallet settes mye høyere. Avspilling av filen /sd/andrea.mp3 kan nå skje med programsetningen

vs1053.play_file("/sd/andrea.mp3")

Brukergrensesnitt, sier du?

Denne artikkelen har som mål å vise hvilken Python-kode som kreves for å spille MP3-data fra nettet eller fra et SD-kort. Dette kan være til hjelp for dem som kjenner til MicroPython-programmering og kan inkludere disse programsetningene i sin egen kode. Den viste programkoden kan i sin helhet lastes ned herfra:

(Filene lastes ned med .pyx på slutten av filnavnet. Endre navnet til .py før bruk)

Skal denne koden brukes i egne programmer bør disse funksjonene legges til:

  1. Feilhåndtering. Alt som kan gå galt, vil trenge kode som fanger opp og behandler feilsituasjonen.
  2. Brukergrensesnitt. Start/stopp/pause i avspilling. Valg av katalog eller spilleliste for serieavspilling. Visning av tittel i et display m.m.

Anders håper at denne artikkelen kan være til nytte, skriv gjerne en kommentar om feil og mulige forbedringer.

Fletrådsprogrammering i MicroPython

 204 total views,  1 views today

Anders Fongen, mars 2023

Dette innlegget handler om programmering med flere tråder i mikrokontrollere som bruker MicroPython. Det vil studere hvordan MicroPython kjører tråder i ESP32 og Raspberry Pico, og gi forslag til hvordan programmene kan gjøres sikre og raske.

Innledning

Noen programmer blir enklere å skrive dersom aktivitetene kan deles opp i uavhengige, samtidige aktiviteter. F.eks. kan et program trenge å reagere på trafikk fra nettverket samtidig som det skal betjene en brukerdialog. Alle moderne operativsystemer tillater at en prosess har flere slike utføringsaktiviteter som kalles tråder. Tråder skal derimot ofte samarbeide om deling og kommunikasjon av data, noe som krever at de benytter seg av synkroniseringsmekanismer. Dette temaet hører til fagfeltet Operativsystemer, og kan ikke diskuteres grundig i dette innlegget. Se kapitlene 6 og 7 i boka som jeg har skrevet om operativsyster. Den finner du her.

“Vanlig” Python har et omfattende programbibliotek for programmering med flere tråder, (en veiledning i bruken finnes f.eks. her). MicroPython, derimot, har et lite og enkelt bibliotek som tilbys på noen microkontrollere, bl.a. ESP32 og Raspberry Pico (ikke ESP8266). Derfor skal resten av innlegget studere forskjeller mellom disse to produktene, og foreslå programmeringsteknikker for programmer som vil kjøre riktig på begge to.

Merk at vi nå diskuterer hvordan tråder utføres i et MicroPython program. Med andre programmeringsspråk kan situasjonen være anderledes. Merk også at innlegget ikke beskriver hvordan PC og mikrokontroller settes opp for programutvikling. En veiledning for dette finner du f.eks. her (ESP32) og her (Raspberry Pico).

Bildet under viser hvordan de to mikrokontrollerne ser ut, og viser også hvorfor jeg foretrekker å bruke Raspberry Pico: ESP32 er noe bredere, og hindrer tilkoplingsledninger på den ene siden når den settes i eksperimentbrett (breadboard) som vist på bildet.

ESP32 (til venstre) og Raspberry Pico

Trådbehandling og CPU-kjerner

ESP32 og Raspberry Pico (heretter kun kalt Pico) har begge 32-bits registre i CPU (noe som gjør regneoperasjoner raskere) og to CPU-kjerner. Med flere CPU-kjerner kan samtidige oppgaver kjøres i separate CPU-kjerner og dermed utføres raskere enn om oppgavene utføres skiftevis på samme CPU-kjerne.

  • Pico kan utføre oppgaver i to tråder, som utføres i hver sin CPU-kjerne.
  • ESP32 kan utføre oppgaver i mange tråder (flere enn to), men alle kjører skiftevis på samme CPU-kjerne.

Hvordan kan vi fastslå dette? Se på programkoden nedenunder. Der kjører en oppgave i “hovedtråden”, og den samme oppgaven utføres i en “undertråd”. De representerer den samme arbeidsmengde og koden er laget kun for at det skal ta litt tid.

1 import _thread, time
2 def subthread():
3    for i in range(4000000):
4        x = 232**10
5    print("Undertråd ferdig")
6    
7 # Hovedtråd
8 x1 = time.time()
9 _thread.start_new_thread(subthread,())
10 for i in range(4000000):
11    x = 232**10
12 print("Hovedtråd ferdig")
13 x2 = time.time()
14 print("Kjøretid:", (x2-x1), "sekunder")

Dersom vi setter et kommentartegn ved linje 9 vil det ikke startes en undertråd, og vi ønsker å se om dette har noen effekt på den totale kjøretiden:

  • Dersom kjøretiden ikke endres, tyder det på at undertråden er blitt utført i en egen CPU-kjerne, som nå ikke lenger blir brukt.
  • Dersom kjøretiden (nær) halveres, tyder det på at begge trådene er blitt utført i samme CPU-kjerne, og at arbeidsmengden for denne nå er blitt halvert.
CPUmed undertråduten undertråd
Pico30 sekunder29 sekunder
ESP3237 sekunder22 sekunder
Kjøretid med to tråder, henholdsvis én.

Ut fra disse målingene konkluderer vi med at ESP32 kun benytter én CPU-kjerne, mens Pico benytter begge to.

Videre skal vi bruke det samme programmet for å studere antall tråder som kan kjøres på de to mikrokontrollerne. Ved å duplisere linje 9 slik at de skapes 2 undertråder, vil ESP32 utføre programmet korrekt (men med lengre kjøretid). Pico derimot, vil avbryte programmet med feilmeldingen “OSError: core1 in use“. Et program som skal være portabelt mellom Pico og ESP32 må derfor ikke lage mer enn én undertråd. Derimot vil Pico tilby raskere utføring av et slikt program, fordi begge CPU-kjernene tas i bruk.

Et program som skal vært portabelt mellom Pico og ESP32 må ikke lage mer enn én undertråd.

Synkronisering av tråder

I mange tilfeller vil trådene i et program samarbeide gjennom delte variabler, og de trenger å varsle hverandre om hendelser, f.eks. om en ny oppgave skal påbegynnes eller en annen er avsluttet og resultatet er klart. Tråder som ikke synkroniseres riktig kan skape såkalt Race Condition, som er en feilkilde som kan være vanskelig å spore opp fordi den ikke skaper feil hver gang. Man unngår Race Condition ved å reservere en ressurs (f.eks. ved endring av en variabel verdi) med en mekanisme som heter Lock (andre ganger kalt Mutex).

Tenk på Lock som en nøkkel du trenger for å låse opp en dodør. Når det er ledig på do henger nøkkelen på en krok utenfor døren. Du ønsker trolig å sitte i fred på do, og tar med deg nøkkelen inn dit. Etter bruk låser du døren og henger nøkkelen tilbake på kroken. Om ikke nøkkelen henger på kroken betyr det at doen er opptatt, og du venter utenfor til nøkkelen igjen dukker opp. Doen kan på denne måten bare brukes av én person, og om det er ledig går du inn uten venting.

Programlistingen under er eksempel på en Race Condition: To tråder tester og modifiserer den felles variabelen x, og dersom begge trådene finner at x==0, og deretter utfører x=x+1, vil verdien av x bli 2, selv om programkoden tilsynelatende ikke lar det skje. Feilen skyldes altså at “test-and-set” sekvensen (linje 10-13 og 20-23) ikke er atomisk, dvs. at andre tråder kan bruke den samme variabelen underveis i operasjonen.

For at programmet skal utføres riktig må vi låse test-and-set sekvensen, slik som vist i linje 9 og 19, og etterpå låse opp slik som vist i linje 14 og 24. Om du vil kjøre dette programmet på en mikrokontroller for å forvisse deg om dette, fjern kommentartegnet på disse linjene og kontrollere resultatet (som er fravær av feilmeldingen “Race Condition error…”).

1 # Race condition demo
2 import _thread, time
3 x=0 # Delt variabel
4 slock = _thread.allocate_lock() # Lås
5 def subthread():
6    global x, slock
7     while True:
8        # Lås for å fohinde race condition
9 #        slock.acquire() 
10         if x==0:
11             x=x+1
12         elif x==1:
13             x=x-1
14 #        slock.release() # Lås opp
   
15 # Hovedtråd
16 _thread.start_new_thread(subthread,())
17 while True:
18     # Lås for å fohinde race condition
19 #    slock.acquire() 
20     if x==0:
21         x=x+1
22     elif x==1:
23         x=x-1
24 #    slock.release()
25     if x<0 or x>1:
26         print("Race condition error, x=%d" % x)
27         break

Kommunikasjon mellom trådene

Tråder som løper uavhengig i et program gjør det oftest for å løse et problem, og må derfor kommunisere, ikke bare beskyttes mot Race Condition slik som vist ovenfor, én tråd skal f.eks. hente data fra en nettradio, og en annen tråd skal dekode mp3 og sende lyddata til en høyttaler via en DAC. I et slikt program må det foregå en synkronisert datastrøm mellom to tråder, som skal ha følgende egenskaper:

  1. Mottakertråden skal dekode dataene slik de mottas, og vente (blokkeres) i de periodene det ikke mottas data.
  2. Sendertråden skal sende data når det er mulig, og dersom lageret som brukes til overføringen går fullt, skal sendertråden vente på at mottakertråden leser data og frigjør plass.
  3. Data skal overføres First-in-First-out, dvs. mottas i samme rekkefølge som de sendes.

Det er mange programtyper som kan ha nytte av å la trådene inngå et såkalt produsent-konsument samarbeid, så her kommer noen klasser i MicroPython som fungerer som ønsket på både Pico og ESP32.

Condition klasse

Denne klassen har en oppførsel omtrent som en Lock, men kan “huske” et antall release-kall slik at mange acquire-kall kan slippe gjennom uten å blokkeres. Objekter av denne klassen kan faktisk erstatte Locks, men kan også brukes til mer. Her er programkoden:

import _thread
class Condition:
    def __init__(self,initvalue=0):
        self.fulfilled =  initvalue
        self.mutex = _thread.allocate_lock()
        self.sync = _thread.allocate_lock()
        self.sync.acquire()
        
    def wait(self):
        self.mutex.acquire()
        self.fulfilled -= 1
        while self.fulfilled < 0:
            self.mutex.release()
            self.sync.acquire()
            self.mutex.acquire()
        self.mutex.release()
            
    def notify(self):
        self.mutex.acquire()
        self.fulfilled += 1
        if self.fulfilled <= 0:
            self.sync.release()
        self.mutex.release()

notify-funksjonen ligner på Locks release-funksjonen, men Condition-klassen kan lagre antallet notify-kall, mens release-funksjonen kan ikke kalles på en Lock som ikke er “låst” (med acquire). Legg merke til variabelen fulfilled som holder rede på dette antallet, og at kall til wait-funksjonen teller ned den verdien og fortsetter dersom fulfilled er større enn null. Dersom verdien er >0 betyr det at den har noen notify-kall “til gode”, om verdien er <0 viser det antall tråder som blokkeres av wait-kall.

Operasjonene på fulfilled må beskyttes av en Lock (kalt mutex) for å unngå Race Condition.

FIFO klasse

FIFO-klassen har et lagringsområde for data som er sendt, men ikke mottatt (i variabelen storage), og funksjoner for å sende (put(object)) og motta (get()). For å synkronisere senderen og mottageren over tilstanden til lagringsområdet, dvs. blokkere senderen når lageret er fullt og blokkere mottageren når lageret er tomt, brukes to Condition-variabler: dataElements og freeSpace. Studer selv hvordan de brukes i get- og put-metodene for å synkronisere tråder over en tilstand, ikke over hvilken programkode som utføres (slik en Lock vil gjøre).

Merk forøvrig at flere tråder kan sende data gjennom put-metoden, selv om det ikke er aktuelt med Pico, som bare kan kjøre to tråder. Setter man ESP32 opp med mer enn to tråder er programmet ikke lenger portabelt til Pico.

import _thread
class FIFO:
    def __init__(self,capacity):
        self.storage = [0 for i in range(capacity)]
        self.CAP = capacity
        self.dataElements = Condition(0)
        self.freeSpace = Condition(capacity)
        self.front = 0
        self.back = 0
        self.mutex = _thread.allocate_lock()
        
    def _in(self,obj): # Put object at back of queue
        with self.mutex:
            self.storage[self.back] = obj
            self.back = (self.back+1) % self.CAP
            
    def _out(self): # Tak object from front of queue
        with self.mutex:
            obj = self.storage[self.front]
            self.front = (self.front+1) % self.CAP
        return obj
    
    def put(self,obj): # Put object in FIFO, block if no space
        self.freeSpace.wait()
        self._in(obj)
        self.dataElements.notify()
        
    def get(self): # Get object from FIFO, block if no data
        self.dataElements.wait()
        obj = self._out()
        self.freeSpace.notify()
        return obj

Legg merke til at self.mutex.acquire og self.mutex.release er nå erstattet med with self.mutex. Bruk av with-statement gjør koden letter å lese, og er forklart f.eks. her.

Et testprogram for FIFO-klassen

Her viser jeg også et kort testprogram som illustrerer hvordan FIFO-klassen kan brukes av flere tråder. Programmet starter med at hovedtråden sender 5 tegnstrenger til FIFO-objektet før den blir blokkert, deretter skriver den tegnstrengene 5-10 i samme takt som undertråden (mottageren) leser strengene 1-5.

# Test-kode

import _thread,time
def subthread(fifo):
    time.sleep(1)
    while True:
        o = fifo.get()
        print(o)
        time.sleep(0.5)

# Now test FIFO
fifo = FIFO(5)
_thread.start_new_thread(subthread,(fifo,))
for i in range(10):
    str = "string nr %d"%i
    fifo.put(str)
    print("Fra hovedtråd: %s"%str)

Sluttord

De nye mikrokontrollerne med flere CPU-kjerner og som kan programmeres i MicroPython representerer en veldig interessant utvikling. Programmering av Internet-of-Things anvendelser trekker fordeler av et godt programmeringsspråk som utnytter maskinvareressursene godt.

Merk at både ESP32 og den versjonen av Pico som kalles Raspberry Pico W, også har en WiFi-modul som lar enheten kople seg til eksisterende trådløse nettverk, eller de kan sette opp sitt eget nettverk som andre maskiner kan kople seg til.

Og jeg håper at Condition- og FIFO-klassen som jeg har laget til dette innlegget kommer til nytte. Det er lettere å lage stabile og korrekte flertrådsprogrammer med hjelpeklasser som støtter en god programmeringsmodell, i dette tilfellet produsent-konsument-modellen.

Morsetrener på en Micro:Bit

 330 total views

Med dette utstyret kan man trene på sending og mottak av morsesignaler sammen med andre. Hver deltaker trenger en Micro:Bit (version 2), og programvaren finner du nederst på siden.

Her demonstrerer jeg hvordan Micro:Bit kan brukes for å sende og motta morse

Med morsealfabetet kan du sende og motta tekst ved å overføre korte og lange pipesignaler. På 1800-tallet kom telegraf-tjenesten i gang i Norge, fra nord til sør ble ledninger trukket som kunne overføre elektrisk strøm for dette formålet. Den gang fantes ikke radio, og heller ikke elektronikk som kunne overføre et talesignal. Profesjonelle operatører som var trent i bruk av morse sørget for at telegrammer ble sendt og mottatt, skrevet ned på papir med vanlige bokstaver og levert til mottakeren med bud.

Etter hver ble radiosenderen oppfunnet, men fortsatt uten muligheter for å overføre tale. Morsesignaler var også her i vanlig bruk mellom skip og til landstasjoner.

Da radioene ble utviklet til også å overføre tale, fortsatte allikevel morsesignaler å være i vanlig bruk. Det viste seg at et system med pipetoner var lettere å oppfatte når det var dårlig radioforbindelse, og man trengte mindre effekt for å få frem meldingene. En radiosender med mindre effekt er lettere og billigere, og egner seg i bærbart utstyr.

Mens profesjonell bruk av morse mellom skip og landstasjoner ble avviklet i år 2000, er det fortsatt mange radioamatører som bruker morse, og det er egne frekvensbånd som brukes til dette formål. Bruk av morse er

  • morsomt, fordi det krever øvelse
  • lett å kombinere med selvbygget radioutstyr
  • effektivt, signalene når over lange avstander med lav effekt og enkle antenner

Morsetrening

Morsealfabetet er lett å være, det finner du mange steder på nettet. Her ser du en slik tabell hvor de korte pipesignalene er vist som prikker, de lange som streker.

Det er grunner til at du ikke anbefales å pugge morse som prikker og streker, fordi det kommer til å hindre deg i å oppfatte signalene som bokstaver og ord når hastigheten øker. Det er mye bra programvare gratis tilgjengelig som trener deg i nettopp dette.

Men det er også viktig å trene morse sammen med andre, hvor dere kan sende til hverandre på skift og lage en faktisk samtale. Slik trening vil lære dere å bruke de vanligste forkortelsene, be om repetisjon av deler av meldingen ved behov osv. Dessuten er det morsommere å lære noe nytt sammen med andre. Om du allerede er radioamatør med tilgang til en radiostasjon kan du øve med virkelige radiosendere, i motsatt fall må du bruke andre overføringsmekanismer, som f.eks.

  • En internet taletjeneste (Messenger, Zoom, Skype m.fl.)
  • Lydsignaler i luft

I begge disse tilfellene ovenfor trenger du en morsenøkkel og en “piper” som avgir et pipesignal når du trykker ned spaken på morsenøkkelen. Som du ser her er en morsenøkkel ganske dyr, men du kan selvsagt lagt din egen fordi det er bare en enkel elektrisk bryter.

Alt i ett med Micro:Bit v2

Micro:Bit er en mikrokontroller som inkluderer en del brytere, sensorer, radio, lamper og lydgivere og koster ca kr.400,- (version 2 kreves for dette formålet). Den kan programmeres og brukes til mange formål, og vi har laget programvare slik at den kan brukes til morsetrening. Programmet er gratis og kan lastes ned fra denne siden via en link lenger ned.

  • Bruker knappene på kretskortet som morsenøkkel
  • Brukere lydgiver og lamper på kretskortet for å vise/spille morsesignaler
  • Bruker den innebygde radioen for at flere deltakere kan sende og motta signaler
  • En “virkelig” morsenøkkel kan koples til kretskortet om ønskelig

Med Micro:Bit v2 og den nødvendige programvaren har en gruppe med deltakere alt de trenger for å sende og motta morsesignaler over radio (men med kort rekkevidde, opp til ca 10 meter).

Innlasting av programvaren

  1. Last ned filen CWtransceiver.hex
  2. Kople MicroBit til PC med en USB-kabel
  3. Nå vil det vises en ny “disk” i FileExplorer, kalt MICROBIT
  4. Kopiere CWtransceiver.hex til denne “disken” (drag and drop i FileExplorer)

For å teste at programvaren er riktig installert, trykk ned Button A (på venstre side av kretskortet. Da skal du høre en pipetone.

Bruksanvisning for morsetreneren

  1. Du kan bruke treneren alene for å øve sending av morsetegn. Da bruker du Button A for å lage pipelyd.
  2. Om flere Micro:Bit-enheter er i nærheten av hverandre, vil signaler som sendes på en enhet spilles av med pipelyder også på de andre enhetene. Der vil også led-lampene på kretskortet lyse opp i samme takt.
  3. Om du berører touch-sensoren (på forsiden av kretskortet like ved USB-kontakten, den har to prikker med en oval rundt), vil LED-displayet skiftevis vise “P” og “S”. Om du vil sende morse slik som vist ovenfor (kalt “Straight Key”) skal det vises en “S” i displayet. Dersom det står en “P” i displayet vil Button A og Button B utgjøre en såkalt paddle keyer. Dette er en mer effektiv måte å sende morsetegn på, men den krever litt øvelse for å beherske. Demonstrasjonsvideoen i starten viser hvordan den kan brukes.
    * Button A gir en serie med prikker mens den holdes nede
    * Button B gir en serie med streker mens den holdes nede
    * Begge knappene nedtrykket gir en serie med skiftevis prikk og strek
    * Mens Button A holdes nede, kan Button B gis et kort trykk. Da vil streken bli sendt ferdig, deretter en prikk før strekene igjen blir sendt. Tilsvarende gjelder i motsatt retning.
  4. Om du ønsker å endre hastigheten på prikker og streker i “P” (paddle keyer) innstillingen gjør du som følger:
    * Hold Button A nede slik at du hører en serie prikker bli sendt.
    * Snu kretskortet på høykant til høyre (slik at Button A er øverst). Da vises en høyrepil i LED-displayet og du hører at hastigheten på prikkene øker. Rett opp kretskortet når hastigheten er passe. Tilsvarende senkes hastigheten om du snur kretskortet til venstre (da vises en venstrepil).

Communication between DMR and a computer network

 253 total views,  2 views today

Anders Fongen, November 2022

Abstract: The integration of a sensor network with services from voice communication, text messaging and Global Positioning System (GPS) creates opportunities for improved situational awareness, better safety for field operators, higher confidence in sensor readings and improved return on equipment investment. Digital Mobile Radio (DMR) has been the choice of communication technology for a series of experiments where these potentials have been investigated. Additional technology components used were mostly inexpensive and open source.

The article was published in the The Sixteenth International Conference on Sensor Technologies and Applications (SENSORCOMM 2022) October 2022, Lisbon Portugal.

Demonstration videos

Below are two demonstration videos from the integration experiment, view these if you are not into reading academic papers:

The first video shows how to integrate a DMR radio into an pub-sub network using the MQTT protocol.

Demonstration video from the integration experiment – pubsub intergation

The second video shows how two MMDVM units can provide routing of IP packets across a DMR radio link.

Demonstration video from the integration experiment – IP routing

Full text of article

Download source code

This link allows you to download the source code in the form of a compressed tar file. Please observe that this is experimental code not meant for production, and:

  • The module gwtest.py is the root module, and the execution starts there
  • You will need to import the module dmr_utils3, paho-mqtt, netifaces, etc.
  • You will need to inspect the source code and modify values for ip-addresses, DMR-ids, UDP port number, MQTT topics etc.
  • You need to configure pi-star so that it connects to the Controller at its IP-address.
  • The code also includes code to route IP-packets over a DMR link. Un-comment this code if necessary, but this will require the program to run in root mode (sudo…)
  • If you make improvements and want to share them with me, you are welcome to contact me. Please accept that I cannot spend much time on advice and support otherwise.