#!/usr/bin/perl
#
# Decode and display Trimble TSIP binary protocol packets
#
# Copyright (c) 2009
#   Chris Adams <gps@cmadams.net>
#
########################################################################
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# 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.
########################################################################
#
# This is a minimal decoder for many types of packets in the Trimble TSIP
# binary GPS communications protocol.  No real attempt is made at interpreting
# the packets, except for converting GPS time to UTC.
#
# This has been tested with an SVeeSix.
#
# Required perl modules: Time::HiRes and Device::SerialPort
#
# The default is to read from /dev/gps, or you can specify another device on
# the command line.
#
# History:
# 1.0 - 2009-11-06
#	initial version
#


use Time::HiRes;
use Device::SerialPort;
use POSIX qw(strftime);
use warnings;
use strict;

# ICD-GPS-200 defined value for PI
use constant PI => 3.1415926535898;
use constant C => 299792458;

my $PORT = shift @ARGV || "/dev/gps";

loop ();

sub loop
{
	my $port = new Device::SerialPort ($PORT) or die "open($PORT): $!\n";
	$port->baudrate (9600);
	$port->databits (8);
	$port->parity ("odd");
	$port->stopbits (1);
	$port->handshake ("none");

	$port->read_char_time (999999);
	$port->read_const_time (999999);

	my $pkt = "";
	my @t;
	while ($pkt .= $port->read (1)) {
		# timestamp the start of the packet
		@t = Time::HiRes::gettimeofday if (! @t);

		# Start with DLE?
		if ($pkt !~ /^\x10/) {
			warn "Bad packet: ", dump_hex ($pkt), "\n";
			$pkt = "";
			@t = ();
			next;
		}

		# Have with DLE ETX?
		next if ($pkt !~ /\x10\x03/);

		# See if it we have a complete packet (DLE followed by odd
		# number of DLEs followed by ETX)
		my $dle = 0;
		my $upkt = $pkt;
		my $dpkt = "";
		while ($upkt =~ /^(.*?\x10)(.*)/s) {
			$dpkt .= $1;
			$upkt = $2;
			$dle ++;
		}
		if ((($dle % 2) == 0) && ($upkt =~ /^(\x03)(.*)/s)) {
			$dpkt .= $1;
			$upkt = $2;
			print "Packet: ", dump_hex ($dpkt), "\n";

			parse_pkt ($dpkt, @t);
			$pkt = $upkt;
			@t = ();
		} else {
			print "Continue ($dle): ", dump_hex ($pkt), "\n";
		}
	}
}


sub dump_hex
{
	return join (" ", map { sprintf ("%02x", $_) } unpack ("C*", $_[0]));
}


sub unstuff
{
	my $pkt = shift;
	my $upkt = "";
	while ($pkt =~ /(.*?\x10)\x10(.*)/s) {
		$upkt .= $1;
		$pkt = $2;
	}
	return $upkt . $pkt;
}


# Big-endian 16 bit integer
sub decode_int
{
	my $ar = shift;

	my $a = shift @$ar;
	my $b = shift @$ar;

	my $val = $a;
	$val = ($val << 8) + $b;
	return $val;
}


# Big-endian 32 bit integer
sub decode_long
{
	my $ar = shift;

	my $a = shift @$ar;
	my $b = shift @$ar;
	my $c = shift @$ar;
	my $d = shift @$ar;

	my $val = $a;
	$val = ($val << 8) + $b;
	$val = ($val << 8) + $c;
	$val = ($val << 8) + $d;
	return $val;
}


# IEEE 754-2008 32 bit single-precision floating point
sub decode_single
{
	my $ar = shift;

	my $a = shift @$ar;
	my $b = shift @$ar;
	my $c = shift @$ar;
	my $d = shift @$ar;

	# 1 bit sign, 8 bit exponent, 23 bit mantissa
	my $sign = $a >> 7;
	my $exp = 0x7f & $a;
	$exp = ($exp << 1) + ($b >> 7);

	my $sig = 0x7f & $b;
	$sig = ($sig << 8) + $c;
	$sig = ($sig << 8) + $d;

	my $normal = 1;
	if ($exp == 0) {
		$normal = 0;
		$exp = 1;
	} elsif ($exp == 0xff) {
		if ($sig) {
			return "NaN";
		} elsif ($sign) {
			return "-Infinity";
		} else {
			return "+Infinity";
		}
	}

	my $val = (1 - 2 * $sign) * ($normal + $sig / (2 ** 23)) *
	    (2 ** ($exp - 127));
	return $val;
}


# IEEE 754-2008 64 bit double-precision floating point
sub decode_double
{
	my $ar = shift;

	my $a = shift @$ar;
	my $b = shift @$ar;
	my $c = shift @$ar;
	my $d = shift @$ar;
	my $e = shift @$ar;
	my $f = shift @$ar;
	my $g = shift @$ar;
	my $h = shift @$ar;

	# 1 bit sign, 11 bit exponent, 52 bit mantissa
	my $sign = $a >> 7;
	my $exp = 0x7f & $a;
	$exp = ($exp << 4) + ($b >> 4);

	my $sig = 0x0f & $b;
	$sig = ($sig << 8) + $c;
	$sig = ($sig << 8) + $d;
	$sig = ($sig << 8) + $e;
	$sig = ($sig << 8) + $f;
	$sig = ($sig << 8) + $g;
	$sig = ($sig << 8) + $h;

	my $normal = 1;
	if ($exp == 0) {
		$normal = 0;
		$exp = 1;
	} elsif ($exp == 0x7ff) {
		if ($sig) {
			return "NaN";
		} elsif ($sign) {
			return "-Infinity";
		} else {
			return "+Infinity";
		}
	}

	my $val = (1 - 2 * $sign) * ($normal + $sig / (2 ** 52)) *
	    (2 ** ($exp - 1023));
	return $val;
}


# Convert a GPS week, time, and GPS/UTC offset (in seconds) to UTC time
sub gps2utc
{
	my $week = shift;
	my $sec = shift;
	my $off = shift || 0;

	our $gpsstart;
	if (! $gpsstart) {
		use Time::Local;
		$gpsstart = timegm (0, 0, 0, 6, 0, 80);
	}
	my $now = $gpsstart + ($week * 86400 * 7) + $sec - $off;
	return $now;
}


# Try to parse and print a packet
sub parse_pkt
{
	my $pkt = shift;
	my $sec = shift;
	my $usec = shift;

	our $utc_off;
	our $week;

	my @pkt = unpack ("C*", unstuff ($pkt));
	if (shift @pkt != 0x10) {
		warn "Invalid packet (no <DLE> at start)\n";
		warn dump_hex ($pkt), "\n";
		return;
	}
	if ((pop @pkt != 0x03) || (pop @pkt != 0x10)) {
		warn "Invalid packet (no <DLE><ETX> at end)\n";
		warn dump_hex ($pkt), "\n";
		return;
	}

	my $id = shift @pkt;

	my $nowfmt = strftime ("%T.", localtime ($sec)) .
	    sprintf ("%06d", $usec);
	printf "%s (%02x): ", $nowfmt, $id;

	my $decode = 0;
	if ($id == 0x13) {
		$decode = 1;
		print "TSIP Parsing Error Notification\n";

		my $pid = shift @pkt;
		print "   Unable to decode packet with ID: ", $pid, "\n";
		print "   Packet: ", dump_pkt (@pkt), "\n";
	} elsif ($id == 0x1a) {
		print "TSIP RTCM Wrapper / Port A Echo\n";
	} elsif ($id == 0x3d) {
		$decode = 1;
		print "Serial Port A Configuration\n";

		my %baud = (0 => 50, 1 => 110, 4 => 300, 5 => 600, 6 => 1200,
		    8 => 2400, 9 => 4800, 11 => 9600, 12 => 38400,
		    28 => 19200);

		my $outb = shift @pkt;
		print "   Output Baud Rate: ", $baud{$outb} ? $baud{$outb} :
		    "(" . $outb . ")", "\n";

		my $inb = shift @pkt;
		print "   Input Baud Rate: ", $baud{$inb} ? $baud{$inb} :
		    "(" . $inb . ")", "\n";

		my $parbits = shift @pkt;
		print "   Parity and # bits/character: ";
		if ($parbits == 2) {
			print "7 bits, Even Parity\n";
		} elsif ($parbits == 3) {
			print "8 bits, Even Parity\n";
		} elsif ($parbits == 6) {
			print "7 bits, Odd Parity\n";
		} elsif ($parbits == 7) {
			print "8 bits, Odd Parity\n";
		} elsif ($parbits == 18) {
			print "7 bits, No Parity\n";
		} elsif ($parbits == 19) {
			print "8 bits, No Parity\n";
		} else {
			print "(", $parbits, ")\n";
		}

		my $stopflow = shift @pkt;
		print "   Stop bits and hardware flow control: ";
		if ($stopflow == 7) {
			print "1 stop bit, heed CTS, normal RTS\n";
		} elsif ($stopflow == 15) {
			print "2 stop bits, heed CTS, normal RTS\n";
		} elsif ($stopflow == 23) {
			print "1 stop bit, ignore CTS, normal RTS\n";
		} elsif ($stopflow == 31) {
			print "2 stop bits, ignore CTS, normal RTS\n";
		} elsif ($stopflow == 39) {
			print "1 stop bit, heed CTS, RTS always\n";
		} elsif ($stopflow == 47) {
			print "2 stop bits, heed CTS, RTS always\n";
		} elsif ($stopflow == 55) {
			print "2 stop bit, ignore CTS, RTS always\n";
		} else {
			print "(", $stopflow, ")\n";
		}

		my $outprot = shift @pkt;
		print "   Output Protocol (A): ";
		if ($outprot == 0) {
			print "TSIP Packets\n";
		} elsif ($outprot == 1) {
			print "Off\n";
		} elsif ($outprot == 5) {
			print "NMEA\n";
		} elsif ($outprot == 6) {
			print "RTCM SC-104\n";
		} elsif ($outprot == 7) {
			print "Packets 0x60 and 0x61\n";
		} else {
			print "(", $outprot, ")\n";
		}

		my $inprot = shift @pkt;
		print "   Input Protocol (A): ";
		if ($inprot == 0) {
			print "TSIP Packets\n";
		} elsif ($inprot == 1) {
			print "RTCM SC-104\n";
		} elsif ($inprot == 6) {
			print "Off (do not decode Port A input)\n";
		} else {
			print "(", $inprot, ")\n";
		}
	} elsif ($id == 0x40) {
		$decode = 1;
		print "Almanac Data for Single Satellite\n";

		my $sat = shift @pkt;
		print "   Satellite: ", $sat, "\n";
		my $t_zc = decode_single (\@pkt);
		printf "   T_zc: %-7g seconds\n", $t_zc;
		$week = decode_int (\@pkt);
		print "   Week number: ", $week, "\n";
		my $ecc = decode_single (\@pkt);
		printf "   Eccentricity: %-7g\n", $ecc;
		my $t_oa = decode_single (\@pkt);
		printf "   T_oa: %-7g seconds\n", $t_oa;
		my $i_o = decode_single (\@pkt);
		printf "   i_o: %-7g radians\n", $i_o;
		my $omega_dot = decode_single (\@pkt);
		printf "   OMEGA_dot: %-7g radians/sec\n", $omega_dot;
		my $sqr_a = decode_single (\@pkt);
		printf "   Square root A: %-7g (meters)^1/2\n", $sqr_a;
		my $omega_o = decode_single (\@pkt);
		printf "   OMEGA o: %-7g radians\n", $omega_o;
		my $omega = decode_single (\@pkt);
		printf "   Omega: %-7g radians\n", $omega;
		my $m_o = decode_single (\@pkt);
		printf "   M o: %-7g radians\n", $m_o;
	} elsif ($id == 0x41) {
		$decode = 1;
		print "GPS Time\n";

		my $time = decode_single (\@pkt);
		printf "   GPS time of week: ";
		if ($time < 0) {
			print "unknown\n";
		} else {
			printf "%-7g\n", $time;
		}
		$week = decode_int (\@pkt);
		print "   GPS week number: ", $week, "\n";
		$utc_off = decode_single (\@pkt);
		printf "   UTC/GPS time offset: %-7g\n", $utc_off;
		print "   -- converted UTC time: ",
		    strftime ("%Y-%m-%d %T", gmtime (gps2utc ($week, $time,
		    $utc_off))), "\n";
	} elsif ($id == 0x42) {
		$decode = 1;
		print "Single-Precision Position Fix (XYZ Cartesian ECEF)\n";

		my $x = decode_single (\@pkt);
		printf "   X: %-7g meters\n", $x;
		my $y = decode_single (\@pkt);
		printf "   Y: %-7g meters\n", $y;
		my $z = decode_single (\@pkt);
		printf "   Z: %-7g meters\n", $z;
		my ($t, $fmt);
		if (scalar (@pkt) > 4) {
			$t = decode_double (\@pkt);
			$fmt = 16;
		} else {
			$t = decode_single (\@pkt);
			$fmt = 7;
		}
		printf "   Time of Fix: %-" . $fmt . "f seconds\n", $t;
	} elsif ($id == 0x43) {
		$decode = 1;
		print "Velocity Fix (XYZ Cartesian ECEF)\n";

		my $dx = decode_single (\@pkt);
		printf "   X Velocity: %-7g meters/sec\n", $dx;
		my $dy = decode_single (\@pkt);
		printf "   Y Velocity: %-7g meters/sec\n", $dy;
		my $dz = decode_single (\@pkt);
		printf "   Z Velocity: %-7g meters/sec\n", $dz;
		my $dbias = decode_single (\@pkt);
		printf "   Bias Rate: %-7g meters/sec\n", $dbias;
		my ($t, $fmt);
		if (scalar (@pkt) > 4) {
			$t = decode_double (\@pkt);
			$fmt = 16;
		} else {
			$t = decode_single (\@pkt);
			$fmt = 7;
		}
		printf "   Time of Fix: %-" . $fmt . "f\n", $t;
		if ($utc_off) {
			print "   -- converted UTC time: ",
			    strftime ("%Y-%m-%d %T", gmtime (gps2utc ($week,
			    $t, $utc_off))), "\n";
		}
	} elsif ($id == 0x44) {
		$decode = 1;
		print "Non-Overdetermined Satellite Selection\n";

		my $mode = shift @pkt;
		print "   Mode: ";
		if ($mode == 1) {
			print "Auto, 1-satellite, 0D\n";
		} elsif ($mode == 3) {
			print "Auto, 3-satellite, 2D\n";
		} elsif ($mode == 4) {
			print "Auto, 4-satellite, 3D\n";
		} elsif ($mode == 11) {
			print "Manual, 1-satellite, 0D\n";
		} elsif ($mode == 13) {
			print "Manual, 3-satellite, 2D\n";
		} elsif ($mode == 14) {
			print "Manual, 4-satellite, 3D\n";
		} else {
			print "?? (", $mode, ")\n";
		}

		print "   Satellites:";
		my $cnt = 0;
		for (my $i = 0; $i < 4; $i ++) {
			my $sat = shift @pkt;
			next if (! $sat);
			$cnt ++;
			print " ", $sat;
		}
		print " (none)" if (! $cnt);
		print "\n";

		my $pdop = decode_single (\@pkt);
		printf "   PDOP: %-7g\n", $pdop;
		my $hdop = decode_single (\@pkt);
		printf "   HDOP: %-7g\n", $hdop;
		my $vdop = decode_single (\@pkt);
		printf "   VDOP: %-7g\n", $vdop;
		my $tdop = decode_single (\@pkt);
		printf "   TDOP: %-7g\n", $tdop;
	} elsif ($id == 0x45) {
		$decode = 1;
		print "Receiver Firmware Information\n";

		my $nav_rel_maj = shift @pkt;
		my $nav_rel_min = shift @pkt;
		my $nav_rel_month = shift @pkt;
		my $nav_rel_day = shift @pkt;
		my $nav_rel_year = shift @pkt;
		printf "   NAV firmware %d.%d release %04d-%02d-%02d\n",
		    $nav_rel_maj, $nav_rel_min, $nav_rel_year + 1900,
		    $nav_rel_month + 0, $nav_rel_day + 0;

		my $sig_rel_maj = shift @pkt;
		my $sig_rel_min = shift @pkt;
		my $sig_rel_month = shift @pkt;
		my $sig_rel_day = shift @pkt;
		my $sig_rel_year = shift @pkt;
		printf "   SIG firmware %d.%d release %04d-%02d-%02d\n",
		    $sig_rel_maj, $nav_rel_min, $nav_rel_year + 1900,
		    $sig_rel_month + 0, $nav_rel_day + 0;

		if (@pkt) {
			my $serial;
			for (my $i = 0; $i < 4; $i++) {
				my $n = shift @pkt;
				my $d = $n & 0x0f;
				$serial .= ($d + 0);
				$d = $n & 0xf0;
				$serial .= ($d + 0);
			}
			my $check = shift @pkt;
			print "   Serial number: ", $serial, " (check: ",
			    $check, ")\n";

			my $revision = shift @pkt;
			$revision += (shift @pkt) * 256;
			print "   Configuration revision: ", $revision;

			my $mid = shift @pkt;
			printf "   Machine ID: %02x\n", $mid;

			my $cflen = shift @pkt;
			print "   Configuration Length: ", $cflen, "\n";

			my $nchan = shift @pkt;
			print "   Number of Channels: ", $nchan, "\n";

			my $rtcm_in = shift @pkt;
			print "   RTCM Input: ";
			if ($rtcm_in == 0) {
				print "Not installed\n";
			} elsif ($rtcm_in == 1) {
				print "Installed (available)\n";
			} elsif ($rtcm_in == 2) {
				print "Default at Clear RAM\n";
			} else {
				print "(", $rtcm_in, ")\n";
			}

			my $rtcm_out = shift @pkt;
			print "   RTCM Output: ";
			if ($rtcm_out == 0) {
				print "Not installed\n";
			} elsif ($rtcm_out == 1) {
				print "Version 2 Installed (available)\n";
			} elsif ($rtcm_out == 2) {
				print "Version 2 and PRC Type 9 Installed (available)\n";
			} else {
				print "(", $rtcm_out, ")\n";
			}

			my $fixrate = shift @pkt;
			print "   Fix Rate: ";
			if ($fixrate == 0) {
				print "1";
			} elsif ($fixrate == 2) {
				print "2";
			} elsif ($fixrate == 4) {
				print "5";
			} elsif ($fixrate == 9) {
				print "10";
			} else {
				print "(", $fixrate, ")";
			}
			print " Hz\n";

			my $syncmes = shift @pkt;
			print "   Synchronized Measurements: ";
			if ($syncmes == 0) {
				print "Not installed\n";
			} elsif ($syncmes == 1) {
				print "Installed (available)\n";
			} elsif ($syncmes == 3) {
				print "Carrier Phase installed (available\n";
			} else {
				print "(", $syncmes, ")\n";
			}

			my $misc = shift @pkt;

			my $nmeaout = shift @pkt;
			print "   NMEA Output: ";
			if ($nmeaout == 0) {
				print "Not installed\n";
			} elsif ($nmeaout == 1) {
				print "Installed (available)\n";
			} elsif ($nmeaout == 2) {
				print "Default at clear RAM\n";
			} else {
				print "(", $nmeaout, ")\n";
			}

			my $ppsout = shift @pkt;
			print "   1 PPS Output: ";
			if ($ppsout == 0) {
				print "Not installed\n";
			} elsif ($ppsout == 1) {
				print "Installed\n";
			} else {
				print "(", $ppsout, ")\n";
			}

			my $prod_id = shift @pkt;
			print "   Product ID: ", $prod_id, "\n";

			@pkt = ();
		}
	} elsif ($id == 0x46) {
		$decode = 1;
		print "Health of Receiver\n";

		print "   ";
		my $status = shift @pkt;
		if ($status == 0) {
			print "Doing position fixes\n";
		} elsif ($status == 1) {
			print "Do not have GPS time yet\n";
		} elsif ($status == 2) {
			print "Reserved (set to zero)\n";
		} elsif ($status == 3) {
			print "PDOP is too high\n";
		} elsif ($status == 8) {
			print "No usable satellites\n";
		} elsif ($status == 9) {
			print "Only 1 usable satellite\n";
		} elsif ($status == 10) {
			print "Only 2 usable satellites\n";
		} elsif ($status == 11) {
			print "Only 3 usable satellites\n";
		} elsif ($status == 12) {
			print "The chosen satellite is unusable\n";
		} else {
			printf "uknown status: %02x\n", $status;
		}

		my $error = shift @pkt;
		print "   Battery-backed Memory Battery Condition: ";
		if ($error & 1) {
			print "Battery failed\n";
		} else {
			print "Good condition\n";
		}
		print "   Antenna Feed Line Status: ";
		if ($error & 16) {
			print "Fault\n";
		} else {
			print "No fault\n";
		}
		print "   Reference Frequency Error Condition: ";
		if ($error & 32) {
			print "Excessive error rate\n";
		} else {
			print "No errors or acceptable rate of errors\n";
		}
	} elsif ($id == 0x47) {
		$decode = 1;
		print "Signal Levels for All Satellites\n";

		my $cnt = shift @pkt;
		print "   Number of satellites: ", $cnt, "\n";

		while ($cnt--) {
			my $prn = shift @pkt;
			my $lvl = decode_single (\@pkt);
			printf "   Satellite %d level: %-7g", $prn,
			    abs ($lvl);
			print " (not locked)" if ($lvl <= 0);
			print "\n";
		}
	} elsif ($id == 0x48) {
		$decode = 1;
		print "GPS System Message\n";

		my $msg = "";
		while (defined (my $b = shift @pkt)) {
			next if (! $b);
			$msg .= chr ($b);
		}
		print "   Message: ";
		if ($msg) {
			print $msg, "\n";
		} else {
			print "(blank)\n";
		}
	} elsif ($id == 0x49) {
		$decode = 1;
		print "Alamanac Health Page\n";

		for (my $i = 0; @pkt; $i++) {
			my $health = shift @pkt;
			print "   Health of Satellite #", $i, ": ";
			if ($health == 0) {
				print "Healthy\n";
			} else {
				print "Unhealthy (", $health, ")\n";
			}
		}
	} elsif ($id == 0x4a) {
		if (scalar (@pkt) == 9) {
			$decode = 1;
			print "Manual 2D Reference Altitude Parameters\n";

			my $ref_alt = decode_single (\@pkt);
			printf "   Reference Altitude: %-7g meters\n", $ref_alt;
			my $inv_alt = decode_single (\@pkt);
			printf "   Inverse Altitude Variance: %-7g\n",
			    $inv_alt;
			my $aflag = shift @pkt;
			print "   Altitude Flag: ";
			if ($aflag == 0) {
				print "Disabled\n";
			} elsif ($aflag == 1) {
				print "Enabled\n";
			} else {
				print "?? (", $aflag, ")\n";
			}
		} else {
			$decode = 1;
			print "Single-Precision LLA Position Fix\n";
			my $lat = decode_single (\@pkt);
			printf "   Latitude: %-7g radians\n", $lat;
			my $long = decode_single (\@pkt);
			printf "   Longitute: %-7g radians\n", $long;
			my $alt = decode_single (\@pkt);
			printf "   Altitude: %-7g meters\n", $alt;
			my $bias = decode_single (\@pkt);
			printf "   Clock Bias: %-7g meters\n", $bias;
			my ($time, $fmt);
			if (scalar (@pkt) > 4) {
				$time = decode_double (\@pkt);
				$fmt = 16;
			} else {
				$time = decode_single (\@pkt);
				$fmt = 7;
			}
			printf "   Time of fix: %-" . $fmt . "f\n", $time;
			if ($utc_off) {
				print "   -- converted UTC time: ",
				    strftime ("%Y-%m-%d %T", gmtime (gps2utc
				    ($week, $time, $utc_off))), "\n";
			}
		}
	} elsif ($id == 0x4b) {
		$decode = 1;
		print "Machine/Code ID and Additional Status\n";

		my $mid = shift @pkt;
		printf "   Machine ID: %02x\n", $mid;

		my $s1 = shift @pkt;
		print "   Battery Powered Time Clock Fault Status: ";
		if ($s1 & 2) {
			print "Fault\n";
		} else {
			print "No fault\n";
		}
		print "   A-to-D Converter Fault Status: ";
		if ($s1 & 4) {
			print "Fault\n";
		} else {
			print "No fault\n";
		}
		print "   Status of Almanac Stored in Receiver Memory: ";
		if ($s1 & 8) {
			print "Not complete or current\n";
		} else {
			print "Compelete & current\n";
		}
		print "   Receiver Reset Status acknowledged with Command Packet 0x1F: ";
		if ($s1 & 16) {
			print "Acknowledged\n";
		}  else {
			print "Not acknowledged\n";
		}

		my $s2 = shift @pkt;
		print "   Output of TSIP Superpackets: ";
		if ($s2 & 1) {
			print "Supported\n";
		} else {
			print "Not supported\n";
		}

	} elsif ($id == 0x4c) {
		$decode = 1;
		print "Operating Parameters\n";

		my $dyn = shift @pkt;
		print "   Dynamics Code: ";
		if ($dyn == 0) {
			print "unchanged\n";
		} elsif ($dyn == 1) {
			print "Land (<2g acceleration)\n";
		} elsif ($dyn == 2) {
			print "Sea (<1g acceleration)\n";
		} elsif ($dyn == 3) {
			print "Air (<4g acceleration)\n";
		} elsif ($dyn == 4) {
			print "Static (Stationary, no acceleration)\n";
		} else {
			print "(", $dyn, ")\n";
		}

		my $ele = decode_single (\@pkt);
		printf "   Elevation Mask: %-7g radians\n", $ele;

		my $sig = decode_single (\@pkt);
		printf "   Signal Level Mask: %-7g AMUs\n", $sig;

		my $pdopm = decode_single (\@pkt);
		printf "   PDOP Mask: %-7g\n", $sig;

		my $pdops = decode_single (\@pkt);
		printf "   PDOP Switch: %-7g\n", $sig;
	} elsif ($id == 0x4d) {
		$decode = 1;
		print "Oscillator Offset\n";
		my $off = decode_single (\@pkt);
		printf "   Oscillator Offset: %-7g\n", $off;
	} elsif ($id == 0x4e) {
		$decode = 1;
		print "GPS Time Command Verification\n";

		my $resp = shift @pkt;
		if ($resp == 89) {
			print "   Time command accepted; time not received from sat\n";
		} elsif ($resp == 78) {
			print "   Time command ignored; time received from sat\n";
		} else {
			print "   ??(", $resp, ")\n";
		}
	} elsif ($id == 0x4f) {
		$decode = 1;
		print "UTC Parameters\n";

		my $a0 = decode_double (\@pkt);
		printf "   A0: %-16g\n", $a0;
		my $a1 = decode_single (\@pkt);
		printf "   A1: %-7g\n", $a1;
		my $dt_ls = decode_int (\@pkt);
		print "   delta T(LS): ", $dt_ls, "\n";
		my $tot = decode_single (\@pkt);
		printf "   TOT: %-7g\n", $tot;
		my $wn_t = decode_int (\@pkt);
		print "   WN(T): ", $wn_t, "\n";
		my $wn_lsf = decode_int (\@pkt);
		print "   WN(LSF): ", $wn_lsf, "\n";
		my $dn = decode_int (\@pkt);
		print "   DN: ", $dn, "\n";
		my $dt_lsf = decode_int (\@pkt);
		print "   delta T(LSF): ", $dt_lsf, "\n";
	} elsif ($id == 0x53) {
		$decode = 1;
		print "Analog-to-Digital Readings\n";

		my $temp = decode_single (\@pkt);
		$temp = 25 + ($temp - 2.98) * 100;
		printf "   Temperature inside receiver: %-7g\n", $temp;
		my $resv1 = decode_single (\@pkt);
		my $resv2 = decode_single (\@pkt);
		my $ant_volt = decode_single (\@pkt);
		printf "   Antenna voltage: %-7g\n", $ant_volt;
		my $rcv_volt = decode_single (\@pkt) * 10;
		printf "   Power source voltage: %-7g\n", $rcv_volt;
		my $ant_amp = decode_single (\@pkt) / 10.34;
		printf "   Antenna current: %-7g\n", $ant_amp;
		my $v25 = decode_single (\@pkt);
		printf "   2.5V line: %-7g\n", $v25;
		my $v50 = decode_single (\@pkt);
		printf "   5.0V line: %-7g\n", $v50;
	} elsif ($id == 0x54) {
		$decode = 1;
		print "One Satellite Bias and Bias Rate\n";

		my $bias = decode_single (\@pkt);
		printf "   Bias: %-7g meters\n", $bias;
		my $rate = decode_single (\@pkt);
		printf "   Bias Rate: %-7g meters/sec\n", $rate;

		my ($time, $fmt);
		if (scalar (@pkt) > 4) {
			$time = decode_double (\@pkt);
			$fmt = 16;
		} else {
			$time = decode_single (\@pkt);
			$fmt = 7;
		}
		printf "   Time of fix: %-" . $fmt . "f\n", $time;
		if ($utc_off) {
			print "   -- converted UTC time: ",
			    strftime ("%Y-%m-%d %T", gmtime (gps2utc ($week,
			    $time, $utc_off))), "\n";
		}
	} elsif ($id == 0x55) {
		$decode = 1;
		print "I/O Options\n";

		my $pos = shift @pkt;
		if ($pos & 1) {
			print "   Automatic Output of XYZ ECEF Position Report\n";
		}
		if ($pos & 2) {
			print "   Automatic Output of LLA Position Report\n";
		}
		print "   Units of LLA Altitude Output: ";
		if ($pos & 4) {
			print "WGS-84 MSL\n";
		} else {
			print "WGS-84 HAE\n";
		}
		print "   Units of Altitude Input: ";
		if ($pos & 8) {
			print "WGS-84 MSL\n";
		} else {
			print "WGS-84 HAE\n";
		}
		print "   Precision of Position Data in Automatic Reports: ";
		if ($pos & 16) {
			print "Double-Precision\n";
		} else {
			print "Single-Precision\n";
		}
		if ($pos & 32) {
			print "   Automatic output of Super Packet data\n";
		}

		my $velo = shift @pkt;
		if ($velo & 1) {
			print "   Automatic Output of XYZ ECEF Velocity Report\n";
		}
		if ($velo & 2) {
			print "   Automatic Output of ENU Velocity Report\n";
		}

		my $timing = shift @pkt;
		print "   Time Type: ";
		if ($timing & 1) {
			print "UTC\n";
		} else {
			print "GPS\n";
		}
		print "   Fix Computation Time: ";
		if ($timing & 2) {
			print "At Integer Second\n";
		} else {
			print "ASAP\n";
		}
		print "   Automatic Output of Fix Time: ";
		if ($timing & 4) {
			print "On request\n";
		} else {
			print "When computed\n";
		}
		if ($timing & 8) {
			print "   Simultaneous Measurements\n";
		}
		if ($timing & 16) {
			print "   Minimum Projection\n";
		}

		my $aux = shift @pkt;
		if ($aux & 1) {
			print "Measurement output\n";
		}
		print "   Codephase Measurement Data Source: ";
		if ($aux & 2) {
			print "Filtered\n";
		} else {
			print "Raw\n";
		}
		if ($aux & 4) {
			print "   Automatic Output of Additional Fix Status Report\n";
		}
		print "   Units for Signal Level Output: ";
		if ($aux & 8) {
			print "dBHz\n";
		} else {
			print "AMUs\n";
		}
	} elsif ($id == 0x56) {
		$decode = 1;
		print "Velocity Fix East-North-Up (ENU)\n";

		my $east = decode_single (\@pkt);
		printf "   East Velocity: %-7g meters/sec\n", $east;
		my $north = decode_single (\@pkt);
		printf "   North Velocity: %-7g meters/sec\n", $north;
		my $up = decode_single (\@pkt);
		printf "   Up Velocity: %-7g meters/sec\n", $up;
		my $brate = decode_single (\@pkt);
		printf "   Clock Bias Rate: %-7g meters/sec\n", $brate;

		my ($time, $fmt);
		if (scalar (@pkt) > 4) {
			$time = decode_double (\@pkt);
			$fmt = 16;
		} else {
			$time = decode_single (\@pkt);
			$fmt = 7;
		}
		printf "   Time of fix: %-" . $fmt . "f\n", $time;
		if ($utc_off) {
			print "   -- converted UTC time: ",
			    strftime ("%Y-%m-%d %T", gmtime (gps2utc ($week,
			    $time, $utc_off))), "\n";
		}
	} elsif ($id == 0x57) {
		$decode = 1;
		print "Last Computed Fix\n";

		my $source = shift @pkt;
		print "   Info Source: ";
		if ($source == 0) {
			print "None\n";
		} elsif ($source == 1) {
			print "Regular fix\n";
		} elsif ($source == 2) {
			print "Initialization diagnostic\n";
		} elsif ($source == 4) {
			print "Initialization disgnostic\n";
		} elsif ($source == 5) {
			print "Entered by Command Packet 0x23 or 0x2B\n";
		} elsif ($source == 6) {
			print "Entered by Command Packet 0x31 or 0x32\n";
		} elsif ($source == 8) {
			print "Default position after RAM battery fail\n";
		} else {
			print "?? (", $source, ")\n";
		}

		my $diag = shift @pkt;
		print "   Diagnostic Code: ", $diag, "\n";

		my $time = decode_single (\@pkt);
		printf "   Time of last position fix: %-7g\n", $time;
		my $lweek = decode_int (\@pkt);
		print "   Week of last position fix: ", $lweek, "\n";
		if ($utc_off) {
			print "   -- converted UTC time: ",
			    strftime ("%Y-%m-%d %T", gmtime (gps2utc ($lweek,
			    $time, $utc_off))), "\n";
		}
	} elsif ($id == 0x58) {
		$decode = 1;
		print "Satellite System Data\n";

		my $oper = shift @pkt;
		print "   Operation: ";
		if ($oper == 0) {
			print "Request acknowledged/Cannot grant request\n";
		} elsif ($oper == 1) {
			print "Request acknowledged\n";
		} elsif ($oper == 2) {
			print "Requested data included in this report\n";
		} elsif ($oper == 3) {
			print "Request data not available for SV\n";
		} else {
			print "?? (", $oper, ")\n";
		}

		my $dtype = shift @pkt;
		print "   Data Type: ";
		if ($dtype == 2) {
			print "Almanac\n";
		} elsif ($dtype == 3) {
			print "Health page, T_oa, WN_oa\n";
		} elsif ($dtype == 4) {
			print "Ionosphere\n";
		} elsif ($dtype == 5) {
			print "UTC\n";
		} elsif ($dtype == 6) {
			print "Ephemeris\n";
		} else {
			print "?? (", $dtype, ")\n";
		}

		my $sv = shift @pkt;
		if (! $sv) {
			print "   Data is not satellite specific\n";
		} else {
			print "   Data is for satellite ", $sv, "\n";
		}

		my $len = shift @pkt;
		# FIXME: rest is not decoded for now
		shift @pkt while ($len--);
	} elsif ($id == 0x59) {
		$decode = 1;
		print "Satellite Attribute Database Status\n";

		my $oper = shift @pkt;
		my (@what, $what);
		if ($oper == 3) {
			@what = ("enabled", "disabled"); 
		} elsif ($oper == 6) {
			@what = ("heeded", "ignored");
			$what = "Health ";
		} else {
			print "   Operation: ?? (", $oper, ")\n";
		}

		if (@what) {
			for (my $i = 0; @pkt; $i ++) {
				my $flag = shift @pkt;
				print "   Satellite #", $i, ": ";
				print $what if ($what);
				if ($what[$flag]) {
					print $what[$flag], "\n";
				} else {
					print "?? (", $flag, ")\n";
				}
			}
		}
	} elsif ($id == 0x5a) {
		$decode = 1;
		print "Raw Measurement Data\n";

		my $sv = shift @pkt;
		print "   Satellite: ", $sv, "\n";

		my $slen = decode_single (\@pkt);
		printf "   Sample Length: %-7g msec\n", $slen;
		my $lev = decode_single (\@pkt);
		printf "   Signal Level: %-7g AMUs\n", $lev;
		my $phase = decode_single (\@pkt);
		printf "   Code phase: %-7g chips/16\n", $phase;
		my $doppler = decode_single (\@pkt);
		printf "   Doppler: %-7g hertz\n", $doppler;
		my $mtime = decode_double (\@pkt);
		printf "   Measure Time: %-16g seconds\n", $mtime;
	} elsif ($id == 0x5b) {
		$decode = 1;
		print "Satellite Ephemeris Status\n";

		my $sv = shift @pkt;
		print "   Satellite: ", $sv, "\n";

		my $ctime = decode_single (\@pkt);
		printf "   Collection Time: %-7g\n", $ctime;
		if ($utc_off) {
			print "   -- converted UTC time: ",
			    strftime ("%Y-%m-%d %T", gmtime (gps2utc ($week,
			    $ctime, $utc_off))), "\n";
		}
		my $health = shift @pkt;
		print "   Health: ", $health, "\n";
		my $iode = shift @pkt;
		print "   IODE: ", $iode, "\n";
		my $toe = decode_single (\@pkt);
		printf "   t(oe): %-7g seconds\n", $toe;
		my $fitflag = shift @pkt;
		print "   Fit Interval Flag: ", $fitflag, "\n";
		my $ura = decode_single (\@pkt);
		printf "   URA: %-7g meters\n", $ura;
	} elsif ($id == 0x5c) {
		$decode = 1;
		print "Satellite Tracking Status\n";

		my $sv = shift @pkt;
		print "   Satellite: ", $sv, "\n";

		my $chanslot = shift @pkt;
		my $slot = $chanslot & 7;
		my $chan = $chanslot >> 3;
		print "   Channel/Slot: ", $chan, "/", $slot, "\n";

		my $aflag = shift @pkt;
		print "   Acquisition Flag: ";
		if ($aflag == 0) {
			print "Never acquired\n";
		} elsif ($aflag == 1) {
			print "Acquired\n";
		} elsif ($aflag == 2) {
			print "Re-opened search\n";
		} else {
			print "?? (", $aflag, ")\n";
		}

		my $eflag = shift @pkt;
		print "   Ephemeris: ";
		if ($eflag == 0) {
			print "not received\n";
		} elsif ($eflag == 33) {
			print "not healthy\n";
		} else {
			print "received, healthy\n";
		}

		my $lev = decode_single (\@pkt);
		printf "   Signal Level: %-7g AMUs\n", $lev;

		my $gtime = decode_single (\@pkt);
		printf "   GPS Time of Last Measurement: %-7g\n", $gtime;
		if ($utc_off) {
			print "   -- converted UTC time: ",
			    strftime ("%Y-%m-%d %T", gmtime (gps2utc ($week,
			    $gtime, $utc_off))), "\n";
		}

		my $ele = decode_single (\@pkt);
		printf "   Elevation: %-7g radians\n", $ele;

		my $az = decode_single (\@pkt);
		printf "   Azimuth: %-7g radians\n", $az;

		my $oflag = shift @pkt;
		print "   Measurement is ";
		if ($oflag == 0) {
			print "new\n";
		} else {
			print "too old\n";
		}

		my $imflag = shift @pkt;
		print "   Integer msec Flag: ";
		if ($imflag == 0) {
			print "Uknown\n";
		} elsif ($imflag == 1) {
			print "Acquired from sub-frame data collection\n";
		} elsif ($imflag == 2) {
			print "Verified by a bit crossing time\n";
		} elsif ($imflag == 3) {
			print "Verified by a successful position fix\n";
		} elsif ($imflag == 4) {
			print "Suspected msec error\n";
		} else {
			print "?? (", $imflag, ")\n";
		}

		my $bdflag = shift @pkt;
		if ($bdflag == 0) {
			print "   Data presumed good\n";
		} elsif ($bdflag == 1) {
			print "   Bad parity\n";
		} elsif ($bdflag == 2) {
			print "   Bad ephemeris health\n";
		} else {
			print "   Bad Data Flag: ?? (", $bdflag, ")\n";
		}

		my $dcflag = shift @pkt;
		if ($dcflag == 0) {
			print "   Not collecting data\n";
		} else {
			print "   Collecting data\n";
		}
	} elsif ($id == 0x5e) {
		$decode = 1;
		print "Additional Fix Status Report\n";

		my $prevmb = shift @pkt;
		my $prevm = $prevmb & 7;
		print "   Number of measurements in current fix used in the previous fix and status: ", $prevm, "\n";
		if ($prevmb & 16) {
			print "   Fix still converging\n";
		} else {
			print "   Doing Fixes\n";
		}
		my $oldm = shift @pkt;
		print "   Number of old measurements in current fix: ", $oldm, "\n";
	} elsif ($id == 0x5f) {
		print "Severe Failure Notification\n";

		my $tag = shift @pkt;
		if ($tag == 2) {
			my $msg = "";
			while (defined (my $b = shift @pkt)) {
				next if (! $b);
				$msg .= chr ($b);
			}
			print "   Message: ", $msg, "\n";
		} else {
			print "   Unknown tag: ", $tag, "\n";
		}
	} elsif ($id == 0x60) {
		print "Differential GPS Pseudorange Corrections\n";
	} elsif ($id == 0x61) {
		print "Differential GPS Delta Pseudorange Corrections\n";
	} elsif ($id == 0x6a) {
		print "Differential Corrections Used in the Fix\n";
	} elsif ($id == 0x6d) {
		$decode = 1;
		print "All-In-View Satellite Selection\n";

		my $mode = shift @pkt;
		my $dmode = $mode & 7;
		my $manual = $mode & 8;
		my $nsat = $mode >> 4;
		print "   GPS position fix mode: ";
		if ($dmode == 0) {
			print "Unknown";
		} elsif ($dmode == 1) {
			print "0D";
		} elsif ($dmode == 2) {
			print "2D Clock Hold";
		} elsif ($dmode == 3) {
			print "2D";
		} elsif ($dmode == 4) {
			print "3D";
		} elsif ($dmode == 5) {
			print "Overdetermined Clock";
		} elsif ($dmode == 6) {
			print "DGPS Reference Station";
		} else {
			print "?? (", $dmode, ")";
		}
		print ", ", $manual ? "Manual" : "Auto", "\n";
		print "   Number of satellites: ", $nsat, "\n";

		my $pdop = decode_single (\@pkt);
		printf "   PDOP: %-7g\n", $pdop;
		my $hdop = decode_single (\@pkt);
		printf "   HDOP: %-7g\n", $hdop;
		my $vdop = decode_single (\@pkt);
		printf "   VDOP: %-7g\n", $vdop;
		my $tdop = decode_single (\@pkt);
		printf "   TDOP: %-7g\n", $tdop;

		my $cnt = 0;
		print "   Satellites:";
		while (@pkt) {
			$cnt ++;
			my $sv = shift @pkt;
			print " ", $sv;
		}
		print " (none)" if (! $cnt);
		print "\n";
	} elsif ($id == 0x6e) {
		print "Synchronized Measurement Parameters\n";
	} elsif ($id == 0x6f) {
		print "Synchronized Measurements\n";
	} elsif ($id == 0x70) {
		print "Position/Velocity Filter Operation\n";
	} elsif ($id == 0x76) {
		print "Overdetermined Mode\n";
	} elsif ($id == 0x78) {
		print "Maximum PRC Age\n";
	} elsif ($id == 0x7b) {
		print "NMEA Output Control\n";
	} elsif ($id == 0x7d) {
		print "Position Fix Rate Configuration\n";
	} elsif ($id == 0x82) {
		$decode = 1;
		print "Differential Position Fix Mode\n";

		my $mode = shift @pkt;
		print "   Mode: ";
		if ($mode == 0) {
			print "Manual GPS (Differential Off)\n";
		} elsif ($mode == 1) {
			print "Manual DGPS (Differential On)\n";
		} elsif ($mode == 2) {
			print "Auto GPS (Differential Currently Off)\n";
		} elsif ($mode == 3) {
			print "Auto DGPS (Differential Currently On)\n";
		} else {
			print "?? (", $mode, ")\n";
		}

		if (@pkt) {
			my $rtcm_ver = shift @pkt;
			print "   RTCM version: ";
			if ($rtcm_ver == 0) {
				print "Auto (Version 1, 2, or PRC Type 9)\n";
			} elsif ($rtcm_ver == 1) {
				print "Version 1 only\n";
			} elsif ($rtcm_ver == 2) {
				print "Version 2 or PRC Type 9 only\n";
			} else {
				print "?? (", $rtcm_ver, ")\n";
			}

			my $refid = decode_int (\@pkt);
			print "   Reference Station ID: ";
			if ($refid == -1) {
				print "Accept any reference station for use\n";
			} else {
				print "Accept only ID ", $refid, "\n";
			}
		}
	} elsif ($id == 0x83) {
		print "Double-Precision XYZ Position Fix & Clock Bias\n";
	} elsif ($id == 0x84) {
		print "Double-Precision LLA Position Fix & Clock Bias\n";
	} elsif ($id == 0x85) {
		print "Differential Correction Status\n";
	} elsif ($id == 0x87) {
		print "Reference Station Parameters\n";
	} elsif ($id == 0x88) {
		print "Mobile Differential Parameters\n";
	} elsif ($id == 0x8b) {
		print "QA/QC\n";
	} elsif ($id == 0x8d) {
		print "Average Position\n";
	} elsif ($id == 0x8f) {
		my $sub = shift @pkt;
		if ($sub == 0x20) {
			$decode = 1;
			print "(20): Super Packet Output Report\n";

			my $key = shift @pkt;
			printf "   Key Byte: %x\n", $key;
			my $evol = decode_int (\@pkt);
			printf "   East Velocity: %d meters/sec\n", $evol;
			my $nvol = decode_int (\@pkt);
			printf "   North Velocity: %d meters/sec\n", $nvol;
			my $uvol = decode_int (\@pkt);
			printf "   Up Velocity: %d meters/sec\n", $uvol;
			my $utime = decode_long (\@pkt);
			printf "   GPS time of week: %d msec\n", $utime;
			my $lat = decode_long (\@pkt);
			printf "   Latitude: %d\n", $lat;
			my $long = decode_long (\@pkt);
			printf "   Longitude: %d\n", $long;
			my $alt = decode_long (\@pkt);
			printf "   Altitude: %d millimeters\n", $alt;
			
			my $r = shift @pkt;
			$r = shift @pkt;
			$r = shift @pkt;

			my $pf = shift @pkt;
			print "   Position Fix Flags:\n";
			if ($pf & 1) {
				print "      No Position Fix available; last position fix used to compute solution\n";
			} else {
				print "      Position fix is available\n";
			}
			if ($pf & 2) {
				print "      RTCM corrections used to compute position solution\n";
			} else {
				print "      GPS position fix\n";
			}
			if ($pf & 4) {
				print "      2D Position Fix\n";
			} else {
				print "      3D Position Fix\n";
			}
			print "      Position or Altitude Filter: ";
			if ($pf & 8) {
				print "On\n";
			} else {
				print "Off\n";
			}

			my $nsvs = shift @pkt;
			printf "   Number of SVs: %d\n", $nsvs;

			$utc_off = shift @pkt;
			printf "   UTC/GPS time offset: %d\n", $utc_off;
			$week = decode_int (\@pkt);
			printf "   GPS week number: %d\n", $week;
			print "   -- converted UTC time: ",
			    strftime ("%Y-%m-%d %T", gmtime (gps2utc ($week,
			    $utime / 1000, $utc_off)));
			printf ".%-04d\n", ($utime % 1000);

			for (my $i = 0; $i < 8; $i ++) {
				my $prnx = shift @pkt;
				my $iode = shift @pkt;
				next if (! $prnx && ! $iode);
				printf "   SV %d: PRNX %02x IODE: %02x\n", $i,
				    $prnx, $iode;
			}

			print "   Ionospheric Data:";
			for (my $i = 0; $i < 8; $i ++) {
				my $b = shift @pkt;
				printf " %02x", $b;
			}
			print "\n";
		} else {
			print "Application Reports\n";
		}
	} elsif ($id == 0xb0) {
		print "PPS and Even\n";
	} elsif ($id == 0xbb) {
		print "Receiver Configuration Parameters\n";
	} elsif ($id == 0xbc) {
		print "Serial Port Configuration Parameters\n";
	} else {
		printf "Unknown ID: %02x\n", $id;
	}

	if ($decode) {
		if (@pkt) {
			print "Invalid decode?  ", scalar (@pkt),
			      " bytes left\n", dump_hex ($pkt), "\n";
		}
	} else {
		print "Got ", dump_hex ($pkt), "\n";
	}
	print "\n";
}
