#!/usr/bin/perl
#           NMEA data groker
#           ----------------
#
# This attempts to make limited sense of NMEA data. It is intended to be
#  used to help people understand NMEA data, or quickly check what their
#  device is sending. 
# It isn't expected to give a nice user view of NMEA data - use something
#  like np for that
# It accepts NMEA data on stdin
#
# Distributed under the GPL
#
# v0.02 - 13/01/2006
#
# Nick Burch <code@gagravarr.org>

use strict;

# A list of NMEA talkers, from http://vancouver-webpages.com/peter/nmeafaq.txt
my %talkers = (
	"PGRM" => "Proprietary Garmin",
	"PRFM" => "Proprietary ?Rikaline?",
	"PSLI" => "Proprietary Starlink",
	"GP" => "GPS",
	"LC" => "Loran-C",
	"OM" => "Omega Navigation",
	"II" => "Integrated Instrumentation"
);

print "Accepting NMEA data on STDIN\n\n";

while(my $line=<>) {
	$line =~ s/\s+$//;
	unless($line =~ /^\$/) {
		warn("Line didn't start with a \$, skipping\n");
		next;
	}

	my ($talker, $sentence_id, $data,$checksum) =
		($line =~ /^\$(..)(.*?)\,(.*?)(\*[0-9A-F]{2})?$/);
	unless($talker) {
		warn("Line in incorrect format, skipping\n");
		next;
	}

	# If a checksum is present, check it's valid
	if($checksum) {
		&verify_checksum($line,$checksum);
	}

	# Check to see if it's a proprietary sentence
	if($talker =~ /^P/) {
		$talker .= substr($sentence_id,0,2);
		$sentence_id =~ s/^..//;
		&grok_proprietary($talker,$sentence_id, $data);
		next;
	}

	# See what we can find out from this data
	if($talkers{$talker}) {
		print $talkers{$talker}." data:\n";
	} else {
		print "Unknown talker '$talker' data:\n";
	}

	# Is it one we know about?
	if($sentence_id eq "GSV") {
		&gsv_satellite_view($data);
	}
	elsif($sentence_id eq "GLL") {
		&gll_geo_postion($data);
	}
	elsif($sentence_id eq "GGA") {
		&gga_gps_fix($data);
	}
	elsif($sentence_id eq "GSA") {
		&gsa_sats_dop($data);
	}
	elsif($sentence_id eq "RMC") {
		&rmc_transit_data($data);
	}
	elsif($sentence_id eq "VTG") {
		&vtg_track_good($data);
	}
	elsif($sentence_id eq "") {
	}
	else {
		print "\tSentence ID: $sentence_id\n";
		print "\tData: $data\n";
	}
}

#########################################################################

sub verify_checksum {
	my ($line,$checksum) = @_;
	$checksum =~ s/^\*//;

	# Checksum is $ to * exclusive
	$line =~ s/^\$//;
	$line =~ s/\*..$//;

	my @bytes = split(//, $line);
	my $cs = "\00";
	foreach my $b (@bytes) {
		$cs = $cs ^ $b;
	}
	my $hex_cs = uc(sprintf("%02x",ord($cs)));

	unless($hex_cs eq $checksum) {
		print "\nWarning: Checksum '$checksum' didn't match '$hex_cs' on line:\n";
	}
}

sub grok_proprietary {
	my ($talker,$sentence_id, $data) = @_;

	my $whose = $talkers{$talker};
	unless($whose) {
		$whose = "Unknown ($talker)";
	}

	print "Proprietary sentence ($whose)\n";
	print "\t$sentence_id = $data\n";
}


#########################################################################

sub format_time {
	my $time = shift;
	my @parts = ($time =~ /^(\d\d)(\d\d)(\d\d)(\.\d\d)?$/);
	if($parts[2]) {
		my $str = "$parts[0]:$parts[1]:$parts[2]";
		if($parts[3]) {
			$str .= $parts[3];
		}
		return $str." UTC";
	} else {
		return "invalid ($time)\n";
	}
}

sub format_date {
	my $date = shift;
	my ($dd,$mm,$yy) = ($date =~ /^(\d\d)(\d\d)(\d\d)/);
	my $yyyy = 2000 + $yy;

	my @months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');

	return "$dd ".$months[($mm-1)]." $yyyy ($yyyy-$mm-$dd)";
}

sub format_latlong {
	my ($lat,$ns,$long,$ew) = @_;

	return format_llnum($lat)." $ns, ".format_llnum($long)." $ew";
}
sub format_llnum {
	my $num = shift;
	my ($deg, $dec) = ($num =~ /^(\d\d)(.*)/);
	return "$deg deg $dec";
}

#########################################################################

# GGA - Global Positioning System Fix Data
sub gga_gps_fix {
	my $data = shift;
	my @d = split(/,/, $data);
	my %fix_type = ( 0=>'invalid', 1=>'GPS', 2=>'DGPS' );

	my $time = &format_time($d[0]);
	my $loc = &format_latlong(@d[1..4]);
	my $sats_tracked = $d[6];

	print "\tGPS Fix from $sats_tracked satellites\n";
	print "\t\tLocation is $loc\n";
	print "\t\tTime found is $time\n";
	print "\t\tFix type is ".$fix_type{$d[5]}."\n";
	print "\t\tHorizontal dilution is $d[7]\n";
	print "\t\tAltitude above MSL is $d[8] $d[9]\n"; 
	print "\t\tMSL height above WGS84 is $d[10] $d[11]\n";
}

# GLL - Geographic Position
sub gll_geo_postion {
	my $data = shift;
	my @d = split(/,/, $data);

	my $loc = &format_latlong(@d[0..3]);
	my $time = &format_time($d[4]);
	my $valid = $d[5];

	print "\tPosition Identified\n";
	print "\t\tLocation is $loc\n";
	print "\t\tTime found is $time\n";
	if($valid) {
		print "\t\tData is considered to be valid\n";
	}
}

# GSA - DOP, active sats and dilution (quality)
#  Based on satelites being in the wrong place for a good fix
sub gsa_sats_dop {
	my $data = shift;
	my @d = split(/,/, $data);

	my $fix_type = "auto";
	if($d[0] = 'M') { $fix_type = "manual"; }
	my @sats = @d[2..13];
	while(! $sats[-1] && scalar @sats > 0) { pop @sats; }
	my $sat_list = join(', ', sort(@sats));

	print "\tSatellites and DOP (dilution of precision)\n";
	print "\t\tUsing satelites $sat_list\n";
	print "\t\tOverall DOP $d[14]\n";
	print "\t\tHorizonal DOP $d[15]\n";
	print "\t\tVertical DOP $d[16]\n";
	print "\t\tFix type $fix_type in $d[1]D\n";
}

# GSV - Satellite view
sub gsv_satellite_view {
	my $data = shift;
	my @d = split(/,/, $data);

	my $full_view_in = shift(@d);
	my $sentence_no = shift(@d);
	my $in_view = shift(@d);

	print "\tSatellite View - sentence $sentence_no of $full_view_in - Can see $in_view satellites\n";

	while(scalar(@d) > 0) {
		my $prn_num = shift(@d);
		my $elevation = shift(@d);
		my $azimuth = shift(@d);
		my $sig_strength = shift(@d);
		print "\t\tSeeing satellite $prn_num at e$elevation a$azimuth, strength $sig_strength\n";
	}
}

# GPS Transit Data
sub rmc_transit_data {
	my $data = shift;
	my @d = split(/,/, $data);
	
	my $time = &format_time($d[0]);
	my $loc = &format_latlong(@d[2..5]);
	my $date = &format_date($d[8]);

	my $nav_rec = "OK";
	if($d[1] ne 'A') { $nav_rec = "Warning"; }

	print "\tGPS transit data\n";
	print "\t\tTime of fix is $time\n";
	print "\t\tDate of fix is $date\n";
	print "\t\tLocation is $loc\n";
	print "\t\tGround speed $d[6] knots\n";
	print "\t\tTrue heading $d[7] deg\n";
	if($d[9]) {
		print "\t\tMagnetic Variation $d[9] deg $d[10]\n";
	}
	print "\t\tNavigation Receiver is $nav_rec\n";
}

# Track made good, ground speed etc
sub vtg_track_good {
	my $data = shift;
	my @d = split(/,/, $data);
	
	print "\tTrack made good, movement details:\n";
	print "\t\tTrue heading $d[0] deg\n";
	if($d[2]) {
		print "\t\tMagnetic heading $d[2] deg\n";
	}
	print "\t\tGround speed $d[4] knots\n";
	print "\t\tGround speed $d[6] kmph\n";
}
