Introduction: Early Warning Raspberry PI Runway Light Using Flight Mapping Data

This lamp came about from several reasons in that I'm always interested in the planes that fly overhead and during the summer at the weekends there are often some pretty exciting ones flying around. Although you only tend to hear them as they go past. Then the other reason is that it seems the flight path for outbound planes from London City airport will sometimes be overheard and they have some pretty noisy flights going. Being as I dabble in recording some videos for YouTube it is really annoying having to scrap a recording because of a noisy plane. So I wondered if the data that you see on sites like flightradar24 is publicly available, once I discovered something similar was available from opensky network the early warning lamp was born. It didn't take too long to then come up with the idea of using a replica of a runway light to house the project.

You can find out more about The OpenSky Network at http://www.opensky-network.org I also wanted this time to make a nice resin lens instead of using translucent PLA and although I have a ADS-B reciever I wanted to keep this simple and cheap. The ADS-B also need an antenna and this wouldnt do for a lamp to go on the shelf in the office. So hopefully you'll find the build interesting as it covers 3D printing, Resin moulding and math as well to extrapolate the positions of aircraft that potentially might pass overhead.

Step 1: Designing the Case

Google search comes up with many different designs of runway lamp and the design of this one was made using design influences from many different makes of real lamps. It's also scaled to sit in a room or on a shelf rather than full size, as they tend to be a lot larger in real life.

Designs were drawn up in Fusion 360 and I imported some previous elements such as the raspberry pi zero holder from previous projects. Being able to re-use elements takes a lot of the headache out of getting the basics down. You can also download the files here https://www.thingiverse.com/thing:3884138

Step 2: Casting the Lenses - #fail

The most important design element of this lamp was going to be the lens. So I tackled this first as without a nice authentic looking glass the project was going to work. I'm documenting here the fails that I had in trying to achieve that, not withstanding that I also initially decided to make the lens amber. Runway lights come in both amber and blue and it was only after I had started making the amber one that I changed my mind and decided that I wanted a blue one.

From what I can make out the Amber ones are used at the holding line and the blue ones are used to line the runway, and these are the ones that seem to be the more archetypal ones found if you search for runway lights. However, here is my first attempt at making an amber lens. To make the lens, I was going to use clearcast resin with a colour additive, and although I have done a few moulds before I wondered if it was going to be possible to print a 3D mould and use that. So I started with making a split mould in 3D and printing it out in PetG. Judicious amounts of mould release I was convinced would be enough to sperate the mould. As it turns out with the couple of attempts I made the resin stuck to the mould like glue and it just didnt seem possible to seperate them. Although I had the full scale one that I was going to use, I decided against it and printed out the lens to use with traditional silicone moulding.

Step 3: Different Types of Resin

As a quick aside, I used 3 types of clear/coloured resins for this project.

The first was a Hobby market type called Pebeo - Gedeo and is normally sold for encapsulating small items and used for jewellery and desk paperweights etc. This worked pretty well and cured nicely in about 24-36 hours. Its however quite pricey for the amount that you get, but is handy and easily available in hobby and craft shops. It's mixed at a 2:1 ratio. The second was a pre-coloured resin that is mixed at a 10:1 ratio with the hardener and this took the longest to cure, about a week to be honest before it had fully cured. The last was a clear resin, that was also mixed in the 2:1 ratio and this cured in about 2 days, you can colour this with drops of pigment, but you need to make sure that you always use the same colour ratio if you are making seperate batches. It also works out the most cost effective. Lastly the RTV for the mould was a GP-3481 RTV and this takes about 24hours to set and has quite a long pot time so you have plenty of time to mix it and then pour it.

At present I dont have a vacumn pot (currently on order) so that you can be beset by bubbles in both the mould and the resin pouring. Not too much an issue for this, but with a clear lens or similar then you'd want to be thinking about someway to get the bubbles out of the mixes.

Step 4: Casting the Lens in Silicone Mould #2

So this is the second attempt at making a Resin lens and the first stage was to make both a lens in Fusion 360 and then print it in ABS as well as a bucket to hold it. This would be the former for the mould and helps keep down the amount of silicone to be used. You can easily make this out of card, but its just a different approach. To give it a better chance of being released from the mould I first varnished it and then gave it a good covering of wax release agent.

I then poured some GP-3481 which is about shore 27 RTV and then let it set over the next 24hrs before demoulding. Once this was done I used the clear resin mixed at 2:1 ratio with about 4/5 drops of the colour pigment and mixed it well for a good four minutes. Poured this into the mould and then placed a shot glass into the resin as well to provide a void later for either a lamp or the LED's. After about 24 hours this resin was ready to remove and the lens came out pretty good. There are air bubbles present , but as yet I don't have a vacuum vessel to degass the resin before pouring.

Step 5: 3D Printing and Preparation

The model was designed in a way that the central section plugs into the base. This was to avoid masking during the painting process. The entire model was printed in Hatchbox ABS and then sanded. Starting with 60 grit up to about 800 grit gave a good enough surface finish for this model.

Step 6: Assembly and Painting

Once the prints are sanded, it was then painted with some high build primer. Lightly sanded and then sprayed in grey primer. The main parts were painted in ford signal yellow, and then brooklands green used for the base. highlights of tamiya silver were then applied to the bolts and some molotow silver chrome used on the lens holder.

Step 7: First Find Planes Within a Bounding Area

With the hardware sorted, the software needed to be worked on. There are a couple of sites now that provide flight tracking,but not many that provide an API to access that data. Some that do , only do so on a commercial basis but fortunately there is one site called https://opensky-network.org that you can use for free.

To access this data you have to register and then you can use their API, it provides several functions and ways to pull the data. We are interested in all the flights within an area and they have a Live API call for that. https://opensky-network.org/apidoc/ called bounding box. The API call requires the corners of the box that you are interested with of course our Lat/Lon as the center point. You can check the math works this site, that draws a box depending on what you type in. "http://tools.geofabrik.de but for now the following script gives the points we need to plug into the API.

<p>function get_bounding_box($latitude_in_degrees, $longitude_in_degrees, $half_side_in_miles){<br>    $half_side_in_km = $half_side_in_miles * 1.609344;<br>    $lat = deg2rad($latitude_in_degrees);
    $lon = deg2rad($longitude_in_degrees);
    $radius  = 6371;
    
    $parallel_radius = $radius*cos($lat);
    $lat_min = $lat - $half_side_in_km/$radius;
    $lat_max = $lat + $half_side_in_km/$radius;
    $lon_min = $lon - $half_side_in_km/$parallel_radius;
    $lon_max = $lon + $half_side_in_km/$parallel_radius;
    $box_lat_min = rad2deg($lat_min);
    $box_lon_min = rad2deg($lon_min);
    $box_lat_max = rad2deg($lat_max);
    $box_lon_max = rad2deg($lon_max);
    return array($box_lat_min,$box_lon_min,$box_lat_max,$box_lon_max);</p>

If you want to test your code , there is a site where you can enter the lat/lon and see the results on a map :

See a bounding box example on a map

Step 8: Calculating the Heading of the Planes in Relation to Us

The results from the bounding box API call give us a list of planes, their Lon/lat, speed,altitude and heading. So the next thing that we need to do is obtain the heading of each plane in relation to us so that we can further process those that are at least heading in our general direction. We can do this as we know our position and can work out the angle from us to each plane.

To do that I use a piece of code from which originally was in Javascript so I converted it here to PHP,

<p>* calculate (initial) bearing between two points<br> *
 * from: Ed Williams' Aviation Formulary, <a href="http://williams.best.vwh.net/avform.htm#Crs"> </a><a href="http://williams.best.vwh.net/avform.htm#Crs">  http://williams.best.vwh.net/avform.htm#Crs

</a>

 * source  = instantglobe.com/CRANES/GeoCoordTool.html
 */
function get_bearing($home_lat,$home_lon,$plane_lat,$plane_lon) {
  $lat1 = deg2rad($home_lat);
  $lat2 = deg2rad($plane_lat);</p><p>  $dLon = deg2rad($plane_lon-$home_lon);</p><p>  $y = sin($dLon) * cos($lat2);
  $x = cos($lat1)*sin($lat2) - sin($lat1)*cos($lat2)*cos($dLon);<br>        $z = atan2($y,$x);<br>  

      $zz = (rad2deg($z) +360)% 360;<br>  return $zz;</p>

If you want to look at the page where the original javascript versions are, this is the link : http://instantglobe.com/CRANES/GeoCoordTool.html

within that code, you can also see the various sub routines for each type of calculation are.

Step 9: Calculating an Intercept by Looking at a Circle

So we now have a plane where the bearing between it and our location is less than 90 (either positive or negative) and so this means there is a chance that it might fly near by. Using the haversine formula we can also work out using the Lon/Lat of the plane and the Lon/Lat of our house the distance that it is away from us.

Looking at the diagram, if we draw a circle around our house of say about 3 miles radius this gives us a chance of seeing anything flying over. We know the difference in heading between the plane and us, we also know the distance of the plane from us so we can then work out the triangle using the good old SOHCAHTOA ,and in this case using the Tan of the angle we can get the opposite side length. So if we compare this value against the radius value of the circle around the house we can then find out if the plane will fly close enough for us to see it. The next bit we can do is work out the time that the plane will fly past by using the air speed and the distance and if this is less than say about 45 seconds or so we turn on the light. This is a bit of the code that I use to work out the chance of a fly over. I do this as there is a nearby airport and when the planes are taxi-ing around they inevitably point at the house. However as their altitude is zero and the speed is walking pace this shouldnt trigger the alarm.

<p>function get_intercept($home_head,$plane_head,$plane_distance) {
</p><p>        $flight_angle =  abs(abs($home_head - $plane_head) - 180);<br>        $flight_angle_r = deg2rad($flight_angle);
        $flight_angle_t = tan($flight_angle_r);
        $flight_intercept = $flight_angle_t * $plane_distance;</p><p>        if (($flight_angle<90) && ($flight_intercept<3)){
		// possible fly past</p><p>        }</p><p>        return $flight_intercept;
}</p>

Step 10: Distance Between Two Points on a Map - Haversine Formula

So we have to calculate the distance between the plane and our location. On short distances on a map you could approximately calculate the distance, but as the earth is spherical, there is a formula called the haversine formula that allows you to take into consideration the curved surface. You can read further into the formula : https://en.wikipedia.org/wiki/Haversine_formula

Now with the distance calculated and we know the airspeed of the plane we can work out how many seconds it will be before the plane is overhead. So the light will come on if there is something within 30 seconds of flypast and we at last have our warning light.

* based 0n JS at instantglobe.com/CRANES/GeoCoordTool.html and turned into PHP */

function get_distHaversine ($home_lat,$home_lon,$plane_lat,$plane_lon) {
  $R = 6371; // earth's mean radius in km
  $dLat = deg2rad($plane_lat-$home_lat);
  $dLon = deg2rad($plane_lon-$home_lon);
  $lat1 = deg2rad($home_lat);
  $lat2 = deg2rad($plane_lat);</p><p>  $a = sin($dLat/2) * sin($dLat/2) + cos($lat1) * cos($lat2) * sin($dLon/2) * sin($dLon/2);
  $c = 2 * atan2(sqrt($a), sqrt(1-$a));
  $d = $R * $c;
  return $d;
}

Step 11: Importing and Defining the Plane Database

One of the other pieces is that the opensky site offers a downloadable database of planes along with their callsigns and idents. Its several hundred thousand entries. So we can download this and load it locally into a MariaDB database for lookup (MySQL). With every plane that appears overhead, we retrieve its details and update a counter to show how many times it has been seen.

I am also currently editing the database to highlight planes that I am interested in. Mainly old warbirds and other similar interesting planes. A couple of times this summer a Mig-15 has flown over. so the aim is to use an alert field I have added and then flash the light fast when something interesting is heading over

Step 12: Improving Results and New Features

So in theory everything works pretty well, but you'll find with the data that there are planes that fly over that dont appear in the API.

This is because not all planes are using the ADS-B transponder and use older transponders based on MLAT. To obtain position data on aircraft using MLAT it requires a series of receivers on the ground to triangulate their position and some sites like flightradar24 have a bigger network of contributors doing this compared to opensky. Hopefully over time their coverage will improve as well and I am setting up my own MLAT receiver to add to this data.

Step 13: Codebase

Don't forget if you are going to use this you might want to remove the SQL statements if you dont have the database of planes and also add your own Lon/Lat value and API key for accessing the flight data.

https://github.com/ajax-jones/runway-light-awacs

<p>define("INTERVAL", (20 * 1) );  <br>	function fexp() {  
	        $lat = "your latitude";
	        $lon = "your longitude";
			$side = 15.75;
	        $box  = get_bounding_box($lat,$lon,$side);
			$latmin = $box[0];
			$lonmin = $box[1];
			$latmax = $box[2];
			$lonmax = $box[3];
	        $flyurl = "https://opensky-network.org/api/states/all?lamin=$latmin&lomin=$lonmin&lamax=$latmax&lomax=$lonmax";
	        echo "Scanning the SKY";
	        $start_time = microtime(true);
	        $json   = file_get_contents($flyurl);
	        $data   = json_decode($json, TRUE);
			$inbound = FALSE;
			$num_planes = count($data['states']);
			if ($num_planes >0)
			{
				echo " and we can see $num_planes planes\n ";
				for ($x =0; $x < $num_planes; $x++)
				{
					error_reporting(E_ALL);
					$icao24   	= $data['states'][$x][0];
		        	$callsign 	= $data['states'][$x][1];
        			$country  	= $data['states'][$x][2];
					$baro_altitude 	= $data['states'][$x][7];
					$geo_altitude_m	= round($data['states'][$x][13]);
					$geo_altitude_f = round(($geo_altitude_m/0.3048));
        			$air_speed_ms   = $data['states'][$x][9];
					$air_speed_kmh  = round( ($air_speed_ms * 3600)/1000   );
					$air_speed_mph  = round( (($air_speed_ms * 3600)/1000)*.621371   );
					$air_speed_kts  = round( ((($air_speed_ms * 3600)/1000)*.621371)*.868976242   );
					$heading	= $data['states'][$x][10];
					$heading_d	= round($heading);
					$latitude	= $data['states'][$x][6];
					$longitude	= $data['states'][$x][5];
					$plane_heading  = get_bearing($lat,$lon,$latitude,$longitude);
					$distplane      = get_distHaversine($lat,$lon,$latitude,$longitude);
					$intercept      = get_intercept($plane_heading,$heading_d,$distplane);
					if ($air_speed_kmh > 0) {
						$plane_eta 	= $distplane/$air_speed_kmh;
					}else {
						$eta = 1;
					}
					if (    ( ($intercept)<5 &&($intercept>0) ) &&    ($distplane<12) && $geo_altitude_m>0){
		       			$inbound = TRUE;
						echo "--------------------------------------------------------------------\n";
						echo "$icao24 - [$country $callsign] at [$geo_altitude_m M -- $geo_altitude_f ft] ";
						echo "[speed  $air_speed_kmh kmh and ",round($distplane,1),"km away]\n";
						echo "[on a heading of ",round($plane_heading,1),"] [homeangle $heading_d] ";
						echo "[$latitude,$longitude]\n";
						echo "[flypast in ",decimal_to_time($plane_eta)," now  ",round($intercept,1),"km away\n";
						echo "--------------------------------------------------------------------\n";
				        $DBi = new mysqli("127.0.0.1", "root", "your password", "awacs");
				        $sql = "select * from aircraftdatabase where `icao24`='$icao24'";
        				mysqli_set_charset($DBi,"utf8");
        				$getplanedata = mysqli_query($DBi, $sql) or die(mysqli_error($DBi));
        				$row_getplanedata = mysqli_fetch_assoc($getplanedata);
        				$rows_getplanedata = mysqli_num_rows($getplanedata);
        				if($rows_getplanedata>0) {
							do {
                				echo "callsign=";
                				echo $row_getplanedata['registration'];
                				echo " is a ";
                				echo $row_getplanedata['manufacturername'];
                				echo " ";
                				echo $row_getplanedata['model'];
                				echo " by ";
                				echo $row_getplanedata['manufacturericao'];
                				echo " owned by  ";
                				echo $row_getplanedata['owner'];
                				echo " seen  ";
                				echo $row_getplanedata['visits'];
                				echo " times  ";
                				echo " special rating=";
                				echo $row_getplanedata['special'];
                				echo "\n";
                				$visits = $row_getplanedata['visits']+1;
            				  } while ($row_getplanedata = mysqli_fetch_assoc($getplanedata));
							mysqli_free_result($getplanedata);
							$sqli = "UPDATE aircraftdatabase SET visits = $visits WHERE icao24 = '$icao24'";
							mysqli_set_charset($DBi,"utf8");
            				$updateplanedata = mysqli_query($DBi, $sqli) or die(mysqli_error($DBi));
						} else {
							echo "Couldn't find this plane in the DB so adding it";
							$sqli = "INSERT INTO aircraftdatabase (icao24,visits,special) VALUES ('$icao24',1,1)";
            				$updateplanedata = mysqli_query($DBi, $sqli) or die(mysqli_error($DBi));
						}
					echo "--------------------------------------------------------------------\n";
				} else {
					//				echo "$callsign ";
				}
			}
		} else {
       			echo " and the skies are clear\n ";
		}
		if ($inbound) {
			echo "Inbound plane\n";
			$command = "pigs w 17 1";
			execInBackground($command);
		} else {
			echo "no inbound flights\n";
			$command = "pigs w 17 0";
			execInBackground($command);
		}
	}
function decimal_to_time($decimal) {
	$offset = 0.002778;
	if ($decimal>$offset) {
		$decimal = $decimal - 0.002778;
	}
	$hours   = gmdate('H', floor($decimal * 3600));
	$minutes = gmdate('i', floor($decimal * 3600));
	$seconds = gmdate('s', floor($decimal * 3600));
    return str_pad($hours, 2, "0", STR_PAD_LEFT) . ":" . str_pad($minutes, 2, "0", STR_PAD_LEFT) . ":" . str_pad($seconds, 2, "0", STR_PAD_LEFT);
}
/*
 * calculate (initial) bearing between two points
 *
 * from: Ed Williams' Aviation Formulary,  http://williams.best.vwh.net/avform.htm#Crs

 * source  = instantglobe.com/CRANES/GeoCoordTool.html
 */
function get_bearing($home_lat,$home_lon,$plane_lat,$plane_lon) {
	$lat1 = deg2rad($home_lat);
	$lat2 = deg2rad($plane_lat);
	$dLon = deg2rad($plane_lon-$home_lon);
	$y = sin($dLon) * cos($lat2);
	$x = cos($lat1)*sin($lat2) - sin($lat1)*cos($lat2)*cos($dLon);
	$z = atan2($y,$x);
	$zz = (rad2deg($z) +360)% 360;
  return $zz;
}
function get_intercept($home_head,$plane_head,$plane_distance) {
	$flight_angle =  abs(abs($home_head - $plane_head) - 180);
	$flight_angle_r = deg2rad($flight_angle);
	$flight_angle_t = tan($flight_angle_r);
	$flight_intercept = $flight_angle_t * $plane_distance;
  	return $flight_intercept;
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/*
 * Use Haversine formula to Calculate distance (in km) between two points specified by
 * latitude/longitude (in numeric degrees)
 *
 * from: Haversine formula - R. W. Sinnott, "Virtues of the Haversine",
 *       Sky and Telescope, vol 68, no 2, 1984
 *        http://williams.best.vwh.net/avform.htm#Crs

 *
 * example usage from form:
 *   result.value = LatLon.distHaversine(lat1.value.parseDeg(), long1.value.parseDeg(),
 *                                       lat2.value.parseDeg(), long2.value.parseDeg());
 * where lat1, long1, lat2, long2, and result are form fields
 * source  = instantglobe.com/CRANES/GeoCoordTool.html
 */
function get_distHaversine ($home_lat,$home_lon,$plane_lat,$plane_lon) {
  $R = 6371; // earth's mean radius in km
  $dLat = deg2rad($plane_lat-$home_lat);
  $dLon = deg2rad($plane_lon-$home_lon);
  $lat1 = deg2rad($home_lat);
  $lat2 = deg2rad($plane_lat);
  $a = sin($dLat/2) * sin($dLat/2) + cos($lat1) * cos($lat2) * sin($dLon/2) * sin($dLon/2);
  $c = 2 * atan2(sqrt($a), sqrt(1-$a));
  $d = $R * $c;
  return $d;
}
function get_bounding_box($latitude_in_degrees, $longitude_in_degrees, $half_side_in_miles){
    $half_side_in_km = $half_side_in_miles * 1.609344;
    $lat = deg2rad($latitude_in_degrees);
    $lon = deg2rad($longitude_in_degrees);
    $radius  = 6371;
    # Radius of the parallel at given latitude;
    $parallel_radius = $radius*cos($lat);
    $lat_min = $lat - $half_side_in_km/$radius;
    $lat_max = $lat + $half_side_in_km/$radius;
    $lon_min = $lon - $half_side_in_km/$parallel_radius;
    $lon_max = $lon + $half_side_in_km/$parallel_radius;
    $box_lat_min = rad2deg($lat_min);
    $box_lon_min = rad2deg($lon_min);
    $box_lat_max = rad2deg($lat_max);
    $box_lon_max = rad2deg($lon_max);
    return array($box_lat_min,$box_lon_min,$box_lat_max,$box_lon_max);
}
function execInBackground($cmd) {
    if (substr(php_uname(), 0, 7) == "Windows"){
        pclose(popen("start /B ". $cmd, "r"));
    }
    else {
        exec($cmd . " > /dev/null &");
    }
}
function checkForStopFlag() { // completely optional
        return(TRUE);
}
function start() {
    echo "starting\n";
    $command = "pigs w 17 1";
    execInBackground($command);
    $active = TRUE;
    while($active) {
        usleep(1000); // optional, if you want to be considerate
        if (microtime(true) >= $nextTime) {
		fexp();
            	$nextTime = microtime(true) + INTERVAL;
        }
        $active = checkForStopFlag();
    }
}
fexp();
start();
?></p>

Step 14: Wiring the LED and the Shutdown Switch

Wiring of this project couldnt be simpler really. There is just the one LED that is connected to pin 17 and ground with a 270R resistor inline.

I also include a shutdown and power up button along with a power LED that runs off the TXd data pin. You can read up more about the shutdown function and the code required at https://github.com/Howchoo/pi-power-button.git from the site https://howchoo.com/g/mwnlytk3zmm/how-to-add-a-pow... You can read about adding a power light here https://howchoo.com/g/ytzjyzy4m2e/build-a-simple-raspberry-pi-led-power-status-indicator

Maps Challenge

Participated in the
Maps Challenge