#!/usr/bin/perl -w # ======================================================================== # FILE: perlAPRS # # AUTHOR: Rich Parry, W9IF # # DESCRIPTION: This perl program will read a command file that # provides a callsign, command, grid sqaure, and command count. # The program will then listen to an APRS port and execute the command # script if that station is heard in the grid square specified. # # Type the following command to get short help menu: # # perlAPRS -h # # ======================================================================== require 5.002; use Socket; use strict; my $inport = "/dev/ttyS1"; # default serial port my $datafile = "callsign.dat"; # default command file my @APRSsigns = qw(AIR ALL AP BEACON CQ GPS DF DGPS DRILL DX ID JAVA MAIL MICE QST QTH RTCM SKY SPACE SPC SYM TEL TEST TLM WX ZIP); # Arrays of hashes (associative array) are used to ease the computation # of the grid squares. Since lat and lon are in mixed format containing # Degrees, Minutes, and Decimal Minutes, the computation is not # straight forward. my %gridchar4 = ( A => {deg => '0', min => '00',}, B => {deg => '1', min => '55',}, C => {deg => '1', min => '50',}, D => {deg => '1', min => '45',}, E => {deg => '1', min => '40',}, F => {deg => '1', min => '35',}, G => {deg => '1', min => '30',}, H => {deg => '1', min => '25',}, I => {deg => '1', min => '20',}, J => {deg => '1', min => '15',}, K => {deg => '1', min => '10',}, L => {deg => '1', min => '05',}, M => {deg => '1', min => '00',}, N => {deg => '2', min => '55',}, O => {deg => '2', min => '50',}, P => {deg => '2', min => '45',}, Q => {deg => '2', min => '40',}, R => {deg => '2', min => '35',}, S => {deg => '2', min => '30',}, T => {deg => '2', min => '25',}, U => {deg => '2', min => '20',}, V => {deg => '2', min => '15',}, W => {deg => '2', min => '10',}, X => {deg => '2', min => '05',}, Y => {deg => '2', min => '00',}, ); my %gridchar5 = ( A => {deg => '0', min => '00', decmin => '.0'}, B => {deg => '0', min => '02', decmin => '.5'}, C => {deg => '0', min => '05', decmin => '.0'}, D => {deg => '0', min => '07', decmin => '.5'}, E => {deg => '0', min => '10', decmin => '.0'}, F => {deg => '0', min => '12', decmin => '.5'}, G => {deg => '0', min => '15', decmin => '.0'}, H => {deg => '0', min => '17', decmin => '.5'}, I => {deg => '0', min => '20', decmin => '.0'}, J => {deg => '0', min => '22', decmin => '.5'}, K => {deg => '0', min => '25', decmin => '.0'}, L => {deg => '0', min => '27', decmin => '.5'}, M => {deg => '0', min => '30', decmin => '.0'}, N => {deg => '0', min => '32', decmin => '.5'}, O => {deg => '0', min => '35', decmin => '.0'}, P => {deg => '0', min => '37', decmin => '.5'}, Q => {deg => '0', min => '40', decmin => '.0'}, R => {deg => '0', min => '42', decmin => '.5'}, S => {deg => '0', min => '45', decmin => '.0'}, T => {deg => '0', min => '47', decmin => '.5'}, U => {deg => '0', min => '50', decmin => '.0'}, V => {deg => '0', min => '52', decmin => '.5'}, W => {deg => '0', min => '55', decmin => '.0'}, X => {deg => '0', min => '57', decmin => '.5'}, Y => {deg => '0', min => '60', decmin => '.0'}, ); # Fixed subscripts into the database array "cinput" # The first 4 values are read in from the user's command file. my $ptrcallsign = 0; my $ptrcommand = 1; my $ptrgridsquare = 2; my $ptrexecutionref = 3; my $ptrexecutionctr = 4; my $ptrheardtime = 5; my $ptrresettime = 6; my $ptrlatlow = 7; my $ptrlonlow = 8; my $ptrlatupr = 9; my $ptrlonupr = 10; my ($addr, $aprs, $argsValid, $char4, $char5, @cinput, $fromcallsign, @grid, $header, $host, $i, $in_addr, $lat, $latdegrees, $latlong, $latlow, $latlowdeg, $latupr, $londegrees, $lonlow, $lonupr, $long, $netport, $payload, @posit, $port, $proto, $testcall, $timenow, $htime, $rtime, $validaprs, $validnmea, $validtnc); # ======================================================================== # COMMAND LINE ARGUMENT PARSING # Parse the input line for command line settings # ======================================================================== my $debug = 0; #default, no non-aprs packets printed my $show = 0; #default, no aprs packets printed my $allArgsOK = 1; #assume all arguments are OK for ($i = 0; $i <= $#ARGV; $i++) { $argsValid = 0; #assume no argument errors if ($ARGV[$i] eq "-h" or $ARGV[$i] eq "-help") { print "\nUsage:\n"; print "\tperlAPRS [-h | -help] [-v | -version] [-s | -show]\n"; print "\t\t [-d | -debug] [-f | -file file] [-p | -port port]\n\n"; print "\t-h or -help for this help screen\n"; print "\t-v or -version to display version\n"; print "\t-s or -show show valid posit APRS packets\n"; print "\t-d or -debug display invalid posit APRS packets\n"; print "\t-f or -file database command file\n"; print "\t-p or -port computer serial port etc.\n\n"; print "Examples:\n"; print "\tperlAPRS &\n"; print "\t\tTo start the program in background with no output\n"; print "\t\tdisplayed.\n"; print "\tperlAPRS -s -d\n"; print "\t\tStart the program to display packets with valid\n"; print "\t\tposit packets and non-posit carrying aprs packets.\n"; print "\tperlAPRS -f callsign2.dat\n"; print "\t\tTo change the name of the database command file from\n"; print "\t\tthe default filename.\n"; print "\tperlAPRS -p /dev/ttyS1\n"; print "\t\tTo change the source of packets from the default\n"; print "\t\tserial port to a different serial port.\n"; print "\tperlAPRS -p www.wa4dsy.radio.org:14579\n"; print "\t\tTo change the source of packets from the default\n"; print "\t\tserial port to an Internet APRS server.\n"; print "\tperlAPRS -p trip.tnc\n"; print "\t\tTo change the source of packets from the default\n"; print "\t\tserial port to a pre-recorded text log file.\n"; exit(0); } # # Check for version request in comand line arguments # if ($ARGV[$i] eq "-v" or $ARGV[$i] eq "-version") { print "\nName: perlAPRS\n"; print "Version: Version 1.1.2\n"; print "Author: Rich Parry, W9IF\n\n"; print "This program may be copied only under the terms of\n"; print "the GNU General Public License.\n\n"; exit(0); } # # Check for aprs print option (print valid aprs posit packets) # if($ARGV[$i] eq "-s" or $ARGV[$i] eq "-show") { $show = 1; $argsValid = 1; #set valid argument flag } # # Check for non-aprs print option (print aprs with no posit) # if($ARGV[$i] eq "-d" or $ARGV[$i] eq "-debug") { $debug = 1; $argsValid = 1; #set valid argument flag } # # Change default command "datafile" if specified by user on command line # if($ARGV[$i] eq "-f" or $ARGV[$i] eq "-file") { $i++; $datafile = $ARGV[$i]; $argsValid = 1; #set valid argument flag } # # Change "port" default serial port, check for Internet address if ":" # if($ARGV[$i] eq "-p" or $ARGV[$i] eq "-port") { $i++; $inport = $ARGV[$i]; if ($inport =~ m/:/) { $netport = 1; # Internet address, not serial port ($host, $port) = split/:/, $inport, 2; } else { $netport = 0; # Serial port, not net address } $argsValid = 1; # Set valid argument flag } # # If not a valid argument, print it as an error message # if (!$argsValid) { print"\t*** Invalid argument = $ARGV[$i]\n"; $allArgsOK = 0; #found at least 1 bad arg. } } # # If invalid argument(s) in the command line, do not proceed, exit! # if (!$allArgsOK) { exit(0); } # ======================================================================== # INITIALIZATION # ======================================================================== # # Read in command file array # open(COMMANDS, "< $datafile") or die "Can't open file=$datafile\n\n"; # # Read callsign, command, gridsquare etc. from data file and store in array. # my $cmdcount = 0; while () { chomp; ($cinput[$cmdcount][$ptrcallsign], $cinput[$cmdcount][$ptrcommand], $cinput[$cmdcount][$ptrgridsquare], $cinput[$cmdcount][$ptrexecutionref], $cinput[$cmdcount][$ptrresettime]) = split(/\s+/, $_, 5); $cinput[$cmdcount][$ptrexecutionctr] = 0; # initialize execution ctr $cinput[$cmdcount][$ptrheardtime] = time; # initialize time heard $cmdcount++; } close(COMMANDS); # For each of the grid squares that we just read in from the file # unpack the 2, 4, or 6 characters for further analysis and computation if ($show) { print "\n\t\t\t*** USER DATA ***\n"; print "\tCallsign\tCommand\t\t\t\tGrid\tExe\tReset\tLwrLat\tLwrLon\tUprLat\tUprLon\n"; } for ($i = 0; $i < $cmdcount; $i++) { # Check for "any" grid square (in continental U.S.) indicated by "*" if ($cinput[$i][$ptrgridsquare] eq '*') { $latlow = 2400.00; $lonlow = -12600.00; $latupr = 5000.00; $lonupr = -6600.00; } @grid = unpack "cccccc", $cinput[$i][$ptrgridsquare]; # # For 2 Letter Grid Square # if (scalar @grid == 2) { $latlow = (($grid[1] - 65) * 10 - 90) * 100; $lonlow = (($grid[0] - 65) * 20 - 180) * 100; $latupr = $latlow + 1000; $lonupr = $lonlow + 2000; } # # For 4 Letter Grid Square # if (scalar @grid == 4) { $latlow = (($grid[1] - 65) * 10 + ($grid[3] - 48) * 1 - 90) * 100; $lonlow = (($grid[0] - 65) * 20 + ($grid[2] - 48) * 2 - 180) * 100; $latupr = $latlow + 100; $lonupr = $lonlow + 200; } # # For 6 Letter Grid Square format "DegreesMinutes.DecimalMinutes" # if (scalar @grid == 6) { # compute latitude of lower corner $latdegrees = ($grid[1] - 65) * 10 + ($grid[3] - 48) - 90 + $gridchar5{chr($grid[5])}{deg}; $latlow = pack "a*a*a*", $latdegrees, $gridchar5{chr($grid[5])}{min}, $gridchar5{chr($grid[5])}{decmin}; # compute longitude of lower corner $londegrees = ($grid[0] - 65) * 20 + ($grid[2] - 48) * 2 - 180 + $gridchar4{chr($grid[4])}{deg}; $lonlow = pack "a*a*", $londegrees, $gridchar4{chr($grid[4])}{min}; # compute latitude of upper corner $char5 = chr($grid[5] + 1); # increment character $latdegrees = ($grid[1] - 65) * 10 + ($grid[3] - 48) - 90 + $gridchar5{$char5}{deg}; $latupr = pack "a*a*a*", $latdegrees, $gridchar5{$char5}{min}, $gridchar5{$char5}{decmin}; # compute longitude of upper corner $char4 = chr($grid[4] + 1); # increment character $londegrees = ($grid[0] - 65) * 20 + ($grid[2] - 48) * 2 - 180 + $gridchar4{$char4}{deg}; $lonupr = pack "a*a*", $londegrees, $gridchar4{$char4}{min}; } # # Place the computed lower and upper coordinates in array # $cinput[$i][$ptrlatlow] = $latlow; $cinput[$i][$ptrlonlow] = $lonlow; $cinput[$i][$ptrlatupr] = $latupr; $cinput[$i][$ptrlonupr] = $lonupr; # Print comparison array which contains all data for each callsign if ($show) { # for ($j = 0; $j <= $ptrlonupr; $j++) { # print $cinput[$i][$j], "\t"; # } printf "%2s %10s %8s %7s %4s %6s %8.1f %8.1f %8.1f %8.1f\n", $i + 1, $cinput[$i][$ptrcallsign], $cinput[$i][$ptrcommand], $cinput[$i][$ptrgridsquare], $cinput[$i][$ptrexecutionref], $cinput[$i][$ptrresettime], $cinput[$i][$ptrlatlow], $cinput[$i][$ptrlonlow], $cinput[$i][$ptrlatupr], $cinput[$i][$ptrlonupr]; } } # end for loop # # Open port. If net address open socket, other open serial port # if (!$netport) { open APRSPORT, "<$inport" or die "Can't open PORT $inport\n\n"; } else { $in_addr = (gethostbyname($host)) [4]; $addr = sockaddr_in($port, $in_addr); $proto = getprotobyname('tcp'); # Create an Internet protocol socket. socket(APRSPORT, AF_INET, SOCK_STREAM, $proto) or die "socket:$!"; # Connect our socket to the server socket connect (APRSPORT, $addr) or die "connect:$!"; # For fflish on socket file handle after every write select (APRSPORT); $| = 1; select(STDOUT); } # ======================================================================== # OPEN PORT # Serial or Internet # ======================================================================== # # Start the main portion of the program here # if ($show) { print "\n\n\t\t*** WAIT FOR INPUT AND PARSE ***\n"; } MAIN: while () { # read in a APRS packet chomp; # remove newline char my $packet = $_; # save the packet if (length $packet eq 0) { next MAIN }; # skip all if a null packet if ($show) { print "Packet= $packet\n"; # print input line } # # Split up the packet and store elements into array # ($fromcallsign) = split(/>/, $packet, 0); # split by ">" delimiter $fromcallsign =~ s/\*//; # remove any trailing * char $fromcallsign =~ s/^[^A-Z]//; # remove leading non-alpha chars # # In this block, search for a valid "TO" address. If none found set flag # and exit block. If found, set flag for valid APRS packet. However, # additional testing will be done later to confirm packet contains valid # latitude and longitude values. # If the portion of the packet that we now have begins with a legitimate # "TO ADDRESS", then accept this packet and proceed, but remove the # TO ADDRESS, it is no longer needed. # APRSBLOCK: { $validaprs = 1; # assume legitimate APRS packet foreach $testcall (@APRSsigns) { if ($packet =~ m/\>$testcall/) { # search for leading ">" + sign last APRSBLOCK; } } $validaprs = 0; # set flag, not APRS packet } # END APRSBLOCK # # Extract Lat and Long if in form 99.99?N/99999.99?W # or 99.99?N\99999.99?W # or 99.99?N[A-Z]99999.99?W # TNCBLOCK: { if($validaprs) { $validtnc = 1; # assume valid latitude or longitude # if ($packet =~ m/([0-9][0-9][0-9][0-9]\.[0-9][0-9][0-9]?[N|n][A-Z|\/][0-9][0-9][0-9][0-9][0-9]\.[0-9][0-9][0-9]?[W|w])/) if ($packet =~ m/([0-9][0-9][0-9][0-9]\.[0-9][0-9][0-9]?[N|n].[0-9][0-9][0-9][0-9][0-9]\.[0-9][0-9][0-9]?[W|w])/) { $latlong = $1; # save latitude and longitude $latlong =~ m/([0-9][0-9][0-9][0-9]\.[0-9][0-9][0-9]?)/; $lat = $1; $latlong =~ m/([0-9][0-9][0-9][0-9][0-9]\.[0-9][0-9][0-9]?)/; $long = $1; last TNCBLOCK; } $validtnc = 0; # not valid latitude or longitude } } # END TNCBLOCK # # Now find out if it is a NMEA standard string # Strings supported are: # $GPGGA Global Positioning System Fix Data # $GPGLL Geographic position, latitude and longitude # $GPRMC Recommended minimum specific GPS/Transit data # NMEASWITCH: { if($validaprs && !$validtnc) { $validnmea = 1; # assume valid NMEA packet if ($packet =~ m/\$GPGGA/) { # separate header and payload, we need only the payload) ($header, $payload) = split(/\$GPGGA/, $packet); $payload =~ s/[A-Z]//g; # remove chars in bad NMEAs @posit = split(/,/, $payload); # disect $GP sentence into fields $lat = $posit[2]; # extract latitude $long = $posit[4]; # extract longitude if ($lat eq "" or $long eq "") { $validnmea = 0} # invalid NMEA last NMEASWITCH; } if ($packet =~ m/\$GPGLL/) { # separate header and payload, we need only the payload) ($header, $payload) = split(/\$GPGLL/, $packet); $payload =~ s/[A-Z]//g; # remove chars in bad NMEAs @posit = split(/,/, $payload); # disect $GP sentence into fields $lat = $posit[1]; # extract latitude $long = $posit[3]; # extract longitude if ($lat eq "" or $long eq "") { $validnmea = 0} # invalid NMEA last NMEASWITCH; } if ($packet =~ m/\$GPRMC/) { # separate header and payload, we need only the payload) ($header, $payload) = split(/\$GPRMC/, $packet); $payload =~ s/[A-Z]//g; # remove chars in bad NMEAs @posit = split(/,/, $payload); # disect $GP sentence into fields $lat = $posit[3]; # extract latitude $long = $posit[5]; # extract longitude if ($lat eq "" or $long eq "") { $validnmea = 0} # invalid NMEA last NMEASWITCH; } $validnmea = 0; # Invalid NMEA packet } } # END NMEA BLOCK # # Got valid APRS packet, check for match in command database, if so execute # if ($validaprs && ($validtnc or $validnmea)) { if ($show) { printf "\t%11s %4.3f %5.3f\n", $fromcallsign, $lat, $long; } # Current version assumes station is in North American, # N,S,E,W parameters ignored, therefore force to negative longitude $long = -$long; # Is packet from within the gridsquare of any callsign in the database for ($i = 0; $i < $cmdcount; $i++) { if(($cinput[$i][$ptrcallsign] eq $fromcallsign or $cinput[$i][$ptrcallsign] eq "*") && ($lat > $cinput[$i][$ptrlatlow]) && ($lat < $cinput[$i][$ptrlatupr]) && ($long > $cinput[$i][$ptrlonlow]) && ($long < $cinput[$i][$ptrlonupr])) { # get current time to time stamp this packet $timenow = time; # Is it time to reset the execution counter? # If so, reset it. if (($cinput[$i][$ptrheardtime] + $cinput[$i][$ptrresettime] * 60) < $timenow) { # timeout, reset execution counter $cinput[$i][$ptrexecutionctr] = 0; # timeout, update time heard for this station $cinput[$i][$ptrheardtime] = $timenow; } # Yes, we have a match, but is the execution count # for this packet at limit, if not execution command. if ($cinput[$i][$ptrexecutionctr] < $cinput[$i][$ptrexecutionref]) { system $cinput[$i][$ptrcommand]; $cinput[$i][$ptrexecutionctr]++; } if ($show) { $htime = scalar localtime($timenow); $rtime = scalar localtime($cinput[$i][$ptrheardtime] + $cinput[$i][$ptrresettime] * 60); printf "\t\*%10s %12s %22s %4s\n\t\t\t\t %22s %4s %12s\n", $cinput[$i][$ptrcallsign], $cinput[$i][$ptrgridsquare], $htime, $cinput[$i][$ptrexecutionref], $rtime, $cinput[$i][$ptrexecutionctr], $cinput[$i][$ptrcommand]; } } else { if ($show) { printf "\t-%10s %12s\n", $cinput[$i][$ptrcallsign], $cinput[$i][$ptrgridsquare]; } } } } else { if ($debug) { print "*No Posit= $packet\n"; } next; } #sleep 1; # For testing only } # END MAIN BLOCK close (APRSPORT); # ======================================================================== # REVISION HISTORY # ======================================================================== # # This section contains comments describing changes made to the software. # # # Ver 1.1.2 Sept 16, 2000 # 1) Completely updated @APRSsigns to conform with new # official APRS standards # # Ver 1.1.1 July 20, 2000 # 1) Added "APD" to @APRSsigns to accomodate packets formatted # by the aprsd server. # # Ver 1.1.0 May 13, 2000 # 1) Added "any grid square" feature to all the user to # specify in the "callsign.dat" configuration file # that a command can be executed when a specific # callsign occurs in "any grid square". For example: # # W9IF-9 cmdKT.sh DM12KT 1 60 <-- standard # W9IF-9 cmdKT.sh * 1 60 <-- new feature # # Ver 1.0.3 May 25, 1998 # 1) Corrected error in formula used to compute the longitude # of 6 letter grid squares. Note, not all 6 letter grid # squares were incorrect. Thanks to Fred Kehler, VE7IPB. # # Ver 1.0.2 March 2, 1998 # 1) Modified @APRSsigns from "APRS" to "APR" to accomodate # packets with DOS APRS format which includes the # version of the software, for example # KJ6ZJ>APR803,KB6TLJ-5,WIDE*:=3352.29N/11818.66WyHello! # # Ver 1.0.1 October 1, 1997 # 1) Made program more bullet proff to bad NMEA strings # For example, # $GPGGA,032315,3559.792,N341.886,W,1,05 is not valid # $GPGGA,032315,3559.792,N,341.886,W,1,05 is valid # Program would give run time error on first example # After fix, program ignors the invalid sentence # Thanks to Alan Crosswell, N2YGK # 2) Grid Squares with a letter "X" gave an error message. # "X" is the last valid grid square character and # requires "Y" to be added to the list of valid chars # in the code, even though it is not a valid grid square # character. This is based on the way the algorithm # used to convert grid square chars to values. # # Ver 1.0 August 14, 1997 # Initial release # # ========================================================================