The Military Mode-S page

Military Mode-S logs
“Wow, that’s cool! I want to have this too. Can I? How did you do it?” is a frequent reaction when people comment on my Military Mode-S logs page. Well, maybe you can. But it isn’t necessarily easy, and contains quite a few prerequisites…

Mode-S: a brief introduction

Mode-S antenna
My Mode-S antenna and receiver up in the attic. Since this photo was taken the “old” Raspberry Pi has been replaced by a Model 3 Pi, the dongle is a newer one, and the Ethernet connection is now a proper one instead of a Powerline adapter. The antenna is on a swiveable mount (low headroom!)

If you’re not aware what Mode-S is, then this whole post is probably of little use to you. Basically, Mode-S or ADS-B are automatic radio transmissions by aircraft with a lot of data about the aircraft’s position, altitude, heading, etcetera. It is of course intended for Air Traffic Control purposes, but some years ago a bunch of very smart people figured out how to receive and decode these signals for aviation enthusiasts use. This is how the virtual radars came into being, and it has changed “the game” for aircraft spotters quite a bit. We dreamed of owning a radar set in our younger days, now we in fact do, with just a computer and a Mode-S receiver. The latter can even be a very cheap one, again a bunch of very smart people figured out how to use a TV-receiver USB dongle for Mode-S, and these can be ordered with some patience from China for less that 10 Euros!

To visualize the signals, and those shared by others over the internet, I use PlanePlotter. Not the easiest piece of software to master, but it is very powerful and comes without censoring of “sensitive” traffic. It also pioneered MLAT’ing, where aircraft which do not disclose their position (like most military aircraft) can still be plotted with enough ground stations receiving the signal and combining these through the sharing system.

PlanePlotter
My PlanePlotter view on a busy Friday morning. Normally I filter out all non-interesting traffic as I am only interested in military aircraft. The picture becomes much less cluttered then!

My Mode-S page

Once I had set up my Mode-S kit and started using it, I soon got the idea for a web page with a log of the day’s aircraft movements in my area. I’m fortunate to be living right under a corridor with  a lot of AMC (US Air Force transport aircraft) traffic flying in and out of Germany, and such a page would help to keep track of movements. Not really for the benefit of anyone but myself, but I was soon surprised how useful others were about to find this page. I’m also quite sure a couple of “non spotting” individuals consult the page on a daily base…if you know what I mean…

“Wow, that’s cool! I want to have this too. Can I? How did you do it?” is a frequent reaction when people comment on my Military Mode-S logs page. Well, maybe you can. But it isn’t necessarily easy, and has quite a few prerequisites…

What follows is not a cookbook, nor will I set it up for you or learn you the basics of PlanePlotter, webservers, PHP scripts, and what more. This is just an explanation of how I set things up, it’s for you to figure out how it’s actually done. Trust me…it’s much more rewarding like that!

The prerequisites

So, should you be playing with the thought of setting up something similar, then what I’m about to say might not sound too hopeful. I don’t only own my internet domain, but host it myself as well. Which means I have full access to the servers, including the web server. If you host your website with a provider, then you might want to check what the possibilities are with these prerequisites:

  • An up-and-running PlanePlotter installation, as an authenticated ground station, running 24/7, with  your own receiver. I have virtualized my PlanePlotter computer as a virtual machine, and it’s Linux, with PlanePlotter on it with Wine. But that’s beside the point, a normal computer with Windows will do just as well. Don’t even start thinking about a web page like mine if you don’t already have PlanePlotter and at least are confident with the basics!
  • You must have implemented a method in PlanePlotter to filter only the aircraft you’re interested in (and want to have displayed on your web page). There is a “interested” field in the basestation.sqb database file which needs to be set for all interested aircraft. This is used by a conditional expression in PlanePlotter.PlanePlotter conditional expression I have also appended the country description of all interested aircraft in my basestation.sqb file with “mil”.
  • A set of flag or military “roundel” images, named after each country similar as in the basestation.sqb database.
  • A web server to host the page on. I use Apache on a (again) virtualized Linux server, but it can just as well be IIS hosted by your provider. You must be able to upload scripts to it, and not just some “point-and-click” web page builder.
  • The web server must support PHP (7.x preferably), and PHP itself must support SQLite through PDO. Ask your hosting provider if unsure…
  • The web server must have access to your PlanePlotter files. There is more than one way to achieve this, but more than a basic access to your web server will be needed. I use Dropbox for this, syncing the required files between the PlanePlotter computer and the web server.
  • Adequate knowledge of HTML, CSS, PHP and (optionally) Python or Powershell.
  • More things I haven’t thought off at this moment, likely.

I never said it would be easy. Still here? Okay, let’s continue…

And now…the setup

PlanePlotter, with its quirks and sometimes daunting menus, actually has a built-in logging functionality capable of generating the kind of logs we need. You just need to enable it. What we want is a log of all aircraft we’re interested in, received by our own antenna, over the course of a singe day. You could also log and display more, but that might become a silly thing to do very fast.

PlanePlotter report settingsIn the PlanePlotter menu, go to File → Report → Setup report format. Tick the boxes as shown here. Note the “Conditional Expression in Force” remark, which you can not set or switch off here but is set in the Options menu (Options → Conditional expressions → Report conditional expression, as in the screen shot shown above). Furthermore we instruct PlanePlotter here to create a “daily first/last database”, to use only local aircraft (in other words only aircraft you have received yourself) and to create a new log file every day.

PlanePlotter will now start generating a log file every day, in the folder you have specified in the PlanePlotter options. These are not text files, but database files in SQLite format:PlanePlotter logs

Now, these log files need to be readable by the PHP scripts on the web server. How you manage this…is up to you. As mentioned before, I do this with Dropbox on both PlanePlotter computer and the web server. To prevent the web server trying to open a log file just when Dropbox is syncing a new version of the file (which it does almost constantly) I don’t let Apache actually read directly from the Dropbox folder, but copy the content from Dropbox to Apache with a cron job (a scheduled task in Windows terms) every 10 minutes.

Still here? Good, it’s time to set up your web site in your web server. That’s completely up to you. The next part is to actually script with PHP, connecting the required log file (remember: it’s actually a SQLite database file) and parsing it to display the log in a decent format.

Just as a reference, this is the code which drives my Mode-S page:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 
 
<?php 
 
   // initialize variables 
   $BSFILE = 'files/basestation_light.sqb'; 
   $FLAGSFILE = 'files/bsflags-mil_web.txt'; 
   $FLAGSFOLDER = 'flags'; 
   $GROUNDSTATION = "'<your sharer code>'"; 
   $LOGDIR = 'logs'; 
   $LOOKUP = 'http://www.airliners.net/search/photo.search?'; 
   $POSITION = '<your position>'; 
   $RADIUS = '<your reception radius>'; 
 
   // fill flags array 
   $FLAGS = array(); 
   foreach(file($FLAGSFILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $LINE) { 
      $FLAGS[] = explode(' ', $LINE, 3); 
   } 
 
   // connect to basestation database 
   $BSDB = new PDO('sqlite:' . $BSFILE); 
 
?> 
 
<html xmlns="http://www.w3.org/1999/xhtml"> 
<head> 
   <title>Military Mode-S logs - Ground Station '<?php echo $GROUNDSTATION ?>'</title> 
   <meta http-equiv="Content-Language" content="en-us"> 
   <meta http-equiv="Content-Type" content="text/html; charset=windows-1252"> 
   <link rel="stylesheet" type="text/css" media="all" href="modeslogs.css"> 
</head> 
 
<body> 

<div class="section" id="header_section">

   <form action="?" method="post" id="header_form">

   <div id="selectlog"> 
      <label for="date_select">Select log date:</label> 
      <select name="date_select" onchange="header_form.submit();"> 
      <?php 
 
         // fill dropdown box with available dates 
         foreach(array_reverse(glob($LOGDIR . '/pp_report20??????00.sqb')) as $LOGFILE) { 
            $DATE = substr($LOGFILE, -14, 8); 
            echo '<option value="' . $LOGFILE . '"';'>' . date('D d-m-Y', strtotime($DATE)); 
            if (isset($_POST['date_select'])) { 
               if ($_POST['date_select'] == $LOGFILE) { 
                  echo ' selected="selected"'; 
                  $SELECTED = $LOGFILE; 
               } 
            } 
            echo '>' . date(' D d-m-Y ', strtotime($DATE)) . '</option>' . PHP_EOL; 
         } 
 
      ?> 
      </select> 
   </div><!-- #selectlog -->
 
   <span>Military Mode-S logs</span> 

   <div class="buttons"> 
      <button type="button" onclick="header_form.submit();">Refresh</button> 
   </div><!-- .buttons --> 
 
   </form>
 
</div><!-- #header_section --> 

<div class="section" id="display_section">
   <?php 
 
      // for the initial page view, selected is todays log file 
      if (!isset($SELECTED)) $SELECTED = $LOGDIR . '/pp_report'. date('Ymd') . '00.sqb'; 
 
      // log date 
      echo '<div id="logdate">' . date('l d-m-Y', strtotime(substr($SELECTED, -14, 8))) . '</div>'; 
 
      // connect to log database and fetch all rows 
      $LOGDB = new PDO('sqlite:' . $SELECTED); 
      $QUERY = $LOGDB->query("SELECT * FROM PPreport ORDER BY firsttime"); 
      $LOGRESULT = $QUERY->fetchAll(PDO::FETCH_ASSOC); 
 
   ?> 
        
   <table>   
      <tr>       
         <th></th>
         <th></th>
         <th>flight</th>
         <th>registration</th>
         <th>type</th>
         <th>UTC time</th>
         <th>ICAO Hex</th>
         <th>squawk</th>
         <th>altitude</th>
      </tr>
      <?php                 
 
         // display all rows
         $LOGROWCLASS = '';
         foreach($LOGRESULT as $LOGROW) {

            // get relevant data from basestation database
            $FLIGHTID = explode(';', $LOGROW['flightid']);
            $SQLSTR = "SELECT ModeSCountry, Manufacturer, [Type] FROM Aircraft WHERE ModeS = '" . $FLIGHTID[0] . "'";
            $QUERY = $BSDB->query($SQLSTR); 
            $BSRESULT = $QUERY->fetch(PDO::FETCH_ASSOC); 
 
            // flag image and text 
            foreach($FLAGS as $FLAG) { 
               if ((castHex($FLIGHTID[0]) >= castHex($FLAG[0])) && (castHex($FLIGHTID[0]) <= castHex($FLAG[1]))) break; 
            } 
            $COUNTRY = explode('.', $FLAG[2], 2); 
            $COUNTRY[0] = str_replace(array($FLAGSFOLDER, '_'), array('', ' '), $COUNTRY[0]); 
            if ((strtolower(substr($BSRESULT['ModeSCountry'], -3))) == 'mil') { 
               $FLAG[2] = str_replace('.', '_Mil.', $FLAG[2]); 
               $COUNTRY[0] .= ' (mil)'; 
            } 
 
            // display row 
            $LOGROWCLASS = ($LOGROWCLASS == 'odd') ? 'even' : 'odd'; 
            echo '<tr class="' . $LOGROWCLASS . '">' . PHP_EOL . '<td id="flag">'; 
            echo '<img src="' . $FLAG[2] . '" title=" ' . $COUNTRY[0] . ' "></td>' . PHP_EOL; 
            echo '<td id="space">&nbsp;</td>' . PHP_EOL . '<td>' . $FLIGHTID[1] . '</td>' . PHP_EOL; 
            echo '<td title=" find \'' . regFormat($FLIGHTID[2]) . '\' on airliners.net "><a href="' . $LOOKUP;
            echo 'regsearch=' . regFormat($FLIGHTID[2]) . '" target="_blank">' . $FLIGHTID[2] . '</a></td>' . PHP_EOL; 
            if ($BSRESULT['Type'] != '') { 
               echo '<td title=" ' . trim($BSRESULT['Manufacturer']) . ' ' . $BSRESULT['Type'] . '  (ICAO: ';
               echo $LOGROW['type'] . ') ">' . $BSRESULT['Type'] . '</td>' . PHP_EOL; 
            } else { 
               echo '<td>' . $LOGROW['type'] . '</td>' . PHP_EOL; 
            } 
            echo '<td title=" GMT+' . (1 + date('I')) . ':   ' . date('H:i:s', $LOGROW['firsttime']) . ' &rarr; ';
            echo date('H:i:s', $LOGROW['lasttime']) . ' ">' . gmdate('H:i:s', $LOGROW['firsttime']) . ' &rarr; '; 
            echo gmdate('H:i:s', $LOGROW['lasttime']) . '</td>' . PHP_EOL . '<td>' . $FLIGHTID[0] . '</td>' . PHP_EOL; 
            echo '<td>' . $LOGROW['firstsquawk']; 
            if ($LOGROW['firstsquawk'] != $LOGROW['lastsquawk']) echo ' &rarr; ' . $LOGROW['lastsquawk']; 
            echo '</td>' . PHP_EOL . '<td>' . $LOGROW['firstalt']; 
            if ($LOGROW['firstalt'] != $LOGROW['lastalt']) echo ' &rarr; ' . $LOGROW['lastalt']; 
            echo '</td>' . PHP_EOL . '</tr>' . PHP_EOL; 
         } 
 
      ?> 
   </table>
 
</div><!-- #display_section --> 

<div class="section" id="footer_section">
   <form action="#footer_section" method="post" id="footer_form">
   <div id="footer_copyright">&copy; Crouze.com</div>
   <?php 
      if (substr($SELECTED, -14, 8) == gmdate('Ymd')) { 
         echo '<div class="buttons">'; 
         echo '<button type="button" onclick="footer_form.submit();">Refresh</button></div>' . PHP_EOL; 
      } 
   ?> 

   <div id="footer_text"> 
      PlanePlotter Ground Station. Sharer id <?php echo $GROUNDSTATION ?>
      approximate position: <?php echo $POSITION ?>
      usual log radius: about <?php echo $RADIUS ?> miles 
   </div><!-- #footer_text --> 

   </form>

</div><!-- #footer_section --> 
 
</body> 
</html> 
 
<?php

   // cast hexadecimal
   function castHex($STR) {
      return '0x' . $STR;
   }

   // format registrations
   function regFormat($STR) {
      return str_replace(array('+', 'LXN904'),
                         array('' , 'LX-N904'),
                         $STR);
   }

?> 

Clean up of old logs

With PlanePlotter generating a new log file every day, you will quickly end up with a lot of old log files. Of course it’s all up to you to decide whether to keep these indefinitely, or delete old files after some time. I decided to keep log files for no more than one month, and then delete them automatically. For this I wrote another script, this time in the Python language, which runs daily on the PlanePlotter computer. Not only does this script remove old log files, but also other files and “garbage” PlanePlotter writes to the log folder. On Windows, you could do the same in PowerShell, PHP, or any other suitable script language. Again, as reference only:

#!/usr/bin/python
import glob, os, time

# array with PlanePlotter log directories to clean (with ending /)
LOGDIRS = ['<path to your log folder>',
           '<path to other log folder (as as many as you need)>']

# array with log types (file name template and after how many days to delete, 0 = immediately)
LOGS = [['gstestYYMMDD.txt', 1],
        ['multilatYYMMDD.log', 1],
        ['planeplotterYYMMDD.log', 1],
        ['pp_reportYYYYMMDD99.log', 0],
        ['pp_reportYYYYMMDD99.sqb', 31],
        ['pp_reportYYYYMMDD99.sqb-journal', 1],
        ['pp_report_YYMMDD99.txt', 0],
        ['RTLYYMMDD99.log', 1]]

# array with additional single files also to delete
SINGLE = ['ipconfigbat.bat',
          'lookup.vbs',
          'metar.txt',
          'readmelog.txt',
          'restartlog.txt']

# process all log directories
for LOGDIR in LOGDIRS:

   # process all log types
   for LOG in LOGS:

      # determine newest of this log type to be deleted
      NEWEST = LOG[0].replace('9', '')
      TIMESTAMP = time.time() - (LOG[1] * 86400)
      if NEWEST.find('YYYY') > -1:
         NEWEST = NEWEST.replace('YYYYMMDD', time.strftime('%Y%m%d', time.localtime(TIMESTAMP)))
      else:
         NEWEST = NEWEST.replace('YYMMDD', time.strftime('%y%m%d', time.localtime(TIMESTAMP)))

      # delete all logs of this type as old or older than the just determined log
      TEMPLATE = LOG[0]
      MAPPING = [('YY','??'),('MM','??'),('DD','??'),('99','??')]
      for ORG, NEW in MAPPING:
         TEMPLATE = TEMPLATE.replace(ORG, NEW)
      for FILENAME in glob.glob(LOGDIR + TEMPLATE):
         if FILENAME < (LOGDIR + NEWEST):
            os.remove(FILENAME)

      # process any additional files
      for FILENAME in SINGLE:
         if os.path.isfile(LOGDIR + FILENAME):
            os.remove(LOGDIR + FILENAME)

# end of script

Concluding…?

So, there it is, more or less. Maybe you’re inspired, maybe not, maybe you’re slightly daunted by the prospect of having to set up all that is mentioned here.. But at least now you know how it’s done. And if you want to pursue this yourself, then my only comment is: “good luck and have fun with it! (you’re on your own)” 😉

Marco

Leave a Reply

Your email address will not be published. Required fields are marked *