ADS-B Radar

Overhead Flight Display

Build your own real-time information display!

Raspberry Pi Pico Display

 

Do more with your data

Know what is flying above you!

Now that you have your own ADS-B receiver, and you’re running ADS-B Radar on your Mac, perhaps it would be great to know what airplane is currently overhead when you’re getting a coffee 🙂 The Raspberry Pico WH and the Pico 1.14" IPS LCD screen from Pimoroni come to the rescue. Although the display is small, it has a low power consumption and you can easily place it somewhere.

What will you need:

Pico WH (WiFi version with header)10 USD
Pimoroni Pico Display Pack unit20 USD
As we are already processing ADS-B signals with a Raspberry Pi Zero and dump1090, we will use this to pre-process the data we need for the Pico W. This will make it easier on the small Pico, which has limited capabilities.

 

Use the adsbdb.com public APIPublic API adsbdb.com

This API offers data on aircraft, airlines, and flight routes

On the Raspberry Pi which is processing the ADS-B data and runs dump1090-fa, we create a little shell script which will run every two minutes once we’re ready. In this case we created a directory 'pi' under the home directory where we create the file “overhead.sh” :

nano overhead.sh

    #!/bin/bash
    python3 /home/pi/overhead.py
    chmod 777 /tmp/overhead.json

Now we create the main Python script in the same directory to process the JSON file from adsbdb.com, edit url/lathome/lonhome to match your installation and name this file “overhead.py”:

nano overhead.py

    #!/usr/bin/env python3
    #!/usr/bin/env python3
    # Find closest airplane flying overhead
    # (c) JF Nutbroek
    # 08/10/2023
    from urllib.request import urlopen
    from datetime import datetime
    from datetime import timezone
    import math
    import json
    # User defined
    url     = "http://localhost/dump1090/data/aircraft.json"
    lathome = 45.9  # Position ADS-B receiver
    lonhome = 6.3   # Position ADS-B receiver
    ### Start
    # Haversine formula example in Python
    # Author: Wayne Dyck
    def distance(origin, destination):
        lat1, lon1 = origin
        lat2, lon2 = destination
        radius = 6371 # km earth radius
        dlat = math.radians(lat2-lat1)
        dlon = math.radians(lon2-lon1)
        a = math.sin(dlat/2) * math.sin(dlat/2) + math.cos(math.radians(lat1)) \
            * math.cos(math.radians(lat2)) * math.sin(dlon/2) * math.sin(dlon/2)
        c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
        d = radius * c
        return d
    # Find flights
    def find_adsbdb(flightid, hex):
        try:
            if hex == "":
                response = urlopen("https://api.adsbdb.com/v0/callsign/" + flightid)
            else:
                response = urlopen("https://api.adsbdb.com/v0/aircraft/" + hex + "?callsign=" + flightid)
            data_json = json.loads(response.read())
            aircraft = data_json['response']
            if flightid in aircraft['flightroute']['callsign_icao']:
                return flightid, aircraft
            else:
                return None, None
        except:
            return None, None
    # Write JSON for Pico W
    def writeresult(flightdict):
        json_object = json.dumps(flightdict)
        with open("/tmp/overhead.json", "w", encoding="utf-8") as outfile:
            outfile.write(json_object)
    ### Find the closest airplane
    aircraft = []
    try:
        response = urlopen(url)
        data_json = json.loads(response.read())
        aircraft = data_json['aircraft']
    except:
        exit()
    closest       = 1000               # km distance of closest plane
    closestflight = ''                 # Will hold flight number
    dictionary    = {"flight":"none"}  # Empty dictionary
    speed         = 0
    vert_rate     = 0
    altitude      = 0
    compass       = ["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW","N"]
    track         = 0
    heading       = ""
    newflight     = True
    hex           = ""
    for airplane in aircraft:
        if 'lat' in airplane:
            if 'lon' in airplane:
                if 'flight' in airplane:
                    if 'altitude' in airplane:
                        altitude = int(airplane['altitude'])
                        if 'hex' in airplane:
                            hex = airplane['hex']
                        if 'vert_rate' in airplane:
                            vert_rate = int(airplane['vert_rate'])
                        if 'speed' in airplane:
                            speed = int(airplane['speed'])
                        if 'track' in airplane:
                            track = int(airplane['track'])
                            heading =  str(track) + " degrees " + compass[min(round((track % 360) / 22.5), 16)]
                        flightdis = distance((lathome, lonhome), (airplane['lat'], airplane['lon']))
                        flighthgt = altitude * 0.0003048
                        dis = math.floor(math.sqrt(flightdis**2 + flighthgt**2))
                        if dis < closest:
                            closest = dis
                            closestflight = airplane['flight'].replace(" ", "").upper()
    if closestflight != '':
        #print(closestflight)
        #print(hex)
        flight, details = find_adsbdb(closestflight, hex)
        if flight != None:
            dictionary = {
                "flight": flight,
                "airline": details["flightroute"]["airline"]["name"],
                "altitude": str(altitude),
                "vert_rate": str(vert_rate),
                "speed": str(speed),
                "origin_iata": details["flightroute"]["origin"]["iata_code"],
                "destination_iata": details["flightroute"]["destination"]["iata_code"],
                "model": details["aircraft"]["type"],
                "origin": details["flightroute"]["origin"]["name"],
                "destination": details["flightroute"]["destination"]["name"],
                "departure": "",
                "eta": "",
                "distance": str(closest),
                "track": heading
            }
            writeresult(dictionary)
        else:
            writeresult(dictionary)
    else:
        writeresult(dictionary)

 

Create a crontabThe Raspberry Pi Crontab Explained

Refresh the data every 2 minutes

Create a 1 or 2 minute crontab to refresh the overhead flight information (note: there is a limit on the amount of requests you can send, a 1 or 2 minute interval should keep you below it):

nano /etc/crontab

    */2 *     * * *   root    sh /home/pi/overhead.sh

We need to make the JSON file we created with our script accessible for the Pico W to download:

    cd /var/www/html/
    sudo ln -s /tmp/overhead.json overhead.json

 

Prepare the PicoThe Raspberry Pico WH

Asssemble & Configure the Pico

To prepare the Pico you can press the display on the header, do it gently and make sure the USB side is indeed on the USB side of the Pico. Now we can connect the Pico W with Display and with an official USB Pico cable (most cables will not work!) to your Mac. Install Thonny for macOS and you will be able to connect to the Pico. Download & install the Pimoroni custom MicroPython (version v1.20.6 was used in this example) for the "Pico W" version (keep the boot select button pressed and then plug it into the Mac) - copy the file over and unplug & replug the Pico in the Mac. Then use Thonny to create two files on the Pico. The first one is named "secrets.py" and will hold your WiFi networks name and password:

    SSID = "Your Wi-Fi Network"
    PASS = "Your Password"

The next file on the Pico should be named "main.py" and will run every time the Pico has power and boots up. Edit the url to point to your ADS-B Raspberry Pi that runs the cron job, in this case we used: "http://192.168.68.120/overhead.json"

    # (c) JF Nutbroek
    # 08/10/2023
    from picographics import PicoGraphics, DISPLAY_PICO_DISPLAY
    from pimoroni import RGBLED
    import network
    import secrets
    import urequests
    from time import sleep
    from math import floor, pi, sin, cos
    from machine import Timer, WDT
    # User defined input
    # URL to local json file using http
    url = "http://192.168.68.120/overhead.json"
    pollinterval = 10    # Interval in seconds for a new JSON poll
    brightness   = 0.5   # Brightness of the display
    sleepafter   = 10    # Number of JSON polls without aircraft
    # ********* Start
    display = PicoGraphics(display=DISPLAY_PICO_DISPLAY, rotate=0)
    display.set_font("bitmap8")
    display.set_backlight(brightness)
    # Colors
    BLACK  = display.create_pen(0, 0, 0)
    GREEN  = display.create_pen(50, 205, 50)
    RED    = display.create_pen(200, 0, 0)
    BLUE   = display.create_pen(0, 191, 255)
    WHITE  = display.create_pen(255, 255, 255)
    ORANGE = display.create_pen(255, 140, 0)
    YELLOW = display.create_pen(255, 255, 0)
        
    # Switch LED off
    led = RGBLED(6, 7, 8)
    led.set_rgb(0, 0, 0)
    # Overhead info
    flightinfo       = {"flight":"none"}
    flight           = ""
    airline          = ""
    altitude         = ""
    vert_rate        = ""
    speed            = ""
    origin_iata      = ""
    destination_iata = ""
    model            = ""
    origin           = ""
    destination      = ""
    departure        = ""
    eta              = ""
    distradar        = ""
    heading          = ""
    xpos             = 20
    textpos          = 0
    infopos          = 0
    timedout         = 0
    # Write text centered or right aligned
    def writetext(text, y, center, size, color):
        width = display.measure_text(text, size)
        display.set_pen(color)
        if center:
            display.text(text, floor((240 - width) / 2), y, 300, size)
        else:
            display.text(text, 240 - width, y, 240, size)
    def radar(steps, rounds, message):
        for _ in range(rounds):
            for n in range(0, steps):
                display.set_pen(BLACK)
                display.clear()
                for y in range(60, 0, -10):
                    display.set_pen(GREEN)
                    display.circle(120, 68, y)
                    display.set_pen(BLACK)
                    display.circle(120, 68, y - 3)
                x1 = 120 + (cos(2 * pi * (n / steps)) * 65)
                y1 = 68 + (sin(2 * pi * (n / steps)) * 65)
                display.line(120, 68, int(x1), int(y1), 5)
                if message != "":
                    display.set_pen(WHITE)
                    display.text(message, 0, 0, 240, 2)
                display.update()
    # Read JSON
    def readjson():
        global flightinfo, timedout
        flightinfo = {"flight":"none"}
        led.set_rgb(138, 43, 226)
        try:
            if wlan.isconnected():
                flightinfo = urequests.get(url).json()
                led.set_rgb(0, 0, 0)
            else:
                led.set_rgb(255, 0, 0)
                radar(180, 1)
        except:
            led.set_rgb(255, 0, 0)
        if 'flight' in flightinfo:
            if flightinfo['flight'] == "none":
                timedout += 1
            else:
                timedout = 0
    def resetwdTimer(wdTimer):
        global wd
        wd.feed()
        
    # Read JSON
    def update_display(myTimer):
        if timedout < sleepafter:
            readjson()
        
    # Startup
    radar(180, 1, "v1.1")
    # Connect to WiFi
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(secrets.SSID, secrets.PASS)
    sleep(1)
    # First JSON download
    if wlan.isconnected():
        readjson()
    else:
        radar(180, 5, "No WiFi")
        readjson()
    # Start the timers
    myTimer = Timer()
    myTimer.init(period = pollinterval * 1000, mode = Timer.PERIODIC, callback = update_display)
    wdTimer = Timer()
    wdTimer.init(period = 5000, mode = Timer.PERIODIC, callback = resetwdTimer)
    # Automatic reboot after 6 seconds of inactivity
    wd = WDT(timeout = 6000)
    while True:
            
        # Sleep Mode when no new data is received
        if timedout == sleepafter:
            display.set_pen(BLACK)
            display.clear()
            display.update()
            display.set_backlight(0.0)
        if timedout >= sleepafter:
            sleep(pollinterval * 10)
            readjson()
            if timedout == 0:
                display.set_backlight(brightness)
            else:
                continue
        
        # Start drawing
        display.set_pen(BLACK)
        display.clear()
            
        if 'flight' in flightinfo:
          flight = flightinfo['flight']
          
        if flight == "none":
            radar(180, 4, "Nothing")
            continue
        
        if 'airline' in flightinfo:
          airline = flightinfo['airline']
        
        if 'altitude' in flightinfo:
          altitude = flightinfo['altitude']
          
        if 'vert_rate' in flightinfo:
          vert_rate = flightinfo['vert_rate']
          
        if 'speed' in flightinfo:
          speed = flightinfo['speed']
          
        if 'origin_iata' in flightinfo:
          origin_iata = flightinfo['origin_iata']
          
        if 'destination_iata' in flightinfo:
          destination_iata = flightinfo['destination_iata']
          
        if 'model' in flightinfo:
          model = flightinfo['model']
          
        if 'origin' in flightinfo:
          origin = flightinfo['origin']
          
        if 'destination' in flightinfo:
          destination = flightinfo['destination']
          
        if 'departure' in flightinfo:
          departure = flightinfo['departure']
          
        if 'eta' in flightinfo:
          eta = flightinfo['eta']
          
        if 'distance' in flightinfo:
            distradar = flightinfo['distance']
              
        if 'track' in flightinfo:
            heading = flightinfo['track']
                
        # Show main flight info
        display.set_pen(YELLOW)
        display.text(origin_iata, 0, 0, 240, 3)
        writetext(destination_iata, 0, False, 3, YELLOW)
        display.set_pen(ORANGE)
        display.text(departure, 0, 48, 240, 2)
        writetext(eta, 48, False, 2, ORANGE)
        if flight != "N/A":
            writetext(flight, 0, True, 3, ORANGE)
        
        # Draw animation
        if xpos < 220:
            xpos += 1
        else:
            xpos = 20
        display.set_pen(BLUE)
        display.line(20, 35, xpos, 35, 2)
        display.circle(25, 35, 8)
        if xpos > 210:
            display.set_pen(WHITE)
        display.circle(225, 35, 8)
        display.set_pen(GREEN)
        display.triangle(xpos, 25, xpos, 45, xpos + 10, 35)
        # Write airline name - shorten if it is too long
        airline_width = display.measure_text(airline, 2)
        timing_width = display.measure_text(departure + eta, 2)
        if (airline_width + timing_width) > 230:
            shorten = airline_width / (230 - timing_width)
            stripped = min(12, len(airline) - int(len(airline) * shorten) - 3)
            writetext(airline[0:stripped] + "..", 48, True, 2, WHITE)
        else:
            writetext(airline, 48, True, 2, WHITE)
        
        # Write scrolling text for full airport names
        longname = origin + " to " + destination
        longwidth = display.measure_text(longname, 2)
        display.set_pen(GREEN)
        display.text(origin + " -> " + destination, 240 - textpos, 78, 1000, 2)
        textpos += 2
        if textpos > (longwidth + 240):
            textpos = 0
           
        # Write scrolling text for all other details
        longinfo = model + ", Altitude " + str(int(int(altitude) * 0.0003048)) + " km, Speed " + str(int(int(speed) * 1.852)) + " kmh, "
        if ((int(vert_rate) > -64) and (int(vert_rate) < 64)):
            longinfo += "Level Flight, "
        elif int(vert_rate) < -64:
            longinfo += "Descending, "
        else:
            longinfo += "Ascending, "
        longinfo += distradar + " km distance, " + heading
        longinfowidth = display.measure_text(longinfo, 3)
        display.set_pen(BLUE)
        display.text(longinfo, 240 - infopos, 111, 5000, 3)
        infopos += 3
        if infopos > (longinfowidth + 240):
            infopos = 0
        # Update everything to the screen
        display.update()
        sleep(0.01)

That should be it, now you need to find a place for your new Pico Display with real-time flight info!

Success!