Mottak og bruk av ADS-B signaler fra fly

 1,317 total views,  1 views today

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

(c) Anders Fongen, februar 2024

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

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

Hva du trenger for å motta ADS-B

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

Oversikt over konfigurasjonen som skal vises her

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

Konfigurasjon av Raspberry pi (2)

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

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

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

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

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

Utdata fra dump1090

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

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

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

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

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

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

1 – Hovedprogram

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

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

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

# Configure wlan
wlan_config()

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

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

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

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

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

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

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

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

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

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

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

3- Styre en direktiv antenne mot flyet

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

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

from machine import Pin, PWM
class AzElPlatform:

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

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

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

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

Oppsummering

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

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

Styring av servomotor fra Raspberry Pico og Micropython

 778 total views

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

(c) Anders Fongen, februar 2024

Innledning

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

Servomotoren SG90

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

Pulsbreddemodulasjon

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

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

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

PWM-signal i Micropython

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

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

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

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

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

Oppkopling av SG90

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

Pinout for Raspberry Pico

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

Her vises hvordan Raspberry Pico koples til en SG90 servomotor

Kalibrering av duty cycle

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

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

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

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

En forbedring av programmet ovenfor er derfor slik:

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

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

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

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

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

Eksempel på kamerastyring i to akser

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

Enkel Azimuth Elevation rotor basert på to SG90 motorer

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

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

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

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

Python-klasse for styring av en Az-El plattform

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

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

from machine import Pin, PWM
class AzElPlatform:

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

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

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

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

Demonstrasjonsvideo

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

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

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

Demonstrasjon av Azimuth-Elevation platform med to SG90 servomotorer.