File Coverage

blib/lib/Ham/APRS/DeviceID.pm
Criterion Covered Total %
statement 90 118 76.2
branch 36 60 60.0
condition n/a
subroutine 10 13 76.9
pod 2 2 100.0
total 138 193 71.5


line stmt bran cond sub pod time code
1              
2             package Ham::APRS::DeviceID;
3              
4             =head1 NAME
5              
6             Ham::APRS::DeviceID - APRS device identifier
7              
8             =head1 SYNOPSIS
9              
10             use Ham::APRS::FAP qw(parseaprs);
11             use Ham::APRS::DeviceID;
12             use Data::Dumper;
13            
14             my $aprspacket = 'OH2RDP>APZMDR,OH2RDG*,WIDE:!6028.51N/02505.68E#PHG7220/RELAY,WIDE, OH2AP Jarvenpaa';
15             my %packet;
16             my $retval = parseaprs($aprspacket, \%packet);
17             if ($retval == 1) {
18             Ham::APRS::DeviceID::identify(\%packet);
19            
20             if (defined $packet{'deviceid'}) {
21             print Dumper($packet{'deviceid'});
22             }
23             }
24              
25             =head1 ABSTRACT
26              
27             This module attempts to identify the manufacturer, model and
28             software version of an APRS transmitter. It looks at details found
29             in the parsed APRS packet (as provided by Ham::APRS::FAP) and updates
30             the hash with the identification information, if possible.
31              
32             The module comes with a device identification database, which is
33             simply a copy of the YAML master file maintained separately
34             at: L
35              
36             =head1 DESCRIPTION
37              
38             Unless a debugging mode is enabled, all errors and warnings are reported
39             through the API (as opposed to printing on STDERR or STDOUT), so that
40             they can be reported nicely on the user interface of an application.
41              
42             This module requires a reasonably recent L module,
43             L to load the device identification database and
44             L for finding it.
45              
46             =head1 EXPORT
47              
48             None by default.
49              
50             =head1 FUNCTION REFERENCE
51              
52             =cut
53              
54 3     3   110872 use strict;
  3         7  
  3         106  
55 3     3   13 use warnings;
  3         3  
  3         93  
56              
57             #use Data::Dumper;
58              
59             require Exporter;
60 3     3   1766 use YAML::Tiny;
  3         15530  
  3         202  
61 3     3   1854 use File::ShareDir ':ALL';
  3         18330  
  3         5406  
62              
63             our @ISA = qw(Exporter);
64              
65             # Items to export into callers namespace by default. Note: do not export
66             # names by default without a very good reason. Use EXPORT_OK instead.
67             # Do not simply export all your public functions/methods/constants.
68              
69             # This allows declaration use Ham::APRS::FAP ':all';
70             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
71             # will save memory.
72             ##our %EXPORT_TAGS = (
73             ## 'all' => [ qw(
74             ##
75             ## ) ],
76             ##);
77              
78             our @EXPORT_OK = (
79             ## @{ $EXPORT_TAGS{'all'} },
80             '&identify',
81             );
82              
83             ##our @EXPORT = qw(
84             ##
85             ##);
86              
87             our $VERSION = '2.01';
88              
89             # Preloaded methods go here.
90              
91             # no debugging by default
92             my $debug = 0;
93              
94             my %result_messages = (
95             'unknown' => 'Unsupported packet format',
96             'no_dstcall' => 'Packet has no destination callsign',
97             'no_format' => 'Packet has no defined format',
98             'mice_no_comment' => 'Mic-e packet with no comment defined',
99             'mice_no_deviceid' => 'Mic-e packet with no device identifier in comment',
100             'no_id' => 'No device identification found',
101             );
102              
103             # these functions are used to report warnings and parser errors
104             # from the module
105              
106             sub _a_err($$;$)
107             {
108 0     0   0 my ($rethash, $errcode, $val) = @_;
109            
110 0         0 $rethash->{'deviceid_resultcode'} = $errcode;
111 0 0       0 $rethash->{'deviceid_resultmsg'}
112             = defined $result_messages{$errcode}
113             ? $result_messages{$errcode} : $errcode;
114            
115 0 0       0 $rethash->{'deviceid_resultmsg'} .= ': ' . $val if (defined $val);
116            
117 0 0       0 if ($debug > 0) {
118 0         0 warn "Ham::APRS::DeviceID ERROR $errcode: " . $rethash->{'deviceid_resultmsg'} . "\n";
119             }
120            
121 0         0 return 0;
122             }
123              
124             sub _a_warn($$;$)
125             {
126 0     0   0 my ($rethash, $errcode, $val) = @_;
127            
128 0         0 push @{ $rethash->{'deviceid_warncodes'} }, $errcode;
  0         0  
129            
130 0 0       0 if ($debug > 0) {
131 0 0       0 warn "Ham::APRS::DeviceID WARNING $errcode: "
    0          
132             . (defined $result_messages{$errcode}
133             ? $result_messages{$errcode} : $errcode)
134             . (defined $val ? ": $val" : '')
135             . "\n";
136             }
137            
138 0         0 return 0;
139             }
140              
141              
142             =over
143              
144             =item debug($enable)
145              
146             Enables (debug(1)) or disables (debug(0)) debugging.
147              
148             When debugging is enabled, warnings and errors are emitted using the warn() function,
149             which will normally result in them being printed on STDERR. Succesfully
150             printed packets will be also printed on STDOUT in a human-readable
151             format.
152              
153             When debugging is disabled, nothing will be printed on STDOUT or STDERR -
154             all errors and parsing results need to be collected from the returned
155             hash reference.
156              
157             =back
158              
159             =cut
160              
161             sub debug($)
162             {
163 0     0 1 0 my $dval = shift @_;
164 0 0       0 if ($dval) {
165 0         0 $debug = 1;
166             } else {
167 0         0 $debug = 0;
168             }
169             }
170              
171             #
172             # Prebaked responses for "legacy" devices, including the VX-8 which has a
173             # space (0x20) character as the last byte, which commonly gets eaten by
174             # UI-View
175             #
176             my %response = (
177             'd7' => {
178             'vendor' => 'Kenwood',
179             'model' => 'TH-D7',
180             'class' => 'ht',
181             'messaging' => 1,
182             },
183             'd72' => {
184             'vendor' => 'Kenwood',
185             'model' => 'TH-D72',
186             'class' => 'ht',
187             'messaging' => 1,
188             },
189             'd700' => {
190             'vendor' => 'Kenwood',
191             'model' => 'TM-D700',
192             'class' => 'rig',
193             'messaging' => 1,
194             },
195             'd710' => {
196             'vendor' => 'Kenwood',
197             'model' => 'TM-D710',
198             'class' => 'rig',
199             'messaging' => 1,
200             },
201             'vx8' => {
202             'vendor' => 'Yaesu',
203             'model' => 'VX-8',
204             'class' => 'ht',
205             'messaging' => 1,
206             },
207             'unknown' => {
208             'vendor' => 'Unknown',
209             'model' => 'Other Mic-E',
210             }
211             );
212              
213             my %mice_codes;
214             my %fixed_dstcalls;
215             my @dstcall_regexps;
216              
217             my %regexp_prefix;
218              
219             #
220             # init: load YAML definitions
221             #
222              
223             sub _load_tocalls(@)
224             {
225 3     3   40 my(@tcl) = @_;
226            
227 3         9 foreach my $t (@tcl) {
228 357         387 my $tocall = $t->{'tocall'};
229 357         324 delete $t->{'tocall'};
230 357 100       1105 if ($tocall =~ /^[A-Z0-9]+$/) {
    50          
231 99         191 $fixed_dstcalls{$tocall} = $t;
232             } elsif ($tocall =~ /^([A-Z0-9]+)([n\?\*]+)([A-Z0-9]*)$/) {
233 258         295 my $prefix = $1;
234 258         215 my $r = $2; # glob (n for numbers, ?/* for single/multi)
235 258         214 my $suffix = $3;
236 258         237 $r =~ s/n/\\d/g;
237 258         433 $r =~ s/\?/./g;
238 258         273 $r =~ s/\*/.*/g;
239 258         326 $r = $prefix . '(' . $r . $suffix . ')';
240 258         516 push @dstcall_regexps, [ $r, $t ];
241             } else {
242 0         0 die "tocall '$tocall' too hard to parse";
243             }
244             }
245            
246             }
247              
248             sub _load_mice(@)
249             {
250 3     3   9 my(@tcl) = @_;
251            
252 3         7 foreach my $t (@tcl) {
253 27         43 my $suffix = $t->{'suffix'};
254 27         29 delete $t->{'suffix'};
255 27         102 $mice_codes{$suffix} = $t;
256             }
257            
258             }
259              
260             sub _load()
261             {
262 3     3   13 my $src = dist_file('Ham-APRS-DeviceID', 'tocalls.yaml');
263 3         541 my $yaml = YAML::Tiny->new;
264 3         27 my $c = YAML::Tiny->read($src);
265 3 50       113898 if (!defined $c) {
266 0         0 die "Failed to read in $src: " . YAML::Tiny->errstr . "\n";
267             }
268            
269             # get the first document of YAML
270 3         12 $c = $c->[0];
271            
272 3         39 _load_tocalls(@{ $c->{'tocalls'} });
  3         57  
273 3         6 _load_mice(@{ $c->{'mice'} });
  3         18  
274              
275             }
276              
277             #
278             # init code: compile the regular expressions to speed up matching
279             #
280              
281             sub _compile_regexps()
282             {
283 3     3   14 for (my $i = 0; $i <= $#dstcall_regexps; $i++) {
284 258         263 my $dmatch = $dstcall_regexps[$i];
285 258         273 my($regexp, $response) = @$dmatch;
286            
287 258         2086 my $compiled = qr/^$regexp$/;
288 258         747 $dstcall_regexps[$i] = [ $regexp, $response, $compiled ];
289             }
290             }
291              
292             #
293             # init: optimize regexps with an initial hash lookup
294             #
295              
296             sub _optimize_regexps()
297             {
298 3     3   5 my @left;
299            
300 3         16 for (my $i = 0; $i <= $#dstcall_regexps; $i++) {
301 258         194 my $dmatch = $dstcall_regexps[$i];
302 258         231 my($regexp, $response, $compiled) = @$dmatch;
303            
304 258 50       561 if ($regexp =~ /^([^\(]{2,5})(\(.*)$/) {
305 258 100       397 if (!defined $regexp_prefix{$1} ) {
306 255         798 $regexp_prefix{$1} = [ $dmatch ];
307             } else {
308 3         7 push @{ $regexp_prefix{$1} }, $dmatch;
  3         15  
309             }
310             } else {
311 0         0 push @left, $dmatch;
312 0         0 warn "optimize: leaving $regexp over\n";
313             }
314             }
315            
316 3         17 @dstcall_regexps = @left;
317             }
318              
319             _load();
320             _compile_regexps();
321             _optimize_regexps();
322              
323             =over
324              
325             =item identify($hashref)
326              
327             Tries to identify the device.
328              
329             =back
330              
331             =cut
332              
333             sub identify($)
334             {
335 21     21 1 7055 my($p) = @_;
336            
337 21         35 $p->{'deviceid_resultcode'} = '';
338            
339 21 50       55 return _a_err($p, 'no_format') if (!defined $p->{'format'});
340 21 50       38 return _a_err($p, 'no_dstcall') if (!defined $p->{'dstcallsign'});
341            
342 21 100       48 if ($p->{'format'} eq 'mice') {
343             #warn Dumper($p);
344             #warn "comment: " . $p->{'comment'} . "\n";
345 17 50       34 if (!defined $p->{'comment'}) {
346 0         0 return _a_err($p, 'mice_no_comment');
347             }
348 17 100       163 if ($p->{'comment'} =~ s/^>(.*)=$/$1/) {
    100          
    100          
    100          
    100          
    50          
349 2         5 $p->{'deviceid'} = $response{'d72'};
350             } elsif ($p->{'comment'} =~ s/^>//) {
351 2         5 $p->{'deviceid'} = $response{'d7'};
352             } elsif ($p->{'comment'} =~ s/^\](.*)=$/$1/) {
353 1         4 $p->{'deviceid'} = $response{'d710'};
354             } elsif ($p->{'comment'} =~ s/^\]//) {
355 2         4 $p->{'deviceid'} = $response{'d700'};
356             } elsif ($p->{'comment'} =~ s/^`(.*)_\s*$/$1/) {
357             # vx-8 has a space as the last character, which commonly gets eaten by ui-view,
358             # so handle it with a relaxed regexp
359 3         10 $p->{'deviceid'} = $response{'vx8'};
360             } elsif ($p->{'comment'} =~ /^([`\'])(.*)(..)$/) {
361 7         24 my($b, $s, $code) = ($1, $2, $3);
362            
363 7 50       15 if (defined $mice_codes{$code}) {
364 7         13 $p->{'deviceid'} = $mice_codes{$code};
365 7         15 $p->{'comment'} = $s;
366             } else {
367 0         0 $p->{'deviceid'} = $response{'unknown'};
368 0         0 $p->{'comment'} = $s . $code;
369             }
370 7 100       19 $p->{'messaging'} = 1 if ($b eq '`');
371             }
372            
373 17 50       35 if ($p->{'deviceid'}) {
374 17 100       38 $p->{'messaging'} = 1 if ($p->{'deviceid'}->{'messaging'});
375 17         38 return 1;
376             }
377            
378 0         0 return _a_err($p, 'mice_no_deviceid');
379             }
380            
381 4 100       12 if (defined $fixed_dstcalls{$p->{'dstcallsign'}}) {
382 2         5 $p->{'deviceid'} = $fixed_dstcalls{$p->{'dstcallsign'}};
383 2         5 return 1;
384             }
385            
386 2         4 foreach my $len (4, 3, 5, 2) {
387 5         7 my $prefix = substr($p->{'dstcallsign'}, 0, $len);
388 5 100       11 if (defined $regexp_prefix{$prefix}) {
389 2         2 foreach my $dmatch (@{ $regexp_prefix{$prefix} }) {
  2         7  
390 3         7 my($regexp, $response, $compiled) = @$dmatch;
391             #warn "trying '$regexp' against " . $p->{'dstcallsign'} . "\n";
392 3 100       24 if ($p->{'dstcallsign'} =~ $compiled) {
393             #warn "match!\n";
394 2         2 my %copy = %{ $response };
  2         14  
395 2         4 $p->{'deviceid'} = \%copy;
396            
397 2 50       4 if ($response->{'version_regexp'}) {
398             #warn "version_regexp set: $1 from " . $p->{'dstcallsign'} . " using " . $regexp . "\n";
399 0         0 $p->{'deviceid'}->{'version'} = $1;
400 0         0 delete $p->{'deviceid'}->{'version_regexp'};
401             }
402            
403 2         6 return 1;
404             }
405             }
406             }
407             }
408            
409 0           return _a_err($p, 'no_id');
410             }
411              
412              
413             1;
414             __END__