File Coverage

blib/lib/Ham/APRS/FAP.pm
Criterion Covered Total %
statement 646 1377 46.9
branch 339 830 40.8
condition 83 237 35.0
subroutine 27 45 60.0
pod 14 14 100.0
total 1109 2503 44.3


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