File Coverage

blib/lib/Ham/APRS/FAP.pm
Criterion Covered Total %
statement 648 1400 46.2
branch 339 848 39.9
condition 82 249 32.9
subroutine 27 45 60.0
pod 14 14 100.0
total 1110 2556 43.4


line stmt bran cond sub pod time code
1              
2             package Ham::APRS::FAP;
3              
4             =head1 NAME
5              
6             Ham::APRS::FAP - Finnish APRS Parser (Fabulous APRS Parser)
7              
8             =head1 SYNOPSIS
9              
10             use Ham::APRS::FAP qw(parseaprs);
11             my $aprspacket = 'OH2RDP>BEACON,OH2RDG*,WIDE:!6028.51N/02505.68E#PHG7220/RELAY,WIDE, OH2AP Jarvenpaa';
12             my %packetdata;
13             my $retval = parseaprs($aprspacket, \%packetdata);
14             if ($retval == 1) {
15             # decoding ok, do something with the data
16             while (my ($key, $value) = each(%packetdata)) {
17             print "$key: $value\n";
18             }
19             } else {
20             warn "Parsing failed: $packetdata{resultmsg} ($packetdata{resultcode})\n";
21             }
22              
23             =head1 ABSTRACT
24              
25             This module is a fairly complete APRS parser. It parses normal,
26             mic-e and compressed location packets, NMEA location packets,
27             objects, items, messages, telemetry and most weather packets. It is
28             stable and fast enough to parse the APRS-IS stream in real time.
29              
30             The package also contains the Ham::APRS::IS module which, in turn,
31             is an APRS-IS client library.
32              
33             =head1 DESCRIPTION
34              
35             Unless a debugging mode is enabled, all errors and warnings are reported
36             through the API (as opposed to printing on STDERR or STDOUT), so that
37             they can be reported nicely on the user interface of an application.
38             This parser is not known to crash on invalid packets. It is used to power
39             the L web site.
40              
41             APRS features specifically NOT handled by this module:
42              
43             =over
44              
45             =item * special objects (area, signpost, etc)
46              
47             =item * network tunneling/third party packets
48              
49             =item * direction finding
50              
51             =item * station capability queries
52              
53             =item * status reports (partially)
54              
55             =item * user defined data formats
56              
57             =back
58              
59             This module is based (on those parts that are implemented)
60             on APRS specification 1.0.1.
61              
62             This module requires a reasonably recent L module.
63              
64             =head1 EXPORT
65              
66             None by default.
67              
68             =head1 FUNCTION REFERENCE
69              
70             =cut
71              
72 21     21   110850 use strict;
  21         35  
  21         553  
73 21     21   75 use warnings;
  21         24  
  21         632  
74 21     21   8450 use Date::Calc qw(check_date Today Date_to_Time Add_Delta_YM Mktime);
  21         145120  
  21         1769  
75 21     21   10514 use Math::Trig;
  21         208969  
  21         299405  
76              
77             require Exporter;
78              
79             our @ISA = qw(Exporter);
80              
81             # Items to export into callers namespace by default. Note: do not export
82             # names by default without a very good reason. Use EXPORT_OK instead.
83             # Do not simply export all your public functions/methods/constants.
84              
85             # This allows declaration use Ham::APRS::FAP ':all';
86             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
87             # will save memory.
88             ##our %EXPORT_TAGS = (
89             ## 'all' => [ qw(
90             ##
91             ## ) ],
92             ##);
93              
94             our @EXPORT_OK = (
95             ## @{ $EXPORT_TAGS{'all'} },
96             '&parseaprs',
97             '&kiss_to_tnc2',
98             '&tnc2_to_kiss',
99             '&aprs_duplicate_parts',
100             '&count_digihops',
101             '&check_ax25_call',
102             '&distance',
103             '&direction',
104             '&make_object',
105             '&make_timestamp',
106             '&make_position',
107             '&mice_mbits_to_message',
108             );
109              
110             ##our @EXPORT = qw(
111             ##
112             ##);
113              
114             our $VERSION = '1.21';
115              
116              
117             # Preloaded methods go here.
118              
119             # no debugging by default
120             my $debug = 0;
121              
122             my %result_messages = (
123             'unknown' => 'Unsupported packet format',
124            
125             'packet_no' => 'No packet given to parse',
126             'packet_short' => 'Too short packet',
127             'packet_nobody' => 'No body in packet',
128            
129             'srccall_noax25' => 'Source callsign is not a valid AX.25 call',
130             'srccall_badchars' => 'Source callsign contains bad characters',
131            
132             'dstpath_toomany' => 'Too many destination path components to be AX.25',
133             'dstcall_none' => 'No destination field in packet',
134             'dstcall_noax25' => 'Destination callsign is not a valid AX.25 call',
135            
136             'digicall_noax25' => 'Digipeater callsign is not a valid AX.25 call',
137             'digicall_badchars' => 'Digipeater callsign contains bad characters',
138            
139             'timestamp_inv_loc' => 'Invalid timestamp in location',
140             'timestamp_inv_obj' => 'Invalid timestamp in object',
141             'timestamp_inv_sta' => 'Invalid timestamp in status',
142             'timestamp_inv_gpgga' => 'Invalid timestamp in GPGGA sentence',
143             'timestamp_inv_gpgll' => 'Invalid timestamp in GPGLL sentence',
144            
145             'packet_invalid' => 'Invalid packet',
146            
147             'nmea_inv_cval' => 'Invalid coordinate value in NMEA sentence',
148             'nmea_large_ew' => 'Too large value in NMEA sentence (east/west)',
149             'nmea_large_ns' => 'Too large value in NMEA sentence (north/south)',
150             'nmea_inv_sign' => 'Invalid lat/long sign in NMEA sentence',
151             'nmea_inv_cksum' => 'Invalid checksum in NMEA sentence',
152            
153             'gprmc_fewfields' => 'Less than ten fields in GPRMC sentence ',
154             'gprmc_nofix' => 'No GPS fix in GPRMC sentence',
155             'gprmc_inv_time' => 'Invalid timestamp in GPRMC sentence',
156             'gprmc_inv_date' => 'Invalid date in GPRMC sentence',
157             'gprmc_date_out' => 'GPRMC date does not fit in an Unix timestamp',
158            
159             'gpgga_fewfields' => 'Less than 11 fields in GPGGA sentence',
160             'gpgga_nofix' => 'No GPS fix in GPGGA sentence',
161            
162             'gpgll_fewfields' => 'Less than 5 fields in GPGLL sentence',
163             'gpgll_nofix' => 'No GPS fix in GPGLL sentence',
164            
165             'nmea_unsupp' => 'Unsupported NMEA sentence type',
166            
167             'obj_short' => 'Too short object',
168             'obj_inv' => 'Invalid object',
169             'obj_dec_err' => 'Error in object location decoding',
170            
171             'item_short' => 'Too short item',
172             'item_inv' => 'Invalid item',
173             'item_dec_err' => 'Error in item location decoding',
174            
175             'loc_short' => 'Too short uncompressed location',
176             'loc_inv' => 'Invalid uncompressed location',
177             'loc_large' => 'Degree value too large',
178             'loc_amb_inv' => 'Invalid position ambiguity',
179            
180             'mice_short' => 'Too short mic-e packet',
181             'mice_inv' => 'Invalid characters in mic-e packet',
182             'mice_inv_info' => 'Invalid characters in mic-e information field',
183             'mice_amb_large' => 'Too much position ambiguity in mic-e packet',
184             'mice_amb_inv' => 'Invalid position ambiguity in mic-e packet',
185             'mice_amb_odd' => 'Odd position ambiguity in mic-e packet',
186            
187             'comp_inv' => 'Invalid compressed packet',
188            
189             'msg_inv' => 'Invalid message packet',
190            
191             'wx_unsupp' => 'Unsupported weather format',
192             'user_unsupp' => 'Unsupported user format',
193            
194             'dx_inv_src' => 'Invalid DX spot source callsign',
195             'dx_inf_freq' => 'Invalid DX spot frequency',
196             'dx_no_dx' => 'No DX spot callsign found',
197            
198             'tlm_inv' => 'Invalid telemetry packet',
199             'tlm_large' => 'Too large telemetry value',
200             'tlm_unsupp' => 'Unsupported telemetry',
201            
202             'exp_unsupp' => 'Unsupported experimental',
203            
204             'sym_inv_table' => 'Invalid symbol table or overlay',
205             );
206              
207             =over
208              
209             =item result_messages( )
210              
211             Returns a reference to a hash containing all possible
212             return codes as the keys and their plain english descriptions
213             as the values of the hash.
214              
215             =back
216              
217             =cut
218              
219             sub result_messages()
220             {
221 0     0 1 0 return \%result_messages;
222             }
223              
224             # these functions are used to report warnings and parser errors
225             # from the module
226              
227             sub _a_err($$;$)
228             {
229 8     8   12 my ($rethash, $errcode, $val) = @_;
230            
231 8         13 $rethash->{'resultcode'} = $errcode;
232             $rethash->{'resultmsg'}
233             = defined $result_messages{$errcode}
234 8 50       28 ? $result_messages{$errcode} : $errcode;
235            
236 8 50       19 $rethash->{'resultmsg'} .= ': ' . $val if (defined $val);
237            
238 8 50       27 if ($debug > 0) {
239 0         0 warn "Ham::APRS::FAP ERROR $errcode: " . $rethash->{'resultmsg'} . "\n";
240             }
241             }
242              
243             sub _a_warn($$;$)
244             {
245 0     0   0 my ($rethash, $errcode, $val) = @_;
246            
247 0         0 push @{ $rethash->{'warncodes'} }, $errcode;
  0         0  
248            
249 0 0       0 if ($debug > 0) {
250             warn "Ham::APRS::FAP WARNING $errcode: "
251             . (defined $result_messages{$errcode}
252 0 0       0 ? $result_messages{$errcode} : $errcode)
    0          
253             . (defined $val ? ": $val" : '')
254             . "\n";
255             }
256             }
257              
258             # message bit types for mic-e
259             # from left to right, bits a, b and c
260             # standard one bit is 1, custom one bit is 2
261             my %mice_messagetypes = (
262             '111' => 'off duty',
263             '222' => 'custom 0',
264             '110' => 'en route',
265             '220' => 'custom 1',
266             '101' => 'in service',
267             '202' => 'custom 2',
268             '100' => 'returning',
269             '200' => 'custom 3',
270             '011' => 'committed',
271             '022' => 'custom 4',
272             '010' => 'special',
273             '020' => 'custom 5',
274             '001' => 'priority',
275             '002' => 'custom 6',
276             '000' => 'emergency',
277             );
278              
279             =over
280              
281             =item mice_mbits_to_message($packetdata{'mbits'})
282              
283             Convert mic-e message bits (three numbers 0-2) to a textual message.
284             Returns the message on success, undef on failure.
285              
286             =back
287              
288             =cut
289              
290             sub mice_mbits_to_message($) {
291 0     0 1 0 my $bits = shift @_;
292 0 0       0 if ($bits =~ /^\s*([0-2]{3})\s*$/o) {
293 0         0 $bits = $1;
294 0 0       0 if (defined($mice_messagetypes{$bits})) {
295 0         0 return $mice_messagetypes{$bits};
296             }
297             }
298 0         0 return undef;
299             }
300              
301             # A list of mappings from GPSxyz (or SPCxyz)
302             # to APRS symbols. Overlay characters (z) are
303             # not handled here
304             my %dstsymbol = (
305             'BB' => q(/!), 'BC' => q(/"), 'BD' => q(/#), 'BE' => q(/$),
306             'BF' => q(/%), 'BG' => q(/&), 'BH' => q(/'), 'BI' => q!/(!,
307             'BJ' => q!/)!, 'BK' => q(/*), 'BL' => q(/+), 'BM' => q(/,),
308             'BN' => q(/-), 'BO' => q(/.), 'BP' => q(//),
309              
310             'P0' => q(/0), 'P1' => q(/1), 'P2' => q(/2), 'P3' => q(/3),
311             'P4' => q(/4), 'P5' => q(/5), 'P6' => q(/6), 'P7' => q(/7),
312             'P8' => q(/8), 'P9' => q(/9),
313              
314             'MR' => q(/:), 'MS' => q(/;), 'MT' => q(/<), 'MU' => q(/=),
315             'MV' => q(/>), 'MW' => q(/?), 'MX' => q(/@),
316              
317             'PA' => q(/A), 'PB' => q(/B), 'PC' => q(/C), 'PD' => q(/D),
318             'PE' => q(/E), 'PF' => q(/F), 'PG' => q(/G), 'PH' => q(/H),
319             'PI' => q(/I), 'PJ' => q(/J), 'PK' => q(/K), 'PL' => q(/L),
320             'PM' => q(/M), 'PN' => q(/N), 'PO' => q(/O), 'PP' => q(/P),
321             'PQ' => q(/Q), 'PR' => q(/R), 'PS' => q(/S), 'PT' => q(/T),
322             'PU' => q(/U), 'PV' => q(/V), 'PW' => q(/W), 'PX' => q(/X),
323             'PY' => q(/Y), 'PZ' => q(/Z),
324              
325             'HS' => q(/[), 'HT' => q(/\\), 'HU' => q(/]), 'HV' => q(/^),
326             'HW' => q(/_), 'HX' => q(/`),
327              
328             'LA' => q(/a), 'LB' => q(/b), 'LC' => q(/c), 'LD' => q(/d),
329             'LE' => q(/e), 'LF' => q(/f), 'LG' => q(/g), 'LH' => q(/h),
330             'LI' => q(/i), 'LJ' => q(/j), 'LK' => q(/k), 'LL' => q(/l),
331             'LM' => q(/m), 'LN' => q(/n), 'LO' => q(/o), 'LP' => q(/p),
332             'LQ' => q(/q), 'LR' => q(/r), 'LS' => q(/s), 'LT' => q(/t),
333             'LU' => q(/u), 'LV' => q(/v), 'LW' => q(/w), 'LX' => q(/x),
334             'LY' => q(/y), 'LZ' => q(/z),
335              
336             'J1' => q(/{), 'J2' => q(/|), 'J3' => q(/}), 'J4' => q(/~),
337              
338             'OB' => q(\\!), 'OC' => q(\\"), 'OD' => q(\\#), 'OE' => q(\\$),
339             'OF' => q(\\%), 'OG' => q(\\&), 'OH' => q(\\'), 'OI' => q!\\(!,
340             'OJ' => q!\\)!, 'OK' => q(\\*), 'OL' => q(\\+), 'OM' => q(\\,),
341             'ON' => q(\\-), 'OO' => q(\\.), 'OP' => q(\\/),
342              
343             'A0' => q(\\0), 'A1' => q(\\1), 'A2' => q(\\2), 'A3' => q(\\3),
344             'A4' => q(\\4), 'A5' => q(\\5), 'A6' => q(\\6), 'A7' => q(\\7),
345             'A8' => q(\\8), 'A9' => q(\\9),
346              
347             'NR' => q(\\:), 'NS' => q(\\;), 'NT' => q(\\<), 'NU' => q(\\=),
348             'NV' => q(\\>), 'NW' => q(\\?), 'NX' => q(\\@),
349              
350             'AA' => q(\\A), 'AB' => q(\\B), 'AC' => q(\\C), 'AD' => q(\\D),
351             'AE' => q(\\E), 'AF' => q(\\F), 'AG' => q(\\G), 'AH' => q(\\H),
352             'AI' => q(\\I), 'AJ' => q(\\J), 'AK' => q(\\K), 'AL' => q(\\L),
353             'AM' => q(\\M), 'AN' => q(\\N), 'AO' => q(\\O), 'AP' => q(\\P),
354             'AQ' => q(\\Q), 'AR' => q(\\R), 'AS' => q(\\S), 'AT' => q(\\T),
355             'AU' => q(\\U), 'AV' => q(\\V), 'AW' => q(\\W), 'AX' => q(\\X),
356             'AY' => q(\\Y), 'AZ' => q(\\Z),
357              
358             'DS' => q(\\[), 'DT' => q(\\\\), 'DU' => q(\\]), 'DV' => q(\\^),
359             'DW' => q(\\_), 'DX' => q(\\`),
360              
361             'SA' => q(\\a), 'SB' => q(\\b), 'SC' => q(\\c), 'SD' => q(\\d),
362             'SE' => q(\\e), 'SF' => q(\\f), 'SG' => q(\\g), 'SH' => q(\\h),
363             'SI' => q(\\i), 'SJ' => q(\\j), 'SK' => q(\\k), 'SL' => q(\\l),
364             'SM' => q(\\m), 'SN' => q(\\n), 'SO' => q(\\o), 'SP' => q(\\p),
365             'SQ' => q(\\q), 'SR' => q(\\r), 'SS' => q(\\s), 'ST' => q(\\t),
366             'SU' => q(\\u), 'SV' => q(\\v), 'SW' => q(\\w), 'SX' => q(\\x),
367             'SY' => q(\\y), 'SZ' => q(\\z),
368              
369             'Q1' => q(\\{), 'Q2' => q(\\|), 'Q3' => q(\\}), 'Q4' => q(\\~),
370             );
371              
372             # conversion constants
373             our $knot_to_kmh = 1.852; # nautical miles per hour to kilometers per hour
374             our $mph_to_kmh = 1.609344; # miles per hour to kilometers per hour
375             our $kmh_to_ms = 10 / 36; # kilometers per hour to meters per second
376             our $mph_to_ms = $mph_to_kmh * $kmh_to_ms; # miles per hour to meters per second
377             our $hinch_to_mm = 0.254; # hundredths of an inch to millimeters
378             our $feet_to_meters = 0.3048;
379              
380             =over
381              
382             =item debug($enable)
383              
384             Enables (debug(1)) or disables (debug(0)) debugging.
385              
386             When debugging is enabled, warnings and errors are emitted using the warn() function,
387             which will normally result in them being printed on STDERR. Succesfully
388             printed packets will be also printed on STDOUT in a human-readable
389             format.
390              
391             When debugging is disabled, nothing will be printed on STDOUT or STDERR -
392             all errors and parsing results need to be collected from the returned
393             hash reference.
394              
395             =back
396              
397             =cut
398              
399             sub debug($)
400             {
401 0     0 1 0 my $dval = shift @_;
402 0 0       0 if ($dval) {
403 0         0 $debug = 1;
404             } else {
405 0         0 $debug = 0;
406             }
407             }
408              
409             # Return a human readable timestamp in UTC.
410             # If no parameter is given, use current time,
411             # else use the unix timestamp given in the parameter.
412              
413             sub _gettime {
414 0     0   0 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday);
415 0 0       0 if (scalar(@_) >= 1) {
416 0         0 my $tstamp = shift @_;
417 0         0 ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime($tstamp);
418             } else {
419 0         0 ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime();
420             }
421 0         0 my $timestring = sprintf('%d-%02d-%02d %02d:%02d:%02d UTC',
422             $year + 1900,
423             $mon + 1,
424             $mday,
425             $hour,
426             $min,
427             $sec);
428 0         0 return $timestring;
429             }
430              
431             =over
432              
433             =item distance($lon0, $lat0, $lon1, $lat1)
434              
435             Returns the distance in kilometers between two locations
436             given in decimal degrees. Arguments are given in order as
437             lon0, lat0, lon1, lat1, east and north positive.
438             The calculation uses the great circle distance, it
439             is not too exact, but good enough for us.
440              
441             =back
442              
443             =cut
444              
445             sub distance($$$$) {
446 0     0 1 0 my $lon0 = shift @_;
447 0         0 my $lat0 = shift @_;
448 0         0 my $lon1 = shift @_;
449 0         0 my $lat1 = shift @_;
450            
451             # decimal to radian
452 0         0 $lon0 = deg2rad($lon0);
453 0         0 $lon1 = deg2rad($lon1);
454 0         0 $lat0 = deg2rad($lat0);
455 0         0 $lat1 = deg2rad($lat1);
456            
457             # Use the haversine formula for distance calculation
458             # http://mathforum.org/library/drmath/view/51879.html
459 0         0 my $dlon = $lon1 - $lon0;
460 0         0 my $dlat = $lat1 - $lat0;
461 0         0 my $a = (sin($dlat/2)) ** 2 + cos($lat0) * cos($lat1) * (sin($dlon/2)) ** 2;
462 0         0 my $c = 2 * atan2(sqrt($a), sqrt(1-$a));
463 0         0 my $distance = $c * 6366.71; # in kilometers
464              
465 0         0 return $distance;
466             }
467              
468             =over
469              
470             =item direction($lon0, $lat0, $lon1, $lat1)
471              
472             Returns the initial great circle direction in degrees
473             from lat0/lon0 to lat1/lon1. Locations are input
474             in decimal degrees, north and east positive.
475              
476             =back
477              
478             =cut
479              
480             sub direction($$$$) {
481 0     0 1 0 my $lon0 = shift @_;
482 0         0 my $lat0 = shift @_;
483 0         0 my $lon1 = shift @_;
484 0         0 my $lat1 = shift @_;
485              
486 0         0 $lon0 = deg2rad($lon0);
487 0         0 $lon1 = deg2rad($lon1);
488 0         0 $lat0 = deg2rad($lat0);
489 0         0 $lat1 = deg2rad($lat1);
490              
491             # direction from Aviation Formulary V1.42 by Ed Williams
492             # by way of http://mathforum.org/library/drmath/view/55417.html
493 0         0 my $direction = atan2(sin($lon1-$lon0)*cos($lat1),
494             cos($lat0)*sin($lat1)-sin($lat0)*cos($lat1)*cos($lon1-$lon0));
495 0 0       0 if ($direction < 0) {
496             # make direction positive
497 0         0 $direction += 2 * pi;
498             }
499              
500 0         0 return rad2deg($direction);
501             }
502              
503             =over
504              
505             =item count_digihops($header)
506              
507             Count the number of digipeated hops in a (KISS) packet and
508             return it. Returns -1 in case of error.
509             The header parameter can contain the full packet or just the header
510             in TNC2 format. All callsigns in the header must be AX.25 compatible
511             and remember that the number returned is just an educated guess, not
512             absolute truth.
513              
514             =back
515              
516             =cut
517              
518             sub count_digihops($) {
519 0     0 1 0 my $header = shift @_;
520              
521             # Do a rough check on the header syntax
522 0         0 $header =~ tr/\r\n//d;
523 0         0 $header = uc($header);
524 0 0       0 if ($header =~ /^([^:]+):/o) {
525             # remove data part of packet, if present
526 0         0 $header = $1;
527             }
528 0         0 my $hops = undef;
529 0 0       0 if ($header =~ /^([A-Z0-9-]+)\>([A-Z0-9-]+)$/o) {
    0          
530             # check the callsigns for validity
531 0         0 my $retval = check_ax25_call($1);
532 0 0       0 if (not(defined($retval))) {
533 0 0       0 if ($debug > 0) {
534 0         0 warn "count_digihops: invalid source callsign ($1)\n";
535             }
536 0         0 return -1;
537             }
538 0         0 $retval = check_ax25_call($2);
539 0 0       0 if (not(defined($retval))) {
540 0 0       0 if ($debug > 0) {
541 0         0 warn "count_digihops: invalid destination callsign ($2)\n";
542             }
543 0         0 return -1;
544             }
545             # no path at all, so zero hops
546 0         0 return 0;
547              
548             } elsif ($header =~ /^([A-Z0-9-]+)\>([A-Z0-9-]+),([A-Z0-9,*-]+)$/o) {
549 0         0 my $retval = check_ax25_call($1);
550 0 0       0 if (not(defined($retval))) {
551 0 0       0 if ($debug > 0) {
552 0         0 warn "count_digihops: invalid source callsign ($1)\n";
553             }
554 0         0 return -1;
555             }
556 0         0 $retval = check_ax25_call($2);
557 0 0       0 if (not(defined($retval))) {
558 0 0       0 if ($debug > 0) {
559 0         0 warn "count_digihops: invalid destination callsign ($2)\n";
560             }
561 0         0 return -1;
562             }
563             # some hops
564 0         0 $hops = $3;
565              
566             } else {
567             # invalid
568 0 0       0 if ($debug > 0) {
569 0         0 warn "count_digihops: invalid packet header\n";
570             }
571 0         0 return -1;
572             }
573              
574 0         0 my $hopcount = 0;
575             # split the path into parts
576 0         0 my @parts = split(/,/, $hops);
577             # now examine the parts one by one
578 0         0 foreach my $piece (@parts) {
579             # remove the possible "digistar" from the end of callsign
580             # and take note of its existence
581 0         0 my $wasdigied = 0;
582 0 0       0 if ($piece =~ /^[A-Z0-9-]+\*$/o) {
583 0         0 $wasdigied = 1;
584 0         0 $piece =~ s/\*$//;
585             }
586             # check the callsign for validity and expand it
587 0         0 my $call = check_ax25_call($piece);
588 0 0       0 if (not(defined($call))) {
589 0 0       0 if ($debug > 0) {
590 0         0 warn "count_digihops: invalid callsign in path ($piece)\n";
591             }
592 0         0 return -1;
593             }
594             # check special cases, wideN-N and traceN-N for now
595 0 0       0 if ($call =~ /^WIDE([1-7])-([0-7])$/o) {
    0          
596 0         0 my $difference = $1 - $2;
597 0 0       0 if ($difference < 0) {
598             # ignore reversed N-N
599 0 0       0 if ($debug > 0) {
600 0         0 warn "count_digihops: reversed N-N in path ($call)\n";
601             }
602 0         0 next;
603             }
604 0         0 $hopcount += $difference;
605              
606             } elsif ($call =~ /^TRACE([1-7])-([0-7])$/o) {
607             # skip traceN-N because the hops are already individually shown
608             # before this
609 0         0 next;
610              
611             } else {
612             # just a normal packet. if "digistar" is there,
613             # increment the digicounter by one
614 0 0       0 if ($wasdigied == 1) {
615 0         0 $hopcount++;
616             }
617             }
618             }
619              
620 0         0 return $hopcount;
621             }
622              
623              
624             # Return a unix timestamp based on an
625             # APRS six (+ one char for type) character timestamp.
626             # If an invalid timestamp is given, return 0.
627             sub _parse_timestamp($$) {
628 15     15   35 my($options, $stamp) = @_;
629            
630             # Check initial format
631 15 50       64 return 0 if ($stamp !~ /^(\d{2})(\d{2})(\d{2})(z|h|\/)$/o);
632            
633 15 100       47 return "$1$2$3" if ($options->{'raw_timestamp'});
634            
635 12         24 my $stamptype = $4;
636            
637 12 100 66     57 if ($stamptype eq 'h') {
    50          
638             # HMS format
639 3         6 my $hour = $1;
640 3         7 my $minute = $2;
641 3         15 my $second = $3;
642            
643             # Check for invalid time
644 3 50 33     46 if ($hour > 23 || $minute > 59 || $second > 59) {
      33        
645 0         0 return 0;
646             }
647            
648             # All calculations here are in UTC, but
649             # if this is run under old MacOS (pre-OSX), then
650             # Date_to_Time could be in local time..
651 3         15 my $currenttime = time();
652 3         40 my ($cyear, $cmonth, $cday) = Today(1);
653 3         21 my $tstamp = Date_to_Time($cyear, $cmonth, $cday, $hour, $minute, $second);
654            
655             # If the time is more than about one hour
656             # into the future, roll the timestamp
657             # one day backwards.
658 3 100       18 if ($currenttime + 3900 < $tstamp) {
    50          
659 1         2 $tstamp -= 86400;
660             # If the time is more than about 23 hours
661             # into the past, roll the timestamp one
662             # day forwards.
663             } elsif ($currenttime - 82500 > $tstamp) {
664 0         0 $tstamp += 86400;
665             }
666 3         8 return $tstamp;
667              
668             } elsif ($stamptype eq 'z' ||
669             $stamptype eq '/') {
670             # Timestamp is DHM, UTC (z) or local (/).
671             # Always intepret local to mean local
672             # to this computer.
673 9         58 my $day = $1;
674 9         12 my $hour = $2;
675 9         11 my $minute = $3;
676            
677 9 50 33     99 if ($day < 1 || $day > 31 || $hour > 23 || $minute > 59) {
      33        
      33        
678 0         0 return 0;
679             }
680            
681             # If time is under about 12 hours into
682             # the future, go there.
683             # Otherwise get the first matching
684             # time in the past.
685 9         26 my $currenttime = time();
686 9         10 my ($cyear, $cmonth, $cday);
687 9 100       26 if ($stamptype eq 'z') {
688 8         87 ($cyear, $cmonth, $cday) = Today(1);
689             } else {
690 1         26 ($cyear, $cmonth, $cday) = Today(0);
691             }
692             # Form the possible timestamps in
693             # this, the next and the previous month
694 9         86 my ($fwdyear, $fwdmonth) = (Add_Delta_YM($cyear, $cmonth, $cday, 0, 1))[0,1];
695 9         27 my ($backyear, $backmonth) = (Add_Delta_YM($cyear, $cmonth, $cday, 0, -1))[0,1];
696 9         10 my $fwdtstamp = undef;
697 9         11 my $currtstamp = undef;
698 9         10 my $backtstamp = undef;
699 9 50       40 if (check_date($cyear, $cmonth, $day)) {
700 9 100       21 if ($stamptype eq 'z') {
701 8         62 $currtstamp = Date_to_Time($cyear, $cmonth, $day, $hour, $minute, 0);
702             } else {
703 1         18 $currtstamp = Mktime($cyear, $cmonth, $day, $hour, $minute, 0);
704             }
705             }
706 9 50       29 if (check_date($fwdyear, $fwdmonth, $day)) {
707 9 100       25 if ($stamptype eq 'z') {
708 8         21 $fwdtstamp = Date_to_Time($fwdyear, $fwdmonth, $day, $hour, $minute, 0);
709             } else {
710 1         16 $fwdtstamp = Mktime($cyear, $cmonth, $day, $hour, $minute, 0);
711             }
712             }
713 9 50       27 if (check_date($backyear, $backmonth, $day)) {
714 9 100       17 if ($stamptype eq 'z') {
715 8         16 $backtstamp = Date_to_Time($backyear, $backmonth, $day, $hour, $minute, 0);
716             } else {
717 1         20 $backtstamp = Mktime($cyear, $cmonth, $day, $hour, $minute, 0);
718             }
719             }
720             # Select the timestamp to use. Pick the timestamp
721             # that is largest, but under about 12 hours from
722             # current time.
723 9 100 66     108 if (defined($fwdtstamp) && ($fwdtstamp - $currenttime) < 43400) {
    50 33        
    0          
724 1         4 return $fwdtstamp;
725             } elsif (defined($currtstamp) && ($currtstamp - $currenttime) < 43400) {
726 8         23 return $currtstamp;
727             } elsif (defined($backtstamp)) {
728 0         0 return $backtstamp;
729             }
730             }
731              
732             # return failure if we haven't returned with
733             # a success earlier
734 0         0 return 0;
735             }
736              
737             # clean up a comment string - remove control codes
738             # but stay UTF-8 clean
739             sub _cleanup_comment($)
740             {
741 35     35   58 $_[0] =~ tr/[\x20-\x7e\x80-\xfe]//cd;
742 35         66 $_[0] =~ s/^\s+//;
743 35         83 $_[0] =~ s/\s+$//;
744            
745 35         55 return $_[0];
746             }
747              
748             # Return position resolution in meters based on the number
749             # of minute decimal digits. Also accepts negative numbers,
750             # i.e. -1 for 10 minute resolution and -2 for 1 degree resolution.
751             # Calculation is based on latitude so it is worst case
752             # (resolution in longitude gets better as you get closer to the poles).
753             sub _get_posresolution($)
754             {
755 42 100   42   246 return $knot_to_kmh * ($_[0] <= -2 ? 600 : 1000) * 10 ** (-1 * $_[0]);
756             }
757              
758              
759             # return an NMEA latitude or longitude.
760             # 1st parameter is the (dd)dmm.m(mmm..) string and
761             # 2nd is the north/south or east/west indicator
762             # returns undef on error. The returned value
763             # is decimal degrees, north and east positive.
764             sub _nmea_getlatlon($$$)
765             {
766 2     2   3 my ($value, $sign, $rh) = @_;
767            
768             # upcase the sign for compatibility
769 2         2 $sign = uc($sign);
770              
771             # Be leninent on what to accept, anything
772             # goes as long as degrees has 1-3 digits,
773             # minutes has 2 digits and there is at least
774             # one decimal minute.
775 2 50       8 if ($value =~ /^\s*(\d{1,3})([0-5][0-9])\.(\d+)\s*$/o) {
776 2         4 my $minutes = $2 . '.' . $3;
777 2         6 $value = $1 + ($minutes / 60);
778             # capture position resolution in meters based
779             # on the amount of minute decimals present
780 2         6 $rh->{'posresolution'} = _get_posresolution(length($3));
781             } else {
782 0         0 _a_err($rh, 'nmea_inv_cval', $value);
783 0         0 return undef;
784             }
785              
786 2 100       7 if ($sign =~ /^\s*[EW]\s*$/o) {
    50          
787             # make sure the value is ok
788 1 50       3 if ($value > 179.999999) {
789 0         0 _a_err($rh, 'nmea_large_ew', $value);
790 0         0 return undef;
791             }
792             # west negative
793 1 50       5 if ($sign =~ /^\s*W\s*$/o) {
794 1         1 $value *= -1;
795             }
796             } elsif ($sign =~ /^\s*[NS]\s*$/o) {
797             # make sure the value is ok
798 1 50       3 if ($value > 89.999999) {
799 0         0 _a_err($rh, 'nmea_large_ns', $value);
800 0         0 return undef;
801             }
802             # south negative
803 1 50       3 if ($sign =~ /^\s*S\s*$/o) {
804 0         0 $value *= -1;
805             }
806             } else {
807             # incorrect sign
808 0         0 _a_err($rh, 'nmea_inv_sign', $sign);
809 0         0 return undef;
810             }
811              
812             # all ok
813 2         3 return $value;
814             }
815              
816              
817             # return a two element array, first containing
818             # the symbol table id (or overlay) and second
819             # containing symbol id. return undef in error
820             sub _get_symbol_fromdst($) {
821 1     1   1 my $dstcallsign = shift @_;
822              
823 1         2 my $table = undef;
824 1         1 my $code = undef;
825              
826 1 50       2 if ($dstcallsign =~ /^(GPS|SPC)([A-Z0-9]{2,3})/o) {
827 0         0 my $leftoverstring = $2;
828 0         0 my $type = substr($leftoverstring, 0, 1);
829 0         0 my $sublength = length($leftoverstring);
830 0 0       0 if ($sublength == 3) {
831 0 0 0     0 if ($type eq 'C' || $type eq 'E') {
832 0         0 my $numberid = substr($leftoverstring, 1, 2);
833 0 0 0     0 if ($numberid =~ /^(\d{2})$/o &&
      0        
834             $numberid > 0 &&
835             $numberid < 95) {
836 0         0 $code = chr($1 + 32);
837 0 0       0 if ($type eq 'C') {
838 0         0 $table = '/';
839             } else {
840 0         0 $table = "\\";
841             }
842 0         0 return ($table, $code);
843             } else {
844 0         0 return undef;
845             }
846             } else {
847             # secondary symbol table, with overlay
848             # Check first that we really are in the
849             # secondary symbol table
850 0         0 my $dsttype = substr($leftoverstring, 0, 2);
851 0         0 my $overlay = substr($leftoverstring, 2, 1);
852 0 0 0     0 if (($type eq 'O' ||
      0        
853             $type eq 'A' ||
854             $type eq 'N' ||
855             $type eq 'D' ||
856             $type eq 'S' ||
857             $type eq 'Q') && $overlay =~ /^[A-Z0-9]$/o) {
858 0 0       0 if (defined($dstsymbol{$dsttype})) {
859 0         0 $code = substr($dstsymbol{$dsttype}, 1, 1);
860 0         0 return ($overlay, $code);
861             } else {
862 0         0 return undef;
863             }
864             } else {
865 0         0 return undef;
866             }
867             }
868             } else {
869             # primary or secondary symbol table, no overlay
870 0 0       0 if (defined($dstsymbol{$leftoverstring})) {
871 0         0 $table = substr($dstsymbol{$leftoverstring}, 0, 1);
872 0         0 $code = substr($dstsymbol{$leftoverstring}, 1, 1);
873 0         0 return ($table, $code);
874             } else {
875 0         0 return undef;
876             }
877             }
878             } else {
879 1         2 return undef;
880             }
881              
882             # failsafe catch-all
883 0         0 return undef;
884             }
885              
886              
887             # Parse an NMEA location
888             sub _nmea_to_decimal($$$$$) {
889             #(substr($body, 1), $srccallsign, $dstcallsign, \%poshash)
890 1     1   3 my($options, $body, $srccallsign, $dstcallsign, $rethash) = @_;
891              
892 1 50       6 if ($debug > 1) {
893             # print packet, after stripping control chars
894 0         0 my $printbody = $body;
895 0         0 $printbody =~ tr/[\x00-\x1f]//d;
896 0         0 warn "NMEA: from $srccallsign to $dstcallsign: $printbody\n";
897             }
898              
899             # verify checksum first, if it is provided
900 1         3 $body =~ s/\s+$//; # remove possible white space from the end
901 1 50       5 if ($body =~ /^([\x20-\x7e]+)\*([0-9A-F]{2})$/io) {
902 1         2 my $checksumarea = $1;
903 1         3 my $checksumgiven = hex($2);
904 1         1 my $checksumcalculated = 0;
905 1         3 for (my $i = 0; $i < length($checksumarea); $i++) {
906 64         75 $checksumcalculated ^= ord(substr($checksumarea, $i, 1));
907             }
908 1 50       3 if ($checksumgiven != $checksumcalculated) {
909             # invalid checksum
910 0         0 _a_err($rethash, 'nmea_inv_cksum');
911 0         0 return 0;
912             }
913             # make a note of the existance of a checksum
914 1         3 $rethash->{'checksumok'} = 1;
915             }
916              
917             # checksum ok or not provided
918              
919 1         5 $rethash->{'format'} = 'nmea';
920            
921             # use a dot as a default symbol if one is not defined in
922             # the destination callsign
923 1         3 my ($symtable, $symcode) = _get_symbol_fromdst($dstcallsign);
924 1 50 33     5 if (not(defined($symtable)) || not(defined($symcode))) {
925 1         2 $rethash->{'symboltable'} = '/';
926 1         1 $rethash->{'symbolcode'} = '/';
927             } else {
928 0         0 $rethash->{'symboltable'} = $symtable;
929 0         0 $rethash->{'symbolcode'} = $symcode;
930             }
931              
932             # Split to NMEA fields
933 1         5 $body =~ s/\*[0-9A-F]{2}$//; # remove checksum from body first
934 1         9 my @nmeafields = split(/,/, $body);
935              
936             # Now check the sentence type and get as much info
937             # as we can (want).
938 1 50       3 if ($nmeafields[0] eq 'GPRMC') {
    0          
    0          
939             # we want at least 10 fields
940 1 50       3 if (@nmeafields < 10) {
941 0         0 _a_err($rethash, 'gprmc_fewfields', scalar(@nmeafields));
942 0         0 return 0;
943             }
944              
945 1 50       4 if ($nmeafields[2] ne 'A') {
946             # invalid position
947 0         0 _a_err($rethash, 'gprmc_nofix');
948 0         0 return 0;
949             }
950              
951             # check and save the timestamp
952 1         2 my ($hour, $minute, $second);
953 1 50       5 if ($nmeafields[1] =~ /^\s*(\d{2})(\d{2})(\d{2})(|\.\d+)\s*$/o) {
954             # if seconds has a decimal part, ignore it
955             # leap seconds are not taken into account...
956 1 50 33     10 if ($1 > 23 || $2 > 59 || $3 > 59) {
      33        
957 0         0 _a_err($rethash, 'gprmc_inv_time', $nmeafields[1]);
958 0         0 return 0;
959             }
960 1         2 $hour = $1 + 0; # force numeric
961 1         2 $minute = $2 + 0;
962 1         1 $second = $3 + 0;
963             } else {
964 0         0 _a_err($rethash, 'gprmc_inv_time');
965 0         0 return 0;
966             }
967 1         4 my ($year, $month, $day);
968 1 50       3 if ($nmeafields[9] =~ /^\s*(\d{2})(\d{2})(\d{2})\s*$/o) {
969             # check the date for validity. Assume
970             # years 0-69 are 21st century and years
971             # 70-99 are 20th century
972 1         5 $year = 2000 + $3;
973 1 50       4 if ($3 >= 70) {
974 0         0 $year = 1900 + $3;
975             }
976             # check for invalid date
977 1 50       13 if (not(check_date($year, $2, $1))) {
978 0         0 _a_err($rethash, 'gprmc_inv_date', "$year $2 $1");
979 0         0 return 0;
980             }
981 1         1 $month = $2 + 0; # force numeric
982 1         3 $day = $1 + 0;
983             } else {
984 0         0 _a_err($rethash, 'gprmc_inv_date');
985 0         0 return 0;
986             }
987             # Date_to_Time() can only handle 32-bit unix timestamps,
988             # so make sure it is not used for those years that
989             # are outside that range.
990 1 50 33     4 if ($year >= 2038 || $year < 1970) {
991 0         0 $rethash->{'timestamp'} = 0;
992 0         0 _a_err($rethash, 'gprmc_date_out', $year);
993 0         0 return 0;
994             } else {
995 1         8 $rethash->{'timestamp'} = Date_to_Time($year, $month, $day, $hour, $minute, $second);
996             }
997              
998             # speed (knots) and course, make these optional
999             # in the parsing sense (don't fail if speed/course
1000             # can't be decoded).
1001 1 50       6 if ($nmeafields[7] =~ /^\s*(\d+(|\.\d+))\s*$/o) {
1002             # convert to km/h
1003 1         6 $rethash->{'speed'} = $1 * $knot_to_kmh;
1004             }
1005 1 50       6 if ($nmeafields[8] =~ /^\s*(\d+(|\.\d+))\s*$/o) {
1006             # round to nearest integer
1007 1         4 my $course = int($1 + 0.5);
1008             # if zero, set to 360 because in APRS
1009             # zero means invalid course...
1010 1 50       6 if ($course == 0) {
    50          
1011 0         0 $course = 360;
1012             } elsif ($course > 360) {
1013 0         0 $course = 0; # invalid
1014             }
1015 1         2 $rethash->{'course'} = $course;
1016             } else {
1017 0         0 $rethash->{'course'} = 0; # unknown
1018             }
1019              
1020             # latitude and longitude
1021 1         4 my $latitude = _nmea_getlatlon($nmeafields[3], $nmeafields[4], $rethash);
1022 1 50       2 if (not(defined($latitude))) {
1023 0         0 return 0;
1024             }
1025 1         3 $rethash->{'latitude'} = $latitude;
1026 1         3 my $longitude = _nmea_getlatlon($nmeafields[5], $nmeafields[6], $rethash);
1027 1 50       6 if (not(defined($longitude))) {
1028 0         0 return 0;
1029             }
1030 1         2 $rethash->{'longitude'} = $longitude;
1031              
1032             # we have everything we want, return
1033 1         7 return 1;
1034              
1035             } elsif ($nmeafields[0] eq 'GPGGA') {
1036             # we want at least 11 fields
1037 0 0       0 if (@nmeafields < 11) {
1038 0         0 _a_err($rethash, 'gpgga_fewfields', scalar(@nmeafields));
1039 0         0 return 0;
1040             }
1041              
1042             # check for position validity
1043 0 0       0 if ($nmeafields[6] =~ /^\s*(\d+)\s*$/o) {
1044 0 0       0 if ($1 < 1) {
1045 0         0 _a_err($rethash, 'gpgga_nofix', $1);
1046 0         0 return 0;
1047             }
1048             } else {
1049 0         0 _a_err($rethash, 'gpgga_nofix');
1050 0         0 return 0;
1051             }
1052              
1053             # Use the APRS time parsing routines to check
1054             # the time and convert it to timestamp.
1055             # But before that, remove a possible decimal part
1056 0         0 $nmeafields[1] =~ s/\.\d+$//;
1057 0         0 $rethash->{'timestamp'} = _parse_timestamp($options, $nmeafields[1] . 'h');
1058 0 0       0 if ($rethash->{'timestamp'} == 0) {
1059 0         0 _a_err($rethash, 'timestamp_inv_gpgga');
1060 0         0 return 0;
1061             }
1062              
1063             # latitude and longitude
1064 0         0 my $latitude = _nmea_getlatlon($nmeafields[2], $nmeafields[3], $rethash);
1065 0 0       0 if (not(defined($latitude))) {
1066 0         0 return 0;
1067             }
1068 0         0 $rethash->{'latitude'} = $latitude;
1069 0         0 my $longitude = _nmea_getlatlon($nmeafields[4], $nmeafields[5], $rethash);
1070 0 0       0 if (not(defined($longitude))) {
1071 0         0 return 0;
1072             }
1073 0         0 $rethash->{'longitude'} = $longitude;
1074              
1075             # altitude, only meters are accepted
1076 0 0 0     0 if ($nmeafields[10] eq 'M' &&
1077             $nmeafields[9] =~ /^(-?\d+(|\.\d+))$/o) {
1078             # force numeric interpretation
1079 0         0 $rethash->{'altitude'} = $1 + 0;
1080             }
1081              
1082             # ok
1083 0         0 return 1;
1084              
1085             } elsif ($nmeafields[0] eq 'GPGLL') {
1086             # we want at least 5 fields
1087 0 0       0 if (@nmeafields < 5) {
1088 0         0 _a_err($rethash, 'gpgll_fewfields', scalar(@nmeafields));
1089 0         0 return 0;
1090             }
1091              
1092             # latitude and longitude
1093 0         0 my $latitude = _nmea_getlatlon($nmeafields[1], $nmeafields[2], $rethash);
1094 0 0       0 if (not(defined($latitude))) {
1095 0         0 return 0;
1096             }
1097 0         0 $rethash->{'latitude'} = $latitude;
1098 0         0 my $longitude = _nmea_getlatlon($nmeafields[3], $nmeafields[4], $rethash);
1099 0 0       0 if (not(defined($longitude))) {
1100 0         0 return 0;
1101             }
1102 0         0 $rethash->{'longitude'} = $longitude;
1103              
1104             # Use the APRS time parsing routines to check
1105             # the time and convert it to timestamp.
1106             # But before that, remove a possible decimal part
1107 0 0       0 if (@nmeafields >= 6) {
1108 0         0 $nmeafields[5] =~ s/\.\d+$//;
1109 0         0 $rethash->{'timestamp'} = _parse_timestamp($options, $nmeafields[5] . 'h');
1110 0 0       0 if ($rethash->{'timestamp'} == 0) {
1111 0         0 _a_err($rethash, 'timestamp_inv_gpgll');
1112 0         0 return 0;
1113             }
1114             }
1115              
1116 0 0       0 if (@nmeafields >= 7) {
1117             # GPS fix validity supplied
1118 0 0       0 if ($nmeafields[6] ne 'A') {
1119 0         0 _a_err($rethash, 'gpgll_nofix');
1120 0         0 return 0;
1121             }
1122             }
1123              
1124             # ok
1125 0         0 return 1;
1126              
1127             ##} elsif ($nmeafields[0] eq 'GPVTG') {
1128             ##} elsif ($nmeafields[0] eq 'GPWPT') {
1129             } else {
1130 0         0 $nmeafields[0] =~ tr/[\x00-\x1f]//d;
1131 0         0 _a_err($rethash, 'nmea_unsupp', $nmeafields[0]);
1132 0         0 return 0;
1133             }
1134              
1135 0         0 return 0;
1136             }
1137              
1138              
1139             # Parse the possible APRS data extension
1140             # as well as comment
1141             sub _comments_to_decimal($$$) {
1142 22     22   48 my $rest = shift @_;
1143 22         25 my $srccallsign = shift @_;
1144 22         27 my $rethash = shift @_;
1145            
1146             # First check the possible APRS data extension,
1147             # immediately following the packet
1148 22 50       56 if (length($rest) >= 7) {
1149 22 100       115 if ($rest =~ /^([0-9. ]{3})\/([0-9. ]{3})/o) {
    100          
    100          
    50          
1150 10         17 my $course = $1;
1151 10         14 my $speed = $2;
1152 10 50 33     71 if ($course =~ /^\d{3}$/o &&
      33        
1153             $course <= 360 &&
1154             $course >= 1) {
1155             # force numeric interpretation
1156 10         11 $course += 0;
1157 10         22 $rethash->{'course'} = $course;
1158             } else {
1159             # course is invalid, set it to zero
1160 0         0 $rethash->{'course'} = 0;
1161             }
1162 10 50       27 if ($speed =~ /^\d{3}$/o) {
1163             # force numeric interpretation
1164             # and convert to km/h
1165 10         23 $rethash->{'speed'} = $speed * $knot_to_kmh;
1166             } else {
1167             # If speed is invalid, don't set it
1168             # (zero speed is a valid speed).
1169             }
1170 10         19 $rest = substr($rest, 7);
1171              
1172             } elsif ($rest =~ /^PHG(\d[\x30-\x7e]\d\d[0-9A-Z])\//o) {
1173             # PHGR
1174 1         4 $rethash->{'phg'} = $1;
1175 1         2 $rest = substr($rest, 8);
1176              
1177             } elsif ($rest =~ /^PHG(\d[\x30-\x7e]\d\d)/o) {
1178             # don't do anything fancy with PHG, just store it
1179 6         15 $rethash->{'phg'} = $1;
1180 6         11 $rest = substr($rest, 7);
1181              
1182             } elsif ($rest =~ /^RNG(\d{4})/o) {
1183             # radio range, in miles, so convert
1184             # to km
1185 0         0 $rethash->{'radiorange'} = $1 * $mph_to_kmh;
1186 0         0 $rest = substr($rest, 7);
1187             }
1188             }
1189              
1190             # Check for optional altitude anywhere in the comment,
1191             # take the first occurrence
1192 22 100       70 if ($rest =~ /^(.*?)\/A=(-\d{5}|\d{6})(.*)$/o) {
1193             # convert to meters as well
1194 10         28 $rethash->{'altitude'} = $2 * $feet_to_meters;
1195 10         20 $rest = $1 . $3;
1196             }
1197              
1198             # Check for new-style base-91 comment telemetry
1199 22         58 $rest = _comment_telemetry($rethash, $rest);
1200            
1201             # Check for !DAO!, take the last occurrence (per recommendation)
1202 22 100       64 if ($rest =~ /^(.*)\!([\x21-\x7b][\x20-\x7b]{2})\!(.*?)$/o) {
1203 6         19 my $daofound = _dao_parse($2, $srccallsign, $rethash);
1204 6 50       14 if ($daofound == 1) {
1205 6         9 $rest = $1 . $3;
1206             }
1207             }
1208            
1209             # Strip a / or a ' ' from the beginning of a comment
1210             # (delimiter after PHG or other data stuffed within the comment)
1211 22         54 $rest =~ s/^[\/\s]//;
1212            
1213             # Save the rest as a separate comment, if
1214             # anything is left (trim unprintable chars
1215             # out first and white space from both ends)
1216 22 100       55 if (length($rest) > 0) {
1217 21         49 $rethash->{'comment'} = _cleanup_comment($rest);
1218             }
1219              
1220             # Always succeed as these are optional
1221 22         33 return 1;
1222             }
1223              
1224             # Parse an object
1225             sub _object_to_decimal($$$$) {
1226 2     2   4 my($options, $packet, $srccallsign, $rethash) = @_;
1227              
1228             # Minimum length for an object is 31 characters
1229             # (or 46 characters for non-compressed)
1230 2 50       6 if (length($packet) < 31) {
1231 0         0 _a_err($rethash, 'obj_short');
1232 0         0 return 0;
1233             }
1234              
1235             # Parse the object up to the location
1236 2         4 my $timestamp = undef;
1237 2 100       9 if ($packet =~ /^;([\x20-\x7e]{9})(\*|_)(\d{6})(z|h|\/)/o) {
1238             # hash member 'objectname' signals an object
1239 1         3 $rethash->{'objectname'} = $1;
1240 1 50       2 if ($2 eq '*') {
1241 1         3 $rethash->{'alive'} = 1;
1242             } else {
1243 0         0 $rethash->{'alive'} = 0;
1244             }
1245 1         3 $timestamp = $3 . $4;
1246             } else {
1247 1         4 _a_err($rethash, 'obj_inv');
1248 1         6 return 0;
1249             }
1250              
1251             # Check the timestamp for validity and convert
1252             # to UNIX epoch. If the timestamp is invalid, set it
1253             # to zero.
1254 1         8 $rethash->{'timestamp'} = _parse_timestamp($options, $timestamp);
1255 1 50       3 if ($rethash->{'timestamp'} == 0) {
1256 0         0 _a_warn($rethash, 'timestamp_inv_obj');
1257             }
1258              
1259             # Forward the location parsing onwards
1260 1         1 my $locationoffset = 18; # object location always starts here
1261 1         2 my $locationchar = substr($packet, $locationoffset, 1);
1262 1         1 my $retval = undef;
1263 1 50       8 if ($locationchar =~ /^[\/\\A-Za-j]$/o) {
    0          
1264             # compressed
1265 1         5 $retval = _compressed_to_decimal(substr($packet, $locationoffset, 13), $srccallsign, $rethash);
1266 1         2 $locationoffset += 13; # now points to APRS data extension/comment
1267             } elsif ($locationchar =~ /^\d$/io) {
1268             # normal
1269 0         0 $retval = _normalpos_to_decimal(substr($packet, $locationoffset), $srccallsign, $rethash);
1270 0         0 $locationoffset += 19; # now points to APRS data extension/comment
1271             } else {
1272             # error
1273 0         0 _a_err($rethash, 'obj_dec_err');
1274 0         0 return 0;
1275             }
1276 1 50       6 return 0 if ($retval != 1);
1277              
1278             # Check the APRS data extension and possible comments,
1279             # unless it is a weather report (we don't want erroneus
1280             # course/speed figures and weather in the comments..)
1281 1 50       3 if ($rethash->{'symbolcode'} ne '_') {
1282 1         4 _comments_to_decimal(substr($packet, $locationoffset), $srccallsign, $rethash);
1283             } else {
1284             # possibly a weather object, try to parse
1285 0         0 _wx_parse(substr($packet, $locationoffset), $rethash);
1286             }
1287              
1288 1         5 return 1;
1289             }
1290              
1291             # Parse a status report. Only timestamps
1292             # and text report are supported. Maidenhead,
1293             # beam headings and symbols are not.
1294             sub _status_parse($$$$) {
1295 1     1   3 my($options, $packet, $srccallsign, $rethash) = @_;
1296              
1297             # Remove CRs, LFs and trailing spaces
1298 1         3 $packet =~ tr/\r\n//d;
1299 1         3 $packet =~ s/\s+$//;
1300              
1301             # Check for a timestamp
1302 1 50       4 if ($packet =~ /^(\d{6}z)/o) {
1303 1         4 $rethash->{'timestamp'} = _parse_timestamp({}, $1);
1304 1 50       4 _a_warn($rethash, 'timestamp_inv_sta') if ($rethash->{'timestamp'} == 0);
1305 1         3 $packet = substr($packet, 7);
1306             }
1307              
1308             # Save the rest as the report
1309 1         1 $rethash->{'status'} = $packet;
1310              
1311 1         5 return 1;
1312             }
1313              
1314             # Parse a station capabilities packet
1315             sub _capabilities_parse($$$) {
1316 0     0   0 my $packet = shift @_;
1317 0         0 my $srccallsign = shift @_;
1318 0         0 my $rethash = shift @_;
1319              
1320             # Remove CRs, LFs and trailing spaces
1321 0         0 $packet =~ tr/\r\n//d;
1322 0         0 $packet =~ s/\s+$//;
1323             # Then just split the packet, we aren't too picky about the format here.
1324             # Also duplicates and case changes are not handled in any way,
1325             # so the last part will override an earlier part and different
1326             # cases can be present. Just remove trailing/leading spaces.
1327 0         0 my @caps = split(/,/, $packet);
1328 0         0 my %caphash = ();
1329 0         0 foreach my $cap (@caps) {
1330 0 0       0 if ($cap =~ /^\s*([^=]+?)\s*=\s*(.*?)\s*$/o) {
    0          
1331             # TOKEN=VALUE
1332 0         0 $caphash{$1} = $2;
1333             } elsif ($cap =~ /^\s*([^=]+?)\s*$/o) {
1334             # just TOKEN
1335 0         0 $caphash{$1} = undef;
1336             }
1337             }
1338              
1339 0         0 my $keycount = keys(%caphash);
1340 0 0       0 if ($keycount > 0) {
1341             # store the capabilities in the return hash
1342 0         0 $rethash->{'capabilities'} = \%caphash;
1343 0         0 return 1;
1344             }
1345            
1346             # at least one capability has to be defined for a capability
1347             # packet to be counted as valid
1348 0         0 return 0;
1349             }
1350              
1351             # Parse a message
1352             # possible TODO: ack piggybacking
1353             sub _message_parse($$$) {
1354 245     245   239 my $packet = shift @_;
1355 245         199 my $srccallsign = shift @_;
1356 245         143 my $rethash = shift @_;
1357              
1358             # Check format
1359 245 50       557 if ($packet =~ /^:([A-Za-z0-9_ -]{9}):([\x20-\x7e\x80-\xfe]+)$/o) {
1360 245         257 my $destination = $1;
1361 245         181 my $message = $2;
1362             # remove trailing spaces from the recipient
1363 245         514 $destination =~ s/\s+$//;
1364 245         270 $rethash->{'destination'} = $destination;
1365             # check whether this is an ack
1366 245 100       345 if ($message =~ /^ack([A-Za-z0-9}]{1,5})\s*$/o) {
1367             # trailing spaces are allowed because some
1368             # broken software insert them..
1369 6         8 $rethash->{'messageack'} = $1;
1370 6         15 return 1;
1371             }
1372             # check whether this is a message reject
1373 239 100       275 if ($message =~ /^rej([A-Za-z0-9}]{1,5})\s*$/o) {
1374 6         7 $rethash->{'messagerej'} = $1;
1375 6         16 return 1;
1376             }
1377             # separate message-id from the body, if present
1378 233 50       461 if ($message =~ /^([^{]*)\{([A-Za-z0-9}]{1,5})\s*$/o) {
1379 233         261 $rethash->{'message'} = $1;
1380 233         247 $rethash->{'messageid'} = $2;
1381             } else {
1382 0         0 $rethash->{'message'} = $message;
1383             }
1384             # catch telemetry messages
1385 233 50       299 if ($message =~ /^(BITS|PARM|UNIT|EQNS)\./i) {
1386 0         0 $rethash->{'type'} = 'telemetry-message';
1387             }
1388 233         601 return 1;
1389             }
1390            
1391 0         0 _a_err($rethash, 'msg_inv');
1392            
1393 0         0 return 0;
1394             }
1395              
1396             #
1397             sub _comment_telemetry($$)
1398             {
1399 32     32   41 my($rethash, $rest) = @_;
1400            
1401 32 100       91 if ($rest =~ /^(.*)\|([!-{]{2})([!-{]{2})([!-{]{2}|)([!-{]{2}|)([!-{]{2}|)([!-{]{2}|)([!-{]{2}|)\|(.*)$/) {
1402 6         15 $rest = $1 . $9;
1403 6 100       107 $rethash->{'telemetry'} = {
    100          
    100          
    100          
1404             'seq' => (ord(substr($2, 0, 1)) - 33) * 91 +
1405             (ord(substr($2, 1, 1)) - 33),
1406             'vals' => [
1407             (ord(substr($3, 0, 1)) - 33) * 91 +
1408             (ord(substr($3, 1, 1)) - 33),
1409             $4 ne '' ? (ord(substr($4, 0, 1)) - 33) * 91 +
1410             (ord(substr($4, 1, 1)) - 33) : undef,
1411             $5 ne '' ? (ord(substr($5, 0, 1)) - 33) * 91 +
1412             (ord(substr($5, 1, 1)) - 33) : undef,
1413             $6 ne '' ? (ord(substr($6, 0, 1)) - 33) * 91 +
1414             (ord(substr($6, 1, 1)) - 33) : undef,
1415             $7 ne '' ? (ord(substr($7, 0, 1)) - 33) * 91 +
1416             (ord(substr($7, 1, 1)) - 33) : undef,
1417             ]
1418             };
1419 6 100       19 if ($8 ne '') {
1420             # bits: first, decode the base-91 integer
1421 4         13 my $bitint = (ord(substr($8, 0, 1)) - 33) * 91 +
1422             (ord(substr($8, 1, 1)) - 33);
1423             # then, decode the 8 bits of telemetry
1424 4         31 $rethash->{'telemetry'}->{'bits'} = unpack('b8', pack('C', $bitint));
1425             }
1426             }
1427            
1428 32         49 return $rest;
1429             }
1430              
1431             # Parse an item
1432             sub _item_to_decimal($$$) {
1433 0     0   0 my $packet = shift @_;
1434 0         0 my $srccallsign = shift @_;
1435 0         0 my $rethash = shift @_;
1436              
1437             # Minimum length for an item is 18 characters
1438             # (or 24 characters for non-compressed)
1439 0 0       0 if (length($packet) < 18) {
1440 0         0 _a_err($rethash, 'item_short');
1441 0         0 return 0;
1442             }
1443              
1444             # Parse the item up to the location
1445 0 0       0 if ($packet =~ /^\)([\x20\x22-\x5e\x60-\x7e]{3,9})(!|_)/o) {
1446             # hash member 'itemname' signals an item
1447 0         0 $rethash->{'itemname'} = $1;
1448 0 0       0 if ($2 eq '!') {
1449 0         0 $rethash->{'alive'} = 1;
1450             } else {
1451 0         0 $rethash->{'alive'} = 0;
1452             }
1453             } else {
1454 0         0 _a_err($rethash, 'item_inv');
1455 0         0 return 0;
1456             }
1457              
1458             # Forward the location parsing onwards
1459 0         0 my $locationoffset = 2 + length($rethash->{'itemname'});
1460 0         0 my $locationchar = substr($packet, $locationoffset, 1);
1461 0         0 my $retval = undef;
1462 0 0       0 if ($locationchar =~ /^[\/\\A-Za-j]$/o) {
    0          
1463             # compressed
1464 0         0 $retval = _compressed_to_decimal(substr($packet, $locationoffset, 13), $srccallsign, $rethash);
1465 0         0 $locationoffset += 13;
1466             } elsif ($locationchar =~ /^\d$/io) {
1467             # normal
1468 0         0 $retval = _normalpos_to_decimal(substr($packet, $locationoffset), $srccallsign, $rethash);
1469 0         0 $locationoffset += 19;
1470             } else {
1471             # error
1472 0         0 _a_err($rethash, 'item_dec_err');
1473 0         0 return 0;
1474             }
1475 0 0       0 return 0 if ($retval != 1);
1476              
1477             # Check the APRS data extension and possible comments,
1478             # unless it is a weather report (we don't want erroneus
1479             # course/speed figures and weather in the comments..)
1480 0 0       0 if ($rethash->{'symbolcode'} ne '_') {
1481 0         0 _comments_to_decimal(substr($packet, $locationoffset), $srccallsign, $rethash);
1482             }
1483              
1484 0         0 return 1;
1485             }
1486              
1487             # Parse a normal uncompressed location
1488             sub _normalpos_to_decimal($$$) {
1489 24     24   28 my $packet = shift @_;
1490 24         36 my $srccallsign = shift @_;
1491 24         26 my $rethash = shift @_;
1492              
1493             # Check the length
1494 24 50       47 if (length($packet) < 19) {
1495 0         0 _a_err($rethash, 'loc_short');
1496 0         0 return 0;
1497             }
1498            
1499 24         41 $rethash->{'format'} = 'uncompressed';
1500            
1501             # Make a more detailed check on the format, but do the
1502             # actual value checks later
1503 24         29 my $lon_deg = undef;
1504 24         22 my $lat_deg = undef;
1505 24         33 my $lon_min = undef;
1506 24         18 my $lat_min = undef;
1507 24         18 my $issouth = 0;
1508 24         19 my $iswest = 0;
1509 24         18 my $symboltable;
1510 24 100       101 if ($packet =~ /^(\d{2})([0-7 ][0-9 ]\.[0-9 ]{2})([NnSs])(.)(\d{3})([0-7 ][0-9 ]\.[0-9 ]{2})([EeWw])([\x21-\x7b\x7d])/o) {
1511 23         48 my $sind = uc($3);
1512 23         41 my $wind = uc($7);
1513 23         36 $symboltable = $4;
1514 23         37 $rethash->{'symbolcode'} = $8;
1515 23 100       57 if ($sind eq 'S') {
1516 7         6 $issouth = 1;
1517             }
1518 23 100       41 if ($wind eq 'W') {
1519 12         14 $iswest = 1;
1520             }
1521 23         26 $lat_deg = $1;
1522 23         24 $lat_min = $2;
1523 23         26 $lon_deg = $5;
1524 23         31 $lon_min = $6;
1525             } else {
1526 1         4 _a_err($rethash, 'loc_inv');
1527 1         2 return 0;
1528             }
1529            
1530 23 100       56 if ($symboltable !~ /^[\/\\A-Z0-9]$/) {
1531 1         2 _a_err($rethash, 'sym_inv_table');
1532 1         2 return 0;
1533             }
1534 22         30 $rethash->{'symboltable'} = $symboltable;
1535            
1536             # Check the degree values
1537 22 50 33     95 if ($lat_deg > 89 || $lon_deg > 179) {
1538 0         0 _a_err($rethash, 'loc_large');
1539 0         0 return 0;
1540             }
1541              
1542             # Find out the amount of position ambiguity
1543 22         30 my $tmplat = $lat_min;
1544 22         70 $tmplat =~ s/\.//; # remove the period
1545             # Count the amount of spaces at the end
1546 22 50       72 if ($tmplat =~ /^(\d{0,4})( {0,4})$/io) {
1547 22         39 $rethash->{'posambiguity'} = length($2);
1548             } else {
1549 0         0 _a_err($rethash, 'loc_amb_inv');
1550 0         0 return 0;
1551             }
1552              
1553 22         23 my $latitude = undef;
1554 22         16 my $longitude = undef;
1555 22 100       47 if ($rethash->{'posambiguity'} == 0) {
    100          
    50          
    50          
    50          
1556             # No position ambiguity. Check longitude for invalid spaces
1557 20 50       47 if ($lon_min =~ / /io) {
1558 0         0 _a_err($rethash, 'loc_amb_inv', 'longitude 0');
1559 0         0 return 0;
1560             }
1561 20         58 $latitude = $lat_deg + ($lat_min/60);
1562 20         26 $longitude = $lon_deg + ($lon_min/60);
1563             } elsif ($rethash->{'posambiguity'} == 4) {
1564             # disregard the minutes and add 0.5 to the degree values
1565 1         2 $latitude = $lat_deg + 0.5;
1566 1         2 $longitude = $lon_deg + 0.5;
1567             } elsif ($rethash->{'posambiguity'} == 1) {
1568             # the last digit is not used
1569 0         0 $lat_min = substr($lat_min, 0, 4);
1570 0         0 $lon_min = substr($lon_min, 0, 4);
1571 0 0 0     0 if ($lat_min =~ / /io || $lon_min =~ / /io) {
1572 0         0 _a_err($rethash, 'loc_amb_inv', 'lat/lon 1');
1573 0         0 return 0;
1574             }
1575 0         0 $latitude = $lat_deg + (($lat_min + 0.05)/60);
1576 0         0 $longitude = $lon_deg + (($lon_min + 0.05)/60);
1577             } elsif ($rethash->{'posambiguity'} == 2) {
1578             # the minute decimals are not used
1579 0         0 $lat_min = substr($lat_min, 0, 2);
1580 0         0 $lon_min = substr($lon_min, 0, 2);
1581 0 0 0     0 if ($lat_min =~ / /io || $lon_min =~ / /io) {
1582 0         0 _a_err($rethash, 'loc_amb_inv', 'lat/lon 2');
1583 0         0 return 0;
1584             }
1585 0         0 $latitude = $lat_deg + (($lat_min + 0.5)/60);
1586 0         0 $longitude = $lon_deg + (($lon_min + 0.5)/60);
1587             } elsif ($rethash->{'posambiguity'} == 3) {
1588             # the single minutes are not used
1589 1         2 $lat_min = substr($lat_min, 0, 1) . '5';
1590 1         2 $lon_min = substr($lon_min, 0, 1) . '5';
1591 1 50 33     6 if ($lat_min =~ / /io || $lon_min =~ / /io) {
1592 0         0 _a_err($rethash, 'loc_amb_inv', 'lat/lon 3');
1593 0         0 return 0;
1594             }
1595 1         3 $latitude = $lat_deg + ($lat_min/60);
1596 1         1 $longitude = $lon_deg + ($lon_min/60);
1597             } else {
1598 0         0 _a_err($rethash, 'loc_amb_inv');
1599 0         0 return 0;
1600             }
1601              
1602             # Finally apply south/west indicators
1603 22 100       41 if ($issouth == 1) {
1604 7         7 $latitude = 0 - $latitude;
1605             }
1606 22 100       40 if ($iswest == 1) {
1607 12         14 $longitude = 0 - $longitude;
1608             }
1609             # Store the locations
1610 22         26 $rethash->{'latitude'} = $latitude;
1611 22         24 $rethash->{'longitude'} = $longitude;
1612             # Calculate position resolution based on position ambiguity
1613             # calculated above.
1614 22         52 $rethash->{'posresolution'} = _get_posresolution(2 - $rethash->{'posambiguity'});
1615              
1616             # Parse possible APRS data extension
1617             # afterwards along with comments
1618              
1619              
1620 22         39 return 1;
1621             }
1622              
1623             # convert a mic-encoder packet
1624             sub _mice_to_decimal($$$$$) {
1625 11     11   20 my ($packet, $dstcallsign, $srccallsign, $rethash, $options) = @_;
1626              
1627             # We only want the base callsign
1628 11         14 $dstcallsign =~ s/-\d+$//;
1629              
1630 11         16 $rethash->{'format'} = 'mice';
1631            
1632             # Check the format
1633 11 50 33     51 if (length($packet) < 8 || length($dstcallsign) != 6) {
1634             # too short packet to be mic-e
1635 0         0 _a_err($rethash, 'mice_short');
1636 0         0 return 0;
1637             }
1638 11 50       28 if (not($dstcallsign =~ /^[0-9A-LP-Z]{3}[0-9LP-Z]{3}$/io)) {
1639             # A-K characters are not used in the last 3 characters
1640             # and MNO are never used
1641 0         0 _a_err($rethash, 'mice_inv');
1642 0         0 return 0;
1643             }
1644            
1645             # check the information field (longitude, course, speed and
1646             # symbol table and code are checked). Not bullet proof..
1647 11         10 my $mice_fixed;
1648 11         12 my $symboltable = substr($packet, 7, 1);
1649 11 100       22 if ($packet !~ /^[\x26-\x7f][\x26-\x61][\x1c-\x7f]{2}[\x1c-\x7d][\x1c-\x7f][\x21-\x7b\x7d][\/\\A-Z0-9]/o) {
1650             # If the accept_broken_mice option is given, check for a known
1651             # corruption in the packets and try to fix it - aprsd is
1652             # replacing some valid but non-printable mic-e packet
1653             # characters with spaces, and some other software is replacing
1654             # the multiple spaces with a single space. This regexp
1655             # replaces the single space with two spaces, so that the rest
1656             # of the code can still parse the position data.
1657 2 100 66     16 if (($options->{'accept_broken_mice'})
1658             && $packet =~ s/^([\x26-\x7f][\x26-\x61][\x1c-\x7f]{2})\x20([\x21-\x7b\x7d][\/\\A-Z0-9])(.*)/$1\x20\x20$2$3/o) {
1659 1         2 $mice_fixed = 1;
1660             # Now the symbol table identifier is again in the correct spot...
1661 1         2 $symboltable = substr($packet, 7, 1);
1662 1 50       3 if ($symboltable !~ /^[\/\\A-Z0-9]$/) {
1663 0         0 _a_err($rethash, 'sym_inv_table');
1664 0         0 return 0;
1665             }
1666             } else {
1667             # Get a more precise error message for invalid symbol table
1668 1 50       4 if ($symboltable !~ /^[\/\\A-Z0-9]$/) {
1669 1         4 _a_err($rethash, 'sym_inv_table');
1670             } else {
1671 0         0 _a_err($rethash, 'mice_inv_info');
1672             }
1673 1         4 return 0;
1674             }
1675             }
1676              
1677             # First do the destination callsign
1678             # (latitude, message bits, N/S and W/E indicators and long. offset)
1679              
1680             # Translate the characters to get the latitude
1681 10         11 my $tmplat = $dstcallsign;
1682 10         15 $tmplat =~ tr/A-JP-YKLZ/0-90-9___/;
1683             # Find out the amount of position ambiguity
1684 10 50       28 if ($tmplat =~ /^(\d+)(_*)$/io) {
1685 10         14 my $amount = 6 - length($1);
1686 10 50       22 if ($amount > 4) {
1687             # only minutes and decimal minutes can
1688             # be masked out
1689 0         0 _a_err($rethash, 'mice_amb_large');
1690 0         0 return 0;
1691             }
1692 10         14 $rethash->{'posambiguity'} = $amount;
1693             # Calculate position resolution based on position ambiguity
1694             # calculated above.
1695 10         19 $rethash->{'posresolution'} = _get_posresolution(2 - $amount);
1696             } else {
1697             # no digits in the beginning, baaad..
1698             # or the ambiguity digits weren't continuous
1699 0         0 _a_err($rethash, 'mice_amb_inv');
1700 0         0 return 0;
1701             }
1702              
1703             # convert the latitude to the midvalue if position ambiguity
1704             # is used
1705 10 50       18 if ($rethash->{'posambiguity'} >= 4) {
1706             # the minute is between 0 and 60, so
1707             # the middle point is 30
1708 0         0 $tmplat =~ s/_/3/;
1709             } else {
1710 10         14 $tmplat =~ s/_/5/; # the first is changed to digit 5
1711             }
1712 10         9 $tmplat =~ s/_/0/g; # the rest are changed to digit 0
1713              
1714             # get the degrees
1715 10         13 my $latitude = substr($tmplat, 0, 2);
1716             # the minutes
1717 10         18 my $latminutes = substr($tmplat, 2, 2) . '.' . substr($tmplat, 4, 2);
1718             # convert the minutes to decimal degrees and combine
1719 10         25 $latitude += ($latminutes/60);
1720              
1721             # check the north/south direction and correct the latitude
1722             # if necessary
1723 10         12 my $nschar = ord(substr($dstcallsign, 3, 1));
1724 10 100       19 if ($nschar <= 0x4c) {
1725 4         4 $latitude = 0 - $latitude;
1726             }
1727              
1728             # Latitude is finally complete, so store it
1729 10         15 $rethash->{'latitude'} = $latitude;
1730              
1731             # Get the message bits. 1 is standard one-bit and
1732             # 2 is custom one-bit. %mice_messagetypes provides
1733             # the mappings to message names
1734 10         14 my $mbitstring = substr($dstcallsign, 0, 3);
1735 10         9 $mbitstring =~ tr/0-9/0/;
1736 10         9 $mbitstring =~ tr/L/0/;
1737 10         6 $mbitstring =~ tr/P-Z/1/;
1738 10         7 $mbitstring =~ tr/A-K/2/;
1739 10         13 $rethash->{'mbits'} = $mbitstring;
1740              
1741             # Decode the longitude, the first three bytes of the
1742             # body after the data type indicator.
1743             # First longitude degrees, remember the longitude offset
1744 10         11 my $longitude = ord(substr($packet, 0, 1)) - 28;
1745 10         10 my $longoffsetchar = ord(substr($dstcallsign, 4, 1));
1746 10 100       19 if ($longoffsetchar >= 0x50) {
1747 5         4 $longitude += 100;
1748             }
1749 10 50 33     50 if ($longitude >= 180 && $longitude <= 189) {
    50 33        
1750 0         0 $longitude -= 80;
1751             } elsif ($longitude >= 190 && $longitude <= 199) {
1752 0         0 $longitude -= 190;
1753             }
1754              
1755             # Decode the longitude minutes
1756 10         16 my $longminutes = ord(substr($packet, 1, 1)) - 28;
1757 10 100       15 if ($longminutes >= 60) {
1758 1         1 $longminutes -= 60;
1759             }
1760             # ... and minute decimals
1761 10         33 $longminutes = sprintf('%02d.%02d',
1762             $longminutes,
1763             ord(substr($packet, 2, 1)) - 28);
1764             # apply position ambiguity to longitude
1765 10 50       47 if ($rethash->{'posambiguity'} == 4) {
    50          
    50          
    50          
    50          
1766             # minute is unused -> add 0.5 degrees to longitude
1767 0         0 $longitude += 0.5;
1768             } elsif ($rethash->{'posambiguity'} == 3) {
1769 0         0 my $lontmp = substr($longminutes, 0, 1) . '5';
1770 0         0 $longitude += ($lontmp/60);
1771             } elsif ($rethash->{'posambiguity'} == 2) {
1772 0         0 my $lontmp = substr($longminutes, 0, 2) . '.5';
1773 0         0 $longitude += ($lontmp/60);
1774             } elsif ($rethash->{'posambiguity'} == 1) {
1775 0         0 my $lontmp = substr($longminutes, 0, 4) . '5';
1776 0         0 $longitude += ($lontmp/60);
1777             } elsif ($rethash->{'posambiguity'} == 0) {
1778 10         17 $longitude += ($longminutes/60);
1779             } else {
1780 0         0 _a_err($rethash, 'mice_amb_odd', $rethash->{'posambiguity'});
1781 0         0 return 0;
1782             }
1783              
1784             # check the longitude E/W sign
1785 10         11 my $ewchar = ord(substr($dstcallsign, 5, 1));
1786 10 100       16 if ($ewchar >= 0x50) {
1787 3         5 $longitude = 0 - $longitude;
1788             }
1789              
1790             # Longitude is finally complete, so store it
1791 10         12 $rethash->{'longitude'} = $longitude;
1792              
1793             # Now onto speed and course.
1794             # If the packet has had a mic-e fix applied, course and speed are likely to be off.
1795 10 100       15 if (!$mice_fixed) {
1796 9         13 my $speed = (ord(substr($packet, 3, 1)) - 28) * 10;
1797 9         8 my $coursespeed = ord(substr($packet, 4, 1)) - 28;
1798 9         13 my $coursespeedtmp = int($coursespeed / 10);
1799 9         8 $speed += $coursespeedtmp;
1800 9         11 $coursespeed -= $coursespeedtmp * 10;
1801 9         9 my $course = 100 * $coursespeed;
1802 9         10 $course += ord(substr($packet, 5, 1)) - 28;
1803             # do some important adjustements
1804 9 100       18 if ($speed >= 800) {
1805 5         24 $speed -= 800;
1806             }
1807 9 50       17 if ($course >= 400) {
1808 9         6 $course -= 400;
1809             }
1810             # convert speed to km/h and store
1811 9         12 $rethash->{'speed'} = $speed * $knot_to_kmh;
1812             # also zero course is saved, which means unknown
1813 9 50       26 if ($course >= 0) {
1814 9         12 $rethash->{'course'} = $course;
1815             }
1816             }
1817              
1818             # save the symbol table and code
1819 10         17 $rethash->{'symbolcode'} = substr($packet, 6, 1);
1820 10         11 $rethash->{'symboltable'} = $symboltable;
1821              
1822             # Check for possible altitude and comment data.
1823             # It is base-91 coded and in format 'xxx}' where
1824             # x are the base-91 digits in meters, origin is 10000 meters
1825             # below sea.
1826 10 50       17 if (length($packet) > 8) {
1827 10         16 my $rest = substr($packet, 8);
1828            
1829             # check for Mic-E Telemetry Data
1830 10 100       22 if ($rest =~ /^'([0-9a-f]{2})([0-9a-f]{2})(.*)$/i) {
1831             # two hexadecimal values: channels 1 and 3
1832 1         3 $rest = $3;
1833 1         7 $rethash->{'telemetry'} = {
1834             'vals' => [ unpack('C*', pack('H*', $1 . '00' . $2)) ]
1835             };
1836             }
1837 10 100       27 if ($rest =~ /^‘([0-9a-f]{10})(.*)$/i) {
1838             # five channels:
1839 1         2 $rest = $2;
1840 1         14 $rethash->{'telemetry'} = {
1841             'vals' => [ unpack('C*', pack('H*', $1)) ]
1842             };
1843             }
1844            
1845             # check for altitude
1846 10 100       24 if ($rest =~ /^(.*?)([\x21-\x7b])([\x21-\x7b])([\x21-\x7b])\}(.*)$/o) {
1847 3         16 $rethash->{'altitude'} = (
1848             (ord($2) - 33) * 91 ** 2 +
1849             (ord($3) - 33) * 91 +
1850             (ord($4) - 33)) - 10000;
1851 3         10 $rest = $1 . $5;
1852             }
1853              
1854             # Check for new-style base-91 comment telemetry
1855 10         21 $rest = _comment_telemetry($rethash, $rest);
1856            
1857             # Check for !DAO!, take the last occurrence (per recommendation)
1858 10 100       24 if ($rest =~ /^(.*)\!([\x21-\x7b][\x20-\x7b]{2})\!(.*?)$/o) {
1859 2         6 my $daofound = _dao_parse($2, $srccallsign, $rethash);
1860 2 50       9 if ($daofound == 1) {
1861 2         5 $rest = $1 . $3;
1862             }
1863             }
1864            
1865             # If anything is left, store it as a comment
1866             # after removing non-printable ASCII
1867             # characters
1868 10 50       18 if (length($rest) > 0) {
1869 10         23 $rethash->{'comment'} = _cleanup_comment($rest);
1870             }
1871             }
1872            
1873 10 100       18 if ($mice_fixed) {
1874 1         2 $rethash->{'mice_mangled'} = 1;
1875             #warn "$srccallsign: fixed packet was parsed\n";
1876             }
1877            
1878 10         45 return 1;
1879             }
1880              
1881             # convert a compressed position to decimal degrees
1882             sub _compressed_to_decimal($$$)
1883             {
1884 5     5   34 my ($packet, $srccallsign, $rethash) = @_;
1885              
1886             # A compressed position is always 13 characters long.
1887             # Make sure we get at least 13 characters and that they are ok.
1888             # Also check the allowed base-91 characters at the same time.
1889 5 50       25 if (not($packet =~ /^[\/\\A-Za-j]{1}[\x21-\x7b]{8}[\x21-\x7b\x7d]{1}[\x20-\x7b]{3}/o)) {
1890 0         0 _a_err($rethash, 'comp_inv');
1891 0         0 return 0;
1892             }
1893              
1894 5         19 $rethash->{'format'} = 'compressed';
1895            
1896 5         9 my $symboltable = substr($packet, 0, 1);
1897 5         11 my $lat1 = ord(substr($packet, 1, 1)) - 33;
1898 5         28 my $lat2 = ord(substr($packet, 2, 1)) - 33;
1899 5         10 my $lat3 = ord(substr($packet, 3, 1)) - 33;
1900 5         9 my $lat4 = ord(substr($packet, 4, 1)) - 33;
1901 5         7 my $long1 = ord(substr($packet, 5, 1)) - 33;
1902 5         8 my $long2 = ord(substr($packet, 6, 1)) - 33;
1903 5         8 my $long3 = ord(substr($packet, 7, 1)) - 33;
1904 5         7 my $long4 = ord(substr($packet, 8, 1)) - 33;
1905 5         7 my $symbolcode = substr($packet, 9, 1);
1906 5         8 my $c1 = ord(substr($packet, 10, 1)) - 33;
1907 5         11 my $s1 = ord(substr($packet, 11, 1)) - 33;
1908 5         5 my $comptype = ord(substr($packet, 12, 1)) - 33;
1909              
1910             # save the symbol table and code
1911 5         18 $rethash->{'symbolcode'} = $symbolcode;
1912             # the symbol table values a..j are really 0..9
1913 5         9 $symboltable =~ tr/a-j/0-9/;
1914 5         10 $rethash->{'symboltable'} = $symboltable;
1915              
1916             # calculate latitude and longitude
1917 5         22 $rethash->{'latitude'} = 90 -
1918             (($lat1 * 91 ** 3 +
1919             $lat2 * 91 ** 2 +
1920             $lat3 * 91 +
1921             $lat4) / 380926);
1922 5         16 $rethash->{'longitude'} = -180 +
1923             (($long1 * 91 ** 3 +
1924             $long2 * 91 ** 2 +
1925             $long3 * 91 +
1926             $long4) / 190463);
1927             # save best-case position resolution in meters
1928             # 1852 meters * 60 minutes in a degree * 180 degrees
1929             # / 91 ** 4
1930 5         7 $rethash->{'posresolution'} = 0.291;
1931              
1932             # GPS fix status, only if csT is used
1933 5 100       15 if ($c1 != -1) {
1934 4 100       14 if (($comptype & 0x20) == 0x20) {
1935 3         7 $rethash->{'gpsfixstatus'} = 1;
1936             } else {
1937 1         1 $rethash->{'gpsfixstatus'} = 0;
1938             }
1939             }
1940              
1941             # check the compression type, if GPGGA, then
1942             # the cs bytes are altitude. Otherwise try
1943             # to decode it as course and speed. And
1944             # finally as radio range
1945             # if c is space, then csT is not used.
1946             # Also require that s is not a space.
1947 5 100 66     49 if ($c1 == -1 || $s1 == -1) {
    50 66        
    100          
    50          
1948             # csT not used
1949             } elsif (($comptype & 0x18) == 0x10) {
1950             # cs is altitude
1951 0         0 my $cs = $c1 * 91 + $s1;
1952             # convert directly to meters
1953 0         0 $rethash->{'altitude'} = (1.002 ** $cs) * $feet_to_meters;
1954             } elsif ($c1 >= 0 && $c1 <= 89) {
1955 2 100       5 if ($c1 == 0) {
1956             # special case of north, APRS spec
1957             # uses zero for unknown and 360 for north.
1958             # so remember to convert north here.
1959 1         3 $rethash->{'course'} = 360;
1960             } else {
1961 1         1 $rethash->{'course'} = $c1 * 4;
1962             }
1963             # convert directly to km/h
1964 2         16 $rethash->{'speed'} = (1.08 ** $s1 - 1) * $knot_to_kmh;
1965             } elsif ($c1 == 90) {
1966             # convert directly to km
1967 2         35 $rethash->{'radiorange'} = (2 * 1.08 ** $s1) * $mph_to_kmh;
1968             }
1969              
1970 5         14 return 1;
1971             }
1972              
1973              
1974             # Parse a possible !DAO! extension (datum and extra
1975             # lat/lon digits). Returns 1 if a valid !DAO! extension was
1976             # detected in the test subject (and stored in $rethash), 0 if not.
1977             # Only the "DAO" should be passed as the candidate parameter,
1978             # not the delimiting exclamation marks.
1979             sub _dao_parse($$$)
1980             {
1981 8     8   14 my ($daocandidate, $srccallsign, $rethash) = @_;
1982              
1983             # datum character is the first character and also
1984             # defines how the rest is interpreted
1985 8         28 my ($latoff, $lonoff) = undef;
1986 8 100       28 if ($daocandidate =~ /^([A-Z])(\d)(\d)$/o) {
    50          
    0          
1987             # human readable (datum byte A...Z)
1988 5         10 $rethash->{'daodatumbyte'} = $1;
1989 5         7 $rethash->{'posresolution'} = _get_posresolution(3);
1990 5         15 $latoff = $2 * 0.001 / 60;
1991 5         7 $lonoff = $3 * 0.001 / 60;
1992              
1993             } elsif ($daocandidate =~ /^([a-z])([\x21-\x7b])([\x21-\x7b])$/o) {
1994             # base-91 (datum byte a...z)
1995             # store the datum in upper case, still
1996 3         8 $rethash->{'daodatumbyte'} = uc($1);
1997             # close enough.. not exact:
1998 3         6 $rethash->{'posresolution'} = _get_posresolution(4);
1999             # do proper scaling of base-91 values
2000 3         8 $latoff = (ord($2) - 33) / 91 * 0.01 / 60;
2001 3         6 $lonoff = (ord($3) - 33) / 91 * 0.01 / 60;
2002              
2003             } elsif ($daocandidate =~ /^([\x21-\x7b]) $/o) {
2004             # only datum information, no lat/lon digits
2005 0         0 my $daodatumbyte = $1;
2006 0 0       0 if ($daodatumbyte =~ /^[a-z]$/o) {
2007 0         0 $daodatumbyte = uc($daodatumbyte);
2008             }
2009 0         0 $rethash->{'daodatumbyte'} = $daodatumbyte;
2010 0         0 return 1;
2011              
2012             } else {
2013 0         0 return 0;
2014             }
2015              
2016             # check N/S and E/W
2017 8 50       19 if ($rethash->{'latitude'} < 0) {
2018 0         0 $rethash->{'latitude'} -= $latoff;
2019             } else {
2020 8         8 $rethash->{'latitude'} += $latoff;
2021             }
2022 8 100       16 if ($rethash->{'longitude'} < 0) {
2023 6         8 $rethash->{'longitude'} -= $lonoff;
2024             } else {
2025 2         2 $rethash->{'longitude'} += $lonoff;
2026             }
2027 8         9 return 1;
2028             }
2029              
2030             =over
2031              
2032             =item check_ax25_call($callsign)
2033              
2034             Check the callsign for a valid AX.25 callsign format and
2035             return cleaned up (OH2XYZ-0) callsign or undef if the callsign
2036             is not a valid AX.25 address.
2037              
2038             Please note that it's very common to use invalid callsigns on the APRS-IS.
2039              
2040             =back
2041              
2042             =cut
2043              
2044             sub check_ax25_call($) {
2045 304 100   304 1 863 if ($_[0] =~ /^([A-Z0-9]{1,6})(-\d{1,2}|)$/o) {
2046 302 100       446 if (length($2) == 0) {
2047 290         480 return $1;
2048             } else {
2049             # convert SSID to positive and numeric
2050 12         24 my $ssid = 0 - $2;
2051 12 100       25 if ($ssid < 16) {
2052             # 15 is maximum in AX.25
2053 11         35 return $1 . '-' . $ssid;
2054             }
2055             }
2056             }
2057              
2058             # no successfull return yet, so error
2059 3         8 return undef;
2060             }
2061              
2062             # _dx_parse($sourcecall, $info, $rethash)
2063             #
2064             # Parses the body of a DX spot packet. Returns the following
2065             # hash elements: dxsource (source of the info), dxfreq (frequency),
2066             # dxcall (DX callsign) and dxinfo (info string).
2067             #
2068              
2069             sub _dx_parse($$$)
2070             {
2071 0     0   0 my ($sourcecall, $info, $rh) = @_;
2072            
2073 0 0       0 if (!defined check_ax25_call($sourcecall)) {
2074 0         0 _a_err($rh, 'dx_inv_src', $sourcecall);
2075 0         0 return 0;
2076             }
2077 0         0 $rh->{'dxsource'} = $sourcecall;
2078            
2079 0         0 $info =~ s/^\s*(.*?)\s*$/$1/; # strip whitespace
2080 0 0       0 if ($info =~ s/\s*(\d{3,4}Z)//) {
2081 0         0 $rh->{'dxtime'} = $1;
2082             }
2083 0 0       0 _a_err($rh, 'dx_inv_freq') if ($info !~ s/^(\d+\.\d+)\s*//);
2084 0         0 $rh->{'dxfreq'} = $1;
2085 0 0       0 _a_err($rh, 'dx_no_dx') if ($info !~ s/^([a-zA-Z0-9-\/]+)\s*//);
2086 0         0 $rh->{'dxcall'} = $1;
2087            
2088 0         0 $info =~ s/\s+/ /g;
2089 0         0 $rh->{'dxinfo'} = $info;
2090            
2091 0         0 return 1;
2092             }
2093              
2094             # _wx_parse($s, $rethash)
2095             #
2096             # Parses a normal uncompressed weather report packet.
2097             #
2098              
2099             sub _fahrenheit_to_celsius($)
2100             {
2101 9     9   44 return ($_[0] - 32) / 1.8;
2102             }
2103              
2104             sub _wx_parse($$)
2105             {
2106 8     8   17 my ($s, $rh) = @_;
2107            
2108             #my $initial = $s;
2109            
2110             # 257/007g013t055r000P000p000h56b10160v31
2111             # 045/000t064r000p000h35b10203.open2300v1.10
2112             # 175/007g007p...P000r000t062h32b10224wRSW
2113 8         11 my %w;
2114 8         15 my ($wind_dir, $wind_speed, $temp, $wind_gust) = ('', '', '', '');
2115 8 100 100     103 if ($s =~ s/^_{0,1}([\d \.\-]{3})\/([\d \.]{3})g([\d \.]+)t(-{0,1}[\d \.]+)//
    50          
    50          
    100          
2116             || $s =~ s/^_{0,1}c([\d \.\-]{3})s([\d \.]{3})g([\d \.]+)t(-{0,1}[\d \.]+)//) {
2117             #warn "wind $1 / $2 gust $3 temp $4\n";
2118 4         11 ($wind_dir, $wind_speed, $wind_gust, $temp) = ($1, $2, $3, $4);
2119             } elsif ($s =~ s/^_{0,1}([\d \.\-]{3})\/([\d \.]{3})t(-{0,1}[\d \.]+)//) {
2120             #warn "$initial\nwind $1 / $2 temp $3\n";
2121 0         0 ($wind_dir, $wind_speed, $temp) = ($1, $2, $3);
2122             } elsif ($s =~ s/^_{0,1}([\d \.\-]{3})\/([\d \.]{3})g([\d \.]+)//) {
2123             #warn "$initial\nwind $1 / $2 gust $3\n";
2124 0         0 ($wind_dir, $wind_speed, $wind_gust) = ($1, $2, $3);
2125             } elsif ($s =~ s/^g(\d+)t(-{0,1}[\d \.]+)//) {
2126             # g000t054r000p010P010h65b10073WS 2300 {UIV32N}
2127 1         5 ($wind_gust, $temp) = ($1, $2);
2128             } else {
2129             #warn "wx_parse: no initial match: $s\n";
2130 3         7 return 0;
2131             }
2132            
2133 5 50 33     15 if (!defined $temp && $s =~ s/t(-{0,1}\d{1,3})//) {
2134 0         0 $temp = $1;
2135             }
2136            
2137 5 50       68 $w{'wind_gust'} = sprintf('%.1f', $wind_gust * $mph_to_ms) if ($wind_gust =~ /^\d+$/);
2138 5 100       22 $w{'wind_direction'} = sprintf('%.0f', $wind_dir) if ($wind_dir =~ /^\d+$/);
2139 5 100       42 $w{'wind_speed'} = sprintf('%.1f', $wind_speed * $mph_to_ms) if ($wind_speed =~ /^\d+$/);
2140 5 50       50 $w{'temp'} = sprintf('%.1f', _fahrenheit_to_celsius($temp)) if ($temp =~ /^-{0,1}\d+$/);
2141            
2142 5 50       23 if ($s =~ s/r(\d{1,3})//) {
2143 5         32 $w{'rain_1h'} = sprintf('%.1f', $1*$hinch_to_mm); # during last 1h
2144             }
2145 5 50       20 if ($s =~ s/p(\d{1,3})//) {
2146 5         19 $w{'rain_24h'} = sprintf('%.1f', $1*$hinch_to_mm); # during last 24h
2147             }
2148 5 50       16 if ($s =~ s/P(\d{1,3})//) {
2149 5         19 $w{'rain_midnight'} = sprintf('%.1f', $1*$hinch_to_mm); # since midnight
2150             }
2151            
2152 5 50       19 if ($s =~ s/h(\d{1,3})//) {
2153 5         16 $w{'humidity'} = sprintf('%.0f', $1); # percentage
2154 5 100       14 $w{'humidity'} = 100 if ($w{'humidity'} eq 0);
2155 5 50 33     46 undef $w{'humidity'} if ($w{'humidity'} > 100 || $w{'humidity'} < 1);
2156             }
2157            
2158 5 50       20 if ($s =~ s/b(\d{4,5})//) {
2159 5         26 $w{'pressure'} = sprintf('%.1f', $1/10); # results in millibars
2160             }
2161            
2162 5 100       16 if ($s =~ s/([lL])(\d{1,3})//) {
2163 1         4 $w{'luminosity'} = sprintf('%.0f', $2); # watts / m2
2164 1 50       3 $w{'luminosity'} += 1000 if ($1 eq 'l');
2165             }
2166            
2167 5 50       12 if ($s =~ s/v([\-\+]{0,1}\d+)//) {
2168             # what ?
2169             }
2170            
2171 5 100       19 if ($s =~ s/s(\d{1,3})//) {
2172             # snowfall
2173 1         4 $w{'snow_24h'} = sprintf('%.1f', $1*$hinch_to_mm);
2174             }
2175            
2176 5 50       10 if ($s =~ s/#(\d+)//) {
2177             # raw rain counter
2178             }
2179            
2180 5         9 $s =~ s/^([rPphblLs#][\. ]{1,5})+//;
2181            
2182 5         8 $s =~ s/^\s+//;
2183 5         18 $s =~ s/\s+/ /;
2184 5 100       13 if ($s =~ /^[a-zA-Z0-9\-_]{3,5}$/) {
2185 1 50       7 $w{'soft'} = substr($s, 0, 16) if ($s ne '');
2186             } else {
2187 4         8 $rh->{'comment'} = _cleanup_comment($s);
2188             }
2189            
2190 5 50 0     19 if (defined $w{'temp'}
      33        
2191             || (defined $w{'wind_speed'} && defined $w{'wind_direction'})
2192             ) {
2193             #warn "ok: $initial\n$s\n";
2194 5         11 $rh->{'wx'} = \%w;
2195 5         36 return 1;
2196             }
2197            
2198 0         0 return 0;
2199             }
2200              
2201             # _wx_parse_peet_packet($s, $sourcecall, $rethash)
2202             #
2203             # Parses a Peet bros Ultimeter weather packet ($ULTW header).
2204             #
2205              
2206             sub _wx_parse_peet_packet($$$)
2207             {
2208 2     2   5 my ($s, $sourcecall, $rh) = @_;
2209            
2210             #warn "\$ULTW: $s\n";
2211             # 0000000001FF000427C70002CCD30001026E003A050F00040000
2212 2         3 my %w;
2213             my $t;
2214 0         0 my @vals;
2215 2         8 while ($s =~ s/^([0-9a-f]{4}|----)//i) {
2216 24 50       30 if ($1 eq '----') {
2217 0         0 push @vals, undef;
2218             } else {
2219             # Signed 16-bit integers in network (big-endian) order
2220             # encoded in hex, high nybble first.
2221             # Perl 5.10 unpack supports n! for signed ints, 5.8
2222             # requires tricks like this:
2223 24         42 my $v = unpack('n', pack('H*', $1));
2224            
2225 24 100       65 push @vals, ($v < 32768) ? $v : $v - 65536;
2226             }
2227             }
2228 2 50       4 return 0 if (!@vals);
2229            
2230 2         2 $t = shift @vals;
2231 2 50       25 $w{'wind_gust'} = sprintf('%.1f', $t * $kmh_to_ms / 10) if (defined $t);
2232 2         6 $t = shift @vals;
2233 2 50       9 $w{'wind_direction'} = sprintf('%.0f', ($t& 0xff) * 1.41176) if (defined $t); # 1/255 => 1/360
2234 2         1 $t = shift @vals;
2235 2 50       9 $w{'temp'} = sprintf('%.1f', _fahrenheit_to_celsius($t / 10)) if (defined $t); # 1/255 => 1/360
2236 2         3 $t = shift @vals;
2237 2 50       14 $w{'rain_midnight'} = sprintf('%.1f', $t * $hinch_to_mm) if (defined $t);
2238 2         3 $t = shift @vals;
2239 2 50 33     15 $w{'pressure'} = sprintf('%.1f', $t / 10) if (defined $t && $t >= 10);
2240 2         1 shift @vals; # Barometer Delta
2241 2         4 shift @vals; # Barometer Corr. Factor (LSW)
2242 2         2 shift @vals; # Barometer Corr. Factor (MSW)
2243 2         2 $t = shift @vals;
2244 2 50       4 if (defined $t) {
2245 2         4 $w{'humidity'} = sprintf('%.0f', $t / 10); # percentage
2246 2 50 33     11 delete $w{'humidity'} if ($w{'humidity'} > 100 || $w{'humidity'} < 1);
2247             }
2248 2         2 shift @vals; # date
2249 2         1 shift @vals; # time
2250 2         2 $t = shift @vals;
2251 2 100       7 $w{'rain_midnight'} = sprintf('%.1f', $t * $hinch_to_mm) if (defined $t);
2252 2         1 $t = shift @vals;
2253 2 100       8 $w{'wind_speed'} = sprintf('%.1f', $t * $kmh_to_ms / 10) if (defined $t);
2254            
2255 2 0 0     4 if (defined $w{'temp'}
      33        
      0        
      0        
2256             || (defined $w{'wind_speed'} && defined $w{'wind_direction'})
2257             || (defined $w{'pressure'})
2258             || (defined $w{'humidity'})
2259             ) {
2260 2         3 $rh->{'wx'} = \%w;
2261 2         11 return 1;
2262             }
2263            
2264 0         0 return 0;
2265             }
2266              
2267             # _wx_parse_peet_logging($s, $sourcecall, $rethash)
2268             #
2269             # Parses a Peet bros Ultimeter weather logging frame (!! header).
2270             #
2271              
2272             sub _wx_parse_peet_logging($$$)
2273             {
2274 1     1   3 my ($s, $sourcecall, $rh) = @_;
2275            
2276             #warn "\!!: $s\n";
2277             # 0000000001FF000427C70002CCD30001026E003A050F00040000
2278 1         2 my %w;
2279             my $t;
2280 0         0 my @vals;
2281 1         5 while ($s =~ s/^([0-9a-f]{4}|----)//i) {
2282 12 100       24 if ($1 eq '----') {
2283 2         10 push @vals, undef;
2284             } else {
2285             # Signed 16-bit integers in network (big-endian) order
2286             # encoded in hex, high nybble first.
2287             # Perl 5.10 unpack supports n! for signed ints, 5.8
2288             # requires tricks like this:
2289 10         22 my $v = unpack('n', pack('H*', $1));
2290            
2291 10 50       43 push @vals, ($v < 32768) ? $v : $v - 65536;
2292             }
2293             }
2294 1 50       4 return 0 if (!@vals);
2295            
2296 1         2 $t = shift @vals; # instant wind speed
2297 1 50       11 $w{'wind_speed'} = sprintf('%.1f', $t * $kmh_to_ms / 10) if (defined $t);
2298 1         2 $t = shift @vals;
2299 1 50       15 $w{'wind_direction'} = sprintf('%.0f', ($t& 0xff) * 1.41176) if (defined $t); # 1/255 => 1/360
2300 1         1 $t = shift @vals;
2301 1 50       5 $w{'temp'} = sprintf('%.1f', _fahrenheit_to_celsius($t / 10)) if (defined $t); # 1/255 => 1/360
2302 1         2 $t = shift @vals;
2303 1 50       5 $w{'rain_midnight'} = sprintf('%.1f', $t * $hinch_to_mm) if (defined $t);
2304 1         5 $t = shift @vals;
2305 1 50 33     9 $w{'pressure'} = sprintf('%.1f', $t / 10) if (defined $t && $t >= 10);
2306 1         1 $t = shift @vals;
2307 1 50       7 $w{'temp_in'} = sprintf('%.1f', _fahrenheit_to_celsius($t / 10)) if (defined $t); # 1/255 => 1/360
2308 1         2 $t = shift @vals;
2309 1 50       2 if (defined $t) {
2310 0         0 $w{'humidity'} = sprintf('%.0f', $t / 10); # percentage
2311 0 0 0     0 delete $w{'humidity'} if ($w{'humidity'} > 100 || $w{'humidity'} < 1);
2312             }
2313 1         1 $t = shift @vals;
2314 1 50       3 if (defined $t) {
2315 0         0 $w{'humidity_in'} = sprintf('%.0f', $t / 10); # percentage
2316 0 0 0     0 delete $w{'humidity_in'} if ($w{'humidity_in'} > 100 || $w{'humidity_in'} < 1);
2317             }
2318 1         1 shift @vals; # date
2319 1         1 shift @vals; # time
2320 1         1 $t = shift @vals;
2321 1 50       6 $w{'rain_midnight'} = sprintf('%.1f', $t * $hinch_to_mm) if (defined $t);
2322 1         1 $t = shift @vals; # avg wind speed
2323 1 50       6 $w{'wind_speed'} = sprintf('%.1f', $t * $kmh_to_ms / 10) if (defined $t);
2324            
2325             # if inside temperature exists but no outside, use inside
2326 1 50 33     12 $w{'temp'} = $w{'temp_in'} if (defined $w{'temp_in'} && !defined $w{'temp'});
2327 1 50 33     3 $w{'humidity'} = $w{'humidity_in'} if (defined $w{'humidity_in'} && !defined $w{'humidity'});
2328            
2329 1 0 0     3 if (defined $w{'temp'}
      33        
      0        
      0        
2330             || (defined $w{'wind_speed'} && defined $w{'wind_direction'})
2331             || (defined $w{'pressure'})
2332             || (defined $w{'humidity'})
2333             ) {
2334 1         2 $rh->{'wx'} = \%w;
2335 1         6 return 1;
2336             }
2337            
2338 0         0 return 0;
2339             }
2340              
2341             # _telemetry_parse($s, $rethash)
2342             #
2343             # Parses a telemetry packet.
2344             #
2345              
2346             sub _telemetry_parse($$)
2347             {
2348 1     1   3 my ($s, $rh) = @_;
2349            
2350 1         2 my $initial = $s;
2351            
2352 1         1 my ($seq, $v1, $v2, $v3, $v4, $v5, $bits);
2353 0         0 my %t;
2354 1 50       12 if ($s =~ s/^(\d+),(-|)(\d{1,6}|\d+\.\d+|\.\d+|),(-|)(\d{1,6}|\d+\.\d+|\.\d+|),(-|)(\d{1,6}|\d+\.\d+|\.\d+|),(-|)(\d{1,6}|\d+\.\d+|\.\d+|),(-|)(\d{1,6}|\d+\.\d+|\.\d+|),([01]{0,8})//) {
2355 1         4 $t{'seq'} = $1;
2356 1         14 my @vals = ( "$2$3", "$4$5", "$6$7", "$8$9", "$10$11" );
2357 1         5 for (my $i = 0; $i <= $#vals; $i++) {
2358 5 50       52 $vals[$i] = $vals[$i] eq '' ? 0 : sprintf('%.2f', $vals[$i]);
2359 5 50 33     30 if ($vals[$i] >= 999999 || $vals[$i] <= -999999) {
2360 0         0 _a_err($rh, 'tlm_large');
2361 0         0 return 0;
2362             }
2363             }
2364 1         2 $t{'vals'} = \@vals;
2365 1         3 $t{'bits'} = $12;
2366             # expand bits to 8 bits if some are missing
2367 1 50       3 if ((my $l = length($t{'bits'})) < 8) {
2368 0         0 $t{'bits'} .= '0' x (8-$l);
2369             }
2370             } else {
2371             # todo: return an error code
2372 0         0 _a_err($rh, 'tlm_inv');
2373 0         0 return 0;
2374             }
2375            
2376 1         6 $rh->{'telemetry'} = \%t;
2377             #warn 'ok: ' . Dumper(\%t);
2378 1         5 return 1;
2379             }
2380              
2381             =over
2382              
2383             =item parseaprs($packet, $hashref, %options)
2384              
2385             Parse an APRS packet given as a string, e.g.
2386             "OH2XYZ>APRS,RELAY*,WIDE:!2345.56N/12345.67E-PHG0123 hi there"
2387             Second parameter has to be a reference to a hash. That hash will
2388             be filled with as much data as possible based on the packet
2389             given as parameter.
2390              
2391             Returns 1 if the decoding was successfull,
2392             returns 0 if not. In case zero is returned, the contents of
2393             the parameter hash should be discarded, except for the error cause
2394             as reported via hash elements resultcode and resultmsg.
2395              
2396              
2397             The third parameter is an optional hash containing any of the following
2398             options:
2399              
2400             B - the packet should be examined in a form
2401             that can exist on an AX.25 network (1) or whether the frame is
2402             from the Internet (0 - default).
2403              
2404             B - if the packet contains corrupted
2405             mic-e fields, but some of the data is still recovable, decode
2406             the packet instead of reporting an error. At least aprsd produces
2407             these packets. 1: try to decode, 0: report an error (default).
2408             Packets which have been successfully demangled will contain the
2409             B flag.
2410              
2411             B - Timestamps within the packets are not decoded
2412             to an UNIX timestamp, but are returned as raw strings.
2413              
2414             Example:
2415              
2416             my %hash;
2417              
2418             my $ret = parseaprs("OH2XYZ>APRS,RELAY*,WIDE:!2345.56N/12345.67E-PHG0123 hi",
2419             \%hash, 'isax25' => 0, 'accept_broken_mice' => 0);
2420              
2421             =back
2422              
2423             =cut
2424              
2425             sub parseaprs($$;%) {
2426 299     299 1 81478 my($packet, $rethash, %options) = @_;
2427 299 50       543 my $isax25 = ($options{'isax25'}) ? 1 : 0;
2428            
2429 299 50       493 if (!defined $packet) {
2430 0         0 _a_err($rethash, 'packet_no');
2431 0         0 return 0;
2432             }
2433 299 50       472 if (length($packet) < 1) {
2434 0         0 _a_err($rethash, 'packet_short');
2435 0         0 return 0;
2436             }
2437            
2438             # Separate the header and packet body on the first
2439             # colon.
2440 299         618 my ($header, $body) = split(/:/, $packet, 2);
2441              
2442             # If no body, skip
2443 299 50       527 if (!defined $body) {
2444 0         0 _a_err($rethash, 'packet_nobody');
2445 0         0 return 0;
2446             }
2447              
2448             # Save all the parts of the packet
2449 299         350 $rethash->{'origpacket'} = $packet;
2450 299         274 $rethash->{'header'} = $header;
2451 299         242 $rethash->{'body'} = $body;
2452              
2453             # Source callsign, put the rest in $rest
2454 299         243 my($srccallsign, $rest);
2455 299 100       1085 if ($header =~ /^([A-Z0-9-]{1,9})>(.*)$/io) {
2456 298         435 $rest = $2;
2457 298 50       340 if ($isax25 == 0) {
2458 298         311 $srccallsign = $1;
2459             } else {
2460 0         0 $srccallsign = check_ax25_call(uc($1));
2461 0 0       0 if (not(defined($srccallsign))) {
2462 0         0 _a_err($rethash, 'srccall_noax25');
2463 0         0 return 0;
2464             }
2465             }
2466             } else {
2467             # can't be a valid amateur radio callsign, even
2468             # in the extended sense of APRS-IS callsigns
2469 1         3 _a_err($rethash, 'srccall_badchars');
2470 1         2 return 0;
2471             }
2472 298         288 $rethash->{'srccallsign'} = $srccallsign;
2473              
2474             # Get the destination callsign and digipeaters.
2475             # Only TNC-2 format is supported, AEA (with digipeaters) is not.
2476 298         582 my @pathcomponents = split(/,/, $rest);
2477             # More than 9 (dst callsign + 8 digipeaters) path components
2478             # from AX.25 or less than 1 from anywhere is invalid.
2479 298 50       467 if ($isax25 == 1) {
2480 0 0       0 if (scalar(@pathcomponents) > 9) {
2481             # too many fields to be from AX.25
2482 0         0 _a_err($rethash, 'dstpath_toomany');
2483 0         0 return 0;
2484             }
2485             }
2486 298 50       430 if (scalar(@pathcomponents) < 1) {
2487             # no destination field
2488 0         0 _a_err($rethash, 'dstcall_none');
2489 0         0 return 0;
2490             }
2491            
2492             # Destination callsign. We are strict here, there
2493             # should be no need to use a non-AX.25 compatible
2494             # destination callsigns in the APRS-IS.
2495 298         487 my $dstcallsign = check_ax25_call(shift @pathcomponents);
2496 298 50       432 if (!defined $dstcallsign) {
2497 0         0 _a_err($rethash, 'dstcall_noax25');
2498 0         0 return 0;
2499             }
2500 298         295 $rethash->{'dstcallsign'} = $dstcallsign;
2501              
2502             # digipeaters
2503 298         205 my @digipeaters;
2504 298 50       335 if ($isax25 == 1) {
2505 0         0 foreach my $digi (@pathcomponents) {
2506 0 0       0 if ($digi =~ /^([A-Z0-9-]+)(\*|)$/io) {
2507 0         0 my $digitested = check_ax25_call(uc($1));
2508 0 0       0 if (not(defined($digitested))) {
2509 0         0 _a_err($rethash, 'digicall_noax25');
2510 0         0 return 0;
2511             }
2512 0         0 my $wasdigied = 0;
2513 0 0       0 if ($2 eq '*') {
2514 0         0 $wasdigied = 1;
2515             }
2516             # add it to the digipeater array
2517 0         0 push(@digipeaters, { 'call' => $digitested,
2518             'wasdigied' => $wasdigied });
2519             } else {
2520 0         0 _a_err($rethash, 'digicall_badchars');
2521 0         0 return 0;
2522             }
2523             }
2524             } else {
2525 298         204 my $seen_qconstr = 0;
2526            
2527 298         309 foreach my $digi (@pathcomponents) {
2528             # From the internet. Apply the same checks as for
2529             # APRS-IS packet originator. Allow long hexadecimal IPv6
2530             # address after the Q construct.
2531 1137 100       1896 if ($digi =~ /^([A-Z0-9a-z-]{1,9})(\*|)$/o) {
2532 1134 100       2460 push(@digipeaters, { 'call' => $1,
2533             'wasdigied' => ($2 eq '*') ? 1 : 0 });
2534 1134 100       2120 $seen_qconstr = 1 if ($1 =~ /^q..$/);
2535             } else {
2536 3 100 100     27 if ($seen_qconstr && $digi =~ /^([0-9A-F]{32})$/) {
2537 1         4 push(@digipeaters, { 'call' => $1, 'wasdigied' => 0 });
2538             } else {
2539 2         6 _a_err($rethash, 'digicall_badchars');
2540 2         7 return 0;
2541             }
2542             }
2543             }
2544             }
2545 296         539 $rethash->{'digipeaters'} = \@digipeaters;
2546            
2547             # So now we have source and destination callsigns and
2548             # digipeaters parsed and ok. Move on to the body.
2549              
2550             # Check the first character of the packet
2551             # and determine the packet type
2552 296         266 my $retval = -1;
2553 296         315 my $packettype = substr($body, 0, 1);
2554 296         267 my $paclen = length($body);
2555              
2556              
2557             # Check the packet type and proceed depending on it
2558              
2559             # Mic-encoder packet
2560 296 100 100     2742 if (ord($packettype) == 0x27 || ord($packettype) == 0x60) {
    100 100        
    100 100        
    100 100        
    100          
    50          
    100          
    50          
    100          
    100          
    50          
    100          
2561             # the following are obsolete mic-e types: 0x1c 0x1d
2562             # mic-encoder data
2563             # minimum body length 9 chars
2564 11 50       23 if ($paclen >= 9) {
2565 11         10 $rethash->{'type'} = 'location';
2566 11         36 return _mice_to_decimal(substr($body, 1), $dstcallsign, $srccallsign, $rethash, \%options);
2567             }
2568              
2569             # Normal or compressed location packet, with or without
2570             # timestamp, with or without messaging capability
2571             } elsif ($packettype eq '!' ||
2572             $packettype eq '=' ||
2573             $packettype eq '/' ||
2574             $packettype eq '@') {
2575             # with or without messaging
2576 29 100 100     90 if ($packettype eq '!' || $packettype eq '/') {
2577 20         37 $rethash->{'messaging'} = 0;
2578             } else {
2579 9         11 $rethash->{'messaging'} = 1;
2580             }
2581            
2582 29 50       42 if ($paclen >= 14) {
2583 29         55 $rethash->{'type'} = 'location';
2584 29 100 100     107 if ($packettype eq '/' || $packettype eq '@') {
2585             # With a prepended timestamp, check it and jump over.
2586             # If the timestamp is invalid, it will be set to zero.
2587 13         42 $rethash->{'timestamp'} = _parse_timestamp(\%options, substr($body, 1, 7));
2588 13 50       37 if ($rethash->{'timestamp'} == 0) {
2589 0         0 _a_warn($rethash, 'timestamp_inv_loc');
2590             }
2591 13         27 $body = substr($body, 7);
2592             }
2593 29         42 $body = substr($body, 1); # remove the first character
2594             # grab the ascii value of the first byte of body
2595 29         37 my $poschar = ord($body);
2596 29 100 100     154 if ($poschar >= 48 && $poschar <= 57) {
    100 66        
    50 66        
      66        
      33        
      66        
2597             # poschar is a digit... normal uncompressed position
2598 23 50       39 if (length($body) >= 19) {
2599 23         60 $retval = _normalpos_to_decimal($body, $srccallsign, $rethash);
2600             # continue parsing with possible comments, but only
2601             # if this is not a weather report (course/speed mixup,
2602             # weather as comment)
2603             # if the comments don't parse, don't raise an error
2604 23 100 100     86 if ($retval == 1 && $rethash->{'symbolcode'} ne '_') {
2605 17         56 _comments_to_decimal(substr($body, 19), $srccallsign, $rethash);
2606             } else {
2607             #warn "maybe a weather report?\n" . substr($body, 19) . "\n";
2608 6         21 _wx_parse(substr($body, 19), $rethash);
2609             }
2610             }
2611             } elsif ($poschar == 47 || $poschar == 92
2612             || ($poschar >= 65 && $poschar <= 90) || ($poschar >= 97 && $poschar <= 106) ) {
2613             # $poschar =~ /^[\/\\A-Za-j]$/o
2614             # compressed position
2615 5 100       12 if (length($body) >= 13) {
2616 4         17 $retval = _compressed_to_decimal(substr($body, 0, 13), $srccallsign, $rethash);
2617             # continue parsing with possible comments, but only
2618             # if this is not a weather report (course/speed mixup,
2619             # weather as comment)
2620             # if the comments don't parse, don't raise an error
2621 4 100 66     30 if ($retval == 1 && $rethash->{'symbolcode'} ne '_') {
2622 3         13 _comments_to_decimal(substr($body, 13), $srccallsign, $rethash);
2623             } else {
2624             #warn "maybe a weather report?\n" . substr($body, 13) . "\n";
2625 1         3 _wx_parse(substr($body, 13), $rethash);
2626             }
2627             }
2628             } elsif ($poschar == 33) { # '!'
2629             # Weather report from Ultimeter 2000
2630 1         2 $rethash->{'type'} = 'wx';
2631 1         4 return _wx_parse_peet_logging(substr($body, 1), $srccallsign, $rethash);
2632             } else {
2633 0         0 _a_err($rethash, 'packet_invalid');
2634 0         0 return 0;
2635             }
2636             } else {
2637 0         0 _a_err($rethash, 'packet_short', 'location');
2638 0         0 return 0;
2639             }
2640              
2641             # Weather report
2642             } elsif ($packettype eq '_') {
2643 1 50       7 if ($body =~ /_(\d{8})c[\- \.\d]{1,3}s[\- \.\d]{1,3}/) {
2644 1         1 $rethash->{'type'} = 'wx';
2645 1         5 return _wx_parse(substr($body, 9), $rethash);
2646             } else {
2647 0         0 _a_err($rethash, 'wx_unsupp', 'Positionless');
2648 0         0 return 0;
2649             }
2650            
2651             # Object
2652             } elsif ($packettype eq ';') {
2653 2 50       5 if ($paclen >= 31) {
2654 2         4 $rethash->{'type'} = 'object';
2655 2         9 return _object_to_decimal(\%options, $body, $srccallsign, $rethash);
2656             }
2657              
2658             # NMEA data
2659             } elsif ($packettype eq '$') {
2660             # don't try to parse the weather stations, require "$GP" start
2661 3 100       11 if (substr($body, 0, 3) eq '$GP') {
    50          
2662             # dstcallsign can contain the APRS symbol to use,
2663             # so read that one too
2664 1         2 $rethash->{'type'} = 'location';
2665 1         6 return _nmea_to_decimal(\%options, substr($body, 1), $srccallsign, $dstcallsign, $rethash);
2666             } elsif (substr($body, 0, 5) eq '$ULTW') {
2667 2         2 $rethash->{'type'} = 'wx';
2668 2         7 return _wx_parse_peet_packet(substr($body, 5), $srccallsign, $rethash);
2669             }
2670              
2671             # Item
2672             } elsif ($packettype eq ')') {
2673 0 0       0 if ($paclen >= 18) {
2674 0         0 $rethash->{'type'} = 'item';
2675 0         0 return _item_to_decimal($body, $srccallsign, $rethash);
2676             }
2677              
2678             # Message, bulletin or an announcement
2679             } elsif ($packettype eq ':') {
2680 245 50       316 if ($paclen >= 11) {
2681             # all are labeled as messages for the time being
2682 245         208 $rethash->{'type'} = 'message';
2683 245         297 return _message_parse($body, $srccallsign, $rethash);
2684             }
2685              
2686             # Station capabilities
2687             } elsif ($packettype eq '<') {
2688             # at least one other character besides '<' required
2689 0 0       0 if ($paclen >= 2) {
2690 0         0 $rethash->{'type'} = 'capabilities';
2691 0         0 return _capabilities_parse(substr($body, 1), $srccallsign, $rethash);
2692             }
2693              
2694             # Status reports
2695             } elsif ($packettype eq '>') {
2696             # we can live with empty status reports
2697 1 50       3 if ($paclen >= 1) {
2698 1         2 $rethash->{'type'} = 'status';
2699 1         4 return _status_parse(\%options, substr($body, 1), $srccallsign, $rethash);
2700             }
2701            
2702             # Telemetry
2703             } elsif ($body =~ /^T#(.*?),(.*)$/) {
2704 1         2 $rethash->{'type'} = 'telemetry';
2705 1         4 return _telemetry_parse(substr($body, 2), $rethash);
2706            
2707             # DX spot
2708             } elsif ($body =~ /^DX\s+de\s+(.*?)\s*[:>]\s*(.*)$/i) {
2709 0         0 $rethash->{'type'} = 'dx';
2710 0         0 return _dx_parse($1, $2, $rethash);
2711            
2712             # Experimental
2713             } elsif ($body =~ /^\{\{/i) {
2714 1         3 _a_err($rethash, 'exp_unsupp');
2715 1         2 return 0;
2716            
2717             # When all else fails, try to look for a !-position that can
2718             # occur anywhere within the 40 first characters according
2719             # to the spec.
2720             } else {
2721 2         4 my $pos = index($body, '!');
2722 2 100 66     10 if ($pos >= 0 && $pos <= 39) {
2723 1         2 $rethash->{'type'} = 'location';
2724 1         1 $rethash->{'messaging'} = 0;
2725 1         2 my $pchar = substr($body, $pos + 1, 1);
2726 1 50       6 if ($pchar =~ /^[\/\\A-Za-j]$/o) {
    50          
2727             # compressed position
2728 0 0       0 if (length($body) >= $pos + 1 + 13) {
2729 0         0 $retval = _compressed_to_decimal(substr($body, $pos + 1, 13), $srccallsign, $rethash);
2730             # check the APRS data extension and comment,
2731             # if not weather data
2732 0 0 0     0 if ($retval == 1 && $rethash->{'symbolcode'} ne '_') {
2733 0         0 _comments_to_decimal(substr($body, $pos + 14), $srccallsign, $rethash);
2734             }
2735             }
2736             } elsif ($pchar =~ /^\d$/io) {
2737             # normal uncompressed position
2738 1 50       4 if (length($body) >= $pos + 1 + 19) {
2739 1         4 $retval = _normalpos_to_decimal(substr($body, $pos + 1), $srccallsign, $rethash);
2740             # check the APRS data extension and comment,
2741             # if not weather data
2742 1 50 33     11 if ($retval == 1 && $rethash->{'symbolcode'} ne '_') {
2743 1         3 _comments_to_decimal(substr($body, $pos + 20), $srccallsign, $rethash);
2744             }
2745             }
2746             }
2747             }
2748             }
2749              
2750             # Return success for an ok packet
2751 30 100       75 if ($retval == 1) {
2752 26         95 return 1;
2753             }
2754            
2755 4         13 return 0;
2756             }
2757              
2758              
2759             # Checks a callsign for validity and strips
2760             # trailing spaces out and returns the string.
2761             # Returns undef on invalid callsign
2762             sub _kiss_checkcallsign($)
2763             {
2764 0 0   0     if ($_[0] =~ /^([A-Z0-9]+)\s*(|-\d+)$/o) {
2765 0 0         if (length($2) > 0) {
2766             # check the SSID if given
2767 0 0         if ($2 < -15) {
2768 0           return undef;
2769             }
2770             }
2771 0           return $1 . $2;
2772             }
2773              
2774             # no match
2775 0           return undef;
2776             }
2777              
2778              
2779             =over
2780              
2781             =item kiss_to_tnc2($kissframe)
2782              
2783             Convert a KISS-frame into a TNC-2 compatible UI-frame.
2784             Non-UI and non-pid-F0 frames are dropped. The KISS-frame
2785             to be decoded should not have FEND (0xC0) characters
2786             in the beginning or in the end. Byte unstuffing
2787             must not be done before calling this function. Returns
2788             a string containing the TNC-2 frame (no CR and/or LF)
2789             or undef on error.
2790              
2791             =back
2792              
2793             =cut
2794              
2795             sub kiss_to_tnc2($) {
2796 0     0 1   my $kissframe = shift @_;
2797              
2798 0           my $asciiframe = "";
2799 0           my $dstcallsign = "";
2800 0           my $callsigntmp = "";
2801 0           my $digipeatercount = 0; # max. 8 digipeaters
2802              
2803             # perform byte unstuffing for kiss first
2804 0           $kissframe =~ s/\xdb\xdc/\xc0/g;
2805 0           $kissframe =~ s/\xdb\xdd/\xdb/g;
2806              
2807             # length checking _after_ byte unstuffing
2808 0 0         if (length($kissframe) < 16) {
2809 0 0         if ($debug > 0) {
2810 0           warn "too short frame to be valid kiss\n";
2811             }
2812 0           return undef;
2813             }
2814              
2815             # the first byte has to be zero (kiss data)
2816 0 0         if (ord(substr($kissframe, 0, 1)) != 0) {
2817 0 0         if ($debug > 0) {
2818 0           warn "not a kiss data frame\n";
2819             }
2820 0           return undef;
2821             }
2822              
2823 0           my $addresspart = 0;
2824 0           my $addresscount = 0;
2825 0           while (length($kissframe) > 0) {
2826             # in the first run this removes the zero byte,
2827             # in subsequent runs this removes the previous byte
2828 0           $kissframe = substr($kissframe, 1);
2829 0           my $charri = substr($kissframe, 0, 1);
2830              
2831 0 0         if ($addresspart == 0) {
    0          
    0          
2832 0           $addresscount++;
2833             # we are in the address field, go on
2834             # decoding it
2835             # switch to numeric
2836 0           $charri = ord($charri);
2837             # check whether this is the last
2838             # (0-bit is one)
2839 0 0         if ($charri & 1) {
2840 0 0 0       if ($addresscount < 14 ||
2841             ($addresscount % 7) != 0) {
2842             # addresses ended too soon or in the
2843             # wrong place
2844 0 0         if ($debug > 0) {
2845 0           warn "addresses ended too soon or in the wrong place in kiss frame\n";
2846             }
2847 0           return undef;
2848             }
2849             # move on to control field next time
2850 0           $addresspart = 1;
2851             }
2852             # check the complete callsign
2853             # (7 bytes)
2854 0 0         if (($addresscount % 7) == 0) {
2855             # this is SSID, get the number
2856 0           my $ssid = ($charri >> 1) & 0xf;
2857 0 0         if ($ssid != 0) {
2858             # don't print zero SSID
2859 0           $callsigntmp .= "-" . $ssid;
2860             }
2861             # check the callsign for validity
2862 0           my $chkcall = _kiss_checkcallsign($callsigntmp);
2863 0 0         if (not(defined($chkcall))) {
2864 0 0         if ($debug > 0) {
2865 0           warn "Invalid callsign in kiss frame, discarding\n";
2866             }
2867 0           return undef;
2868             }
2869 0 0         if ($addresscount == 7) {
    0          
    0          
2870             # we have a destination callsign
2871 0           $dstcallsign = $chkcall;
2872 0           $callsigntmp = "";
2873 0           next;
2874             } elsif ($addresscount == 14) {
2875             # we have a source callsign, copy
2876             # it to the final frame directly
2877 0           $asciiframe = $chkcall . ">" . $dstcallsign;
2878 0           $callsigntmp = "";
2879             } elsif ($addresscount > 14) {
2880             # get the H-bit as well if we
2881             # are in the path part
2882 0           $asciiframe .= $chkcall;
2883 0           $callsigntmp = "";
2884 0 0         if ($charri & 0x80) {
2885 0           $asciiframe .= "*";
2886             }
2887 0           $digipeatercount++;
2888             } else {
2889 0 0         if ($debug > 0) {
2890 0           warn "Internal error 1 in kiss_to_tnc2()\n";
2891             }
2892 0           return undef;
2893             }
2894 0 0         if ($addresspart == 0) {
2895             # more address fields will follow
2896             # check that there are a maximum
2897             # of eight digipeaters in the path
2898 0 0         if ($digipeatercount >= 8) {
2899 0 0         if ($debug > 0) {
2900 0           warn "Too many digipeaters in kiss packet, discarding\n";
2901             }
2902 0           return undef;
2903             }
2904 0           $asciiframe .= ",";
2905             } else {
2906             # end of address fields
2907 0           $asciiframe .= ":";
2908             }
2909 0           next;
2910             }
2911             # shift one bit right to get the ascii
2912             # character
2913 0           $charri >>= 1;
2914 0           $callsigntmp .= chr($charri);
2915              
2916             } elsif ($addresspart == 1) {
2917             # control field. we are only interested in
2918             # UI frames, discard others
2919 0           $charri = ord($charri);
2920 0 0         if ($charri != 3) {
2921 0 0         if ($debug > 0) {
2922 0           warn "not UI frame, skipping\n";
2923             }
2924 0           return undef;
2925             }
2926             #print " control $charri";
2927 0           $addresspart = 2;
2928              
2929             } elsif ($addresspart == 2) {
2930             # PID
2931             #printf(" PID %02x data: ", ord($charri));
2932             # we want PID 0xFO
2933 0           $charri = ord($charri);
2934 0 0         if ($charri != 0xf0) {
2935 0 0         if ($debug > 0) {
2936 0           warn "PID not 0xF0, skipping\n";
2937             }
2938 0           return undef;
2939             }
2940 0           $addresspart = 3;
2941              
2942             } else {
2943             # body
2944 0           $asciiframe .= $charri;
2945             }
2946             }
2947              
2948             # Ok, return whole frame
2949 0           return $asciiframe;
2950             }
2951              
2952             =over
2953              
2954             =item tnc2_to_kiss($tnc2frame)
2955              
2956             Convert a TNC-2 compatible UI-frame into a KISS data
2957             frame (single port KISS TNC). The frame will be complete,
2958             i.e. it has byte stuffing done and FEND (0xC0) characters
2959             on both ends. If conversion fails, return undef.
2960              
2961             =back
2962              
2963             =cut
2964              
2965             sub tnc2_to_kiss($) {
2966 0     0 1   my $gotframe = shift @_;
2967              
2968 0           my $kissframe = chr(0); # kiss frame starts with byte 0x00
2969 0           my $body;
2970             my $header;
2971              
2972             # separate header and body
2973 0 0         if ($gotframe =~ /^([A-Z0-9,*>-]+):(.+)$/o) {
2974 0           $header = $1;
2975 0           $body = $2;
2976             } else {
2977 0 0         if ($debug > 0) {
2978 0           warn "tnc2_to_kiss(): separation into header and body failed\n";
2979             }
2980 0           return undef;
2981             }
2982              
2983             # separate the sender, recipient and digipeaters
2984 0           my $sender;
2985             my $sender_ssid;
2986 0           my $receiver;
2987 0           my $receiver_ssid;
2988 0           my $digipeaters;
2989 0 0         if ($header =~ /^([A-Z0-9]{1,6})(-\d+|)>([A-Z0-9]{1,6})(-\d+|)(|,.*)$/o) {
2990 0           $sender = $1;
2991 0           $sender_ssid = $2;
2992 0           $receiver = $3;
2993 0           $receiver_ssid = $4;
2994 0           $digipeaters = $5;
2995             } else {
2996 0 0         if ($debug > 0) {
2997 0           warn "tnc2_to_kiss(): separation of sender and receiver from header failed\n";
2998             }
2999 0           return undef;
3000             }
3001              
3002             # Check SSID format and convert to number
3003 0 0         if (length($sender_ssid) > 0) {
3004 0           $sender_ssid = 0 - $sender_ssid;
3005 0 0         if ($sender_ssid > 15) {
3006 0 0         if ($debug > 0) {
3007 0           warn "tnc2_to_kiss(): sender SSID ($sender_ssid) is over 15\n";
3008             }
3009 0           return undef;
3010             }
3011             } else {
3012 0           $sender_ssid = 0;
3013             }
3014 0 0         if (length($receiver_ssid) > 0) {
3015 0           $receiver_ssid = 0 - $receiver_ssid;
3016 0 0         if ($receiver_ssid > 15) {
3017 0 0         if ($debug > 0) {
3018 0           warn "tnc2_to_kiss(): receiver SSID ($receiver_ssid) is over 15\n";
3019             }
3020 0           return undef;
3021             }
3022             } else {
3023 0           $receiver_ssid = 0;
3024             }
3025             # pad callsigns to 6 characters with space
3026 0           $sender .= ' ' x (6 - length($sender));
3027 0           $receiver .= ' ' x (6 - length($receiver));
3028             # encode destination and source
3029 0           for (my $i = 0; $i < 6; $i++) {
3030 0           $kissframe .= chr(ord(substr($receiver, $i, 1)) << 1);
3031             }
3032 0           $kissframe .= chr(0xe0 | ($receiver_ssid << 1));
3033 0           for (my $i = 0; $i < 6; $i++) {
3034 0           $kissframe .= chr(ord(substr($sender, $i, 1)) << 1);
3035             }
3036 0 0         if (length($digipeaters) > 0) {
3037 0           $kissframe .= chr(0x60 | ($sender_ssid << 1));
3038             } else {
3039 0           $kissframe .= chr(0x61 | ($sender_ssid << 1));
3040             }
3041              
3042             # if there are digipeaters, add them
3043 0 0         if (length($digipeaters) > 0) {
3044 0           $digipeaters =~ s/,//; # remove the first comma
3045             # split into parts
3046 0           my @digis = split(/,/, $digipeaters);
3047 0           my $digicount = scalar(@digis);
3048 0 0 0       if ($digicount > 8 || $digicount < 1) {
3049             # too many (or none?!?) digipeaters
3050 0 0         if ($debug > 0) {
3051 0           warn "tnc2_to_kiss(): too many (or zero) digipeaters: $digicount\n";
3052             }
3053 0           return undef;
3054             }
3055 0           for (my $i = 0; $i < $digicount; $i++) {
3056             # split into callsign, SSID and h-bit
3057 0 0         if ($digis[$i] =~ /^([A-Z0-9]{1,6})(-\d+|)(\*|)$/o) {
3058 0           my $callsign = $1 . ' ' x (6 - length($1));
3059 0           my $ssid = 0;
3060 0           my $hbit = 0x00;
3061 0 0         if (length($2) > 0) {
3062 0           $ssid = 0 - $2;
3063 0 0         if ($ssid > 15) {
3064 0 0         if ($debug > 0) {
3065 0           warn "tnc2_to_kiss(): digipeater nr. $i SSID ($ssid) invalid\n";
3066             }
3067 0           return undef;
3068             }
3069             }
3070 0 0         if ($3 eq '*') {
3071 0           $hbit = 0x80;
3072             }
3073             # add to kiss frame
3074 0           for (my $k = 0; $k < 6; $k++) {
3075 0           $kissframe .= chr(ord(substr($callsign, $k, 1)) << 1);
3076             }
3077 0 0         if ($i + 1 < $digicount) {
3078             # more digipeaters to follow
3079 0           $kissframe .= chr($hbit | 0x60 | ($ssid << 1));
3080             } else {
3081             # last digipeater
3082 0           $kissframe .= chr($hbit | 0x61 | ($ssid << 1));
3083             }
3084            
3085             } else {
3086 0 0         if ($debug > 0) {
3087 0           warn "tnc2_to_kiss(): digipeater nr. $i parsing failed\n";
3088             }
3089 0           return undef;
3090             }
3091             }
3092             }
3093              
3094             # add frame type (0x03) and PID (0xF0)
3095 0           $kissframe .= chr(0x03) . chr(0xf0);
3096             # add frame body
3097 0           $kissframe .= $body;
3098             # perform KISS byte stuffing
3099 0           $kissframe =~ s/\xdb/\xdb\xdd/g;
3100 0           $kissframe =~ s/\xc0/\xdb\xdc/g;
3101             # add FENDs
3102 0           $kissframe = chr(0xc0) . $kissframe . chr(0xc0);
3103              
3104 0           return $kissframe;
3105             }
3106              
3107             =over
3108              
3109             =item aprs_duplicate_parts($packet)
3110              
3111             Accepts a TNC-2 format frame and extracts the original
3112             sender callsign, destination callsign (without ssid) and
3113             payload data for duplicate detection. Returns
3114             sender, receiver and body on success, undef on error.
3115             In the case of third party packets, always gets this
3116             information from the innermost data. Also removes
3117             possible trailing spaces to improve detection
3118             (e.g. aprsd replaces trailing CRs or LFs in a packet with a space).
3119              
3120             =back
3121              
3122             =cut
3123              
3124             sub aprs_duplicate_parts($)
3125             {
3126 0     0 1   my ($packet) = @_;
3127              
3128             # If this is a third party packet format,
3129             # strip out the outer layer and focus on the inside.
3130             # Do this several times in a row if necessary
3131 0           while (1) {
3132 0 0         if ($packet =~ /^[^:]+:\}(.*)$/io) {
3133 0           $packet = $1;
3134             } else {
3135 0           last;
3136             }
3137             }
3138              
3139 0 0         if ($packet =~ /^([A-Z0-9]{1,6})(-[A-Z0-9]{1,2}|)>([A-Z0-9]{1,6})(-\d{1,2}|)(:|,[^:]+:)(.*)$/io) {
3140 0           my $source;
3141             my $destination;
3142 0           my $body = $6;
3143 0 0         if ($2 eq "") {
3144             # ssid 0
3145 0           $source = $1 . "-0";
3146             } else {
3147 0           $source = $1 . $2;
3148             }
3149             # drop SSID for destination
3150 0           $destination = $3;
3151             # remove trailing spaces from body
3152 0           $body =~ s/\s+$//;
3153 0           return ($source, $destination, $body);
3154             }
3155              
3156 0           return undef;
3157             }
3158              
3159             =over
3160              
3161             =item make_object($name, $tstamp, $lat, $lon, $symbols, $speed, $course, $altitude, $alive, $usecompression, $posambiguity, $comment)
3162              
3163             Creates an APRS object. Returns a body of an APRS object, i.e. ";OBJECTNAM*DDHHMM/DDMM.hhN/DDDMM.hhW$CSE/SPDcomments..."
3164             or undef on error.
3165              
3166             Parameters:
3167              
3168             1st: object name, has to be valid APRS object name, does not need to be space-padded
3169             2nd: object timestamp as a unix timestamp, or zero to use current time
3170             3rd: object latitude, decimal degrees
3171             4th: object longitude, decimal degrees
3172             5th: object symbol table (or overlay) and symbol code, two bytes if the given symbole length is zero (""), use point (//)
3173             6th: object speed, -1 if non-moving (km/h)
3174             7th: object course, -1 if non-moving
3175             8th: object altitude, -10000 or less if not used
3176             9th: alive or dead object (0 == dead, 1 == alive)
3177             10th: compressed (1) or uncompressed (0)
3178             11th: position ambiguity (0..4)
3179             12th: object comment text
3180              
3181              
3182             Note: Course/speed/altitude/compression is not implemented.
3183              
3184             This function API will probably change in the near future. The long list of
3185             parameters should be changed to hash with named parameters.
3186              
3187             =back
3188              
3189             =cut
3190              
3191             sub make_object($$$$$$$$$$$$) {
3192             # FIXME: course/speed/altitude/compression not implemented
3193 0     0 1   my $name = shift @_;
3194 0           my $tstamp = shift @_;
3195 0           my $lat = shift @_;
3196 0           my $lon = shift @_;
3197 0           my $symbols = shift @_;
3198 0           my $speed = shift @_;
3199 0           my $course = shift @_;
3200 0           my $altitude = shift @_;
3201 0           my $alive = shift @_;
3202 0           my $usecompression = shift @_;
3203 0           my $posambiguity = shift @_;
3204 0           my $comment = shift @_;
3205              
3206 0           my $packetbody = ";";
3207              
3208             # name
3209 0 0         if ($name =~ /^([\x20-\x7e]{1,9})$/o) {
3210             # also pad with whitespace
3211 0           $packetbody .= $1 . " " x (9 - length($1));
3212             } else {
3213 0           return undef;
3214             }
3215              
3216             # dead/alive
3217 0 0         if ($alive == 1) {
    0          
3218 0           $packetbody .= "*";
3219             } elsif ($alive == 0) {
3220 0           $packetbody .= "_";
3221             } else {
3222 0           return undef;
3223             }
3224              
3225             # timestamp, hardwired for DHM
3226 0           my $aptime = make_timestamp($tstamp, 0);
3227 0 0         if (not(defined($aptime))) {
3228 0           return undef;
3229             } else {
3230 0           $packetbody .= $aptime;
3231             }
3232              
3233             # actual position
3234 0           my $posstring = make_position($lat, $lon, $speed, $course, $altitude, $symbols, $usecompression, $posambiguity);
3235 0 0         if (not(defined($posstring))) {
3236 0           return undef;
3237             } else {
3238 0           $packetbody .= $posstring;
3239             }
3240              
3241             # add comments to the end
3242 0           $packetbody .= $comment;
3243              
3244 0           return $packetbody;
3245             }
3246              
3247             =over
3248              
3249             =item make_timestamp($timestamp, $format)
3250              
3251             Create an APRS (UTC) six digit (DHM or HMS) timestamp from a unix timestamp.
3252             The first parameter is the unix timestamp to use, or zero to use
3253             current time. Second parameter should be one for
3254             HMS format, zero for DHM format.
3255              
3256             Returns a 7-character string (e.g. "291345z") or undef on error.
3257              
3258             =back
3259              
3260             =cut
3261              
3262             sub make_timestamp($$) {
3263 0     0 1   my $tstamp = shift @_;
3264 0           my $tformat = shift @_;
3265              
3266 0 0         if ($tstamp == 0) {
3267 0           $tstamp = time();
3268             }
3269              
3270 0           my ($day, $hour, $minute, $sec) = (gmtime($tstamp))[3,2,1,0];
3271 0 0         if (not(defined($day))) {
3272 0           return undef;
3273             }
3274              
3275 0           my $tstring = "";
3276 0 0         if ($tformat == 0) {
    0          
3277 0           $tstring = sprintf("%02d%02d%02dz", $day, $hour, $minute);
3278             } elsif ($tformat == 1) {
3279 0           $tstring = sprintf("%02d%02d%02dh", $hour, $minute, $sec);
3280             } else {
3281 0           return undef;
3282             }
3283 0           return $tstring;
3284             }
3285              
3286             =over
3287              
3288             =item make_position($lat, $lon, $speed, $course, $altitude, $symbols, $optionref)
3289              
3290             Creates an APRS position for position/object/item. Parameters:
3291              
3292             1st: latitude in decimal degrees
3293             2nd: longitude in decimal degrees
3294             3rd: speed in km/h, -1 == don't include
3295             4th: course in degrees, -1 == don't include. zero == unknown course, 360 == north
3296             5th: altitude in meters above mean sea level, -10000 or under == don't use
3297             6th: aprs symbol to use, first table/overlay and then code (two bytes). If string length is zero (""), uses default.
3298             7th: hash reference for options:
3299            
3300             "compressed": 1 for compressed format
3301             "ambiguity": Use amount (0..4) of position ambiguity. Note that position ambiguity and compression can't be used at the same time.
3302             "dao": Use !DAO! extension for improved precision
3303              
3304             Returns a string such as "1234.56N/12345.67E/CSD/SPD" or in
3305             compressed form "F*-X;n_Rv&{-A" or undef on error.
3306              
3307             Please note: course/speed/altitude are not supported yet, and neither is compressed format or position ambiguity.
3308              
3309             This function API will probably change in the near future. The long list of
3310             parameters should be changed to hash with named parameters.
3311              
3312             =back
3313              
3314             =cut
3315              
3316             sub make_position($$$$$$;$)
3317             {
3318             # FIXME: course/speed/altitude are not supported yet,
3319             # neither is compressed format or position ambiguity
3320 0     0 1   my($lat, $lon, $speed, $course, $altitude, $symbol, $options) = @_;
3321            
3322 0 0         if (!$options) {
3323 0           $options = { };
3324             }
3325            
3326 0 0         if ($options->{'ambiguity'}) {
3327             # can't be ambiguous and then add precision with !DAO!
3328 0           delete $options->{'dao'};
3329             }
3330              
3331 0 0 0       if ($lat < -89.99999 ||
      0        
      0        
3332             $lat > 89.99999 ||
3333             $lon < -179.99999 ||
3334             $lon > 179.99999) {
3335             # invalid location
3336 0           return undef;
3337             }
3338              
3339 0           my $symboltable = "";
3340 0           my $symbolcode = "";
3341 0 0         if (length($symbol) == 0) {
    0          
3342 0           $symboltable = "/";
3343 0           $symbolcode = "/";
3344             } elsif ($symbol =~ /^([\/\\A-Z0-9])([\x21-\x7b\x7d])$/o) {
3345 0           $symboltable = $1;
3346 0           $symbolcode = $2;
3347             } else {
3348 0           return undef;
3349             }
3350              
3351 0 0         if ($options->{'compression'}) {
3352 0           my $latval = 380926 * (90 - $lat);
3353 0           my $lonval = 190463 * (180 + $lon);
3354 0           my $latstring = "";
3355 0           my $lonstring = "";
3356 0           for (my $i = 3; $i >= 0; $i--) {
3357             # latitude character
3358 0           my $value = int($latval / (91 ** $i));
3359 0           $latval = $latval % (91 ** $i);
3360 0           $latstring .= chr($value + 33);
3361             # longitude character
3362 0           $value = int($lonval / (91 ** $i));
3363 0           $lonval = $lonval % (91 ** $i);
3364 0           $lonstring .= chr($value + 33);
3365             }
3366             # encode overlay character if it is a number
3367 0           $symboltable =~ tr/0-9/a-j/;
3368             # FIXME: no altitude/radiorange encoding
3369 0           my $retstring = $symboltable . $latstring . $lonstring . $symbolcode;
3370 0 0 0       if ($speed >= 0 && $course > 0 && $course <= 360) {
      0        
3371             # In APRS spec unknown course is zero normally (and north is 360),
3372             # but in compressed aprs north is zero and there is no unknown course.
3373             # So round course to nearest 4-degree section and remember
3374             # to do the 360 -> 0 degree transformation.
3375 0           my $cval = int(($course + 2) / 4);
3376 0 0         if ($cval > 89) {
3377 0           $cval = 0;
3378             }
3379 0           $retstring .= chr($cval + 33);
3380             # speed is in knots in compressed form. round to nearest integer
3381 0           my $speednum = int((log(($speed / $knot_to_kmh) + 1) / log(1.08)) + 0.5);
3382 0 0         if ($speednum > 89) {
3383             # limit top speed
3384 0           $speednum = 89;
3385             }
3386 0           $retstring .= chr($speednum + 33) . "A";
3387             } else {
3388 0           $retstring .= " A";
3389             }
3390 0           return $retstring;
3391              
3392             # normal position format
3393             } else {
3394             # convert to degrees and minutes
3395 0           my $isnorth = 1;
3396 0 0         if ($lat < 0.0) {
3397 0           $lat = 0 - $lat;
3398 0           $isnorth = 0;
3399             }
3400 0           my $latdeg = int($lat);
3401 0           my $latmin = ($lat - $latdeg) * 60;
3402 0           my $latmin_s;
3403             my $latmin_dao;
3404             # if we're doing DAO, round to 6 digits and grab the last 2 characters for DAO
3405 0 0         if ($options->{'dao'}) {
3406 0           $latmin_s = sprintf("%06.0f", $latmin * 10000);
3407 0           $latmin_dao = substr($latmin_s, 4, 2);
3408             } else {
3409 0           $latmin_s = sprintf("%04.0f", $latmin * 100);
3410             }
3411 0           my $latstring = sprintf("%02d%02d.%02d", $latdeg, substr($latmin_s, 0, 2), substr($latmin_s, 2, 2));
3412 0           my $posambiguity = $options->{'ambiguity'};
3413 0 0 0       if (defined $posambiguity && $posambiguity > 0 && $posambiguity <= 4) {
      0        
3414             # position ambiguity
3415 0 0         if ($posambiguity <= 2) {
    0          
    0          
3416             # only minute decimals are blanked
3417 0           $latstring = substr($latstring, 0, 7 - $posambiguity) . " " x $posambiguity;
3418             } elsif ($posambiguity == 3) {
3419 0           $latstring = substr($latstring, 0, 3) . " . ";
3420             } elsif ($posambiguity == 4) {
3421 0           $latstring = substr($latstring, 0, 2) . " . ";
3422             }
3423             }
3424 0 0         if ($isnorth == 1) {
3425 0           $latstring .= "N";
3426             } else {
3427 0           $latstring .= "S";
3428             }
3429 0           my $iseast = 1;
3430 0 0         if ($lon < 0.0) {
3431 0           $lon = 0 - $lon;
3432 0           $iseast = 0;
3433             }
3434 0           my $londeg = int($lon);
3435 0           my $lonmin = ($lon - $londeg) * 60;
3436 0           my $lonmin_s;
3437             my $lonmin_dao;
3438             # if we're doing DAO, round to 6 digits and grab the last 2 characters for DAO
3439 0 0         if ($options->{'dao'}) {
3440 0           $lonmin_s = sprintf("%06.0f", $lonmin * 10000);
3441 0           $lonmin_dao = substr($lonmin_s, 4, 2);
3442             } else {
3443 0           $lonmin_s = sprintf("%04.0f", $lonmin * 100);
3444             }
3445 0           my $lonstring = sprintf("%03d%s.%s", $londeg, substr($lonmin_s, 0, 2), substr($lonmin_s, 2, 2));
3446 0 0 0       if (defined $posambiguity && $posambiguity > 0 && $posambiguity <= 4) {
      0        
3447             # position ambiguity
3448 0 0         if ($posambiguity <= 2) {
    0          
    0          
3449             # only minute decimals are blanked
3450 0           $lonstring = substr($lonstring, 0, 8 - $posambiguity) . " " x $posambiguity;
3451             } elsif ($posambiguity == 3) {
3452 0           $lonstring = substr($lonstring, 0, 4) . " . ";
3453             } elsif ($posambiguity == 4) {
3454 0           $lonstring = substr($lonstring, 0, 3) . " . ";
3455             }
3456             }
3457 0 0         if ($iseast == 1) {
3458 0           $lonstring .= "E";
3459             } else {
3460 0           $lonstring .= "W";
3461             }
3462            
3463 0           my $retstring;
3464            
3465 0 0         if ($options->{'timestamp'}) {
3466 0           my($sec,$min,$hour) = gmtime($options->{'timestamp'});
3467 0           $retstring = sprintf('/%02d%02d%02dh', $hour, $min, $sec);
3468             } else {
3469 0           $retstring = '!';
3470             }
3471 0           $retstring .= $latstring . $symboltable . $lonstring . $symbolcode;
3472            
3473             # add course/speed, if given
3474 0 0 0       if (defined $speed && defined $course && $speed >= 0 && $course >= 0) {
      0        
      0        
3475             # convert speed to knots
3476 0           $speed = $speed / $knot_to_kmh;
3477 0 0         if ($speed > 999) {
3478 0           $speed = 999; # maximum speed
3479             }
3480 0 0         if ($course > 360) {
3481 0           $course = 0; # unknown course
3482             }
3483 0           $retstring .= sprintf("%03d/%03d", $course, $speed);
3484             }
3485            
3486 0 0         if (defined $altitude) {
3487 0           $altitude = $altitude / $feet_to_meters;
3488             # /A=(-\d{5}|\d{6})
3489 0 0         if ($altitude >= 0) {
3490 0           $retstring .= sprintf("/A=%06.0f", $altitude);
3491             } else {
3492 0           $retstring .= sprintf("/A=-%05.0f", $altitude * -1);
3493             }
3494             }
3495            
3496 0 0         if ($options->{'comment'}) {
3497 0           $retstring .= $options->{'comment'};
3498             }
3499            
3500 0 0         if ($options->{'dao'}) {
3501             # !DAO! extension, use Base91 format for best precision
3502             # /1.1 : scale from 0.99 to 0..90 for base91, int(... + 0.5): round to nearest integer
3503 0           my $dao = '!w' . chr(int($latmin_dao/1.1 + 0.5) + 33) . chr(int($lonmin_dao/1.1 + 0.5) + 33) . '!';
3504 0           $retstring .= $dao;
3505             }
3506            
3507 0           return $retstring;
3508             }
3509             }
3510              
3511              
3512             1;
3513             __END__