#!/usr/bin/php
<?php
/*
    *** README ***
    Assuming your php lives at /usr/bin/php you can chmod 744 this file and just ./squawkwatch.php for testing.
    To 'daemonise' it: /usr/bin/nohup ./squawkwatch.php&    works just fine (tail -f nohup.out to watch output)
    This script will restart itself if the socket connection to dump1090 breaks

    Web & Author: http://lee.smallbone.com/tag/dump1090/

    *** COMPANION SCRIPT ***
    Squawkwatch will now empty its array of processed events when receiving SIGHUP. You can do this periodically
    manually by $ kill -HUP `cat squawkwatch.pid`     or you can do it regularly via crontab. Example script here:
    http://lee.smallbone.com/src/hup_squawkwatch.sh.txt

    *** LICENCE ***
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/


// *****************************
// *** CONFIGURATION OPTIONS ***
// *****************************

// where is dump1090 or Virtual Radar Server rebroadcaster (VRS)?
// port 30003 for dump1090 or 33001 for VRS
$host    "";
$port    "";
$timeout 15;

// where is g-info database?
$mysql_host "";
$mysql_user "";
$mysql_pass "";
$mysql_db   "";

// email address/pushover to notify
$email_to    '[email protected]';
$pushover_to '[email protected]';

// true  = JUST output all messages as they come in
// false = only listen for emergencies in the air
$debug   false;

// look out for and act upon the following squawk codes
// (throw in a good signal one for testing purposes)
// 7500 - hijack
// 7600 - comm fail
// 7700 - emergency
// others of possible interest:
// 0020 - air ambulance heli - emergency medivac
// 0023 - engaged in SAR
// 0026 - special tasks (mil)
// 0032 - NPAS (general?)
// 0037 - Royal heli flight
// 0047 - NPAS (Surrey & Sussex - I assume?)
// more: http://www.kinetic-avionics.co.uk/forums/viewtopic.php?t=2115
$monitored_squawks = array('7500''7600''7700''0020''0032''0037''0047');

// seconds to wait to try and build complete data picture
$incident_timeout 120;

// max packets to process per aircraft incident
$max_packets 300;

// print heartbeat every x seconds
// (stdout if console or nohup.out if backgrounded)
$heartbeat 300// 10 mins

// pid path and filename
$pidfile "/root/squawkwatch.pid"// current dir by default







// *********************************************
// PROGRAM CODE AND NUTS AND BOLTS AND DUCT TAPE
// *********************************************

// ignore any script execution time limits
set_time_limit(0);
ignore_user_abort(1);

// error printing
//ini_set('display_errors',1);  // top 2, or bottom 1
//error_reporting(E_ALL);
error_reporting(0);

// begin program output
echo "\n\n----------------------------------------------\nSQUAWKWATCH - Starting up!\n<http://lee.smallbone.com>\n\n";

// this will become true when we find a monitored_squawk
$of_interest false;

// array of *minimum* detail we'd like to know before notification
$details = array('date'      => null,
         
'time'      => null,
         
'icao'      => null,
         
'callsign'  => null,
         
'altitude'  => null,
         
'velocity'  => null,
         
'heading'   => null,
         
'latitude'  => null,
         
'longitude' => null,
         
'squawk'    => null,
         
'ac_data'   => null
        
);

// declare array for aircraft already proccessed (don't want 5,000 emails; 1 will do!)
$processed = array();

// interrupt  signal handler - playing nice with sockets and dump1090
pcntl_signal(SIGINT,  function($signo) {
    global 
$sock$db$fp;
    echo 
"\n\nctrl-c or kill signal received. Tidying up ... ";
    
socket_shutdown($sock0);
    
socket_close($sock);
    
flock($fpLOCK_UN);
    
fclose($fp);
    
$db null;
    die(
"Bye!\n");
});

// hup signal handler - empty the processed event array
pcntl_signal(SIGHUP, function($signo) {
    if(
count($GLOBALS['processed'])>0) {
        echo 
"***SIGHUP " date("H:i:s d-m-Y") . " - Processed Event array contains " count($GLOBALS['processed']) . " event pairs. Resetting ... ";
        unset(
$GLOBALS['processed']);
        
$GLOBALS['processed'] = array();
        echo 
"done.\n";
    } else  echo 
"***SIGHUP " date("H:i:s d-m-Y") . " - Processed Event array already 0. Skipping reset.\n";
});

pcntl_signal_dispatch();

// attempt to write a pid file (can use this to cron SIGHUPs, check no dupe proc, etc)
$fp fopen($pidfile"r+");
if (
flock($fpLOCK_EX LOCK_NB)) {
    
ftruncate($fp0);
    
fwrite($fpgetmypid());
    
fflush($fp);
} else {
    echo 
"FATAL ERROR: Could not acquire file lock on pidfile. ";
    
$running_pid = (int) file_get_contents($pidfile);
    if(
posix_getpgid($running_pid)) die("Squawkwatch is already running under process " $running_pid "\n");
    else die(
"Pidfile is stale/squawatch not running but pidfile still locked?\n");
}

// keep our output file somewhat session readable
echo 'PID: ' .  getmypid() . "\n";
echo 
'NOW: ' date("H:i:s d/m/Y") . "\n\n";

// create our socket and set it to non-blocking
$sock socket_create(AF_INETSOCK_STREAMSOL_TCP) or die("Unable to create socket\n");

// get the time (so we can figure the timeout)
$time time();

// current packet counter
$current_packets 0;

// total packet counter
$total_packets 0;

// total event counter
$total_events 0;

// for later use
$watching false;

// set our notification function (could be called in a couple of places)
function notify($details) {
    
// affects variables outside function scope
    
global $of_interest$details$processed$email_to$pushover_to$current_packets$watching$total_events;

    
// set some headers
    
$headers 'From: [email protected]"\r\n" .
           
'X-Mailer: PHP-SQUAWKWATCH/' phpversion() . "\r\n" .
           
"MIME-Version: 1.0\r\n" .
           
"Content-Type: text/html; charset=ISO-8859-1\r\n";

    
// set subject and body
    
$subject  "[SQUAWKWATCH] {$details['callsign']} ({$details['icao']}{$details['squawk']}!";
    
$message  "<html><body><h2>SQUAWKWATCH ALERT</h2>";
    
$message .= '<pre>' print_r($detailstrue) . '</pre>';
    
$message .= "<a href=\"http://fr24.com/{$details['callsign']}\">See flight on FR24.com</a>";
    
$message .= '<pre>--END--</pre>';

    
// send email
    
mail($email_to$subject$message$headers);

    
// send pushover
    
if(!empty($pushover_to)) mail($pushover_to'Nearby Aircraft Event Detected'"Squawk {$details['squawk']} detected from {$details['callsign']} {$details['icao']}. Check email for further information.");

    
// we have now handled this ICAO & squawk combo
    
$processed[$details['icao']] = $details['squawk'];
    
$total_events++;

    
// reset our internal program
    
$of_interest false;
    
$details = @array_fill(NULL);
    
$current_packets 0;
    
$watching false;

    return 
true;
}

// database connector
function PDOconnectDB() {
    global 
$mysql_host$mysql_db$mysql_user$mysql_pass;
        try {
        echo 
"Connecting to MySQL DB ... ";
                
$db = new PDO('mysql:host='.$mysql_host.'; dbname='.$mysql_db$mysql_user$mysql_pass);
                
$db->setAttribute(PDO::ATTR_ERRMODEPDO::ERRMODE_EXCEPTION);
                
$db->setAttribute(PDO::ATTR_EMULATE_PREPARESfalse);
                return(
$db);

        } catch(
PDOException $ex) {
            echo 
"MYSQL CONNECTION ERROR: ";
                        echo 
$ex->getMessage() . "\n";
            die();
        }
}


// let's try and connect
echo "Connecting to dump1090 ... ";
while ([email protected]
socket_connect($sock$host$port))
    {
      
$err socket_last_error($sock);
      if (
$err == 115 || $err == 114)
      {
        if ((
time() - $time) >= $timeout)
        {
          
socket_close($sock);
          die(
"Connection timed out.\n");
        }
      }
      die(
socket_strerror($err) . "\n");
 }

    
// connected - lets do some work
    
echo "Connected!\n";

    
// open our MySQL connection
    
$db PDOconnectDB();
    echo 
"Connected!\n";

    
sleep(1);
    echo 
"\nSCAN MODE\n\n";
    while(
$buffer socket_read($sock3000PHP_NORMAL_READ)) {
        
// lets play nice and handle signals such as ctrl-c/kill properly
        
pcntl_signal_dispatch();

        
// SBS format is CSV format
        
$line explode(','$buffer);
        if(
is_array($line) && isset($line[4])) {
            
$total_packets++;

            
// output periodic 1 line health check
            
if(($time time()) % $heartbeat == 0) {
                if(
$printed_heartbeat === false) {
                    
$printed_heartbeat true;
                    echo 
"HEARTBEAT - " date("H:i:s d-m-Y") . " - PID: " getmypid() . " - Packets: {$total_packets} - Events: {$total_events} - Array: " count($processed) . " - SYSTEM OK\n";
                }
            } else  
$printed_heartbeat false;

            if(
$debug) {
                
// look up g-info
                
if(!$ginfo[$line4]) {
                    
$stmt $db->prepare("SELECT Registration FROM `g-info` WHERE icao_hex = :icao");
                    
$stmt->bindParam(':icao'$line[4]);
                    
$stmt->execute();

                    if(
$stmt->rowCount() == 1) {
                        
// store it so we only do one expensive db lookup
                        
$lookup $stmt->fetchAll(PDO::FETCH_ASSOC);
                        
$ginfo[$line[4]] = $lookup[0]['Registration'];
                    } else  
$ginfo[$line[4]] = '******';
                }

                echo 
"{$line[8]} {$line[7]} - ICAO:{$line[4]}   REG:{$ginfo[$line[4]]}  CALLSIGN:{$line[10]}   ALT:{$line[11]}   VEL:{$line[12]}   HDG:{$line[13]}   LAT:{$line[14]}   LON:{$line[15]}   VR:{$line[16]}   SQUAWK:{$line[17]}\n";
            } else {
                
// not in debug mode
                
if($of_interest) {
                    
// we have already started compiling an interesting contact, so let's continue
                    
if(!$watching) echo "Waiting for further aircraft message from ICAO {$details['icao']} ...\n";

                    
// we only need to see that once
                    
$watching true;

                    
// check incident timeout. If plane falls from the sky, we won't get to check it later as no further packets will be received from the aircraft
                    
if(time() - $incident_time >= $incident_timeout) {
                        
// send notification
                        
echo "INCIDENT TIMEOUT. Sending what we have.";
                        
notify($details);
                        
print_r($details);
                        echo 
"\n\nResuming SCAN MODE.\n";
                    }

                    
// as this code can only handle one emergency at a time, we will ignore all other flights for the moment
                    
if($line[4] != $details['icao']) continue;

                    else {
                        
// the aircraft we have an interest in has returned with another packet
                        
$current_packets++;
                        echo 
"{$line[7]} - Processing new packet from ICAO {$line[4]} ... ";

                        
// lets see what is still missing from our details array, and populate if given
                        // no, this won't win any tidy/compact code awards ... but you can see what it's doing this way
                        
if(empty($details['callsign'])  && !empty($line[10])) $details['callsign']  = $line[10];
                        if(empty(
$details['altitude'])  && !empty($line[11])) $details['altitude']  = $line[11];
                        if(empty(
$details['velocity'])  && !empty($line[12])) $details['velocity']  = $line[12];
                        if(empty(
$details['heading'])   && !empty($line[13])) $details['heading']   = $line[13];
                        if(empty(
$details['latitude'])  && !empty($line[14])) $details['latitude']  = $line[14];
                        if(empty(
$details['longitude']) && !empty($line[15])) $details['longitude'] = $line[15];

                        
// let's see if we have all data yet.
                        
if(count(array_filter($details)) == count($details) || $current_packets >= $max_packets || (time() - $incident_time) >= $incident_timeout) {
                            
// all fields now not null OR we overdosed on packets (not all a/c broadcast their callsign for example) OR we've waited long enough already -  send notification
                            
echo "MONITOR COMPLETE (or Packet Overdose, or Timeout). Sending what we have.\n";
                            echo 
print_r($details);
                            
notify($details);
                            echo 
"Resuming SCAN MODE.";

                        
// (of_interest will ensure we end up back in this loop if data is still missing)
                        
} else {
                            echo 
"awaits next packet (want " count($details) . " data pieces, have " count(array_filter($details)) . " pieces)\t";
                            echo 
sprintf('%03d'$current_packets) . "/{$max_packets} packets & " sprintf('%03d'abs((time() - $incident_time) - $incident_timeout)) . " seconds of timeout remaining\n";
                        }
                    }

                } else {
                    
// nothing interesting found yet. Continue scanning for monitored squawks
                    
if(in_array($line[17], $monitored_squawks) && $processed[$line[4]] != $line[17]) {
                        
// interesting squawk we haven't seen yet - go into monitor mode
                        
$of_interest true;

                        
// start our data collection timeout
                        
$incident_time time();
                        echo 
"**** SQUAWK {$line[17]} **** from ICAO {$line[4]}\nEntering MONITOR MODE\n";

                        
// fill in what we have so far - first 4 are always present (squawk not, but we detected it on this packet so it must be here...)
                        
$details['date']   = $line[8];
                        
$details['time']   = $line[9];
                        
$details['icao']   = $line[4];
                        
$details['squawk'] = $line[17];

                        
// blind write attempts. data may not be present in this packet, but it's quicker to write it than check-then-write it
                        
$details['callsign'] = $line[10];
                        
$details['altitude'] = $line[11];
                        
$details['velocity'] = $line[12];
                        
$details['heading'] = $line[13];
                        
$details['latitude'] = $line[14];
                        
$details['longitude'] = $line[15];

                        
// look up g-info
                        // it seems PDO connection is timing out causing script failure at the point of a long-running scan->monitor.
                        // workaround: re-fresh/re-establish connection prior to attempting query
                        
$db PDOconnectDB();
                        
$stmt $db->prepare("SELECT * FROM `g-info` WHERE icao_hex = :icao");
                        
$stmt->bindParam(':icao'$details['icao']);
                        
$stmt->execute();
                        if(
$stmt->rowCount() == 1) {
                            
// ac_data contains all aircraft info (could limit this down to Registration, etc if you want)
                            
$details['ac_data'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
                        } else  
$details['ac_data'] = 'NOT FOUND IN G-INFO';


                    }
                }
            }
        }
    }

// only way we get to here is if socket_read === false
// likely cause - broken socket pipe
echo "Broken pipe.\n";

// relaunch ourself after 10 seconds
$_ $_SERVER['_']; 
register_shutdown_function(function () {
    echo 
"Sleeping for 10 seconds prior to restart\n";
    for(
$i 1$i <= 10$i++) sleep(1);
        
flock($fpLOCK_UN);
        
fclose($fp);
        echo 
'Process ' getmypid() . " now terminated.\n";
    die(
exec(join(' ',  $GLOBALS['argv']) . ' >> squawkwatch.log &'));
});
?>