#!/usr/bin/perl
#
# $Id: NMEAfields.pm,v 1.2 2005/09/02 14:51:20 ben Exp $
# Module for interpreting NMEA strings
# Ben Smith, S/V Mother of Perl
#
# This program is free software, You may copy or redistribute it under the
# same terms as Perl itself.
#
#########################################################################
my $VERSION=sprintf("%d.%02d", q$Revision: 1.2 $ =~ /(\d+)\.(\d+)/);
=head1 NAME
NMEAfields -- read captured NMEA data strings and return the data fields
as part of a structure.
=head1 SYNOPSIS
Make an interpretor with new(), and use it to generate data objects.
use NMEAfields;
my $I = NMEAfields->new(); # one one interpreter needed
my $line;
my $dataP;
while($line = <STDIN>){
$dataP = $I->Interp($line); # get a data object
# do something with the $dataP
}
=head1 DESCRIPTION
These classes and methods are used to create a common interface to NMEA data
streams, such as from GPS, Depth sounders, Roll-Pitch-Heave sensors, navigation
systems, etc. In this first edition, only GGA and DPT strings are interpreted.
As I have time, I (or others) can add what they find.
The interpreter separates the instrument code from the data type and
interprets based on the data type. For example, C<$IIGGA> and C<$GPGGA> are
interpreted in the same way.
This module does NOT do sum checks on the data streams.
The data objects returned by Interp are of their own class, B<NMEAdata>,
with their own methods.
For conversion from Lat/Long to UTM, the NMEAdata methods
use Geo::Coordinates::UTM.
For conversion from GPS time to UNIX time notation (different epoch),
the NMEAdata methods
use Time::Local.
=head2 Understood Devices
The following device codes (first two letters of the string) are understood
by NMEAfields:
=over 10
=item AI => 'Automatic Identification System (AIS)'
=item AP => 'Heading Track Controller (AutoPilot), Magnetic'
=item GP => 'GPS - Global Positioning System'
=item HC => 'Heading Calculation'
=item II => 'Integrated Instrument'
=item IN => 'Integrated Navigation'
=item PG => 'Proprietry Device'
=item SD => 'Sounder Depth'
=item VD => 'Doppler Velocity'
=back
=head2 Understood Elements (Sentence Types)
The following element codes (third, fourth, and fifth letters of the
string) are understood by NMEAfields:
=over 10
=item GGA => '3-dimension fix with time and accuracy'
COG => 'course over ground',
BOD => 'Course waypoint to waypoint',
BWC => 'bearing from fix to waypoint',
BWW => 'Course waypoint to waypoint',
DBS => 'depth below surface',
DBT => 'depth below transducer',
DPT => 'depth',
GLL => 'geographic Position - Latitude/Longitude',
GSA => 'GPS DOP and active satellites',
GSV => 'satellites in view',
HDG => 'heading magnetic',
HDM => 'heading magnetic',
HDT => 'heading true',
MTW => 'Water temperature',
RMA => 'Recommended Minimum Specific Loran-C Data',
RMB => 'Recommended Minimum Navigation Information',
RMC => 'Recommended Minimum Specific GNSS Data',
RMZ => 'Altitude (Garmin)',
RME => 'Estimated Error Information (Garmin)',
RMF => 'GPS Fix Data (Garmin)',
RTE => 'Routes RTE - Routes',
VHW => 'Velocity through water',
VLW => 'Distance traveled through water',
VWR => 'Relative velocity of wind',
VTG => 'track made good and ground speed',
WPL => 'waypoint position',
XTE => 'cross track error',
ZDE => 'date and time',
ZDA => 'date and time',
=back
=head2 C<NMEAfields> Methods
=cut
###################### GLOBALS to module ####################
my $ellipsoid = 'WGS-84';
# the list of known devices
my %devices = (
AI => 'Automatice Identification System (AIS)',
AP => 'Heading Track Controller (AutoPilot), Magnetic',
GP => 'GPS - Global Positioning System',
HC => 'Heading Calculation',
II => 'Integrated Instrument',
IN => 'Integrated Navigation',
PG => 'Proprietry Device',
SD => 'Sounder Depth',
VD => 'Doppler Velocity',
);
# the list of known sentences
my %elements = (
GGA => '3-dimension fix with time and accuracy',
COG => 'course over ground',
BOD => 'Course waypoint to waypoint',
BWC => 'bearing from fix to waypoint',
BWW => 'Course waypoint to waypoint',
DBS => 'depth below surface',
DBT => 'depth below transducer',
DPT => 'depth',
GLL => 'geographic Position - Latitude/Longitude',
GSA => 'GPS DOP and active satellites',
GSV => 'satellites in view',
HDG => 'heading magnetic',
HDM => 'heading magnetic',
HDT => 'heading true',
MTW => 'Water temperature',
RMA => 'Recommended Minimum Specific Loran-C Data',
RMB => 'Recommended Minimum Navigation Information',
RMC => 'Recommended Minimum Specific GNSS Data',
RMZ => 'Altitude (Garmin)',
RME => 'Estimated Error Information (Garmin)',
RMF => 'GPS Fix Data (Garmin)',
RTE => 'Routes RTE - Routes',
VHW => 'Velocity through water',
VLW => 'Distance traveled through water',
VWR => 'Relative velocity of wind',
VTG => 'track made good and ground speed',
WPL => 'waypoint position',
XTE => 'cross track error',
ZDE => 'date and time',
ZDA => 'date and time',
);
############################# NMEAfields
package NMEAfields;
require Exporter;
@ISA = qw(Exporter);
@EXPORT = qw(
new
Interp
String
SetError
ClrError
Error
Checksum
);
############# new
=head3 C<NMEAfields-E<gt>new($GPSmaker)>
Creates a new interpreter for NMEA streams. The NMEA strings are not actually
proprietary information of the equipment manufacturers, and so vary from
one manufacturer to another. Many are not publicly documented.
I<Takes an option argument for the GPS manufacturer. By default, GARMIN>
=cut
sub new {
my $class = shift;
my $gpsMaker = shift;
my $self = {
gpsMaker => 'GARMIN', # default mfgr
string => '', # will hold the string for Interp
device => '', # first two characters of device-string
element => '', # last three characters of device-string
data => [] , # list of data fields
fields => {}, # hash of data fields with field names if known
ERROR => undef,
};
if($gpsMaker) {
$gpsMaker =~ tr/a-z/A-Z/; # all uppercase
$self->{gpsMaker} = $gpsMaker;
}
return bless $self, $class;
}
############# Checksum
=head3 C<NMEAfields-E<gt>Checksum()>
Returns 1 (True) if the calculated checksum equals the transmitted (embedded)
checksum. Returns 0 (False) if they do not agree. If FALSE, then an error
is placed in the Error attribute. See NMEA-<gt>Error.
=cut
sub Checksum {
my $self = shift;
if( $self->{CKS} eq $self->{calcCKS}) {
return 1;
} else {
$self->SetError(
"Checksum error: calcualte=$self->{calcCKS} provided=$self->{CKS}"
);
return 0;
}
}
############ SetError
=head3 C<NMEAdata-E<gt>SetError(string)>
Sets the Error attribute to string.
=cut
sub SetError {
my $self = shift;
my $error =shift;
$self->{ERROR} = "!Error: \t$error\n!String:\t $self->{string}\n";
}
############# ClrError
=head3 C<NMEAfields-E<gt>ClrError()>
Clears the Error attribute to undef.
=cut
sub ClrError {
my $self = shift;
$self->{ERROR} = undef;
}
############ Error
=head3 C<NMEAfields-E<gt>Error()>
Clears the Error attribute to undef and
returns any error string that it was
set to.
=cut
sub Error {
my $self = shift;
my $errorStr = $self->{ERROR};
$self->ClrError();
return $errorStr;
}
sub DeviceCode {
my $self = shift;
return $self->{device};
}
sub Device {
my $self = shift;
return (defined $devices{$self->{device}}) ?
$devices{$self->{device}} :
'';
}
sub ElementCode {
my $self = shift;
return $self->{element};
}
sub Element {
my $self = shift;
return $elements{$self->{element}};
}
###### Interp
=head3 C<NMEAfields-E<gt>Interp($string)>
This method takes the NMEA string as its argument. It returns a new
object of the class B<NMEAdata>, which has its own set of methods.
C<Interp> will strip leading and trailing whitespace before parsing
the string.
If the argument string is empty or can not be interpreted as NMEA, C<Interp>
will return C<under>.
I<It takes a string as its argument>
=cut
sub Interp { # interprets the NMEA strings
my $self = shift;
my $string = shift;
$self->Clear();
# remove leading and trailing whitespace
$string =~ s/^\s+//;
$string =~ s/\s+$//;
return undef if $string !~ /^\$/; # not a valid NMEA string
$self->{string} = $string; # for debugging mostly
# pull off checksum value if it is included
if($string =~ s/\*(.+)$//) {
$self->{CKS} = $1;
}
$self->calcChecksum();
# break remainder into comma delimited fields, the first being the prefix
my($prefix, @fields) = split(/,/,$string);
return undef if not $prefix;
$self->{device} = substr($prefix,1,2);
$self->{element} = substr($prefix,3,3);
$self->{data} = [@fields];
# Let's see what we can recognize
my $element = $self->{element};
if(grep /$element/, keys %elements) {
eval("\$self->$element()"); # run the corresponding method
$self->Cook(); # process those fields that can be cooked
return NMEAdata->new($self); # return a new fields class object
} else {
return undef;
}
}
sub String {
my $self = shift;
return $self->{string};
}
################ Clear
# private method used by Interp in preparation for evaluating
# a new string
sub Clear { # clears the fields that are used or Interp
my $self = shift;
$self->{string} = '' ;
$self->{device} = '' ;
$self->{element} = '' ;
$self->{data} = [] ;
$self->{fields} = {};
}
############### mapData ###################
sub mapData {
my $self = shift;
my $fldListPtr = shift;
my $at = shift;
$at = 0 unless $startAt; # default value
for my $field (@$fldListPtr) {
$self->{fields}->{$field} = $self->{data}[$at];
++ $at;
}
}
################### calcChecksum
sub calcChecksum {
my $self = shift;
# delete leading $ and existing checksum
my $stringToCheck = $self->{string};
$stringToCheck =~ s/(^\$|\*.*$)//g;
my $sum = 0;
foreach(split(//,$stringToCheck)){
$sum ^= ord($_); # XOR sum
}
# generate the string with alphanumeric in uppercase
$self->{calcCKS} = unpack('H*',pack("C",$sum));
$self->{calcCKS} =~ tr/a-z/A-Z/;
}
############################ the element field readers
#### BOD
sub BOD { # Course waypoint to waypoint
my $self = shift;
my $fieldsRef = [qw(
BearingTrue
True
BearingMagn
Magn
GoalWP_ID
BeginingWP_ID
)];
$self->mapData($fieldsRef);
}
#### BWC
sub BWC { # Bearing to waypoint
my $self = shift;
my $fieldsRef = [qw(
RawFixTime
WPRawLat
WPNorthSouth
WPRawLong
WPEastWest
Bearing2wpTrue
Bearing2wpMagn
Distance2wpNM
WP_ID
)];
$self->mapData($fieldsRef);
}
##### BWW
sub BWW { # Course waypoint to waypoint
my $self = shift;
my $fieldsRef = [qw(
BearingTrue
True
BearingMagn
Magn
GoalWP_ID
BeginingWP_ID
)];
$self->mapData($fieldsRef);
}
#### COG
sub COG {
my $self = shift;
my $fieldsRef = [qw(
RawFixTime
RawLat
NorthSouth
RawLong
EastWest
SOG
COG
)];
$self->mapData($fieldsRef,0);
if($self->{gpsMaker} eq 'LOWRANCE') {
$fieldsRef = [qw(
RawFixDate
TrueMagn
)];
$self->mapData($fieldsRef,7); # Lowrance has these extra 2 fields
}
}
##### DBS
sub DBS { # Depth Below Surface
my $self = shift;
my $fieldsRef = [qw(
DBSfeet
DBSf
DBSmeters
DBSM
DBSfathoms
DBSF
)];
$self->mapData($fieldsRef);
}
##### DBT
sub DBT { # Depth Below Transducer
my $self = shift;
my $fieldsRef = [qw(
DBTfeet
DBTf
DBTmeters
DBTM
DBTfathoms
DBTF
)];
$self->mapData($fieldsRef);
}
##### DPT
sub DPT { # Depth
my $self = shift;
my $fieldsRef = [qw(
DBTmeters
OffsetMeters
MaxRange
)];
$self->mapData($fieldsRef);
}
#### GGA
sub GGA {
my $self = shift;
my $fieldsRef = [qw(
RawFixTime
RawLat
NorthSouth
RawLong
EastWest
)];
$self->mapData($fieldsRef);
if($self->{gpsMaker} eq 'GARMIN') {
$fieldsRef = [qw(
TypeFix
HDOP
Altitude
GeodSeparation
SeparationUnits
)];
$self->mapData($fieldsRef,5);
}elsif($self->{gpsMaker} eq 'LOWRANCE') {
$fieldsRef = [qw(
SOG
COG
RawFixDate
TrueMagn
)];
$self->mapData($fieldsRef,5);
}
}
##### GLL Geographic Position - Latitude/Longitude
sub GLL {
my $self = shift;
my $fieldsRef = [qw(
RawLat
NorthSouth
RawLong
EastWest
RawFixTime
Status
Mode
)];
$self->mapData($fieldsRef);
}
##### GSA
sub GSA { # Active Satellites
my $self = shift;
my $fieldsRef = [qw(
AutoManual
fix3D
PRN_01
PRN_02
PRN_03
PRN_04
PRN_05
PRN_06
PRN_07
PRN_08
PRN_09
PRN_10
PRN_11
PRN_12
PDOP
HDOP
VDOP
)];
$self->mapData($fieldsRef);
}
##### GSV
sub GSV { # Satellites in view - a multi-sentence message
my $self = shift;
my $fieldsRef = [qw(
NumberMessages
SequenceNumber
SatellitesInView
Satellite_ID_1
Elevation_1
Azimuth_1
SRN_1
Satellite_ID_2
Elevation_2
Azimuth_2
SRN_2
Satellite_ID_3
Elevation_3
Azimuth_3
SRN_3
Satellite_ID_4
Elevation_4
Azimuth_4
SRN_4
)];
$self->mapData($fieldsRef);
}
##### HDG
sub HDG { # Heading, Deviation, Variation
my $self = shift;
my $fieldsRef = [qw(
MagneticHeading
)];
$self->mapData($fieldsRef);
}
##### HDM
sub HDM { # Heading Magnetic
my $self = shift;
my $fieldsRef = [qw(
MagneticHeading
HDMM
)];
$self->mapData($fieldsRef);
}
##### HDT
sub HDT { # Heading True
my $self = shift;
my $fieldsRef = [qw(
TrueHeading
HDTT
)];
$self->mapData($fieldsRef);
}
##### MTW
sub MTW { # Water Tempurature
my $self = shift;
my $fieldsRef = [qw(
WaterTemp
)];
$self->mapData($fieldsRef);
}
##### RMA
sub RMA { # Recommended Minimum Specific Cloran-C Data
my $self = shift;
my $fieldsRef = [qw(
Status
RawLat
NorthSouth
RawLong
EastWest
TimeDiffAuS
TimeDiffBus
SOGk
COGt
MagnVariation
VarEastWest
Mode
)];
$self->mapData($fieldsRef);
}
##### RMB
sub RMB { # Recommended Minimum Navigation Information
my $self = shift;
my $fieldsRef = [qw(
Status
XTAnm
DirToSteer
OrigWayptID
DestWayptID
DestRawLat
DestNorthSouth
DestRawLong
DestEastWest
Range
Bearing
VMGkt
Status
Mode
)];
$self->mapData($fieldsRef);
}
##### RMC
sub RMC { # Recommended minimum Transit Data
my $self = shift;
my $fieldsRef = [qw(
RawFixTime
ActiveVoid
RawLat
NorthSouth
RawLong
EastWest
SOG
COG
RawFixDate
MagnVariation
VarEastWest
)];
$self->mapData($fieldsRef);
}
##### RMZ Altitude (Garmin)
sub RMZ {
my $self = shift;
my $fieldsRef = [qw(
Altitude
Unit
PosFixDim
)];
$self->mapData($fieldsRef);
}
##### RME 'Estimated Error Information (Garmin)',
sub RME {
my $self = shift;
my $fieldsRef = [qw(
EstHorizErrMeters
EstVertErrMeters
EPE-EstPosErrMeters
)];
$self->mapData($fieldsRef);
}
##### RMF 'GPS Fix Data (Garmin)',
sub RMF {
my $self = shift;
my $fieldsRef = [qw(
GPSweek
GPSseconds
RawFixDate
RawFixTime
GPSleapSeconds
RawLat
NorthSouth
RawLong
EastWest
FixType
SOG
COG
PositionDilutionPrecision
TimeDilutionPrecision
)];
$self->mapData($fieldsRef);
}
##### RTE
sub RTE { # Routes RTE - Routes
my $self = shift;
my $fieldsRef = [qw(
TotalMssgsXmt
MessageNum
MessageMode
RouteID
WayptID[0]
)];
$self->mapData($fieldsRef);
# add additional Waypt IDs
# hmmm!
}
##### VLW
sub VLW { # Distance Through Water (very sketchy)
my $self = shift;
my $fieldsRef = [qw(
TotalCumulativeDistance
Nm1
DistanceSinceReset
Nm2
)];
$self->mapData($fieldsRef);
}
##### VHW
sub VHW { # Water Speed (this one is sketchy)
my $self = shift;
my $fieldsRef = [qw(
HeadingTrue
True
HeadingMagn
Magn
WaterSpeedKts
WaterSpeedKPH
)];
$self->mapData($fieldsRef);
}
##### VTG
sub VTG { # Track Made Good & Ground Speed
my $self = shift;
my $fieldsRef = [qw(
COG_true
VTG_T
COG_magn
COG_M
SOG_knots
SOG_N
SOG_kph
SOG_K
)];
$self->mapData($fieldsRef);
}
##### VWR
sub VWR { # Relative Wind Speed
my $self = shift;
my $fieldsRef = [qw(
RelativeAngle
LRofBow
WindSpeedKts
WindSpeedMPS
WindSpeedKPH
)];
$self->mapData($fieldsRef);
}
##### WPL
sub WPL { # Waypoint position
my $self = shift;
my $fieldsRef = [qw(
RawLat
NorthSouth
RawLong
EastWest
WP_Name
)];
$self->mapData($fieldsRef);
}
##### XTE
sub XTE { # Cross Track Error
my $self = shift;
my $fieldsRef = [qw(
WarningFlag
LoranLockFlag
XTE_distance
XTE_LeftRight
XTE_units
)];
$self->mapData($fieldsRef);
}
##### ZDA
sub ZDA { # Date and Time for Synchronization
my $self = shift;
my $fieldsRef = [qw(
RawFixTime
Day
Month
Year
LocalZoneHours
LocalZoneMins
)];
$self->mapData($fieldsRef);
}
##### ZDE
sub ZDE { # Fix Date and Time
my $self = shift;
my $fieldsRef = [qw(
RawFixTime
RawFixDate
LocalZoneHours
LocalZoneMins
)];
$self->mapData($fieldsRef);
}
################### Cook
sub Cook {
my $self = shift;
if(defined $self->{fields}{RawFixTime}) {
($self->{fields}{FixTimeH},
$self->{fields}{FixTimeM},
$self->{fields}{FixTimeS}) =
$self->CookTime($self->{fields}{RawFixTime});
}
if(defined $self->{fields}{RawFixDate}) {
($self->{fields}{FixDateD},
$self->{fields}{FixDateM},
$self->{fields}{FixDateY}) =
$self->CookDate($self->{fields}{RawFixDate});
}
if(defined $self->{fields}{RawLat}) {
$self->{fields}{LatDecDeg} =
$self->CookLatitude(
$self->{fields}{RawLat},
$self->{fields}{NorthSouth}
);
}
if(defined $self->{fields}{RawLong}) {
$self->{fields}{LongDecDeg} =
$self->CookLongitude(
$self->{fields}{RawLong},
$self->{fields}{EastWest}
);
}
if(defined $self->{fields}{WPRawLat}) {
$self->{fields}{WPLatDecDeg} =
$self->CookLatitude(
$self->{fields}{WPRawLat},
$self->{fields}{WPNorthSouth}
);
}
if(defined $self->{fields}{WPRawLong}) {
$self->{fields}{WPLongDecDeg} =
$self->CookLongitude(
$self->{fields}{WPRawLong},
$self->{fields}{WPEastWest}
);
}
}
sub CookTime { # break raw fields into sensible values
my $self = shift;
my $rawTime = shift;
return(
substr($rawTime,0,2), # hour
substr($rawTime,2,2), # minute
substr($rawTime,4)); # second
}
sub CookDate {
my $self = shift;
my $rawDate = shift;
return (
substr($rawDate,0,2), # day
substr($rawDate,2,2), # month
substr($rawDate,4)); # year
}
sub CookLatitude {
my $self = shift;
my $rawLat = shift;
my $NS = shift;
my $LatDecDeg =
substr($rawLat,0,2) +
substr($rawLat,2) / 60;
$LatDecDeg *= ($NS eq 'S') ? -1 : 1;
return $LatDecDeg;
}
sub CookLongitude {
my $self = shift;
my $rawLong = shift;
my $EW = shift;
my $LongDecDeg =
substr($rawLong,0,3) +
substr($rawLong,3) / 60;
$LongDecDeg *= ($EW eq 'W') ? -1 : 1;
return $LongDecDeg;
}
#####################################################################
######################### NMEAdata ##################################
package NMEAdata;
use Geo::Coordinates::UTM;
use Time::Local;
require Exporter;
@ISA = qw(Exporter);
@EXPORT = qw(
new
Data
Cooked
All
Checksum
GetFields
DeviceCode
ElementCode
Element
PositionStr
TimeStr
LL2UTM
Zone
Easting
Northing
UNIXtime
Checksum
SetError
ClrError
Error
);
# the field lists == these lists are a pain to maintain. Maybe we should
# have the users add what they want in these lists
# and then they can customize them the way they wish
my @rawfields = qw(RawFixTime
RawFixDate
RawLat
NorthSouth
RawLong
EastWest
WPRawLat
WPNorthSouth
WPRawLong
WPNorthSouth
TrueMagn
COG
SOGunit
SOG
dptDepthFeet
dptDepthMeters
dptDepthFathoms
);
my @cookedfields = qw(FixTimeH
FixTimeM
FixTimeS
FixDateD
FixDateM
FixDateY
LatDecDeg
LongDecDeg
COG
SOGunit
SOG
TrueMagn
dptDepthFeet
dptDepthMeters
dptDepthFathoms
);
############## new
############ a method that is used by only the NMEAfields->Interp
sub new {
my $class = shift;
my $NMEAfields = shift;
my $self = $NMEAfields->{fields} ;
# add on the device and element info
$self->{_DEVICE_} = $NMEAfields->{device};
$self->{_ELEMENT_} = $NMEAfields->{element};
$self->{_CKS_} = $NMEAfields->{CKS};
$self->{_calcCKS_} = $NMEAfields->{calcCKS};
$self->{_STRING_} = $NMEAfields->{string};
$self->{_ERROR_} = $NMEAfields->{ERROR};
return bless $self, $class;
}
############ field list output methods
=head2 C<NMEAdata> Methods
=cut
############## Raw
=head3 C<NMEAdata-E<gt>Raw>
C<Raw> returns a subset of the total C<NMEAdata> structure.
These fields are not particularly useful to outside programs.
The main reason for the C<NMEAfields::> module is to produce data
that doesn t require any interpretation outside of the module.
The subset has any of the available following fields:
=over 4
=item C<RawFixTime>
The general format of time is C<hhmmss.sss>
=item C<RawFixDate>
The general format of time is C<ddmmyy>
=item C<RawLat>
The general format is C<ddmm.mmmm>
=item C<NorthSouth>
A character C<N> or C<S> designating hemisphere of latitude
=item C<RawLong>
The general format is C<ddmm.mmmm>
=item C<EastWest>
A character C<E> or C<W> designating hemisphere of longitude
=item C<COG>
Course over ground (True if not otherwise specified by C<TrueMagn>
=item C<TrueMagn>
A character C<T> or C<M> designating type of bearing/course. Assume
True (rather than Magnetic) if this field is missing.
=item C<SOG>
Speed over ground. Assume knots if not specified by a C<SOGunit>
=item C<SOGunit>
A character C<N> (knots), C<K> (kilometers per hour), C<M> (miles per hour).
Assume knots if this field is missing.
=item C<dptDepthFeet>
Depth below transducer in feet.
=item C<dptDepthMeters>
Depth below transducer in meters.
=item C<dptDepthFathoms>
Depth below transducer in fathoms.
=back
These values are in the NMEA native format which is usually readable, but
often a munged up version of the data.
=cut
sub Raw {
my $self = shift;
return $self->GetFields(@rawfields);
}
############## Cooked
=head3 C<NMEAdata-E<gt>Cooked>
C<Cooked> returns a subset of the total C<NMEAdata> structure.
These fields are the more useful versions of the data that comes
from the raw fields. But, it doesn t stop here, there are many
other methods that give specific formats and transformations of the
data. (See Below.)
The subset has any of the available following fields:
=over 4
=item C<FixTimeH>
The hour
=item C<FixTimeM>
The minute
=item C<FixTimeS>
The second
=item C<FixDateD>
The day of the month
=item C<FixDateM>
The month of the year (January = 1)
=item C<FixDateY>
The year (only the last two digits)
=item C<LatDecDeg>
The latitude in signed decimal degrees. Negative latitudes are b<S>outh
of the equator.
=item C<LongDecDeg>
The longitude in signed decimal degrees. Negative longitudes are b<W>est
of the prime meridian.
=item C<COG>
Course over ground (True if not otherwise specified by C<TrueMagn>
(Unchanged from the C<Raw> method.)
=item C<TrueMagn>
A character C<T> or C<M> designating type of bearing/course. Assume
True (rather than Magnetic) if this field is missing.
(Unchanged from the C<Raw> method.)
=item C<SOG>
Speed over ground. Assume knots if not specified by a C<SOGunit>
(Unchanged from the C<Raw> method.)
=item C<SOGunit>
A character C<N> (knots), C<K> (kilometers per hour), C<M> (miles per hour).
Assume knots if this field is missing.
(Unchanged from the C<Raw> method.)
=item C<dptDepthFeet>
Depth below transducer in feet.
(Unchanged from the C<Raw> method.)
=item C<dptDepthMeters>
Depth below transducer in meters.
(Unchanged from the C<Raw> method.)
=item C<dptDepthFathoms>
Depth below transducer in fathoms.
(Unchanged from the C<Raw> method.)
=back
These values are in the NMEA native format which is usually readable, but
often a munged up version of the data.
=cut
sub Cooked {
my $self = shift;
return $self->GetFields(@cookedfields);
}
############ All
sub All {
my $self = shift;
my @All = keys %$self;
return $self->GetFields(@All);
}
# process some of the formats into more common forms
############### GetFields -- gets fields from a specified struct base
# a private method used by other methods in this class
sub GetFields {
my $self = shift;
my @fields = @_; # the rest of the arguments
my $field;
my $result = {};
foreach $field (@fields) {
next if $field =~ /^_\S_$/; # ignore special fields
if(defined $self->{$field}) {
$result->{$field} = $self->{$field};
}
}
bless $result;
return $result;
}
################### Data ##############################################
# a general data retrieval routine which returns a list of data
# corresponding to the list of field names. This can be applied
# to any dataset.
=head3 C<NMEAdata-E<gt>Data(fieldList)>
Returns the list of data values corresponding to the fields
in the fieldlist. A single field name returns a single value
provided that that data exists.
=cut
sub Data {
my $self = shift;
my @fieldlist = @_;
my @results = ();
for(@fieldlist){
push(@results,$self->{$_});
}
return @results;
}
################### Special Data and Formats ##########################
############### Checksum
=head3 C<NMEAdata-E<gt>Checksum()>
Returns 1 (True) if the calculated checksum equals the transmitted (embedded)
checksum. Returns 0 (False) if they do not agree. If FALSE, then an error
is placed in the Error attribute. See NMEA-<gt>Error.
=cut
sub Checksum {
my $self = shift;
if( $self->{_CKS_} eq $self->{_calcCKS_}) {
return 1;
} else {
$self->SetError(
"Checksum error: calcualte=$self->{_calcCKS_} provided=$self->{_CKS_}"
);
return 0;
}
}
############## SetError
=head3 C<NMEAdata-E<gt>SetError(string)>
Sets the Error attribute to string.
=cut
sub SetError {
my $self = shift;
my $error = shift;
$self->{_ERROR_} = "!Error: \t$error\n!String:\t $self->{string}\n";
}
=head3 C<NMEAdata-E<gt>ClrError(string)>
Clears the Error attribute to undef.
=cut
sub ClrError {
my $self = shift;
$self->{_ERROR_} = undef;
}
=head3 C<NMEAdata-E<gt>Error(string)>
Clears the Error attribute to undef and
returns any error string that it was
set to.
=cut
sub Error {
my $self = shift;
my $errorStr = $self->{_ERROR_};
$self->ClrError();
return $errorStr;
}
sub DeviceCode {
my $self = shift;
return $self->{_DEVICE_};
}
sub Device {
my $self = shift;
return (defined $devices{$self->{_DEVICE_}}) ?
$devices{$self->{_DEVICE_}} :
'';
}
sub ElementCode {
my $self = shift;
return $self->{_ELEMENT_};
}
sub Element {
my $self = shift;
return $elements{$self->{_ELEMENT_}};
}
################ PositionStr
sub PositionStr { # just a data formatting method
my $self = shift;
return sprintf("%08.5f, %08.5f",
$self->{LatDecDeg},
$self->{LongDecDeg});
}
################ TimeStr
=head3 C<NMEAdata-E<gt>TimeStr>
Returns a zero padded string of the format C<hh:mm:ss MM/DD/YY> for
the time of the fix
=cut
sub TimeStr { # another formatting method
my $self = shift;
return undef unless defined $self->{FixTimeH};
return sprintf("%02d:%02d:%05.3f %02d/%02d/%02d",
$self->{FixTimeH},
$self->{FixTimeM},
$self->{FixTimeS},
$self->{FixDateM},
$self->{FixDateD},
$self->{FixDateY});
}
################ LL2UTM
=head3 C<NMEAdata-E<gt>LL2UTM>
Returns the three element list C<($UTMzone,$Easting,$Northing)>. The
values of easting and northing are in meters. The UTM projection facilitates
plotting because it represents the surface of the planet as a rectiliniar grid.
The projection is based on the WGS84 Ellipsoid, the standard for most GPS
fixes.
Uses the module C<Geo::Coordinates::UTM>
=cut
sub LL2UTM { # converts from Lat, Long (and ellipsoid) to UTM
my $self = shift; # zone, easting, and northing
($self->{zone},
$self->{easting},
$self->{northing}) =
latlon_to_utm($ellipsoid,
$self->{LatDecDeg},
$self->{LongDecDeg});
return($self->{zone},
$self->{easting},
$self->{northing});
}
############# Zone
=head3 C<NMEAdata-E<gt>Zone>
Returns the UTM Zone. For a better understand of UTM zones see
C<Geo::Coordinates::UTM>
=cut
sub Zone {
my $self = shift;
# run the conversion if it hasn't already been done
$self->LL2UTM() unless defined $self->{zone};
return $self->{zone};
}
############# Easting
=head3 C<NMEAdata-E<gt>Easting>
Returns the easting in meters. See C<LL2UTM> for information
on zones, easting, and northing.
=cut
sub Easting {
my $self = shift;
# run the conversion if it hasn't already been done
$self->LL2UTM() unless defined $self->{easting};
return $self->{easting};
}
############## Northing
=head3 C<NMEAdata-E<gt>Northing>
Returns the easting in meters. See C<LL2UTM> for information
on zones, easting, and northing.
=cut
sub Northing {
my $self = shift;
# run the conversion if it hasn't already been done
$self->LL2UTM() unless defined $self->{northing};
return $self->{northing};
}
############### UNIXtime
=head3 C<NMEAdata-E<gt>UNIXtime>
Returns the long-integer/float value of the fixtime, using the UNIX
style epoch of January 1, 1970. This is a convenient value
for timestamping. If the device returns an fractional second
in its time string, the floating point version is returned.
This uses the module C<Time::Local>
=cut
sub UNIXtime {
my $self = shift;
if(($self->{RawFixTime} =~ /^\d+$/) && ($self->{RawFixDate} =~ /^\d+$/)) {
$self->{UNIXtime} = timegm(
$self->{FixTimeS},
$self->{FixTimeM},
$self->{FixTimeH},
$self->{FixDateD},
$self->{FixDateM}-1,
$self->{FixDateY}
) unless defined $self->{UNIXtime};
} else {
$self->{UNIXtime} = undef;
}
return $self->{UNIXtime};
}
sub PrintDump {
my $self = shift;
for my $key (sort keys %$self) {
next if $key =~ /^_\S+_$/;
print "\t$key => $$self{$key}\n";
}
}
1;
__END__
=head1 AUTHOR
Ben Smith, Captain of the Sailing Vessel Mother of Perl, in the Caribbean
Seas
=head1 BUGS
To be determined
=head1 SEE ALSO
=head1 COPYRIGHT
This program is free software, You may copy or redistribute it under the
same terms as Perl itself.
=cut
########################################################################
#
# $Log: NMEAfields.pm,v $
# Revision 1.2 2005/09/02 14:51:20 ben
# Added many new data types and device types.
# Added the Data method.
#
# Revision 1.1 2004/11/22 17:10:51 ben
# Initial revision
#
#
########################################################################