#!/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
#
# This program was revised by Kevin Wittmer, KB8VME, to pass in the 
# latitude, longitude and grid during command execution.
#
# ========================================================================


require 5.002;
use Socket;
use diagnostics;
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 (<COMMANDS>) {
  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";
}

 #print APRSPORT "user kb8vme pass -1 vers testsoft 1.1.1 filter r/36/-75/2400\n";
 print APRSPORT "user kb8vme pass -1 filter r/36/-75/2400\n";

 MAIN: while (<APRSPORT>) {                    # 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])) {
                if ($show) {
                    print "We have a match!\n";
                }
                
		 # 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]) {
			
    		     my $grid = $cinput[$i][$ptrgridsquare];
                     print "*";
                     my $callsign_lat = $lat / 100;
		     my $callsign_long = $long / 100;
		     system($cinput[$i][$ptrcommand] . " $fromcallsign $callsign_long $callsign_lat $grid");
		     $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
#
# ========================================================================

