Dette blir en kort artikkel om en app (Android og IOS) som tilbyr en meldingsutveksling over alle slags radioer som har et talesamband.
(c) 2024 Anders Fongen
Appen heter Rattlegram, og gjør én eneste jobb: Konverterer tekstmeldinger til et lydsignal, og omvendt. Den enkleste måten å demonstrere appen på er å legge to enheter (Android eller IOS) side om side og la begge kjøre appen. Det som skrives inn på den ene enheten, vises på den andre, og vi hører klart lydsignalet om bærer innholdet av meldingen. Man kan også eksperimentere med ulik avstand mellom enhetene og varierende bakgrunnsstøy for å danne seg et inntrykk av hvor robust kommunikasjonen er.
Merk allerede nå at mobilenheten aldri bruker sine radiosendere: Ingen WiFi, Bluetooth eller 5G. Ingen av eksperimentene her forutsetter mobildekning på stedet.
Det neste trinnet i eksperimenteringen er å sende via to transceivere som hver er innen hørevidde for sin enhet som kjører rattlegram. Først tester vi med noen lisensfrie UHF-radioer fra Biltema, for å vise at radioamatørutstyr ikke er påkrevet for å utveksle tekstmeldinger. For å sende en melding holdes sendeknappen (PTT) på radioen inne mens sendefunksjonen på appen aktiveres. På mottagersiden ligger radioen og Andoid/IOS-enheten ved siden av hverandre og trenger ingen betjening for å motta meldinger.
Her er en video hvor dette blir demonstrert:
Siden forsøker vi det samme eksperimentet med to VHF-transceivere for amatørbåndet (144 MHz), og oppnår det samme resultatet.
Fordelen med “ubetjent” mottak av meldinger
Hva er nå fordelen med en slik meldingsutveksling fremfor ganske enkelt å sende talemeldinger? Den mest opplagte fordelen er at mottageren trenger ikke å være betjent, men kan samle opp meldinger til neste gang operatøren kommer innom og sjekker displayet. En annen er muligheten for å sende komplisert tekstinnhold, f.eks. en url, og innhold fra copy&paste uten behov for bokstavering over et talesamband. En tredje er at sendte og mottatte meldinger blir lagt i en liste som fungerer som en logg over meldingsutvekslingen.
Et tenkt brukstilfelle er slik: Operatørene har alle radioene stand by i lyttemodus, og med Rattlegram på en mobil ved siden av. Dersom operatøren må forlate radiostasjonen for et stund, vil mobilenheten i mellomtiden samle opp mottatte meldinger og vise dem i listen. En stasjon A som anroper stasjon B uten å få svar, kan derfor følge opp med en Rattlegram-melding med innholdet “Gjør et oppkall når du er tilbake” eller lignende.
Parrot-funksjonen: Digipeater
I et tredje eksperiment ble “parrot”-funksjonen i Rattlegram undersøkt. Med denne funksjonen aktivert vil mottatte meldinger sendes ut igjen etter noen sekunder. En lik funksjon forvisser avsenderen om at meldingen er korrekt mottatt, og den gjør det mulig med indirekte overføring av melding via en digipeater (på en fjelltopp el.lign) mellom parter som ikke har direkte radiokontakt.
En slik funksjon forutsetter at radiosenderen kan sende ubetjent, uten at noen trykker inn PTT-knappen. Appen har ingen mulighet for å styre en PTT-funksjon, så radioen må settes opp med VOX-kontroll (sending starter når mikrofonen detekterer lyd). Radioen kan fortsatt være akustisk koplet til mobilenheten (se bilde), men i dette eksperimentet måtte følsomheten på radiomikrofonen settes til høyest følsomhet, og mobilenheten måtte stille høyttaleren på full styrke og plasseres nærmest mulig radioens mikrofon.
Et annet alternativ er å kople en kabel mellom radio og mobilenheten, muligens via et eksternt lydkort som koples til mobilenhetens USB-port med en OTG-kabel. Her er det en del plunder som venter, så det kan kreve litt prøving og feiling før dette fungere som ønsket. En kablet konfigurasjon er vist i videoen nedenfor, og i dette tilfellet ble det valgt å bruke to radioer for henholdsvis sending og mottak. En opplagt fordel med en kablet forbindelse er at operasjonen blir lydløs og mindre følsom for annen støy i rommet.
Her vises en video som demonstrerer digipeater-konfigurasjon av Rattlegram:
Systemdiagram
På bildet nedenfor vises et slags systemdiagram, hentet fra Ribbit-prosjektet, som står bak Rattlegram-appen. Den viser hvordan to mobilenheter (uten bruk av 5G, WiFi eller BT) kan utveksle meldinger gjennom radioer (akustisk koplet eller kablet) med direkte link mellom seg, eller via en talerepeater eller digipeater. Skissen viser også en mulighet for å bruke mobilenhetens egen 5G-radio, men det er ikke implementert i denne versjonen av Rattlegram.
Hvilke radioforbindelser virker/virker ikke?
Rattlegram benytter en såkalt OFDM-modulasjon og såkalt feilrettende koder for å gjøre overføringen robust og effektiv. Det oppnås en bitrate på noe over 2 kbit/sekund, noe er veldig bra. Men erfaringene med ulike radioer og modulasjonstyper viser at mikrofon og høyttaler bør være av god kvalitet med god lydgjengivelse.
En DMR-link er sterkt komprimert og gir kun forståelig tale med dårlig lydkvalitet, og viser seg ikke å fungere for Rattlegram, noe som ikke er overraskende.
En FM-link i VHF- (145 MHz) og UHF-båndet (433 MHz) virker, som vist i eksperimentvideoene.
En FM-link via en lokal VHF/UHF crossband-repeater fungerer fint, som ventet
En FM-link via en “offentlig” VHF-repeater fungerte ikke, skyldtes muligens for svakt signal
Overføring via en HF-link fungerer fint med FM- eller USB-modulasjon på frekvensene 7-29 MHz. De to HF-transceiverne sto like ved hverandre, så en link med “realistisk” signal-støy forhold ble ikke testet.
LoRa er en radioteknologi som overfører digitale data med lav hastighet, men som oppnår lang rekkevidde med lav utstrålt effekt. Radioene er billige, men kan kreve noe programmering for å kople dem til en datamaskin. Vi viser noen eksempler på hvordan.
(c) Anders Fongen, mai 2024
Innledning
Fra radioteorien kan vi finne formler som viser den øvre grensen for overføringshastighet av digitale data, og at denne grensen synker ved svakere mottatt signal (dårligere signal/støy forhold), Dersom vi ikke trenger høy overføringshastighet kan vi altså benytte radioer med lav utgangseffekt og lang rekkevidde. Radioer med lav utgangseffekt kan være små og lette, ha liten antenne og lang driftstid på batteriet. Dette er akkurat hva vi trenger i såkalte “Internet-of-Things”-anvendelser, hvor f.eks. batteridrevne sensorer skal overvåke miljøvariabler i et gartneri, eller som skal detektere inntrengere på et sperret område.
Long Range radio
En radiotype som egner seg for dette formålet kalles LoRa, en forkortelse for “Long Range”. Vi snakker ikke om en tradisjonell radio med boks og knapper, men et kretskort som koples opp og kontrolleres av en datamaskin eller mikrokontroller. Under vises bilder av LoRa-radioer i to utgaver:
Modulen til venstre er billigst, men vanskeligst å jobbe med, fordi endekontaktene står veldig tett og er vriene å lodde. Denne modulen har SPI-grensesnsitt, i motsetning til modulen til høyre, som har et seriegrensesnitt. Den til høyre har pinner med 0.1 tommes avstand som passer i vanlige eksperimentbrett (breadboard) og er derfor enklere å jobbe med. Vi har gjort eksperimenter med denne modulen, den lages av EByte og kan levere opptil 1 watt (30 dBm) utgangseffekt.
LoRa-modulen koples til en datamaskin/mikrokontroller for konfigurasjon og datatrafikk. I denne artikkelen har vi benyttet Raspberry Pico. Andre enheter, som Arduino, ESP32 eller Raspberry Pi kan også brukes, men da med nødvendige endringer i konfigurasjonen og programmet. Vi vil referere til denne enheten som mikrokontrolleren.
LoRa-moduler er alle basert på en brikke fra Semtech med betegnelsen SX1276/77/78/79. Disse 4 modellene er realtivt like og adskiller seg i hovedsak med ulike radiofrekvenser. Det er ikke likegyldig hvilke frekvenser du kan bruke! Norske myndigheter (NKOM) har strenge regler for radiosendinger som bør overholdes. Flere opplysninger om dette kommer senere i artikkelen. Her følger noen detaljer:
Selve databladet til Semtech’s LoRa-brikke finnes på denne lenken, Den trenger du ikke lese før du vil gå i dybden på virkemåten.
E32 LoRa-modul fra Ebyte
E32 LoRa-modulen som er brukt i dette eksperimentet er vist til høyre på bildet over. Der fremgår det også hvilke funksjoner som ligger på de ulike pinnene. Disse pinnene brukes på denne måten:
M0, M1 – inputsignal (fra mikrokontrolleren til E32) som avgjør om LoRa-modulen skal settes i konfigurasjonsmodus eller dataoverføringsmodus. De koples til to porter på mikrokontrolleren, i vårt tilfelle GPIO18 og GPIO19.
RXD, TXD – for å sende og motta data (konfigurasjonsdata eller nyttetrafikk, avhengig av signalene på M0 og M1). De koples “i kryss” til tilsvarende porter på mikrokontrolleren, altså med TXD til RX-porten og RXD til TX-porten.
AUX – Et utgående signal fra LoRa-modulen som signaliserer om den kan motta data, eller om den ønsker å overføre data fra mottatte radiosignaler. I vårt eksperiment viste det seg at denne pinnen trenger en ekstern pullup motstand!
Vcc og GND – Koples til 3.3 volt spenningsforsyning og jord.
Tilkopling til Raspberry Pico
Pinnene på E32 LoRa-modulen ble koplet til pinner/porter på Raspberry Pico på denne måten.
M0 – GPIO18, M1 – GPIO19
RXD – TX på UART0, pin 21
TXD – RX på UART0, pin 22
AUX – GPIO20, med pullup-motstand (1 kOhm) til 3.3v
Vcc – 3.3v pin 36
GND – GND på pin 38
E32-modulen til høyre koplet til en Raspberry Pico. Pullup-mostanden for AUX-pinnen skimtes mellom ledningene.
Nødvendig programvare
Det finnes programvarebiblioteker i microPython for kontroll av brikken SX1276 (og de andre variantene) med SPI-grensesnitt, men de kan ikke brukes mot EByte’s LoRa-moduler. EByte har plassert sin egen mikrokontroller på modulen med sitt eget grensesnitt for konfigurasjon. Programvare vi kan bruke må altså være tilpasset dette.
Vi fant et bibliotek på GitHub hvor enkeltfiler i Python kan lastes ned og installeres i mikrokontrolleren fra denne lenken. De var til stor hjelp, men krevde også noen endringer for å virke korrekt med de modulene som vi eksperimenterte med. Forklaring følger.
Nødvendige endringer i programvaren
Programvaren fra lenken vist ovenfor virket ikke uten endringer. Vi kan ikke fastslå om det skyldes programmeringsfeil eller endret firmware i E32-modulen. Dersom det siste er tilfelle (noe vi antar) vil det være nødvendig med litt eksperimentering dersom disse endringene ikke gir ønsket resultat.
Programkoden for å konfigurere E32-modulen ligger i filen lora_e32.py. I funksjonen get_configuration() vil responsen fra modulen leses i linje 463 og siden analyseres. Vår observasjon er at de ønskede 6 bytes kommer med en ekstra byte foran og bak (med verdien 0), og disse må fjernes før responsen blir analysert.
data = self.uart.read()
skal derfor erstattes med
datax = self.uart.read()
print(datax) # For kontrollformål
data = datax[1:-1]
Funksjonen clear_UART_buffer() på linje 591 vil tømme mottaksbufferen ved å lese dataene som ligger der, inntil det skjer en “timeout”. Dersom det er trafikk på kanalen vil den stadig motta data, og denne funksjonen vil tilsynelatende “henge”. En endret løsning for denne funksjonen er å spørre etter antall bytes i bufferet, for deretter å lese dette antall bytes. Funksjonen foreslås gitt dette innholdet:
def clean_UART_buffer(self):
n = self.uart.any()
self.uart.read(n)
I filen set_configuration.py er det en feil i linje 56. Leddet wakeUpTime skal erstattes med wirelessWakeupTime.
Eksempel på enkelt sending og mottak
Med de endringene som er vist ovenfor, og med AUX-linjen koplet til en ekstern pullup-motstand (vi brukte en uke på å finne ut at dette var årsaken til at ingenting funket som forventet), skal det nå ikke mange programlinjene til for å sette opp en forbindelse mellom to E32-moduler. Her er eksempel på sende- og mottaks-modul:
Sending av en tegnstreng (klokkeslett) med faste intervaller:
from lora_e32 import LoRaE32, Configuration
from machine import UART, Pin
import utime
from lora_e32_constants import FixedTransmission, TransmissionPower, AirDataRate
from lora_e32_operation_constant import ResponseStatusCode
# Initialize the LoRaE32 module
uart2 = UART(0, rx=Pin(17), tx=Pin(16))
lora = LoRaE32('433T30D', uart2, aux_pin=20, m0_pin=18, m1_pin=19)
code = lora.begin()
print("Initialization: {}", ResponseStatusCode.get_description(code))
# Set the configuration to default values and print the updated configuration to the console
# Not needed if already configured
configuration_to_set = Configuration('433T20D')
configuration_to_set.CHAN = 27
configuration_to_set.OPTION.fixedTransmission = FixedTransmission.TRANSPARENT_TRANSMISSION
configuration_to_set.OPTION.transmissionPower = TransmissionPower('433T30D').\
get_transmission_power().POWER_30
configuration_to_set.SPED.airDataRate = AirDataRate.AIR_DATA_RATE_010_24
code, confSetted = lora.set_configuration(configuration_to_set)
print("Set configuration: {}", ResponseStatusCode.get_description(code))
# Send a string message (transparent)
while True:
message = str(utime.time()) + '\n'
code = lora.send_transparent_message(message)
print(message)
utime.sleep(5)
(her brekker noen kodelinjer, men med copy-paste inn i en teksteditor retter dette seg).
For å motta disse meldingene på en annen radio (som også varsles med et pip, og innholdet vises i et OLED-display), kan denne koden brukes:
from lora_e32 import LoRaE32, Configuration, BROADCAST_ADDRESS
from machine import UART, Pin, I2C
import ssd1306
import utime
from lora_e32_constants import FixedTransmission, AirDataRate
from lora_e32_operation_constant import ResponseStatusCode
# using default address 0x3C
i2c = I2C(1,sda=Pin(14), scl=Pin(15))
display = ssd1306.SSD1306_I2C(128, 64, i2c)
print("I2C ok")
# Initialize the beeper
beep = Pin(2,Pin.OUT)
# Initialize the LoRaE32 module
uart2 = UART(0, rx=Pin(17), tx=Pin(16))
lora = LoRaE32('433T30D', uart2, aux_pin=20, m0_pin=18, m1_pin=19)
print("UART ok")
code = lora.begin()
print("Lora started")
configuration_to_set = Configuration('433T30D')
configuration_to_set.CHAN = 27
configuration_to_set.OPTION.fixedTransmission = FixedTransmission.TRANSPARENT_TRANSMISSION
configuration_to_set.SPED.airDataRate = AirDataRate.AIR_DATA_RATE_010_24
print("Configration ready")
code, confSetted = lora.set_configuration(configuration_to_set)
#print("Set configuration: {}", ResponseStatusCode.get_description(code))
print("Waiting for counter messages...")
while True:
if lora.available() > 0:
code, value = lora.receive_message(delimiter=b'\n')
#print(ResponseStatusCode.get_description(code))
# Display on OLED display
display.fill(0)
display.text(value,5,8)
display.show()
# Beep to notify the reception
beep.on()
utime.sleep(0.2)
beep.off()
print(value) #Print to console also
#utime.sleep_ms(2000)
Noen av disse programlinjene kan utelates om ønskelig, fordi de kun gir informasjon til konsollet, eller de setter konfigurasjonsverdier som er standard ved oppstart av modulene. Merk også at verdiene som beskriver pin-nummer og gpio-nummer må endres om du ønsker å bruke andre porter på Raspberry Pico, eller om du bruker en annen mikrokontroller, f.eks. ESP32.
Adressering og kringkasting
I et nett av LoRa-moduler vil det være behov for å sende meldinger til enkeltmottakere (Unicast), basert på deres adresser, samt å sende til alle (Broadcast). Også sendinger til enkeltmottakere kan avlyttes av andre, så det er ingen beskyttelse av konfidensialitet i LoRa. Sendinger kan skje på tre måter:
Fixed Unicast: Sendingen påføres mottakerens adresse sendes som Unicast
Fixed Broadcast: Sendingen påføres en kringkastingsadresse (xFFFF), og sendes som Broadcast
Transparent: Sendingen påføres ikke adresse, og sendes som Broadcast
Sender og mottaker må selvfølgelig være konfigurert til samme radiokanal. Maksimal lengde på en melding er 58 bytes i Transparent modus, 55 bytes i Fixed modus.
En mottaker kan konfigureres som Fixed Unicast, Fixed Monitor (og motta også Unicast-meldinger adressert til andre), eller som Transparent. Tabellen under viser hvilke kombinasjoner som gir overføring (merket med ‘+’-tegnet) mellom sender og mottaker.
Sending
Mottak
Fixed Unicast
Fixed Monitor
Transparent
Fixed Unicast
adresse
+
–
Fixed Broadcast
+
+
+
Transparent
–
+
+
Forhold mellom sender- og mottakerkonfigurasjon og hvilke kombinasjoner som tillater overføring
Sendingsvarighet og kanalbredde
LoRa har relativt lav overføringshastighet, maksimum 19200 bits/sekund og egner seg ikke til overføring av bilder, video eller lyd. Ved høy hastighet vil rekkevidden være lavere og feilraten høyere. Programeksemplet ovenfor sender meldinger hvert 5 sekund ved 2400 bits/sekund, og ved å studere dette signalet med en SDR-dongle og programmet GQRX (på Linux, på Windows kan SDR# brukes) kan vi fremstille et “fossefall” som viser både kanalbredde (horisontal akse) og sendingsvarighet (vertikal akse). Slik ser filmen ut:
Det er mulig å anslå båndbredden på dette signalet til ca 500 kHz, fra 432,75-433,25 MHz. Det er relativt bredt, og byr på problemer knyttet til lovligheten av å bruke så “brede” signaler. Båndbredden varierer ikke ved forskjellig sendeeffekt og overføringshastighet. Selve LoRa-kretsen kan konfigureres til ulike båndbredder, men det er ikke tilgjengelig på E32-modulen.
Overføringstiden lar seg vanskelig måle på vannfallet ovenfor, men vi lyttet til signalet med en vanlig radio og gjorde lydopptak til programmet Audacity. Dette programmet kan vise et lydsignal (vertikal akse) langs en tidsakse (horisontal akse) og gi oss en pekepinn om når signalet starter og slutter. Slik ser bildet ut på Audacity:
Ved å velge ut det tidsintervallet som sendingen foregår i, vil programmet vise varigheten nederst på skjermen, nemlig 302 millisekunder. Som ventet blir dette intervallet kortere ved høyere overføringshastighet: 63 millisekunder ved 9600 bits/sekund, og 32 millisekunder ved 19200 bits per sekund.
Rekkevidde for meldingsoverføring
LoRa er designet for å gi lang rekkevidde ved lav utstrålt effekt og lav overføringshastighet. I tillegg til disse to parametrene vil antennekonstruksjon og -plassering ha en betydning. Vi satte opp et eksperiment med en LoRa-mottaker i bilen koplet til en antenne på biltaket. Denne antennen er en “pisk” med ca 35 cm lengde.
Antennevalg: Senderen befant seg i huset, koplet til én av to antenner: (1) En kort antenne for håndapparat, plassert i vinduskarmen, eller (2) en 5-elements beam plassert på loftet, pekende ut av loftsvinduet.
Utstrålt effekt: Effekt (tilført antennen) var henholdsvis (a) 21 dBm=125 mW, eller (b) 30 dBm=1 W.
Ved å kjøre rundt i området (Lillehammer) og observere når mottak skjer (programkoden vist ovenfor gir et kort pip ved mottak), danner vi oss et uformelt inntrykk av rekkevidden. Terrenget i området er bebygget og kupert. Resultatene var som følger:
(1)(a) – Stabilt mottak til ca 750 meters avstand, sporadisk mottak utover dette.
(2)(a) – Stabilt mottak til ca 750 meters avstand, men også andre steder med synslinje til huset opp til 3,3 km avstand.
(2)(b) – Stabilt mottak i hele byområdet, langs E6 sydover opptil 10 km avstand. Stedvis mottak med en åskam mellom sender og mottaker (Saksumdalen). Ingen mottak “bak” antennen (en beam-antenne er direktiv og stråler mest i én retning).
LoRa og myndighetenes forskrifter
Bruk av radiosendere er strengt regulert i Norge, og det er på sin plass med noen vurderinger om hvordan LoRa-sendinger kan foregå på lovlig vis. Det er to regelverk som kommer til anvendelse: Fribruksforskriften, som stiller krav til radiosendinger for allmenheten, og Forskrift om radioamatørlisens, som angir krav til radiosendinger for personer med radioamatørlisens (radioamatørlisens tildeles personer som har bestått en såkalt lisensprøve).
La oss først ta for oss vilkårene i Fribruksforskriften, siden den vedkommer alle. Kapittel IV (§11) lister opp en del frekvenser som kan brukes, med oppgitt maksimal utstrålt effekt, båndbredde og sendetid. E32-modulen opererer med 500 kHz kanalbredde, og det er punkt (1), (4) og (5) som tillater dette, men da begrenset til 25 mW utstrålt effekt (14 dBm). Disse frekvensene ligger alle i området 800-900 MHz, som ikke støttes av den E32-modulen som er brukt i dette eksperimentet, men derimot med en annen modell: E32-900T20D. Den opererer i dette frekvensbåndet (862-931 MHz) og med mulighet for å sette utgangseffekten til 14 dBm. Merk at denne utgangseffekten vil gi betydelig kortere rekkevidde, og bruk av antenne med “gain” ikke er tillatt, siden 14 dBm er grensen for utstrålt effekt, ikke effekt tilført antennen.
Med en båndbredde satt til maksimalt 200 kHz kan også punkt (2), (3) og (6) i Fribruksforskriftens §11 brukes, opp til 500 mW (27 dBm). Slik båndbredde er ikke støttet av E32-modulen, så en annen modul basert på SX1276-kretsen må brukes. Spesifikasjonene til SX1276-kretsen oppgis til å kunne operere innenfor denne båndbredden. Rekkevidden og feilraten for en slik konfigurasjon er ikke undersøkt av oss ennå.
Merk at Fribruksforskriften også setter krav til såkalt sendetid, oppgitt i prosent. Denne verdien angir forholdet mellom sendetid og “stilletid”. 1% sendetid betyr at senderen skal være taus i gjennomsnittlig 99% av tiden.
For brukere med radioamatørlisens gjelder §7 i Forskrift om radioamatørlisens, som angir maksimal båndbredde og utstrålt effekt for de ulike frekvensbåndene. Det eneste aktuelle frekvensbånd for LoRa er 432-438 MHz, hvor maksimal kanabåndbredde er 30 kHz. E32-modulen kan ikke operere med 30 kHz båndbredde, men en annen SX-1276 basert modul kan muligens oppnå dette, men dette er ikke undersøkt av oss. Maksimal sendeeffekt for en slik konfigurasjon er hele 300 W, men vi tviler på om det finnes noe ferdiglaget produkt som tilbyr slik utstrålt effekt. Dette blir i så fall noe som må designes og konstrueres av en radioamatør.
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.
Pin 3 og 6 på GPIO-bussen koplet til en knapp i 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)
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
Radiosignalet fra fly mottas av SDR-dongelen som er koplet til en Linux-maskinens USB-port.
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).
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.
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:
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:
Et hovedprogram som etablerer en TCP-forbindelse til dump1090-noden (2), kaller så på Python-moduler for analyse, beregning og antennestyring
En modul for å analysere SBS-1 melding for posisjon og identifikasjon av fly, og for å beregne antenneretning til flyet.
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.
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:
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:
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.
Dersom Azimuth er 180-359 grader, skal den horisontale rotoren stilles til Azimuth-180 grader, og den vertikale skal stilles til 180-Elevation grader. Dvs, at den vertikale rotoren legger seg “bakover” for å dekke den venstre delen av kompassrosen. Dersom det er et kamera på denne plattformen må du ta hensyn til at bildene da blir opp-ned.
Når du skriver kode for en slik innretning må du dessuten ta hensyn til at azimuth-rotoren går “mot klokka” med økende duty cycle-verdi, og du må snu litt om på regnestykket, som vist over.
Python-klasse for styring av en Az-El plattform
Nå setter vi sammen tidligere detaljer, inkludert observerte verdier for 0 og 180 graders rotasjon, til en Python-klasse som styrer begge aksene under ett:
# Python class to control a simple Azimuth-Elevation
# platform. It uses two SG90 servo motors for the two
# axes. Since they only rotate 180 degrees, the 180
# degree range of the elevation rotor is used to cover
# the left half (180-359 degrees) of the azimuth
from machine import Pin, PWM
class AzElPlatform:
def __init__(self,azrotor, elrotor):
self.azport = PWM(Pin(azrotor))
self.elport = PWM(Pin(elrotor))
self.azport.freq(50)
self.elport.freq(50)
def setDirection(self,az,el): # Angle in degrees
# Check parameters: 0-359 and 0-90 allowed
if not az in range(0,360): return
if not el in range(0,91): return
if az>180:
az = az-180
el = 180-el # Bend elevation backwards for left half
# Experimentally established values for
# Calculation of duty cycles corresponding
# to rotor angles
dutyAz = 7800 - az * 6600/180
#dutyAz = ((180-az)/180*6600) + 1200
dutyEl = el * 7000/180 + 1200
self.azport.duty_u16(int(dutyAz))
self.elport.duty_u16(int(dutyEl))
Demonstrasjonsvideo
Her følger en video hvor denne Python-klassen blir demonstrert med følgende testprogram:
from AzElPlatform import AzElPlatform
import time
azel = AzElPlatform(16,17) # GPIO-porter for Az og El
el = 20
for az in range(90,271,10):
azel.setDirection(az,el)
time.sleep(0.5)
Denne programkoden beveger plattformen fra 90 til 270 grader i horisontalplanet, og må derfor skifte mellom innstilling nr.1 og 2 fra diskusjonen ovenfor. Legg derfor merke til på videoen hvordan Elevation-rotoren snur seg rundt samtidig som Azimuth-rotoren vrir seg en halv omdreining. Videoen demonstrerer altså hvordan vi dekker hele himmelkuppelen med to rotorer med rekkevidde 180 grader.
Demonstrasjon av Azimuth-Elevation platform med to SG90 servomotorer.
SSL (TLS) -protokoll er svært vanlig i bruk for de fleste web-stedene i Internet. Om du lager en web-applikasjon bør du sette deg inn i hvordan du konfigurerer den for bruk med SSL. Om du bruker web.py for å lage web-applikasjoner i Python, får du en veiledning i denne artikkelen.
(c) 16.01.2024 Anders Fongen
Innledning
Det første leddet i en web-adresse (URL) angir hvilken protokoll som brukes. Oftest står det https:// der, sjeldnere http://. Den første formen forteller web-leseren at forbindelsen til web-tjeneren skal bruke SSL-protokoll (også kalt TLS) for å beskytte mot avlytting og forfalskning.
Jeg gir ingen detaljert beskrivelse av SSL her, fordi det finnes andre steder på Internet, og fordi det ikke er av vesentlig betydning å kjenne detaljene i denne protokollen.
SSL gir kryptert overføring av data, slik at andre ikke kan lese innholdet av dataoverføringen om de lytter til kanalen som brukes. IP-adresser til sender og mottaker er derimot ikke kryptert, og kan oppfattes gjennom avlytting.
SSL gir også autentisering av én eller begge parter i dataoverføringen, slik at det er vanskelig å utgi seg for å hete f.eks. www.vg.no uten å presentere et digitalt nøkkelsertifikat som attesterer at det er web-tjenerens virkelige navn.
Mindre brukt, men allikevel viktig, er at SSL også kan autentisere web-leseren ved hjelp av et digitalt sertifikat, som attesterer at brukeren av web-leseren heter f.eks. Anders Fongen. Denne funksjonen i SSL kan erstatte tradisjonell innlogging med brukernavn og passord.
Bruk av sertifikater og nøkler, og hvordan sertifikater kan validere andre sertifikater, er et noe omfattende tema som jeg heller vil behandle i en egen artikkel. Begrepet kalles Public Key Infrastructure, og det finnes mye informasjon om dette på Internet.
Jeg har i en tidligere artikkel vist en web-applikasjon bygget på web.py-rammeverket og programmert i Python. Denne artikkelen vil videreføre det samme fokus på rammeverk og programmeringsspråk, og vise hvordan SSL-protokoll konfigureres i en web-applikasjon bygget på web.py.
Nøkkelpar og sertifikater
For å konfigurere en web-tjener for SSL-protokoll trenger den et nøkkelpar. Det kan utstedes av en kommersiell leverandør, noe som koster en del (fra 50€/år og oppover). Med et slikt nøkkelpar kan din web-tjener autentisere seg overfor alle klienter i Internet. Det er derimot ikke praktisk mulig å be alle brukerne av web-applikasjonen din om å betale et slikt beløp for å oppnå klient-autentisering.
For web -applikasjoner som skal brukes innenfor en gruppe, firma, forening etc., er det hensiktsmessig å selv stå for utstedelsen av nøkkelpar og sertifikater. Alle som skal bruke den aktuelle web-tjeneren må da installere et sertifikat og et nøkkelpar i sin egen web-leser, og jeg skal vise hvordan dette gjøres i Google Chrome. For Firefox er fremgangsmåten ganske lik.
Bruk helst en Linux-maskin som web-tjener, da fungerer den presenterte metoden best. Her følger et skall-skript som bruker programmet openssl til å lage et rotsertifikat (også kalt CA-sertifikat), og nøkkelpar for web-tjeneren og et nøkkelpar for én klient. Skallskriptet er ment som et eksempel som du modifiseres etter eget ønske.
Når dette skallskriptet kjøres (du blir bedt om å skrive et passord underveis, det skal brukes når du installerer klientens nøkkel i web-leseren) skapes det følgende filer på samme katalog:
ca_key.pem – Dette er utstederens private nøkkel og skal aldri kopieres eller sendes til andre
ca_cert.pem – Dette er utstederens nøkkelsertifikat som skal installeres både i web-tjeneren og i web-leseren
server_key.pem, server_cert.pem – Dette er web-tjenerens private nøkkel og nøkkelsertifikat som som skal installeres der, men ikke gis til web-leserne
client_keypair.p12 – Dette er nøkkelen og sertifikatet til én klient, beskyttet med det passordet du nettopp skrev inn.
Installasjon av nøkler og sertifikater i Google Chrome
I Google Chrome, klikk på knappen med “tre prikker i vertikal ordning” øverst til høyre
Velg så “Settings” fra menyen
I listen til venstre, klikke “Privacy and Security”
Klikk deretter på “Security”, så på “Manage Certificates”
Klikk på fanen “Authorities”, og så på knappen “Import”
Fra den fildialogen du nå får se, velg filen “ca_cert.pem”
Etter at import-jobben er fullført skal du nå se dette rotsertifikatet i listen. Navnet som vises er “org-FONGEN” dersom du ikke har endret dette i skallskriptet (noe du bør gjøre).
Nå skal klientsertifikatet og nøkkelen installeres.
Klikk på “Your Certificates” og deretter på knappen “Import”.
Fra fildialogen, velg “client_keypair.p12”.
Du må nå skrive inn passordet som du valgte da sertifikatet ble laget for litt siden.
Nå skal sertifikatet vises i listen med det navnet som du har valgt å bruke.
Installasjon av nøkler og sertifikater i web-tjeneren
Nå er web-leseren klargjort, og vi skal sette opp web-tjeneren med sine nøkler og sertifikater:
Jeg har laget en demonstrasjonsvideo som tar deg gjennom disse stegene enkeltvis, og også hvordan web-tjeneren startes og testes. Du kan se den her:
Demonstrasjon av SSL-autentisering i web.py
Takeaway
Avslutningsvis i denne artikkelen vil jeg påpeke at en del av dette stoffet også er til nytte ved bruk av andre web-rammeverk enn web.py. De fleste web-tjenere har støtte for klientautentisering med SSL, og de konfigurerer dette på forskjellige måter. Men laging av sertifikater og nøkler, samt installasjonen av klientnøkler i web-leseren, er de samme operasjonene, derfor kommer det presenterte skallskriptet til nytte.
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.
Her har jeg laget et verktøy for betjening og konfigurasjon av en enkel Internetradio basert på mikrokontolleren Raspberry Pico (tidligere presentert i denne artikkelen). Det hele blir presentert i denne videoen. Ta kontakt med meg om du ønsker å se programkoden (MikroPython)
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
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.
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.
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.
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.
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åtrykk
Beskrivelse
Koples til RPico pin
Navn på RPico
5V
Driftsspenning
40
VBUS
GND
Jordforbindelse
38
GND
CS
Chip select for SD-kortleseren
2
GPIO 1
MISO
SPI datalinje fra VS1053
6
GPIO 4
SI
SPI datalinje til VS1053
5
GPIO 3
SCK
SPI klokkesignal
4
GPIO 2
XCS
Chip select for MP3-dekoderen
9
GPIO 6
XRES
Hard reset (aktiv lav)
1
GPIO 0
XDCS
Data select (RPico->VS1053)
10
GPIO 7
DREQ
Data request (VS1053->RPico)
11
GPIO 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:
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.
Les verdien av et register. Ikke strengt nødvendig i denne artikkelen, men nyttig for å kontrollere at en skriveoperasjon er vellykket.
Sende data. I denne artikkelen vil vi sende MP3-kodet lyd til dekoderen, og det skjer på denne måten.
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:
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.
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:
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: