Konfigurasjon av “Virtual Private Net” med Wireguard

 43 total views,  4 views today

Det finnes flere alternative teknologier og produkter som kan tilby Virtuelle Private Nett (VPN), og i denne artikkelen skal vi studere WireGuard (WG). WG sies å være enklere å sette opp og bruke enn f.eks. OpenVPN, som vi har omtalt i en tidligere artikkel.

For en innledende forklaring om hva et VPN kan tilby, og en presentasjon av noen brukstilfeller, vil jeg foreslå de første par sidene av denne artikkelen.

Med en forutsetning om litt bakgrunnskunnskap om tunneler og røde/sorte endepunkter skal vi gjennomgå arkitekturen til WG og hvordan disse tre brukstilfeller kan løses:

  1. Host-til-host: To maskiner ønsker beskyttet kommunikasjon seg i mellom, nettverkene på begge sider er ikke en del av løsningen. Maskinenes IP-adresser er kjent og konstante.
  2. Road Warrior: Et kallenavn på en omreisende enkeltmaskin som trenger adgang til et beskyttet nettverk. En omreisende medarbeider som trenger adgang til bedriftens interne forretningssystemer fra hoteller og flyplasser er et eksempel på dette brukstilfellet.
  3. Nett-til-nett: Flere interne nett ønskes sammenkoplet med beskyttede forbindelser slik at ressurs- og klientmaskiner kan kommunisere som om de var ett felles nett.

Wireguard er enklere enn OpenVPN

Underveis i denne artikkelen WG bli sammenlignet med OpenVPN, som er populær og utbredt, men har ord på seg for å være komplisert å sette opp. WG er et enklere produkt, som i mindre grad løser alle behovene i sine konfigurasjonsfiler. WG forutsetter at du selv, eller hjelpeprogrammer, setter opp nettverksporter, rutingtabeller og brannvegger med riktige parametre.

WG er en nettverksport

WG oppretter en nettverksport (network interface) med bestemte egenskaper, og den kan kommunisere med en WG nettverksport i en annen maskin. For at denne kommunikasjonen kan foregå over et ubeskyttet nettverk (slik som Internet er) er det valgt å lage en tunnel hvor IP-pakkene som utveksles (kalt røde) blir kryptert og lagt inni en annen IP-pakke (kalt sorte). De røde IP-adressene er adskilt fra de sorte IP-adressene (tilhører ulike subnett). De sorte IP-adressene er de “virkelige” IP-adressene som ruterne i Internet bruker for å sende pakken til riktig mottaker, mens de røde IP-adressene vises bare hos de partene som kommuniserer. Figuren nedenfor illustrerer dette forholdet.

Røde og sorte deler av en VPN-tunnel

Innledningsvis vil jeg beskrive oppsettet av WG i Linux-maskiner. Dette gir best forståelse av hvordan de ulike delene av et VPN spiller sammen. Siden vil jeg vise hvordan WG settes opp i Windows, ChromeOS, Android og iPadOS. For å sette opp en WG nettverksport med sort adresse 10.10.0.1/24 må du være i “sudo”-modus og skrive slik i et Linux konsollvindu:

$ sudo bash
# ip link add wg1 type wireguard
# ip addr add 10.10.0.1/24 dev wg1
# ip link set up wg1

(Kommandolinjer som må skrives i “sudo”-modus er angitt med #-tegnet, ellers $-tegnet.)

Overføringen av IP-pakker skal beskyttes

En god beskyttelse av overføringen gjennom et ubeskyttet nett skal sikre:

  1. Konfidensialitet: Ingen kan tilegne seg kunnskap om den røde IP-pakken gjennom avlytting av overføringen
  2. Integritet: Ingen kan endre innholdet i den røde IP-pakken uten at det oppdages hos mottakeren.
  3. Autentisitet: Det skal være mulig for mottakeren å fastslå avsenderens identitet.

Alt dette kan man oppnå gjennom kryptering. WG benytter seg av å kryptere med et nøkkelpar, dvs at mottaker og avsender benytter ulike nøkler. Avsender benytter seg av en privat nøkkel for å kryptere og signere en rød IP-pakke før sending, og mottakeren benytter avsenderens offentlige nøkkel for å gjenopprette innholdet og kontrollere signaturen.

Kommunikasjon mellom to maskiner gjennom en WG-tunnel vil ikke forholde seg til navn eller andre identifikatorer på partene, men kun de offentlige nøklene. Fordi motparten presenterer data signert med sin private nøkkel, vil mottageren av denne signaturen forvisse seg om at det er eieren av den offentlige nøkkelen som sender, ikke en som bruker en offentlig nøkkel som tilhører en annen. Sikkerheten i en slik metode hviler på at den private nøkkelen ikke blir stjålet eller delt med andre, og det er en forutsetning som faktisk er ganske krevende å oppnå.

Jeg har tidligere skrevet en artikkel om Autentisering, og anbefaler å lese den om disse mekanismene er ukjent.

For å fremskaffe et nøkkelpar skriver du følgende inn i et Linux-konsoll (WG foretrekker at du legger disse filene på filområdet /etc/wireguard):

$ sudo bash
# cd /etc/wireguard
# wg genkey | cat > privkey
# wg pubkey < privkey > pubkey
# chmod 400 privkey 

Den offentlige nøkkelen (pubkey) deles fritt med andre, mens din private nøkkel (privkey) er din hemmelighet. Den skal lagres slik at ingen andre kan lese den.

WG og klienten må kjenne hverandres offentlige nøkler for å gi denne beskyttelsen

Teksten videre vil kreve noe kunnskaper om hvordan IP-adresser representeres og inngår i subnett og rutingtabeller. Om dette er ukjent stoff kan jeg anbefale denne artikkelen nå. Den gir nødvendig bakgrunninformasjon.

Ved overføring av en IP-pakke må avsenderens maskin kjenne dens private nøkkel, og mottakerens maskin må kjenne avsenderens offentlige nøkkel. Partene må konfiguere WG for dette formålet eksempelvis på denne måten:

# wg set wg1 listen-port 51820 private-key privkey \
peer <offentlig-nøkkel> allowed-ips 10.10.0.2/32 \
endpoint 192.168.9.12:51820

I dette eksemplet skrives det at

  1. Parten ønsker å lytte etter mottatte IP-pakker på UDP port 51820
  2. Den private nøkkelen ligger i filen privkey
  3. Motpartens sorte IP-adresse er 10.10.0.2/32
  4. Motpartens offentlige nøkkel er angitt bak ordet peer
  5. Motpartens sorte endepunkt (adresse:port) er 192.168.9.12:51820

Et praktisk eksempel – Host-til-Host

En demonstrasjonsvideo for dette eksemplet:

Den enkleste konfigurasjonen, nemlig å kople opp en beskyttet kanal mellom to parter, skjer på følgende måte. Her viser jeg konfigurasjonsfilene etter hverandre, merk at de sorte ip-adressene (192.168.2….) er kun eksempler, og må endres for bruk andre steder.

Digresjon: WG opererer med “to lag” av sorte ip-adresser: Den virkelige adressen som viser vei gjennom det virkelige nettet (i dette tilfellet 192.168.2.124) og adresser knyttet til WG-nettverksportene (i dette tilfellet 10.10.0.1/30)

Vi forutsetter videre at nøklene er bli generert på forhånd, og at partenes private nøkler ligger i filen privkey. Den offentlige nøkkelen er med i konfigurasjonen hos motsatt part, så partene må ha utvekslet disse verdiene på forhånd.

Venstre maskin – wg-left.sh:

$ sudo bash
# ip link add wg1 type wireguard
# ip addr add 10.10.0.1/30 dev wg1
# ip link set up wg1
# cd /etc/wireguard
# wg set wg1 listen-port 51820 private-key privkey \
  peer 3aTXg7SbFDOc7rbscBUqKZsRqoHpvowHRkb+mNwuXRk= \
  allowed-ips 10.10.0.2/32 \
  endpoint 192.168.2.124:51820

Høyre maskin – wg-right.sh

$ sudo bash
# ip link add wg1 type wireguard
# ip addr add 10.10.0.2/30 deb wg1
# ip link set up wg1
# cd /etc/wireguard
# wg set wg1 listen-port 51820 private-key privkey \
  peer 9/c6yQloq6h3RRxyPrHHXO2b72GFT6aN5hGGRPY6YXw= \
  allowed-ips 10.10.0.1/32
 

Etter at disse kommandofilene er utført på begge partene kan de “pinge” hverandre med hverandres røde adresser: left pinger 10.10.0.2 og tilsvarende 10.10.0.1 for right. Om vi gir kommandoen sudo wgleft, vil utskriften se slik ut:

root@anders-medion:/home/anders# wg
interface: wg1
public key: 9/c6yQloq6h3RRxyPrHHXO2b72GFT6aN5hGGRPY6YXw=
private key: (hidden)
listening port: 51820

peer: 3aTXg7SbFDOc7rbscBUqKZsRqoHpvowHRkb+mNwuXRk=
endpoint: 192.168.2.124:51820
allowed ips: 10.10.0.2/32

Dynamisk adresse i endepunktet

En detalj som er nødvendig å merke seg er at right ikke har oppgitt noe endpoint for sin motpart. Den kan følgelig ikke pinge left uten denne informasjon, men den tilegner seg den informasjonen første gang den mottar et ping fra left. Det kan vi fastslå ved å gi wg-kommandoen på right før og etter den har mottsatt sin første ping fra left. Dette er en nyttig detalj dersom den ene parten ikke har fast IP-adresse, noe som er tilfelle når man kopler seg til Internet fra hoteller og restauranter. En slik part kalles ofte for Road Warrior, og neste avsnitt vil beskrive hvordan vi kan lage en konfigurasjon med flere Road Warriors.

Avslutningsvis vil vi påpeke to egenskaper med denne konfigurasjonen: (1) Den er mer nyttig enn den først synes, fra et terminalkonsoll i left kan en bruker logge seg inn på right, og deretter benytte seg av tjenester og innlogging tilgjengelig i right sitt beskyttede nettverk. (2) Én av de to partene må ha en adresse som kan nås via en internet-adresse. Enten ved maskinen faktisk står med en virkelig Internet IP-adresse, eller bak en brannvegg (med Internet IP-adresse) som har satt opp port forwarding til maskinen.

Road Warriors – for å bruke hovedkontorets interne systemer

En bedrift har sine forretningssystemer som trolig er beskyttet med passord for innlogging, men et enkelt passord gir for dårlig beskyttelse mot angrep fra Internet. En bedre beskyttelse kan de oppnå med et VPN. En Road Warrior konfigurasjon vil skille seg fra det i forrige avsnitt ved at:

  1. Det er ikke kun én Road Warrior som har ukjent IP-adresse og som vil kople seg til en sentralt plassert part, men flere. Det er altså behov for lagring av flere offentlige nøkler og sorte IP-adresser.
  2. Det er ikke den sentralt plasserte parten som kjører de ønskede forretningssystemene, så den sentrale parten må rute trafikken videre inn i det beskyttede nettet.

    Den sentrale parten blir heretter kalt en VPN-ruter. Klientkonfigurasjonen (som brukes av den omreisende parten) trenger de samme opplysningene som før, men vi skal nå lagre dem i en konfigurasjonsfil.

Her følger en demonstrasjonsvideo om Road Warrior-konfigurasjon:

WG konfigurasjonsfil

Vi vil konfigurere VPN-ruteren med en konfigurasjonsfil fremfor bruk av direkte kommandoparametre. I en konfigurasjonsfil har vi plass til å liste opp en rekke motparter og deres offentlige nøkler og sorte IP-adresser. Konfigurasjonsfilen (i dette tilfellet kalt wg1.conf) kan se slik ut:

[Interface]
PrivateKey = oOWwjJHkckYU3uNgI5UoJpLENPjtDa4bvW3Rj82bV0M=
Listenport = 51820

[Peer]
# Desktop Linux
PublicKey = M0YMsn88TZPEedCKzI33RhdS9VWR0qpXm6e2zhQBpSA=
allowedIPs = 10.0.1.2/32

[Peer]
# Raspberry Pi 5
PublicKey = 60cdqn+GgVodrNDdfPVCv39aZCvpeNKm75kF2lJC+Fs=
allowedIPs = 10.0.1.3/32

[Peer]
# Medion Laptop
PublicKey = n/Eriwa1pZ7T2R5Qd4n96tQ8Ptq0SUfblIY8ZvswehY=
allowedIPs = 10.0.1.4/32

Og selve kjørefilen (shell-skriptet) kan se slik ut (må kjøres i sudo-modus):

ip link del wg1 || true
ip link add wg1 type wireguard
ip addr add 10.0.1.1/24 dev wg1
wg setconf wg1 wg1.conf
ip link set up wg1

Husk å legge disse filene på /etc/wireguard. Merk at konfigurasjonsfilen ikke inneholder opplysninger om andres endpoint, av samme grunn som nevnt i forrige avsnitt. Disse partene kan nemlig kople til VPN-ruteren med ulike IP-adresser hver gang.

Setningen allowedIPs angir hvilke IP-adresser som VPN-ruteren kan bruke for å sende til en klient med en bestemt offentlig nøkkel. Ved å angi dette som en enkelt adresse, angitt med /32 prefiks, tvinges klienten til å bruke denne røde adressen, og på den måten unngå at flere klienter bruker samme sorte IP-adresse.

Denne konfigurasjonen tillater tre Road Warriors å kople seg til, presentere sin offentlige nøkkel, sin sorte IP-adresse og sitt endepunkt (sorte IP-adresse og UDP port). I tillegg presenterer de et dataelement kryptert med sin private nøkkel, for å bevise at de “eier” denne nøkkelen.

Konfigurasjonen av en Road Warrior

Klientkonfigurasjonen vil nå bruke en egen konfigurasjonsfil. I det følgende ekemplet skal klienten kunne adressere IP-pakker til nettverket “bakenfor” VPN -ruteren, og dette må deklareres i en allowed-ips-setning som et IP subnett, i dette eksemplet 192.168.2.0/24. Det samme subnettet må legges inn i maskinens rutingtabell med ip route add...Vi lar shell-skriptet for klientkonfigurasjonen se slik ut:

ip link del wg1 || true
ip link add wg1 type wireguard
ip addr add 10.0.1.4/24 dev wg1
ip link set up wg1
ip route add 192.168.2.0/24 via 10.0.1.4
wg setconf wg1 wg1.conf

Skriptet henviser til konfigurasjonsfilen wg1.conf, innholdet av den er som følger:

[Interface]
PrivateKey = n/Eriwa1pZ7T2R5Qd4n96tQ8Ptq0SUfblIY8ZvswehY=
ListenPort = 51820

[Peer]
PublicKey = 9gUrCU32BQS4pzBe+jbs1P5T1VMDcti11tg7VkRYH0U=
Endpoint = hos.fongen.no:51820
AllowedIPs = 10.0.1.0/24
AllowedIPs = 192.168.2.0/24

I dette tilfellet er det [Interface]-delen som beskriver konfigurasjonen av egen maskin, mens [Peer]-delen beskriver VPN-ruteren. Forøvrig er all informasjon i disse to filene allerede drøftet, så ingen ytterlig forklaring vil bli gitt.

VPN-ruteren må videresende IP-pakker

En Road Warrior kan altså kople seg til VPN-ruteren på nevnte måte, men er først og fremst interessert i å kommunisere med et internt/beskyttet nett innenfor ruteren, koplet til ruteren gjennom en nettverksport. For dette eksemplet kaller vi denne nettverksporten for eth0. IP-pakker med mottakeradresser i det interne nettet skal sendes dit, og IP-pakker i motsatt retning skal sendes gjennom WG-tunnelen tilbake til klienten. To kommandoer i VPN-ruteren samt en mulig konfigurasjon av stedets brannvegg vil fikse dette:

  1. Ruteren må settes til å rute IP-pakker. Skriv denne kommandoen i sudo-modus:
    # echo 1 > /proc/sys/net/ipv4/ip_forward
    Denne innstillingen må gjøres igjen for hver oppstart, men du kan også redigere filen /etc/sysctl.conf (evt. opprette den) og skrive inn setningen net.ipv4.ip_forward = 1 og deretter restarte maskinen.
  2. IP-pakkene som rutes fra VPN -ruteren til det interne nettet vil ha avsenderens røde IP-adressen som avsenderadresse, og maskinene der vil ikke uten videre vite hvordan IP-pakker med slike adresser skal sendes tilbake til VPN-ruteren. En løsning på dette problemet er at avsenderadressen på IP-pakkene fra en RoadWarrior gis VPN-ruterens adresse med en såkalt NAT-funksjon. Les denne artikkelen for å lære om NAT dersom dette er ukjent stoff. NAT-funksjon på eth0 lages med denne kommandoen i sudo-modus:
    # iptables -t nat - A POSTROUTING -o eth0 -j MASQUERADE
  3. Det beskyttede nettet (som VPN-ruteren befinner seg i) vil mest sannsynlig være isolert fra Internet gjennom en brannvegg. For at IP-pakker utenfra skal finne veien til VPN-ruteren kreves en konfigurasjon i brannveggen som kalles port forwarding. Dette innebærer at IP/UDP-pakker med brannveggens IP-adresse og UDP-portnummer (i dette eksemplet) 51820 vil sendes inn i nettet med VPN-ruterens IP-adresse. Alle hjemmerutere og brannvegger har denne muligheten.

Bruk av WG på andre plattformer enn Linux

Programvare som støtter en Road Warrior finnes på flere plattformer enn kun Linux. Her følger erfaringer med et antall av dem

Android/iPadOS

Appene for RoadWarrior for iPadOS og Android kommer fra samme utvikler, og de er så lik hverandre at vi ikke gir begge en separat omtale.

Jeg har installert og testet Android-appen “WireGuard” fra “Wireguard development Team” (det høres ut som en offisiell app). Versjonen jeg testet var 1.0.2. For å opprette og konfigurere et VPN, gjør følgende:

  • Fra grunnbildet i appen, trykk på ‘+’-tegnet nederst til høyre for å opprette et VPN. I dette tilfellet, velg “Create from scratch”. Appen kan generere et nøkkel om du ikke ønsker å skrive inn en eksisterende.
    • I skjemaet som nå vises skrives følgende informasjon:
    • Name – selvvalgt navn (uten mellomrom)
    • Private key – Skriv inn en du har, eller få den laget av appen
    • Public key – Denne avledes av privatnøkkelen, men du kan kopiere fra dette feltet for å sende til andre.
    • Adresses – Din sorte IP-adresse. Valgt fritt, men må være i samme IP subnett som VPN-ruterens adresse
    • Listen port – 51820 er en fin verdi
    • DNS servers – Om det er behov for å benytte en DNS-tjeneste i det interne nettet, kan du skrive inn dens IP-adresse her.
    • [Peer] Public key– VPN-ruterens offentlige nøkkel, tilsendt.
    • PreShared key – Ikke brukt i disse eksemplene. Kan gi styrket kryptografisk beskyttelse
    • PersistentKeepalive – Ikke brukt i disse eksemplene. Kan lage “hjerteslag” for å holde brannveggen “åpen”
    • Endpoint: VPN -ruterens sorte endepunkt. En “virkelig” IP-adresse
    • AllowedIPs – subnett som klienten ønsker å adressere “bak” VPN-ruteren.

Trykk på diskettsymbolet for å lagre, og du kan siden slå denne oppkoplingen av og på.

ChromeOS

WireGuard er forhåndsinstallert i ChromeOS, som er operativsystemet i en Chromebook. På panelet for for bl.a. nettverksinnstillinger (se bilde nedenfor) er det en knapp for “VPN”, og bak den knappen ligger det mulighet for å opprette nye VPN-instanser merket “+”. Panelet for å konfigurere et VPN ber om de samme opplysningene som i konfigurasjonsfilene for Linux.

Konfigurasjonspanelet for nettverk har en “VPN”-knapp

Opplysningene som kreves for en konfigurasjon av et WG VPN er disse (lskjemaet leses fra toppen og ned)

Tjenestenavn – selvvalgt
Leverandørtype – “WireGuard”
Klient IPadresse – klientens sorte IP-adresse, i samme subnett som VPN-ruterens.
Navnetjenere – DNS-tjener i det beskyttede nettet, ellers ubrukt.
Nøkkel – Om det allerede er laget nøkler, velg “jeg har et nøkkelpar”, og kopiere inn privatnøkkel i feltet under (ikke vist på bildet). Man kan også velge “generer et tilfeldig nøkkelpar”. Når skjemaet er utfylt og “kople til” er trykket, gå tilbake til panelet og finne den offentlige nøkkelen som må oppgi til VPN-ruterens administrator
Motpart/offentlig nøkkel – her kopieres inn VPN-ruterens offentlige nøkkel
Forhåndsdelt nøkkel – ikke brukt i disse eksemplene
Sluttpunkt – VPN-ruterens sorte IP-adresse og UDP portnummer
Tillatte IP-adresser – VPN-ruterens røde IP-adresse, og det subnettet du ønsker adgang til. Om du velger å skrive 0.0.0.0/0, vil all trafikk, også til Internet forøvrig, gå via VPN-ruteren.
Vedvarende keepalive-intervall – kan stå tomt for denne gang

Se knappene for “Kople opp” og “Kople ned”. Nå bør ressursene i det aktuelle nettet være tilgjengelig. Også Linux-konsollet og Android-apper på Chromebook får adgang til de samme ressursene.

Windows

WG er ikke ferdig installert i Windows, og må lastes ned og installeres separat. Appen kan lastes ned fra https://download.wireguard.com/windows-client/ og installeres på vanlig måte.

Når WG for Windows startes, viuses et vindu som lister opp eksisterende WG-konfigurasjoner, og som med tastetrykket Ctrl-N vil generere nøkler for en ny WG-konfigurasjon og vise redigeringvinduet som er vist nedefor. I bildet nedenfor vises de vante opplysningene fra Linux-konfigurasjonen. Her brukes en konfigurasjonsfil for klienten, slik at [Interface]-delen nå beskriver maskines egen konfigurasjon, mens [Peer]-delen viser endepunkt og offentlig nøkkel for VPN-ruteren. Tilbake til startvinduet finnes en “Aktivér”-knapp som kopler opp WG-tunnelen. Som tidligere må den genererte offentlige nøkkelen bli installert i VPN-ruterens konfigurasjon på foirhånd. Et nytt element i konfigurasjonsfilen er “Address”-linjen, som inneholder klientens sorte IP-adresse. Ellers er dette helt likt Linux-konfigurasjonen, med den forskjell at det ikke er nøvendig å sette opp rutingtabeller selv.

Redigeringsvinduet for WG på Windows

WireGuard i nett-til-nett konfigurasjon, et fyldig eksempel

Som nevnt gir WireGuard en en beskyttet link mellom to endepunkter, og befatter seg ikke med f.eks. ruting. Når vi ønsker at interne nett skal knyttes sammen slik at maskinene i alle nettene skal kunne kommunisere, trenges en ruter i hvert nett og linker mellom dem. En slik konfigurasjon kan vises på denne måten:

To separate IP-nett knyttet sammen med to rutere.

IP-adressene i hvert av de 4 nettene på figuren må danne separate subnett, og ruterne må kjenne til adressene på alle “grenene”. Figuren over viser slik informasjon, men hver enkelt maskin trenger også informasjon om hvilken “vei” IP-pakkene skal sendes for å nå mottakeren i flere hopp. Les denne artikkelen dersom dette ikke er kjent stoff.

Linken mellom R1 og R2 på figuren over bør beskyttes om den går gjennom offentlige nettverk, og VPN-teknologi er godt egnet for det formålet. Tidligere i denne artikkelen brukte i WireGuard for å beskytte linken mellom to maskiner, og vi skal bruke et lignende oppsett for å knytte sammen VPN-rutere. Vi skal altså ta med elementer fra Host-til-host og RoadWarrior løsningene videre.

Vi velger å sette opp et fyldig eksempel: Tre nettverk, A, B og C, skal knyttes sammen med beskyttede WG-linker. Sammenknytningen skal gjøres med bruker av VPN-rutere som både håndterer WG-parametrene og videresending av IP-pakker basert på rutingtabeller.

I tillegg skal en gruppe Road Warriors kunne kople seg til fra hoteller og flyplasser og få adgang til ressurser i alle de tre andre nettverkene. Her kommer erfaringene med Road Warrior eksemplet tidligere i artikkelen til nytte.

PDF-tegningen nedenfor er stor og viser arkitekturen i denne nettverket, og utdrag av rutingtabellene og WG-parametrene som inngår. Trykk på Download-knappen for å se tegningen i full størrelse. De sorte linjene er linker som bruker Internet og må derfor beskyttes. En full presentasjon av konfigurasjonsfilene for hver maskin i dette nettverket vil presenteres på slutten av denne artikkelen.

Rutingtabellene på figuren kan studeres for å fastslå at IP-pakker sendes til riktige røde nettverk (A-D) basert på deres IP-adresse. Detaljer knyttet til nøkkelutveksling og konfigurasjon av WG-parametre er i noen grad utelatt, men følger de samme reglene som ble presentert i gjennomgangen av Host-til-Host kommunikasjon. På figuren representerer noden A-h1 en enkel brukernode (host) i nett A, men svitsj-symbolet indikerer at mange maskiner kan befinne seg i nett A, og de vil ha identisk konfigurasjon.

IP-adressene i nettverkene A, B og C følger reglene for IP-subnett og gjør det overflødig å benytte noen NAT-funksjoner. Basert på rutinginformasjonen i maskiner og VPN-rutere vil alle adresser som forekommer i dette samlede nettet finne frem til sin adressat.

Noe annet gjelder RoadWarriors som utgjør nett D. De har alle mulig adresser ettersom hvor de befinner seg, og addresser som endrer seg fra gang til gang. Dette er ikke et problem for WG, som gjerne setter opp forbindelser under slik forhold, men det finnes ingen fornuftig metode for å sørge for at IP-pakker med alle slags ukjente avsenderadresser skal finne tilbake til riktig sted når de ankommer på denne måten. Derfor er det plassert en NAT-enhet i utgangen fra D-vpn, på samme måte som i RoadWarrior-eksemplet tidligere.

En kort sammenligning mellom OpenVPN og WireGuard

WireGuard kommer med en enklere idé om å sette opp en tunnel mellom ta maskiner, eksponert gjennom vanlige nettverksporter. Den bruker kryptonøkler på en enklere måte enn hva man gjør i et “Public Key Infrastructure” (PKI), men mister da også noen av fordelene som et PKI faktisk tilbyr:

Et PKI vil bruke digitale sertifikater for å attestere hvilken person/bruker/gjenstand den offentlige nøkkelen tilhører. OpenVPN kan settes opp til å godta offentlige nøkler som er sertifisert med bestemte navneregler eller utsteder. Det gjør det unødvenig å registrere hver klient direkte inn i VPN-tjenerens konfigurasjon, noe som gjør administrasjonen av et stort VPN med mange klienter mer håndterlig. Et prinsipp jeg fremholder er at “infrastrukturkomponenter skal ikke drive tillitshåndtering”, og det oppnår vi ved å skille trafikkhåndtering og brukeradministrasjon slik OpenVPN tilbyr.

En veldig viktig egenskap med nøkkelsertifikater (og en av hovedgrunnene til at nøkkelsertifikater ble oppfunnet) er at man unngår man-in-middle angrep fordi nøklene er uløselig knyttet til en identifikator (som representerer eieren av nøkkelen). Den sikkerheten tilbyr ikke WireGuard, og overføringen av offentlige nøkler mellom VPN -tjeneren og klientene (og alle parter som vil kommunisere via WG) må skje via autentiserte kanaler som hindrer partene å utgi seg for andre.

Men gitt at dette blir håndtert på en trygg måte, så unngår WG den jungelen av interoperabilitetsproblemer som herjer i anvendelser som benytter PKI. Sertifikater kan utformes og valideres på et utall forskjellige måter. Det resulterer i at sertifikater laget for én anvendelse ofte ikke kan brukes i en annen, og at sikkerheten i sertifikatvalidering ikke lar seg teste og kontrollere på en tilfredsstillende måte.

Min overfladiske observasjon av WG er at den enkle bruken av nøkler og kryptoalgoritmer gjør det enklere å f.eks. flytte sin klientkonfigurasjon fra én maskin til den neste. Nøklene har en enkel og kortfattet tekstrepresentasjon som kan overføres gjennom alle slags tekstbasert kanaler. Og jeg ble positivt overrasket over at WG-appene for Windows, Android m.m. var enkle å sette opp og virket tilfredsstillende uten for mye plunder.

Min vurdering er at WireGuard er god egnet for småskala applikasjoner, men har egenskaper som gjør den mindre egnet enn OpenVPN dersom antallet maskiner og klienter er høyt.

Samlede konfigurasjonsfiler

Som et appendix vises her konfigurasjonsopplysninger for hver av de 8 nodene på figuren. Netplan (deklarasjon av IP-adresser), rutingtabell, oversikt over alle nettverksportene, oppstartsskript for WG og WG-konfigurasjonsfil. Noen detaljer i disse filene er ikke blitt behandlet i teksten, f.eks. er nettverket 192.168.100.0/24 brukt som underliggende nettverk for de sorte linkene. Videre er VirtualBox brkt for å kjøre nodene som Virtuall Maskiner, og nettverk mellom VM’ene er ikke vist direkte, men i hovedsak er de konfigurert separat for hvert rødt nett, og ett felles for de sorte linkene.

A-h1

KONFIGURASJON AV A-h1:

NETPLAN:
network:
    ethernets:
        enp0s3:
            dhcp4: true
        enp0s8:
            dhcp4: no
            addresses:
                - 192.168.10.101/24
            routes:
                - to: 192.168.20.0/24
                  via: 192.168.10.1
                - to: 192.168.30.0/24
                  via: 192.168.10.1
    version: 2

RUTINGABELL:
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         _gateway        0.0.0.0         UG    100    0        0 enp0s3
10.0.2.0        0.0.0.0         255.255.255.0   U     100    0        0 enp0s3
_gateway        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
10.0.2.3        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
192.168.10.0    0.0.0.0         255.255.255.0   U     0      0        0 enp0s8
192.168.20.0    192.168.10.1    255.255.255.0   UG    0      0        0 enp0s8
192.168.30.0    192.168.10.1    255.255.255.0   UG    0      0        0 enp0s8

IP ADRESSER:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 10.0.2.15/24 metric 100 brd 10.0.2.255 scope global dynamic enp0s3
       valid_lft 85671sec preferred_lft 85671sec
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 192.168.10.101/24 brd 192.168.10.255 scope global enp0s8
       valid_lft forever preferred_lft forever

B-h1

KONFIGURASJON AV B-h1

NETPLAN:
network:
    ethernets:
        enp0s3:
            dhcp4: true
        enp0s8:
            dhcp4: no
            addresses:
                - 192.168.20.101/24
            routes:
                - to: 192.168.10.0/24
                  via: 192.168.20.1
                - to: 192.168.30.0/24
                  via: 192.168.20.1
    version: 2

RUTINGTABELL:
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.0.2.2        0.0.0.0         UG    100    0        0 enp0s3
10.0.2.0        0.0.0.0         255.255.255.0   U     100    0        0 enp0s3
10.0.2.2        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
10.0.2.3        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
192.168.10.0    192.168.20.1    255.255.255.0   UG    0      0        0 enp0s8
192.168.20.0    0.0.0.0         255.255.255.0   U     0      0        0 enp0s8
192.168.30.0    192.168.20.1    255.255.255.0   UG    0      0        0 enp0s8

IP-ADRESSER:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 10.0.2.15/24 metric 100 brd 10.0.2.255 scope global dynamic enp0s3
       valid_lft 84917sec preferred_lft 84917sec
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 192.168.20.101/24 brd 192.168.20.255 scope global enp0s8
       valid_lft forever preferred_lft forever

C-h1

KONFIGURASJON AV C-h1:

NETPLAN:
network:
    ethernets:
        enp0s3:
            dhcp4: true
        enp0s8:
            dhcp4: no
            addresses:
                - 192.168.30.101/24
            routes:
                - to: 192.168.10.0/24
                  via: 192.168.30.1
                - to: 192.168.20.0/24
                  via: 192.168.30.1
    version: 2

RUTINGTABELL:
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         _gateway        0.0.0.0         UG    100    0        0 enp0s3
10.0.2.0        0.0.0.0         255.255.255.0   U     100    0        0 enp0s3
_gateway        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
10.0.2.3        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
192.168.10.0    192.168.30.1    255.255.255.0   UG    0      0        0 enp0s8
192.168.20.0    192.168.30.1    255.255.255.0   UG    0      0        0 enp0s8
192.168.30.0    0.0.0.0         255.255.255.0   U     0      0        0 enp0s8

IP-ADRESSER:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 10.0.2.15/24 metric 100 brd 10.0.2.255 scope global dynamic enp0s3
       valid_lft 84260sec preferred_lft 84260sec
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 192.168.30.101/24 brd 192.168.30.255 scope global enp0s8
       valid_lft forever preferred_lft forever

A-vpn

KONFIGURASJON AV A-vpn:

NETPLAN:
network:
    ethernets:
        enp0s3:
            dhcp4: true
        enp0s8:
            dhcp4: no
            addresses:
                - 192.168.10.1/24
        enp0s9:
            dhcp4: no
            addresses:
                - 192.168.100.1/28
    version: 2

RUTINGTABELL:
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.0.2.2        0.0.0.0         UG    100    0        0 enp0s3
10.0.2.0        0.0.0.0         255.255.255.0   U     100    0        0 enp0s3
10.0.2.2        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
10.0.2.3        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
10.11.12.0      0.0.0.0         255.255.255.252 U     0      0        0 wg1
10.11.12.4      0.0.0.0         255.255.255.252 U     0      0        0 wg2
192.168.10.0    0.0.0.0         255.255.255.0   U     0      0        0 enp0s8
192.168.20.0    10.11.12.1      255.255.255.0   UG    0      0        0 wg1
192.168.30.0    10.11.12.5      255.255.255.0   UG    0      0        0 wg2
192.168.100.0   0.0.0.0         255.255.255.240 U     0      0        0 enp0s9

IP-ADRESSER:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 10.0.2.15/24 metric 100 brd 10.0.2.255 scope global dynamic enp0s3
       valid_lft 83258sec preferred_lft 83258sec
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 192.168.10.1/24 brd 192.168.10.255 scope global enp0s8
       valid_lft forever preferred_lft forever
4: enp0s9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 192.168.100.1/28 brd 192.168.100.15 scope global enp0s9
       valid_lft forever preferred_lft forever
5: wg1: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 10.11.12.1/30 scope global wg1
       valid_lft forever preferred_lft forever
6: wg2: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 10.11.12.5/30 scope global wg2
       valid_lft forever preferred_lft forever

STARTSKRIPT:
# Create link to AVPN
ip link del wg1 || true
ip link add wg1 type wireguard
ip addr add 10.11.12.1/30 dev wg1
ip link set up wg1
wg setconf wg1 wg1.conf
ip route add 192.168.20.0/24 via 10.11.12.1

# Create link to CVPN
ip link del wg2 || true
ip link add wg2 type wireguard
ip addr add 10.11.12.5/30 dev wg2
ip link set up wg2
wg setconf wg2 wg2.conf
ip route add 192.168.30.0/24 via 10.11.12.5

WG1.CONF:
[interface]
privateKey = iD9fo3ezXcn8njwhmireyxea6vP9HA8s0rVWf9Wjcl4=
listenPort = 51820

[peer]
publicKey = EIhO9XcyQG8yYvlR11aORUQ8NBYGcQMI0yHeguPfeh8=
allowedIPs = 10.11.12.2/32
allowedIPs = 192.168.20.0/24
endpoint = 192.168.100.2:51820

WG2.CONF
[interface]
privateKey = iD9fo3ezXcn8njwhmireyxea6vP9HA8s0rVWf9Wjcl4=
listenPort = 51821

[peer]
publicKey = 6tl3fRAgUPDhAqOrT0Nr2tyIecp7UDgpl2nKh0n1+HM=
allowedIPs = 10.11.12.6/32
allowedIPs = 192.168.30.0/24
endpoint = 192.168.100.3:51821

B-vpn

KONFIGURASJON AV B-vpn:

NETPLAN:
network:
    ethernets:
        enp0s3:
            dhcp4: true
        enp0s8:
            dhcp4: no
            addresses:
                - 192.168.20.1/24
        enp0s9:
            dhcp4: no
            addresses:
                - 192.168.100.2/28
    version: 2

RUTINGTABELL:
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.0.2.2        0.0.0.0         UG    100    0        0 enp0s3
10.0.2.0        0.0.0.0         255.255.255.0   U     100    0        0 enp0s3
10.0.2.2        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
10.0.2.3        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
10.11.12.0      0.0.0.0         255.255.255.252 U     0      0        0 wg1
192.168.10.0    10.11.12.2      255.255.255.0   UG    0      0        0 wg1
192.168.20.0    0.0.0.0         255.255.255.0   U     0      0        0 enp0s8
192.168.30.0    10.11.12.2      255.255.255.0   UG    0      0        0 wg1
192.168.100.0   0.0.0.0         255.255.255.240 U     0      0        0 enp0s9

IP-ADRESSER:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 10.0.2.15/24 metric 100 brd 10.0.2.255 scope global dynamic enp0s3
       valid_lft 85780sec preferred_lft 85780sec
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 192.168.20.1/24 brd 192.168.20.255 scope global enp0s8
       valid_lft forever preferred_lft forever
4: enp0s9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 192.168.100.2/28 brd 192.168.100.15 scope global enp0s9
       valid_lft forever preferred_lft forever
5: wg1: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 10.11.12.2/30 scope global wg1
       valid_lft forever preferred_lft forever

STARTSKRIPT:
ip link del wg1 || true
ip link add wg1 type wireguard
ip addr add 10.11.12.2/30 dev wg1
ip link set up wg1
wg setconf wg1 wg1.conf
ip route add 192.168.10/24 via 10.11.12.2
ip route add 192.168.30/24 via 10.11.12.2

WG1.CONF
[interface]
privateKey = GBXJpAu9SV+5HGQKitiur1By0lVDSA2WtT7JSN2iLF8=
listenPort = 51820

[peer]
publicKey = AGlXvCzKsYF7rooofmzNbIB3tSHUJ6R5vkyTd1+BUlc=
allowedIPs = 10.11.12.1/32
allowedIPs = 192.168.10.0/24
allowedIPs = 192.168.30.0/24
endpoint = 192.168.100.1:51820

C-vpn

KONFIGURASJON AV C-vpn:

NETPLAN:
network:
    ethernets:
        enp0s3:
            dhcp4: true
        enp0s8:
            dhcp4: no
            addresses:
                - 192.168.30.1/24
        enp0s9:
            dhcp4: no
            addresses:
                - 192.168.100.3/28
    version: 2

RUTINGTABELL:
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         _gateway        0.0.0.0         UG    100    0        0 enp0s3
10.0.2.0        0.0.0.0         255.255.255.0   U     100    0        0 enp0s3
_gateway        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
10.0.2.3        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
10.11.12.4      0.0.0.0         255.255.255.252 U     0      0        0 wg1
192.168.10.0    10.11.12.6      255.255.255.0   UG    0      0        0 wg1
192.168.20.0    10.11.12.6      255.255.255.0   UG    0      0        0 wg1
192.168.30.0    0.0.0.0         255.255.255.0   U     0      0        0 enp0s8
192.168.100.0   0.0.0.0         255.255.255.240 U     0      0        0 enp0s9

IP-ADRESSER:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 10.0.2.15/24 metric 100 brd 10.0.2.255 scope global dynamic enp0s3
       valid_lft 85155sec preferred_lft 85155sec
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 192.168.30.1/24 brd 192.168.30.255 scope global enp0s8
       valid_lft forever preferred_lft forever
4: enp0s9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 192.168.100.3/28 brd 192.168.100.15 scope global enp0s9
       valid_lft forever preferred_lft forever
5: wg1: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 10.11.12.6/30 scope global wg1
       valid_lft forever preferred_lft forever

STARTSKRIPT:
ip link del wg1 || true
ip link add wg1 type wireguard
ip addr add 10.11.12.6/30 dev wg1
ip link set up wg1
wg setconf wg1 wg1.conf
ip route add 192.168.10.0/24 via 10.11.12.6
ip route add 192.168.20.0/24 via 10.11.12.6

WG1.CONF
[interface]
privateKey = gOhx12lAqnqcVOF9qVXeaQlaDuMsOumDvAUpoSF4O1I=
listenPort = 51821

[peer]
publicKey = AGlXvCzKsYF7rooofmzNbIB3tSHUJ6R5vkyTd1+BUlc=
allowedIPs = 10.11.12.5/32
allowedIPs = 192.168.10.0/24
allowedIPs = 192.168.20.0/24
endpoint = 192.168.100.1:51821

D-vpn

KONFIGURASJON AV D-vpn:

NETPLAN:
network:
    ethernets:
        enp0s3:
            dhcp4: true
        enp0s8:
            dhcp4: no
            addresses:
                - 192.168.10.2/24
            routes:
                - to: 192.168.20.0/24 
                  via: 192.168.10.1
                - to: 192.168.30.0/24
                  via: 192.168.10.1
        enp0s9:
            dhcp4: no
            addresses:
                - 192.168.2.9/24
    version: 2

RUTINGTABELL:
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         _gateway        0.0.0.0         UG    100    0        0 enp0s3
10.0.2.0        0.0.0.0         255.255.255.0   U     100    0        0 enp0s3
_gateway        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
10.0.2.3        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
192.168.2.0     0.0.0.0         255.255.255.0   U     0      0        0 enp0s9
192.168.10.0    0.0.0.0         255.255.255.0   U     0      0        0 enp0s8
192.168.20.0    192.168.10.1    255.255.255.0   UG    0      0        0 enp0s8
192.168.30.0    192.168.10.1    255.255.255.0   UG    0      0        0 enp0s8

IP-ADRESSER:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 10.0.2.15/24 metric 100 brd 10.0.2.255 scope global dynamic enp0s3
       valid_lft 84672sec preferred_lft 84672sec
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 192.168.10.2/24 brd 192.168.10.255 scope global enp0s8
       valid_lft forever preferred_lft forever
4: enp0s9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 192.168.2.9/24 brd 192.168.2.255 scope global enp0s9
       valid_lft forever preferred_lft forever


STARTSKRIPT:
ip link del wg1 || true
ip link add wg1 type wireguard
ip addr add 10.11.13.1/24 dev wg1
ip link set up wg1
wg setconf wg1 wg1.conf
iptables -t nat -A POSTROUTING -o enp0s8 -j MASQUERADE

WG1.CONF:
[interface]
privateKey = iABCVCYnoe3Hh4xup1L7GPgJ+pEGsN71l+stTq+mamg=
listenPort = 51820

[peer]
# Linux client
publicKey = 3aTXg7SbFDOc7rbscBUqKZsRqoHpvowHRkb+mNwuXRk=
allowedIPs = 10.11.13.2/32

[peer]
# Android client
publicKey = c9pYVjABCNWyA6BhAjUhjnjXtMsR42Bim5G5X+Da+yk=
allowedIPs = 10.11.13.3/32

D-rw

KONFIGURASJON AV D-rw:

RUTINGTABELL:
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.2.1     0.0.0.0         UG    100    0        0 enp0s31f6
10.11.13.0      0.0.0.0         255.255.255.0   U     0      0        0 wg1
192.168.2.0     0.0.0.0         255.255.255.0   U     100    0        0 enp0s31f6
192.168.10.0    10.11.13.2      255.255.255.0   UG    0      0        0 wg1
192.168.20.0    10.11.13.2      255.255.255.0   UG    0      0        0 wg1
192.168.30.0    10.11.13.2      255.255.255.0   UG    0      0        0 wg1

IP-ADRESSER:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: enp0s31f6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    inet 192.168.2.206/24 brd 192.168.2.255 scope global dynamic noprefixroute enp0s31f6
       valid_lft 38451sec preferred_lft 38451sec
3: wg1: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 10.11.13.2/24 scope global wg1
       valid_lft forever preferred_lft forever

STARTSKRIPT:
ip link del wg1 || true
ip link add wg1 type wireguard
ip addr add 10.11.13.2/24 dev wg1
ip link set up wg1
wg setconf wg1 wg1.conf
ip route add 192.168.10/24 via 10.11.13.2
ip route add 192.168.20/24 via 10.11.13.2
ip route add 192.168.30/24 via 10.11.13.2

WG1.CONF:
[interface] 
privateKey = eA8MDscVpkhm/cAJcE8AeOx2+ozz+CeD0OADuFdrznk=
listenPort = 51820

[peer]
publicKey = 76+YJu/TMaGJgh2dEZaB3RasxeZ1fXTPw5a2u9XXAB0=
allowedIPs = 10.11.12.9/32,192.168.0.0/16
endpoint = 192.168.2.9:51820

Verdens minste Linux-computer, koster 220 kroner

 80 total views

Luckfox Pico Mini er en mikrokontroller til 220 kroner, som kjører fullt Linux operativsystem. Den plasserer seg dermed mellom enklere mikrokontrollere (ESP32, RP2040) og dyrere datamaskiner som Raspberry Pi. Vi har brukt litt tid på å studere den, og formidler noen erfaringer og observasjoner i denne artikkelen.

(c) Anders Fongen, april 2026

Som kjent kjører operativsystemet Linux i såkalte “embedded systems”, i små ettkorts datamaskiner, hvor Raspberry Pi utmerker seg som mest kjent. Vi kom over en produkt som kalles Luckfox Pico Mini, som presser et komplett Linux-system inn på en frimerkestort kretskort og som koster ca 220 kroner. Ikke til å motstå, altså.

Luckfox Pico Mini B med WiFi-adapter og minnekort

Utrustningen

Kretskortet har en port for tilkopling av et kamera, et kortspor for mikroSD, en USB-C kontakt, 64 MB minne (som deles mellom lager og arbeidsminne). Programvaren på maskinen inkluderer de vanligste programmene og kommandoene, og etablerer en IP-forbindelse til en vertsmaskin via USB-kabelen, ved bruk av RNDIS-protokoll. Konsolltjenester (SSH og Telnet) tillater innlogging fra vertsmaskinen for betjening med et vanlig kommandoskall (shell).

Filsystem på et microSD-kort blir “automountet” i filsystemet slik vi ønsker det. Et minnekort blir dermed et opplagt medium både for overføring av filer og utvidelse av lagerplassen.

Kontrolleren er ikke utstyrt med WiFi- eller ethernet-adapter, så nettverkskommunikasjonen foregår gjennom USB-forbindelsen via vertmaskinen som blir satt opp som en IP-ruter. Videoen som er lenket inn nedenfor demonstrerer dette.

Det kan tilføyes at det finnes et WiFi-kort som kan plugges inn i miniSD-sprekken, men bruken av den krever en ny bygging av OS-kjernen, og vil dessuten hindre brukes av et minnekort. WiFi-kortet er vist på bildet ovenfor.

Installering av programvare

Kontrolleren er beskjedent utstyrt med minne og lager, og en del programmer er utelatt. Vi noterer oss (med forbehold om hva vi kan ha oversett) at dns, cc, perl, rsync, cron, pip er utelatt.

Det finnes ikke noe programvarelager (repository) for dette produktet, heller ikke er det ressurser for å kompilere programvare internt i kontrolleren. Nødvendig programvare kan fremskaffes på én av to måter:

  1. Krysskompilering på en ordinær Linux-maskin, med påfølgende filoverføring til kontrolleren (med f.eks. SCP)
  2. Overføring av Python kildekode og programbiblioteker til kontrolleren, evt. redigering av kildekode med en editor på kontrolleren (nano). Siden Python-kode er portabel mellom maskinarkitekturer så kunne pip-programmet vært kjekt å ha her.

Pkt. 2 beror på at Luckfox har “voksenpython” versjon 3.11 installert, i motsetning til ESP32- og RP2040-baserte kontrollere som har Mikropython. Dette tillater bruk av større og mer avanserte programbiblioteker. Erfaringsmessig gir lokal redigering av programvare som kjøres i en tolk uten nødvendig krysskompilering og filoverføring en raskere “testing/feilretting”-sløyfe. Trolig er programutvikling med Python det mest attraktive alternativet på dette produktet. Men dersom noen tar seg bryderiet med å krysskompilere populære opensource-programmer for denne prosessoren, vil et arkiv med kjørbar kode være svært velkommen.

I/O-porter

Kontrolleren er som ventet utstyrt med et antall gpio-porter som støtter de vanlige funksjonene: digital i/o (både 3,3v og 1.8v), pulsbreddemodulerte signaler, analog-til-digitale innganger, I2C- og SPI-buss, samt seriekommunikasjon. Dette skulle rekke for et bredt spekter av anvendelser. Bruk av gpio-portene kan skje både fra Python-programmer og skall-programmer (siden portene også er lagt ut i filsystemet), og leverandørens wiki-side har gode programeksempler.

Demonstrasjonsvideo

Denne videoen demonstrerer hvordan vertsmaskinen og kontrolleren kan konfigureres for betjening og progamutvikling.

Oppgradering av programvare

Selve operativsystemet på kontrolleren kan lett oppgraderes, og det finnes to versjoner av Linux som kan brukes: Buildroot og Ubuntu 22.04. Ubuntu er for stor til å få plass i det interne minnet og krever lagring på et microSD-kort (med de ytelseskonsekvenser det gir). Eksemplaret som jeg anskaffet kjørte en eldre versjon av Buildroot som ikke støttet SSH, men den lot seg lett oppgradere:

  1. Programmet upgrade_tool lastes ned fra nettet og pakkes ut (adresser i lenken nedenfor)
  2. Ønsket versjon av Buildroot lastes ned fra nettet og pakkes ut på vertsmaskinens disk.
  3. Buildroot-filene innholder filen update.img Flytt til denne katalogen.
  4. Luckfox-kontrolleren koples til vertsmaskinen mens boot-knappen holdes inne.
  5. sudo upgrade_tool uf update.img

Disse Wiki-sidene gir nøyaktive opplysninger om nettadresser o.l., så de vises ikke her.

Konklusjon

LuckFox er absolutt et interessant produkt, som plasserer seg mellom Micropython-baserte kontrollere og små Linux-maskiner som Raspberry Pi. Den gjør ingenting som ikke Raspberry Pi kan gjøre, men Pi koster tusenlappen, mens Luckfox koster 220 kroner og bruker dessuten vesentlig mindre strøm. Hovedfordelen, etter vårt syn er tilbudet om “voksen-Python”, men vi ser også et paradoks mellom å kunne titlby store og komplekse Python-anvendelser i en maskinvare som i mindre grad klarer å kjøre dem.

Les gjerne denne blogartikkelen for en lignende vurdering av LuckFox Pico.

Teknologiaktiviteter for barn 10-12 år

 479 total views

Basert på erfaringer fra en sommerleir for barn, her er noen idéer til aktiviteter for barn knyttet til teknologi og koding.

Anders Fongen, 2025

Innledning

Undertegnede (heretter betegnet “jeg”) deltok på KFUM-KFUKs sommerleir på Knattholmen 23-27 juni 2025, med oppdraget med å lage en “teknologi-aktivitet” over 3 dager x 5 timer. Aldersgruppen for denne leiren er 9-12 år.

8 deltakere hadde meldt seg på aktiviteten, fra dag 2 var det 7 deltakere i gruppen. Vi var to instruktører for gruppen: 

nr.1 (meg) har 20 års erfaring med IKT-undervisning på høgskolenivå, men har liten erfaring med barn i denne aldergruppen.

nr.2 er pensjonert grunnskolelærer med mye erfaring med aldersgruppen, ingen spesiell kompetanse innen IKT.

Målsetning og pedagogisk grunntanke

  • Viktigst: Magi, lek
  • Litt viktig: Kunnskap, innsikt, ferdigheter
  • Deltagelse, ikke beskuelse
  • Utendørs aktivitet om været tillater det

Den opprinnelige listen over “læringsmål” var:

  • Grunnleggende lodding av elektronikk
  • Forstå energiformer og -overganger (optisk, kinetisk, kjemisk, elektromagnetisk)
  • Beherske sambandsdisiplin og -protkoller
  • Beherske enkel bruk av multimeter
  • Gjennom eksperimenter, forstå forskjellen mellom likespenning og vekselspenning
  • Forstå og beherske sammenkopling av en solcelleanlegg
  • Forstå prinsippene ved satellittkommunikasjon
  • Beherske enkel bruk av morsesignaler
  • Tilegne seg (økte) programmeringsferdigheter i Micro:Bit og Scratch

Aktiviteter med sterkt veiledningsbehov er vanskelig å gjennomføre med kun én instruktør med IKT-faglig kompetanse. Noen aktiviteter ble derfor ikke gjennomført av denne grunn. Se eget avsnitt som utdyper dette.

Beskrivelse av aktivitetene

Her følger en mer detaljert beskrivelse av de aktivitetene som ble gjennomført over de tre dagene som var til rådighet.

Generelle observasjoner fra gjennomføringen

  • Aldersgruppen 9-12 år har kort “attention span”, og forklaringer og instrukser i forkant av aktiviteter “nådde ikke” helt frem. I den grad gruppen var samlet var det enklere å gi slike forklaringer etterhvert som behovet oppsto.
  • Naturfagkunnskapene er minimale i den aldersgruppen, og elementære begreper som elektroner, magnetisme, energiformer måte forklares fra bunnen av. 
  • Digital avlesning i måleinstrumenter var barna fortolig med, men analog avlesning (galvanometer, kompass) ga bedre forståelse av resulatverdiene (mye/lite, større/mindre, fort/langsomt). Et analogt oscilloskop på lav sweep-hastighet gir bedre forståelse av vekselspenning, fordi tidsaksen kunne oberveres direkte.

Rommet som vi disponerte

Til denne aktivitetsgruppen disponerte vi salen kalt “Svenner”, som var ca. 80 kvm med løse bord og stoler. Rommet hadde en veranda, denne ble brukt til å etablere antenner for HF- og VHF-frekvensbåndet. Verandaen ble også brukt til å sette opp et solcelleanlegg.

Ulike bord i salen ble brukt til diverse akitivteter: To bord for elektronikk-eksperimenter, ett bord til radiostasjon (basert på en Yaesu FT-897 transceiver), ett bord for PCer, tiltenkt programmering av Micro:Bit og Scratch, ett bord for å bygge morsenøkler.

Rommet kunne avlåses utenom aktivitetstiden, noe som var nødvendig fordi deltakerne hadde med egne laptop’er, og utstyret forøvrig var verdifullt.

Omtale av hver enkelt aktivitet

Gjennomførte aktiviteter var:

Gjemsel med sambandsradioer

For å trene deltakerne i bruk av sambandsradioer for å løse en konkret oppgave, ble de utstyrt med en lisensfri håndholdt UHF-radio for bruk i PMR-frekvensområdet (446 MHz). De ble så delt opp i par, og ett par skulle gjemme seg et sted på leirområdet. De andre tre parene skulle så bruke radiosambandet til å smarbeide om å lete etter dem. Regelen var at paret som gjemte seg, måtte svare på anrop og spørsmål om hva de kunne se fra gjemmestedet: “Kan du se parkeringsplassen? Kan du se kapellet?”. Alle parene fikk etter tur lov til å gjemme seg, og i alt gjorde vi denne leken tre ganger. Været var fint og innbød til utendørsaktiviteter fremfor å sitte inne på laboratoriet vårt. Aktiviteten vakte litt hyggelig oppmerksomhet blant leirdeltakerne forøvrig, der barna sprang rundt i terrenget med sambandsradioene og pratet. 

Deltakerne brukte en stund på å motstå fristelsen til å lage tullelyder på sambandet, og etter hvert vokste det frem en forståelse for hvordan sambandet kunne utnyttes bedre, og hvordan letingen etter hvert ble mer planmessig. 

Bygge morsenøkkel og trene morsesignaler

Basert på et byggesett utviklet og produsert av Torbjørn Skauli (kallesignal LA4ZCA) fikk deltakerne anledning til å bygge sin egen 3d-printede morsenøkkel. Byggeveiledningen var tydelig og instruktiv, og byggingen tok i snitt ca 30 minutter. Som bildet viser er morsenøkkelen plassert på en plate sammen med batteri og en “buzzer” med det formål å trene morsesignaler uten tilkoplet radio. Med en enkel modifikasjon kan nøkkelen også benyttes på en radiosender. Én enkel sammenkopling skjer med å vikle to ledninger sammen, noe som fort kan løsne. Derfor gikk jeg over alle morsenøklene i ettertid og loddet denne koplingen.

For trening av morsesignaler hadde jeg i tankene å bruke sambandsradioene sammen med morsenøklene og sende morsesignaler med radioenes FM-modulasjon (såkalt “modulated CW”). På denne måten kunne deltakerne trene morsesignaler seg i mellom utenfor synsrekkevidde. Det som viste seg var derimot at tonen fra “buzzeren” var for lys (høy frekvens) til å overføres over radiosambandet.

Treningen på morsesignalering skjedde derfor ved at noen av deltakerne satt sammen med meg og vi brukte en morsenøkkel til å lytte og sende. INGEN TABELL OVER MORSETEGNENE BLE DELT UT. Vi drillet på bokstaver som f.eks. hadde 1-5 prikker (E,I,S,H,5) og streker (T,M,O,0), og sende morsenøkkelen rundt så alle fikk drill på rytme og signalvarighet. Videre drillet vi på bokstaver som har nærliggende morsetegn (F,L) (Y,Q) (B,D,6) (U,V,4) og økte gradvis alfabetet som deltakerne kunne skille mellom kun ved lytting.

Jeg var overrasket hvor konsentrert deltakerne i den aktuelle alderen (10-12 år) var i denne drillingen, og hvor raskt de tilegnet seg deler av morsealfabetet kun ved lytting. Jeg føler meg nokså sikker på at de på kort tid kunne tilegnet seg ferdigheter til en morsesamtale på lav hastighet.

Grunnleggende eksperimenter innen elektronikk, elektromagnetisme og energiformer

Jeg hadde forberedt noen enkle eksperimenter for at deltakerne skal erfare:

  • Likestrømmens retning, pluss- og minuspoler. Måling med multimeter
  • Galvaniske elementer (“potetbatteri”, sink-kobber-saltvann)
  • Elektrisk generert magnetfelt
  • Prinsippet med dynamo/elektromotor
  • Vekselstrøm, vist med lysdioder, signalgenerator og oscilloskop
  • Oppkopling og måling på solcellanlegg (inkl. ladekontroller og batteri)

På det ellers nedsnakkede nettstedet Temu.com kan man anskaffe en del materiell knyttet til realfagsundervisning. Jeg kjøpte en del materiell derfra, billig og av dårlig kvalitet:

  • En enkel elektromotor for å vise hvordan elektromagnetisme kan omsette elektrisk energi til kinetisk, og hvordan rotasjonsretningen bestemmes av strømmens retning.
  • En dynamo koplet til en LED og drevete av en sveiv med utveksling, for å demonstrere motsatt energiovergang, men som også fungerte som elektromotor. 
  • Et lite kar med tre rom og seriekoplet med elektroder i sink og  kobber. Ble brukt til å konstruere et galvanisk element med tilstrekkelig strøm til å drive en LED.

Også noen hjemmelagde komponenter ble brukt:

  • En krets med tre motstander i serie. Sammen med et voltmeter brukt til å demonstrere spenningsfordeling i en likestrømskrets.
  • En krets med to LED koplet i motsatt retning. Ved påført likespenning indikerte LEDene strømmens retning.

Eksperimentene som ble utført kan sammenfattes slik:

  • Skifte batteriene på en lommelykt, plassere dem med riktig polaritet
  • Bruke multimeter for å finne det defekte batteriet blant flere
  • Sette 9 volt batteri mot stålull og se at det antennes
  • Bruke multimeter for å avlese motstandsverdier i en krets av seriekoplede motstander (for å erfare hvordan de adderes)
  • Sette likespenning på motstandskretsen og måle hvordan spenningsverdien fordeler seg over motstandene. Deler så spenningsverdien på mostandsverdien for gi “et hint” om Ohms lov.
  • Dytte en sink- og en kobberbit i en potet og måle spenningsforskjellen. Gjenta med å bruke håndflatene og et kar med saltvann.
  • Sette spenning på en spole som er viklet rundt et kompass og observere at kompassnålen snur seg.
  • Kople et batteri til en elektromotor og observere at den roterer. Bytt polaritet og observere at motoren reverserer.
  • Kople en LED til en dynamo med sveiv og observere at lampen lyser når man sveiver. Vis også at LED ikke lyser ved omvendt polaritet på spenningen. Kople så dynamoen til et batteri og observere at den også er en elektromotor. 
  • Kople et batteri til kretsen med to LEDer i motsatt retning. Lampen som lyser viser strømmens retning. Snu polariteten og se at den andre lampen lyser.
  • Kople denne kretsen til en signalgenerator ved lav utgangsfrekvens, og studer hvordan LED-lampene lyser vekselvis i takt med signalet, og hvordan sinus- og firkanbølger viser ulike lysmønstre.
  • Øke signalfrekvensen slik at begge LED-lampene tilsynelatende lyser hele tiden. Bestem frekvensen da dette fenomenet inntrer. Bruk så et digitalkamera med kort lukkertid og ta et bilde av lampene, for å vise at kun én av dem lyser til enhver tid.  
  • Kople signalgeneratoren til en høyttaler og bestem den høyeste og laveste frekvensen som øret kan oppfatte. Her kan man også sammenligne hørselen til en ung deltaker og en voksen instruktør.
  • Kople signalgeneratoren til et oscilloskop, med laveste frekvens og lavest mulig sweep-rate. Da fremstår signalet som en enkelt prikk på skjermen som svinger vertikalt. Ved å øke både frekvens og sweep-rate vil etterhvert de kjente bølgemønstrene danne seg på oscilloskopet. Når forståelsen av den horisontale tidsdimensjonen er etablert, er det også lettere å diskutere ulike bølgemønstre (sinus vs. firkant).
  • Kople signalgeneratoren til en uskjermet ledning og sett frekvensen i MHz-området. Lytt etter dette signalet i en radio med SSB-modus. Etablerer forståelse av at et radiosignal er intet annet enn et vekselstrømssignal.
  • Kople opp et solcelleanlegg med panel, ladekontroller og batteri. Kople på en last, f.eks. en radiostasjon. Sett inn et “toveis” amperemeter i batterikretsen for å se når batteriet tappes og når det lades. Viser hvor stor forskjell i ladestrøm det er på sollys og skygge.

Bruk av radiosamband på amatørbåndene

Det ble satt opp en komplett HF/VHF radiostasjon i aktivitetsrommet (basert på en Yaesu FT-897 transceiver) for å demonstrere og øve på radiobruk utover det interne radiosambandet omtalt tidligere.

Deltakerne brukte mitt kallesignal (LA6UIA) for å gjennomføre oppkall (i henhold til § 9 i “Forskrift om radioamatørlisens”). Siden deltakerne ikke hadde tilstrekkelig engelskkunnskaper til en internasjonal forbindelse ble det gjort oppkall via norske repeatere på VHF-båndet. Deltakerne fikk hyggelig svar på oppkall, og alle fikk anledning til å bruke radiostasjonen for å presentere seg og øve på god mikrofonbruk. De var veldig fornøyd og inspirert etter denne aktiviteten.

Navigasjon etter GPS-signaler

Leirstedet er omgitt av et skogholt egnet for “skattejakt”. Jeg hang opp refleksbrikker langs en spasertur i skogen på ca 1,5 km. Den nøyaktige GPS-posisjonen ved hver brikke ble notert og publisert på flipover i aktivitetsrommet.

Deltakerne ble delt inn i par, utstyrt med gamle android-baserte mobiltelefoner (de fikk ikke ha sine egne telefoner i aktivitetstiden) med app’en “GPS Test Plus”. I denne app’en kan GPS-posisjoner legges inn som waypoints, og siden navigere til disse. Appen viser ikke kartinformasjon, kun retning og avstand til målet. Målet med aktiviteten var å lære hvordan posisjonsdata (lengde- og breddegrad) beskriver et punkt på jordoverflaten, og at det er mulig å regne ut retning og avstand til en posisjon fra eget ståsted.

Deltakerne skrev inn posisjonene til alle refleksbrikkene (jeg kontrollerte at de var korrekte) inn i appen, deretter sendt ut i to par for å finne brikkene i terrenget. De to parene ble sendt ut i motsatt retning for at de ikke skulle dilte etter hverandre. Til turen ble de også utstyrt med sambandsradio i tilfelle de skulle trenge hjelp underveis.

Skogsterrenget har kystpreg med mye kratt, og deltakerne ville ikke fullføre oppgaven ved å følge retningen i en rett linje. De måtte derfor “lese” terrenget i tillegg til å avlese GPS-appen. Alle deltakerne fullførte oppdraget og var ved godt mot. 

Aktiviteter som var planlagt, men ikke gjennomført

En del planlagte aktiviteter ble planlagt, men ikke gjennomført. Her er en kort begrunnelse hvorfor:

Lodding

Det finnes byggesett for elektroniske kretser som egner seg for nybegynnere. De er billige og finnes på de vanlige Internett-butikkene (Aliexpress, Banggood). Loddebolter var tatt med sammen med diverse verktøy. Lodding ble allikevel ikke gjennomført fordi:

  1. Aktivitetsrommet var nyoppusset og jeg ønsket ikke svimerker på bordene. Jeg ville trengt plater eller gamle bord for formålet for dekke dem, noe som ikke var mulig å finne.
  2. Opplæring i lodding krever mye oppmerksomhet fra instruktøren, og med 8 deltakere vurderte jeg det som ugjennomførbart.
  3. Ganske mye tid, kanskje mer enn en full dag, ville vært nødvendig for gjennomføringen, og ville fortrengt andre aktiviteter.
  4. Selv for enkle kretser er sjansen stor for mislykket konstruksjon og en krets som ikke virker. Dårlig erfaring for en deltaker.
  5. En loddestasjon krever mye mer verktøy enn bare en loddebolt, noe som ikke var tilgjengelig. Mye verktøy måtte gå på omgang.

Programmering/koding

Jeg hadde skaffet et tilstrekkelig antall PCer for å kunne gjennomføre programmering på Micro:Bit og med Scratch. Når dette ikke ble gjennomført etter planen skyldtes det disse faktorene:

  1. Deltakerne hadde sterk trang til å game når de fikk anledning til å bruke PC, noe som ikke var ønskelig.
  2. Deltakerne hadde varierende ferdigheter og erfaring med programmering, så de ville trengt ulike oppgaver, med øket veiledningsbehov som resultat.
  3. Tiden som ville krevdes for et antall programmeringsprosjekter ville fortrenge andre ønskede aktiviteter.

Satelittkommunikasjon

Jeg hadde tatt med nødvendige radioer, antenner og programvare for å ta ned samtaletrafikk fra lavtflyvende satelliter, og jeg hadde valgt SO-50 som kandidat. Den sender (downlink) på 145,800 MHz med FM-modulasjon. Den har en banevinkel på 64 grader og befinner seg innen rekkevidde et par ganger om dagen. Planen var å bruke en håndholdt beam-antenne og følge satelitten med den idet den passerte, basert på beregninger gjort i programmet “Gpredict”. Når dette ikke ble gjennomført skyldtes dette følgende forhold:

  1. Eksperimentet ville kreve en del teknisk teoriprat i forkant, og deltakerne hadde lite tålmodighet for å sitte stille og høre på.
  2. Jeg følte meg ikke sikker på hvor “magisk” deltakerne ville oppleve det å høre noen som pratet på radioen, selv om de visste at dette gikk via verdensrommet.

Oppkopling av en radiostasjon

Aktiviteten var planlagt til å gi deltakerne innsikt i sammenkopling av de komponentene som inngår i en radiostasjon, og gi dem forståelse for resonans og avstemming av en antenne. Dette ble forlatt fordi:

  1. Det erfaringsmessig kan ta lang tid før man oppnår kontakt med noen.
  2. Utstyret var best egnet for morsekommunikasjon, som deltakerne ikke ville forstå.
  3. Forståelsen av resonans og avstemming var trolig utenfor deltakernes modenhet.

Morsekommunikasjon

Med de egenbygde morsenøklene var idéen at de skulle bruke dem til virkelig kommunikasjon, gjennom spill. Jeg hadde forberedt spillet “battleship” slik at deltakerne kunne sitte parvis med sine morsenøkler og gi hverandre de nødvendige beskjedene med morsetegn. Ikke gjennomført fordi:

  1. Det ville krevet mye øvelse i forkant, da spillets regler må forstås og trenes på før man kan erstatte den talte kommunikasjonen med morsesignaler
  2. Det var ikke mulig å bruke de selvbygde morsenøklene over sambandsradioene, da pipelyden derfra var for lys. Det ville derfor vært nødvendig at deltakerne satt parvis innenfor hørevidde, noe som ville tatt vekk litt av “magien” ved bruk av morse.

Konklusjonen fra disse ikke gjennomførte aktivitetene er at jeg hadde overvurdert hva som lot seg gjennomføre innen den tiden vi hadde til rådlighet (3×5 timer). Jeg hadde også overvurdert hvor mottakelige deltakerne var for prat om teori, og undervurdert hvor mye assistanse deltakerne trenger i slike aktiviteter. 

Vedlegg: Lysark knyttet til forberedelsene

Til informasjon vedlegges et sett med lysark knyttet til forbredelsene til leiren. Lysarkene ble i noen utstrekning brukt som “bruksanvisninger” til de ulike eksperimentene. De inneholder enkelte detaljer som ikke er nevnt i denne rapporten, og har dessuten en del ekstra bilder.

Raspberry Pi som Internett-radio og Bluetooth-mottaker

 962 total views,  2 views today

Har du en Raspberry Pi liggende er det en enkel jobb å lage en Internet-radio med akkurat den betjeningen du liker best. Her forklarer vi hvor enkel og grei denne oppgaven er.

(c) Anders Fongen, april 2024

Innledning

Min daglige radiolytting starter til frokosten, hvor jeg foretrekker at ett trykk skal starte radioen og gi meg morgensendingen fra NRK. Jeg har en DAB-radio som gjør dette, men jeg ønsker også å lytte på stasjoner som ikke finnes på DAB-nettverket der jeg bor, men som kun finnes som Internett-strømmer.

I noen år hadde jeg en stemmestyrt Google Nest som tok inn radiostasjoner basert på TuneIn-appen, men siden den gang har NRK sluttet å strømme via TuneIn, så da ble det en periode DAB-radio side om side med en Bluetooth-mottaker for å spille TuneIn-stasjoner.

Jeg er ingen “storbruker” av Internet-radio, og er fornøyd med 6-8 stasjoner, inkludert NRK sine. Jeg stiller heller ingen andre krav til betjeningen enn at den skal være enkel og uten krav til øyekontakt. Den skal kunne betjenes i mørke og når jeg ikke har brillene på meg. Altså ingen touch-skjerm, men heller en knapperad.

Eller enda bedre: KUN ÉN KNAPP!

Betjeningsdesign

Internet-radioen trenger egentlig bare én knapp, som slår radioen av og på, og som kan brukes til å stege gjennom listen av forhåndlagrede stasjoner.

  • Radio av: Ett trykk slår på radioen og spiller den første stasjonen i listen
  • Radio på: Ett trykk skifter avspillingen til neste stasjon i listen, eller går til toppen av listen ved siste stasjon.
  • Radio på: Langt trykk slår av radioen

Lydsignalet fra Internet-radioen går til en separat forsterker, der finnes volumkontrollen.

Valg av maskinvare

I denne bloggartikkelen har jeg forklart hvordan en Internett-radio kan bygges på en mikrokontroller (Raspberry Pico) og en separat MP3-dekoder. Dette var et morsomt prosjekt, men denne gangen skal jeg presentere noe enklere som lar seg realisere på én kveld:

Jeg har noen eldre Raspberry Pi versjon 3 liggende. Disse kan startes med Linux, de har programvare for å dekode MP3, og de har en analog lydutgang som kan koples til en forsterker eller et headsett. De har også WLAN-adapter som kan motta datastrømmer via hjemmenettet. I sum er Raspberry Pi noe dyrere enn de to komponentene jeg brukte i nevnte bloggartikkel, men i tillegg til en enklere konstruksjon oppnår vi også å ha hele kretsen inni et standard kabinett for Raspberry Pi.

Den ene knappen som tillater brukerbetjeningen er av den typen som ofte brukes til eksperimentering. Den koples til GPIO-bussen og limes til lokket på kabinettet.

Den analoge lydporten på Raspberry Pi hadde et rykte for dårlig lydkvalitet i tidligere versjoner av maskinvaren, men i Pi versjon 3 er denne kvaliteten tilfredsstillende.

Valg og konstruksjon av programvare

Ved installasjon av den anbefalte Linux-versjonen for Raspberry Pi, kalt Raspbian, blir det også installert et avspillingsprogram som heter VLC. Dersom man kopler skjerm og tastatur til Pi er VLC egentlig alt vi trenger for å strømme radioprogrammer. Brukerdesignet som vi har bestemt, utelukker derimot denne løsningen, vi skal kun ha Rasperry Pi i en boks ved siden av forsterkeren vår.

Vi kommer derfor til å lage et enkelt Python-program som avleser tilstanden på betjeningsknappen (trykket ned eller fri), som har en liste over adresser til de lagrede radiostasjonene, og som starter og stopper VLC-programmet med den ønskede radiostasjonen. Programkoden er vist lenger ned i denne artikkelen.

La oss starte med hvordan betjeningsknappen kan avleses av Pythonprogrammet. Python på Raspberry Pi kommer med ferdiginstallerte moduler for å behandle data (sende og motta) via GPIO-bussen, som er raden med pinner langs kanten av kretskortet.

Raspberry Pi GPIO pinout

Vi kopler knappen til pin 3 (også kalt GPIO2) og pin 6, som er jordforbindelse. GPIO2 er koplet til en intern pull-up motstand, som medfører at det står en spenning på porten når knappen er fri (avlest verdi 1) og med jordkontakt når knappen er trykket ned (avlest verdi 0). Koden for å avlese verdien på pin 3 ser slik ut:

import RPi.GPIO as GPIO
GPIO.setmode(GPIO.board)
GPIO.setup(3,GPIO.IN) # Setter pin 3 som inndata-port
verdi = GPIO.input(3) # avleser verdien på pin 3.

Når man avleser knapper på denne måten må man ta hensyn til såkalt prell (switch bouncing), og vi har i denne koden valgt å måle tilstanden over noen millisekunder for at prellet skal roe seg. Derfor er Python-koden mer omstendelig enn om prell ikke fantes.

Utenom denne koden for å avlese langt og kort trykk på betjeningsknappen finner vi også kode for å starte VLC-programmet gjennom en såkalt subprosess, som utføres i parallell med resten av Python-programmet. Dermed kan vi avlese knappetrykk også mens VLC-programmet kjøres.

Trenger ikke grafisk brukergrensesnitt

VLC kjører normalt med et grafisk brukergrensesnitt, men det er bortkastet ressursbruk i vårt tilfelle hvor ingen skjerm er tilkoplet. Vi bruker derfor programmet CVLC, som gjør det vi ønsker med konsollbetjening (kommandolinje) og mindre ressursbruk. Et eksempel på hvordan vi starter og stopper CVLC i en subprosess ser slik ut:

import subprocess,time
url = "http://lyd1.lokalradio.no/oradio_hq"
p = subprocess.Popen(['cvlc',url]) # Starter avspillng
time.sleep(5)                      # Venter i 5 sekunder mens cvlc spiller
p.kill()                           # Stopper så avspillingen

Full programkode

Denne forklaringen av virkemåten burde være dekkende for å forstå den aktuelle programkoden som brukes. Den ser slik ut:

import RPi.GPIO as GPIO
import time,subprocess

button = 3
GPIO.setmode(GPIO.BOARD)
GPIO.setup(button,GPIO.IN)

b = GPIO.input(button)

radio_urls = ["http://lyd.nrk.no/nrk_radio_p1_innlandet_mp3_m?_hdr=0",\
              "http://lyd.nrk.no/nrk_radio_klassisk_mp3_m?_hdr=0",\
              "http://lyd.nrk.no/nrk_radio_jazz_mp3_m?_hdr=0",\
              "https://dispatcher.rndfnk.com/br/br2/live/mp3/mid",\
              "http://lyd1.lokalradio.no/oradio_hq",\
              "http://freshgrass.streamguys1.com/folkalley-128mp3"]
current_channel_number = 0

def start_radio_channel(url):
    return subprocess.Popen(['cvlc',url])

def this_channel():
    return radio_urls[current_channel_number]

def switch_channel():
    global current_channel_number
    global radio_urls
    current_channel_number = (current_channel_number+1) % len(radio_urls)
    return this_channel()

def button_switch():
    global button
    if GPIO.input(button) == 0:
        # Debounce, wait for 100 ms
        time.sleep(0.1)
        if GPIO.input(button) == 0:
            return 1 # True
    return 0

def button_wait():
    # If button already pressed, wait until released
    while button_switch() == 1:
        time.sleep(0.1)

    while button_switch() == 0: #This call returns in 0.1s
        time.sleep(0.1) # Loop consumes 0.2s per iteration
    # Do not return until the button is released, but allow
    # a long press to switch off the radio
    loop_counter = 0
    while button_switch() == 1: # returns i 0.1s
        loop_counter += 1
        if loop_counter == 15: # 3 seconds hold
            return 2
        time.sleep(0.1) # Another 0.1s
    return 1

radio_process = None
while True:
    c = button_wait()
    if c==2 and radio_process != None:
        radio_process.kill()
        radio_process = None
    elif c==1:
        if radio_process == None:
            current_channel_number = 0
            radio_process = start_radio_channel(this_channel())
        else:
            radio_process.kill()
            radio_process = start_radio_channel(switch_channel())

Demonstrasjonsvideo

Her følger en kort demonstrasjonsvideo:

Her vises hvordan Internet-radioen lar seg betjene med en enkel knapp

Oppsummering, forslag til flere funksjoner

Det finnes masse eksempler på Internet-radioer som bygges selv, denne som jeg her viser er svært enkelt og er laget for mine behov. For dine egne behov, bruk gjerne deler av denne programkoden og lag dine egne funksjoner. Eksempler på en videreutvikling kan inkludere slike funksjoner:

  • Flere knapper, kanskje én for hver forhåndslagrede stasjon
  • En roterende bryter (rotary encoder) for å velge stasjon
  • Et display (LED eller OLED) som viser stasjonsnavn
  • Et web-grensesnitt for å redigere stasjonslisten
  • Klokkefunksjoner for automatisk avspilling (vekkerklokke)
  • Bluetooth-mottaker for avspilling fra mobiltelefonen (beskrevet her)

Mottak og bruk av ADS-B signaler fra fly

 1,232 total views,  3 views today

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

 734 total views,  3 views today

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.

CMS – Et lettvekts samarbeidssystem for mindre brukergrupper

 773 total views

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)

 1,060 total views

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

 975 total views

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.