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__ |