File Coverage

blib/lib/GPSD/Parse.pm
Criterion Covered Total %
statement 143 159 89.9
branch 64 82 78.0
condition 4 6 66.6
subroutine 27 30 90.0
pod 14 14 100.0
total 252 291 86.6


line stmt bran cond sub pod time code
1             package GPSD::Parse;
2              
3 13     13   26262 use strict;
  13         28  
  13         333  
4 13     13   57 use warnings;
  13         25  
  13         382  
5              
6 13     13   65 use Carp qw(croak);
  13         32  
  13         660  
7 13     13   5685 use IO::Socket::INET;
  13         220543  
  13         72  
8              
9             our $VERSION = '1.01';
10              
11             BEGIN {
12              
13             # look for JSON::XS, and if not available, fall
14             # back to JSON::PP to avoid requiring non-core modules
15              
16 13     13   5339 my $json_ok = eval {
17 13         2015 require JSON::XS;
18 0         0 JSON::XS->import;
19 0         0 1;
20             };
21 13 50       86 if (! $json_ok){
22 13         7386 require JSON::PP;
23 13         173065 JSON::PP->import;
24             }
25             }
26              
27             sub new {
28 24     24 1 5180 my ($class, %args) = @_;
29 24         78 my $self = bless {}, $class;
30              
31 24         123 $self->_file($args{file});
32 24         118 $self->_is_metric($args{metric});
33 24         111 $self->_is_signed($args{signed});
34              
35 24 100       70 if (! $self->_file) {
36 7         35 $self->_port($args{port});
37 7         34 $self->_host($args{host});
38 7         33 $self->_socket;
39 0         0 $self->_is_socket(1);
40 0         0 $self->on;
41             }
42              
43 17         91 return $self;
44             }
45             sub on {
46 0     0 1 0 $_[0]->_socket->send('?WATCH={"enable": true}' . "\n");
47             }
48             sub off {
49 0     0 1 0 $_[0]->_socket->send('?WATCH={"enable": false}' . "\n");
50             }
51             sub poll {
52 26     26 1 2623 my ($self, %args) = @_;
53            
54 26         129 $self->_file($args{file});
55              
56 26         53 my $gps_json_data;
57              
58 26 50       75 if ($self->_file){
59 26         69 my $fname = $self->_file;
60              
61 26 100       1325 open my $fh, '<', $fname or croak "can't open file '$fname': $!";
62              
63             {
64 25         67 local $/;
  25         104  
65 25         509 $gps_json_data = <$fh>;
66 25 50       361 close $fh or croak "can't close file '$fname': $!";
67             }
68             }
69             else {
70 0         0 $self->_socket->send("?POLL;\n");
71 0         0 local $/ = "\r\n";
72 0         0 while (my $line = $self->_socket->getline){
73 0         0 chomp $line;
74 0         0 my $data = decode_json $line;
75 0 0       0 if ($data->{class} eq 'POLL'){
76 0         0 $gps_json_data = $line;
77 0         0 last;
78             }
79             }
80             }
81              
82 25 50       119 die "no JSON data returned from the GPS" if ! defined $gps_json_data;
83              
84 25         117 my $gps_perl_data = decode_json $gps_json_data;
85              
86 25 50       369757 if (! defined $gps_perl_data->{tpv}[0]){
87 0         0 warn "\n\nincomplete or empty dataset returned from GPS...\n\n";
88             }
89              
90 25         200 $self->_parse($gps_perl_data);
91              
92 25 100 66     213 return $gps_json_data if defined $args{return} && $args{return} eq 'json';
93 24         207 return $gps_perl_data;
94             }
95             sub tpv {
96 66     66 1 20590 my ($self, $stat) = @_;
97              
98 66 100       202 if (defined $stat){
99 65 100       201 return '' if ! defined $self->{tpv}{$stat};
100 64         369 return $self->{tpv}{$stat};
101             }
102 1         2 return $self->{tpv};
103             }
104             sub sky {
105 1     1 1 8 return shift->{sky};
106             }
107             sub time {
108 1     1 1 8 return shift->{time};
109             }
110             sub device {
111 1     1 1 7 return shift->{device};
112             }
113             sub direction {
114 32 50   32 1 120 shift if @_ > 1;
115              
116 32         64 my ($deg) = @_;
117              
118 32         133 my @directions = qw(
119             N NNE NE ENE E ESE SE SSE S SSW SW WSW W WNW NW NNW N
120             );
121              
122 32         70 my $calc = (($deg % 360) / 22.5) + .5;
123              
124 32         160 return $directions[$calc];
125             }
126             sub satellites {
127 68     68 1 99983 my ($self, $sat_num, $stat) = @_;
128              
129 68 100       214 if (defined $sat_num){
130 66 100       201 return undef if ! defined $self->{satellites}{$sat_num};
131             }
132              
133 67 100 66     330 if (defined $sat_num && defined $stat){
134 65 100       252 return undef if ! defined $self->{satellites}{$sat_num}{$stat};
135 52         177 return $self->{satellites}{$sat_num}{$stat};
136             }
137 2         10 return $self->{satellites};
138             }
139             sub feet {
140 3     3 1 10 return $_[0]->_is_metric(0);
141             }
142             sub metres {
143 4     4 1 13 return $_[0]->_is_metric(1);
144             }
145             sub signed {
146 27     27 1 3415 my $self = shift;
147              
148 27 100       103 if (! @_){
149             # caller just wants to set is_signed
150 1         4 return $self->_is_signed(1);
151             }
152              
153 26         69 my ($lat, $lon) = @_;
154              
155 26 100       314 return ($lat, $lon) if $lat !~ /[NESW]$/;
156 4 50       10 return ($lat, $lon) if $lon !~ /[NESW]$/;
157              
158 4         21 my %directions = (
159             W => '-',
160             E => '',
161             N => '',
162             S => '-',
163             );
164              
165 4         10 for ($lat, $lon){
166 8         21 my ($dir) = $_ =~ /([NESW])$/;
167 8         22 s/([NESW])$//;
168 8         19 $_ = $directions{$dir} . $_;
169             }
170              
171 4         15 return ($lat, $lon);
172             }
173             sub unsigned {
174 8     8 1 5432 my $self = shift;
175              
176 8 100       70 if (! @_){
177             # caller just wants to set unsigned
178 1         4 return $self->_is_signed(0);
179             }
180 7         24 my ($lat, $lon) = @_;
181              
182 7 50       173 return ($lat, $lon) if $lat =~ /[NESW]$/;
183 7 50       60 return ($lat, $lon) if $lon =~ /[NESW]$/;
184              
185 7 100       46 if ($lat =~ /^-/) {
186 2         29 $lat =~ s/-(.*)/${1}S/;
187             }
188             else {
189 5         24 $lat .= 'N';
190             }
191              
192 7 100       50 if ($lon =~ /^-/) {
193 5         86 $lon =~ s/-(.*)/${1}W/;
194             }
195             else {
196 2         9 $lon .= 'E';
197             }
198              
199 7         83 return ($lat, $lon);
200             }
201             sub _convert {
202 25     25   59 my $self = shift;
203              
204 25         131 my @convertable_stats = qw(alt climb speed);
205              
206 25 100       99 if (! $self->_is_metric){
207 5         17 for (@convertable_stats) {
208 15         32 my $num = $self->{tpv}{$_};
209 15         26 $num = $num * 3.28084;
210 15         95 $self->{tpv}{$_} = substr($num, 0, index($num, '.') + 1 + 3);
211             }
212             }
213             }
214             sub _file {
215 133     133   272 my ($self, $file) = @_;
216 133 100       390 $self->{file} = $file if defined $file;
217 133         326 return $self->{file};
218             }
219             sub _host {
220 21     21   48 my ($self, $host) = @_;
221 21 50       55 $self->{host} = $host if defined $host;
222 21 100       59 $self->{host} = '127.0.0.1' if ! defined $self->{host};
223 21         55 return $self->{host};
224             }
225             sub _is_metric {
226             # whether we're in feet or metres mode
227 56     56   163 my ($self, $metric) = @_;
228 56 100       187 $self->{metric} = $metric if defined $metric;
229 56 100       219 $self->{metric} = 1 if ! defined $self->{metric};
230 56         171 return $self->{metric};
231             }
232             sub _is_signed {
233             # set whether we're in signed or unsigned mode
234 51     51   136 my ($self, $signed) = @_;
235 51 100       155 $self->{signed} = $signed if defined $signed;
236 51 100       169 $self->{signed} = 1 if ! defined $self->{signed};
237 51         186 return $self->{signed};
238             }
239             sub _port {
240 21     21   41 my ($self, $port) = @_;
241 21 50       53 $self->{port} = $port if defined $port;
242 21 100       58 $self->{port} = 2947 if ! defined $self->{port};
243 21         93 return $self->{port};
244             }
245             sub _parse {
246             # parse the GPS data and populate the object
247 25     25   83 my ($self, $data) = @_;
248              
249 25         86 $self->{tpv} = $data->{tpv}[0];
250 25         124 $self->{time} = $self->{tpv}{time};
251 25         77 $self->{device} = $self->{tpv}{device};
252 25         73 $self->{sky} = $data->{sky}[0];
253              
254             # perform conversions on metric/standard if necessary
255              
256 25         149 $self->_convert;
257              
258             # perform conversions on the lat/long if necessary
259              
260 25         88 my ($lat, $lon) = ($self->{tpv}{lat}, $self->{tpv}{lon});
261              
262 25 100       101 ($self->{tpv}{lat}, $self->{tpv}{lon}) = $self->_is_signed
263             ? $self->signed($lat, $lon)
264             : $self->unsigned($lat, $lon);
265              
266 25         65 my %sats;
267              
268 25         53 for my $sat (@{ $self->{sky}{satellites} }){
  25         100  
269 325         504 my $prn = $sat->{PRN};
270 325         471 delete $sat->{PRN};
271 325 100       1018 $sat->{used} = $sat->{used} ? 1 : 0;
272 325         2426 $sats{$prn} = $sat;
273             }
274 25         93 $self->{satellites} = \%sats;
275             }
276             sub _is_socket {
277             # check if we're in socket mode
278 24     24   70 my ($self, $status) = @_;
279 24 50       92 $self->{is_socket} = $status if defined $status;
280 24         564 return $self->{is_socket};
281             }
282             sub _socket {
283 7     7   19 my ($self) = @_;
284              
285 7 50       17 return undef if $self->_file;
286              
287 7 50       30 if (! defined $self->{socket}){
288 7         20 $self->{"socket"}=IO::Socket::INET->new(
289             PeerAddr => $self->_host,
290             PeerPort => $self->_port,
291             );
292             }
293              
294 7         4432 my ($h, $p) = ($self->_host, $self->_port);
295              
296 7 50       1197 croak "can't connect to gpsd://$h:$p" if ! defined $self->{socket};
297            
298 0         0 return $self->{'socket'};
299             }
300             sub DESTROY {
301 24     24   20443 my $self = shift;
302 24 50       98 $self->off if $self->_is_socket;
303             }
304       0     sub _vim {} # fold placeholder
305              
306             1;
307              
308             =head1 NAME
309              
310             GPSD::Parse - Parse, extract use the JSON output from GPS units
311              
312             =for html
313            
314             Coverage Status
315              
316             =head1 SYNOPSIS
317              
318             use GPSD::Parse;
319             my $gps = GPSD::Parse->new;
320              
321             # poll for data
322              
323             $gps->poll;
324              
325             # get all TPV data in an href
326              
327             my $tpv_href = $gps->tpv;
328              
329             # get individual TPV stats
330              
331             print $gps->tpv('lat');
332             print $gps->tpv('lon');
333              
334             # timestamp of the most recent poll
335              
336             print $gps->time;
337              
338             # get all satellites in an href of hrefs
339              
340             my $sats = $gps->satellites;
341              
342             # get an individual piece of info from a single sattelite
343              
344             print $gps->satellites(16, 'ss');
345              
346             # check which serial device the GPS is connected to
347              
348             print $gps->device;
349              
350             # toggle between metres and feet (metres by default)
351              
352             $gps->feet;
353             $gps->metres;
354              
355             =head1 DESCRIPTION
356              
357             Simple, lightweight (core only) distribution that polls C for data
358             received from a UART (serial/USB) connected GPS receiver over a TCP connection.
359              
360             The data is fetched in JSON, and returned as Perl data.
361              
362             =head1 NOTES
363              
364             =head2 Requirements
365              
366             A version of L that returns results in
367             JSON format is required to have been previously installed. It should be started
368             at system startup, with the following flags with system-specific serial port.
369             See the above link for information on changing the listen IP and port.
370              
371             sudo gpsd /dev/ttyS0 -n -F /var/log/gpsd.sock
372              
373             =head2 Available Data
374              
375             Each of the methods that return data have a table in their respective
376             documentation within the L section. Specifically, look at the
377             C, C and the more broad C method sections to
378             understand what available data attributes you can extract.
379              
380             =head2 Conversions
381              
382             All output where applicable defaults to metric (metres). See the C
383             parameter in the C method to change this to use imperial/standard
384             measurements. You can also toggle this at runtime with the C and
385             C methods.
386              
387             For latitude and longitude, we default to using the signed notation. You can
388             disable this with the C parameter in C, along with the
389             C and C methods to toggle this conversion at runtime.
390              
391             =head1 METHODS
392              
393             =head2 new(%args)
394              
395             Instantiates and returns a new L object instance.
396              
397             Parameters:
398              
399             host => 127.0.0.1
400              
401             Optional, String: An IP address or fully qualified domain name of the C
402             server. Defaults to the localhost (C<127.0.0.1>) if not supplied.
403              
404             port => 2947
405              
406             Optional, Integer: The TCP port number that the C daemon is running on.
407             Defaults to C<2947> if not sent in.
408              
409             metric => Bool
410              
411             Optional, Integer: By default, we return measurements in metric (metres). Send
412             in a false value (C<0>) to use imperial/standard measurement conversions
413             (ie. feet). Note that if returning the raw *JSON* data from the C
414             method, the conversions will not be done. The default raw Perl return will have
415             been converted however.
416              
417             signed => Bool
418              
419             Optional, Integer: By default, we use the signed notation for latitude and
420             longitude. Send in a false value (C<0>) to disable this. Here's an example:
421              
422             enabled (default) disabled
423             ----------------- --------
424              
425             lat: 51.12345678 51.12345678N
426             lon: -114.123456 114.123456W
427              
428             We add the letter notation at the end of the result if C is disabled.
429              
430             NOTE: You can toggle this at runtime by calling the C and
431             C methods. The data returned at the next poll will reflect any
432             change.
433              
434             file => 'filename.ext'
435              
436             Optional, String: For testing purposes. Instead of reading from a socket, send
437             in a filename that contains legitimate JSON data saved from a previous C
438             output and we'll operate on that. Useful also for re-running previous output.
439              
440             =head2 poll(%args)
441              
442             Does a poll of C for data, and configures the object with that data.
443              
444             Parameters:
445              
446             All parameters are sent in as a hash.
447              
448             file => $filename
449              
450             Optional, String: Used for testing, you can send in the name of a JSON file
451             that contains C JSON data and we'll work with that instead of polling
452             the GPS device directly. Note that you *must* instantiate the object with the
453             C parameter in new for this to have any effect and to bypass the socket
454             creation.
455              
456             return => 'json'
457              
458             Optional, String: By default, after configuring the object, we will return the
459             polled raw data as a Perl hash reference. Send this param in with the value of
460             C<'json'> and we'll return the data exactly as we received it from C.
461              
462             Returns:
463              
464             The raw poll data as either a Perl hash reference structure or as the
465             original JSON string.
466              
467             =head2 tpv($stat)
468              
469             C stands for "Time Position Velocity". This is the data that represents
470             your location and other vital statistics.
471              
472             By default, we return a hash reference. The format of the hash is depicted
473             below.
474              
475             Parameters:
476              
477             $stat
478              
479             Optional, String. You can extract individual statistics of the TPV data by
480             sending in the name of the stat you wish to fetch. This will then return the
481             string value if available. Returns an empty string if the statistic doesn't
482             exist.
483              
484             Available statistic/info name, example value, description. This is the default
485             raw result:
486              
487             time => '2017-05-16T22:29:29.000Z' # date/time in UTC
488             lon => '-114.000000000' # longitude
489             lat => '51.000000' # latitude
490             alt => '1084.9' # altitude (metres)
491             climb => '0' # rate of ascent/decent (metres/sec)
492             speed => '0' # rate of movement (metres/sec)
493             track => '279.85' # heading (degrees from true north)
494             device => '/dev/ttyS0' # GPS serial interface
495             mode => 3 # NMEA mode
496             epx => '3.636' # longitude error estimate (metres)
497             epy => '4.676' # latitude error estimate (metres)
498             epc => '8.16' # ascent/decent error estimate (meters)
499             ept => '0.005' # timestamp error (sec)
500             epv => '4.082' # altitude error estimate (meters)
501             eps => '9.35' # speed error estimate (metres/sec)
502             class => 'TPV' # data type (fixed as TPV)
503             tag => 'ZDA' # identifier
504              
505             =head2 satellites($num, $stat)
506              
507             This method returns a hash reference of hash references, where the key is the
508             satellite number, and the value is a hashref that contains the various
509             information related to the specific numbered satellite.
510              
511             Note that the data returned by this function has been manipuated and is not
512             exactly equivalent of that returned by C. To get the raw data, see
513             C.
514              
515             Parameters:
516              
517             $num
518              
519             Optional, Integer: Send in the satellite number and we'll return the relevant
520             information in a hash reference for the specific satellite requested, as
521             opposed to returning data for all the satellites. Returns C if a
522             satellite by that number doesn't exist.
523              
524             $stat
525              
526             Optional, String: Like C, you can request an individual piece of
527             information for a satellite. This parameter is only valid if you've sent in
528             the C<$num> param, and the specified satellite exists.
529              
530             Available statistic/information items available for each satellite, including
531             the name, an example value and a description:
532              
533             NOTE: The PRN attribute will not appear unless you're using raw data. The PRN
534             can be found as the satellite hash reference key after we've processed the
535             data.
536              
537             PRN => 16 # PRN ID of the satellite
538              
539             # 1-63 are GNSS satellites
540             # 64-96 are GLONASS satellites
541             # 100-164 are SBAS satellites
542              
543             ss => 20 # signal strength (dB)
544             az => 161 # azimuth (degrees from true north)
545             used => 1 # currently being used in calculations
546             el => 88 # elevation in degrees
547              
548             =head2 sky
549              
550             Returns a hash reference containing all of the data that was pulled from the
551             C information returned by C. This information contains satellite
552             info and other related statistics.
553              
554             Available information, with the attribute, example value and description:
555              
556             satellites => [] # array of satellite hashrefs
557             xdop => '0.97' # longitudinal dilution of precision
558             ydop => '1.25' # latitudinal dilution of precision
559             pdop => '1.16' # spherical dilution of precision
560             tdop => '2.2' # time dilution of precision
561             vdop => '0.71' # altitude dilution of precision
562             gdop => '3.87' # hyperspherical dilution of precision
563             hdop => '0.92' # horizontal dilution of precision
564             class => 'SKY' # object class, hardcoded to SKY
565             tag => 'ZDA' # object ID
566             device => '/dev/ttyS0' # serial port connected to the GPS
567              
568             =head2 direction($degree)
569              
570             Converts a degree from true north into a direction (eg: ESE, SW etc).
571              
572             Parameters:
573              
574             $degree
575              
576             Mandatory, Ineger/Decimal: A decimal ranging from 0-360. Returns the direction
577             representing the degree from true north. A common example would be:
578              
579             my $heading = $gps->direction($gps->tpv('track'));
580              
581             Degree/direction map:
582              
583             N 348.75 - 11.25
584             NNE 11.25 - 33.75
585             NE 33.75 - 56.25
586             ENE 56.25 - 78.75
587              
588             E 78.75 - 101.25
589             ESE 101.25 - 123.75
590             SE 123.75 - 146.25
591             SSE 146.25 - 168.75
592              
593             S 168.75 - 191.25
594             SSW 191.25 - 213.75
595             SW 213.75 - 236.25
596             WSW 236.25 - 258.75
597              
598             W 258.75 - 281.25
599             WNW 281.25 - 303.75
600             NW 303.75 - 326.25
601             NNW 326.25 - 348.75
602              
603             =head2 device
604              
605             Returns a string containing the actual device the GPS is connected to
606             (eg: C).
607              
608             =head2 time
609              
610             Returns a string of the date and time of the most recent poll, in UTC.
611              
612             =head2 signed
613              
614             This method works on the latitude and longitude output view. By default, we use
615             signed notation, eg:
616              
617             -114.1111111111 # lon
618             51.111111111111 # lat
619              
620             If you've switched to C, calling this method will toggle it back,
621             and the results will be visible after the next C.
622              
623             You can optionally use this method to convert values in a manual way. Simply
624             send in the latitude and longitude in that order as parameters, and we'll return
625             a list containing them both after modification, if it was necessary.
626              
627             =head2 unsigned
628              
629             This method works on the latitude and longitude output view. By default, we use
630             signed notation, eg:
631              
632             -114.1111111111 # lon
633             51.111111111111 # lat
634              
635             Calling this method will convert those to:
636              
637             114.1111111111W # lon
638             51.11111111111N # lat
639              
640             If you've switched to C, calling this method will toggle it back,
641             and the results will be visible after the next C.
642              
643             You can optionally use this method to convert values in a manual way. Simply
644             send in the latitude and longitude in that order as parameters, and we'll return
645             a list containing them both after modification, if it was necessary.
646              
647             =head2 feet
648              
649             By default, we use metres as the measurement for any attribute that is measured
650             in distance. Call this method to have all attributes converted into feet
651             commencing at the next call to C. Use C to revert back.
652              
653             =head2 metres
654              
655             We measure in metres by default. If you've switched to using feet as the
656             measurement unit, a call to this method will revert back to the default.
657              
658             =head2 on
659              
660             Puts C in listening mode, ready to poll data from.
661              
662             We call this method internally when the object is instantiated with C if
663             we're not in file mode. Likewise, when the object is destroyed (end of program
664             run), we call the subsequent C method.
665              
666             If you have long periods of a program run where you don't need the GPS, you can
667             manually run the C and C methods to disable and re-enable the GPS.
668              
669             =head2 off
670              
671             Turns off C listening mode.
672              
673             Not necessary to call, but it will help preserve battery life if running on a
674             portable device for long program runs where the GPS is used infrequently. Use in
675             conjunction with C. We call C automatically when the object goes
676             out of scope (program end for example).
677              
678             =head1 EXAMPLES
679              
680             =head2 Basic Features and Options
681              
682             Here's a simple example using some of the basic features and options. Please
683             read through the documentation of the methods (particularly C and
684             C to get a good grasp on what can be fetched.
685              
686             use warnings;
687             use strict;
688             use feature 'say';
689              
690             use GPSD::Parse;
691              
692             my $gps = GPSD::Parse->new(signed => 0);
693              
694             $gps->poll;
695              
696             my $lat = $gps->tpv('lat');
697             my $lon = $gps->tpv('lon');
698              
699             my $heading = $gps->tpv('track');
700             my $direction = $gps->direction($heading);
701              
702             my $altitude = $gps->tpv('alt');
703              
704             my $speed = $gps->tpv('speed');
705              
706             say "latitude: $lat";
707             say "longitude: $lon\n";
708              
709             say "heading: $heading degrees";
710             say "direction: $direction\n";
711              
712             say "altitude: $altitude metres\n";
713              
714             say "speed: $speed metres/sec";
715              
716             Output:
717              
718             latitude: 51.1111111N
719             longitude: 114.11111111W
720              
721             heading: 31.23 degrees
722             direction: NNE
723              
724             altitude: 1080.9 metres
725              
726             speed: 0.333 metres/sec
727              
728             =head2 Displaying Satellite Information
729              
730             Here's a rough example that displays the status of tracked satellites, along
731             with the information on the one's we're currently using.
732              
733             use warnings;
734             use strict;
735              
736             use GPSD::Parse;
737              
738             my $gps = GPSD::Parse->new;
739              
740             while (1){
741             $gps->poll;
742             my $sats = $gps->satellites;
743              
744             for my $sat (keys %$sats){
745             if (! $gps->satellites($sat, 'used')){
746             print "$sat: unused\n";
747             }
748             else {
749             print "$sat: used\n";
750             for (keys %{ $sats->{$sat} }){
751             print "\t$_: $sats->{$sat}{$_}\n";
752             }
753             }
754             }
755             sleep 3;
756             }
757              
758             Output:
759              
760             7: used
761             ss: 20
762             used: 1
763             az: 244
764             el: 20
765             29: unused
766             31: used
767             el: 12
768             az: 64
769             used: 1
770             ss: 17
771             6: unused
772             138: unused
773             16: used
774             ss: 17
775             el: 53
776             used: 1
777             az: 119
778             26: used
779             az: 71
780             used: 1
781             el: 46
782             ss: 27
783             22: used
784             ss: 28
785             el: 17
786             used: 1
787             az: 175
788             3: used
789             ss: 24
790             az: 192
791             used: 1
792             el: 40
793             9: unused
794             23: unused
795             2: unused
796              
797             =head1 TESTING
798              
799             Please note that we init and disable the GPS device on construction and
800             deconstruction of the object respectively. It takes a few seconds for the GPS
801             unit to initialize itself and then lock on the satellites before we can get
802             readings. For this reason, please understand that one test sweep may pass while
803             the next fails.
804              
805             I am considering adding specific checks, but considering that it's a timing
806             thing (seconds, not microseconds that everyone is in a hurry for nowadays) I am
807             going to wait until I get a chance to take the kit into the field before I do
808             anything drastic.
809              
810             For now. I'll leave it as is; expect failure if you ram on things too quickly.
811              
812             =head1 SEE ALSO
813              
814             A very similar distribution is L. However, it has a long line of
815             prerequisite distributions that didn't always install easily on my primary
816             target platform, the Raspberry Pi.
817              
818             This distribution isn't meant to replace that one, it's just a much simpler and
819             more lightweight piece of software that pretty much does the same thing.
820              
821             =head1 AUTHOR
822              
823             Steve Bertrand, C<< >>
824              
825             =head1 LICENSE AND COPYRIGHT
826              
827             Copyright 2017 Steve Bertrand.
828              
829             This program is free software; you can redistribute it and/or modify it
830             under the terms of either: the GNU General Public License as published
831             by the Free Software Foundation; or the Artistic License.
832              
833             See L for more information.