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   1833451 use strict;
  3         4  
  3         117  
55 3     3   13 use warnings;
  3         4  
  3         121  
56              
57             #use Data::Dumper;
58              
59             require Exporter;
60 3     3   1601 use YAML::Tiny;
  3         15417  
  3         198  
61 3     3   1880 use File::ShareDir ':ALL';
  3         15835  
  3         3596  
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.02';
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   29 my(@tcl) = @_;
226            
227 3         7 foreach my $t (@tcl) {
228 360         312 my $tocall = $t->{'tocall'};
229 360         254 delete $t->{'tocall'};
230 360 100       938 if ($tocall =~ /^[A-Z0-9]+$/) {
    50          
231 102         145 $fixed_dstcalls{$tocall} = $t;
232             } elsif ($tocall =~ /^([A-Z0-9]+)([n\?\*]+)([A-Z0-9]*)$/) {
233 258         221 my $prefix = $1;
234 258         172 my $r = $2; # glob (n for numbers, ?/* for single/multi)
235 258         180 my $suffix = $3;
236 258         212 $r =~ s/n/\\d/g;
237 258         339 $r =~ s/\?/./g;
238 258         209 $r =~ s/\*/.*/g;
239 258         256 $r = $prefix . '(' . $r . $suffix . ')';
240 258         388 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   7 my(@tcl) = @_;
251            
252 3         5 foreach my $t (@tcl) {
253 27         28 my $suffix = $t->{'suffix'};
254 27         19 delete $t->{'suffix'};
255 27         85 $mice_codes{$suffix} = $t;
256             }
257            
258             }
259              
260             sub _load()
261             {
262 3     3   8 my $src = dist_file('Ham-APRS-DeviceID', 'tocalls.yaml');
263 3         418 my $yaml = YAML::Tiny->new;
264 3         22 my $c = YAML::Tiny->read($src);
265 3 50       83511 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         9 $c = $c->[0];
271            
272 3         43 _load_tocalls(@{ $c->{'tocalls'} });
  3         50  
273 3         4 _load_mice(@{ $c->{'mice'} });
  3         15  
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   12 for (my $i = 0; $i <= $#dstcall_regexps; $i++) {
284 258         162 my $dmatch = $dstcall_regexps[$i];
285 258         179 my($regexp, $response) = @$dmatch;
286            
287 258         1618 my $compiled = qr/^$regexp$/;
288 258         596 $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   9 my @left;
299            
300 3         13 for (my $i = 0; $i <= $#dstcall_regexps; $i++) {
301 258         162 my $dmatch = $dstcall_regexps[$i];
302 258         192 my($regexp, $response, $compiled) = @$dmatch;
303            
304 258 50       466 if ($regexp =~ /^([^\(]{2,5})(\(.*)$/) {
305 258 100       357 if (!defined $regexp_prefix{$1} ) {
306 255         649 $regexp_prefix{$1} = [ $dmatch ];
307             } else {
308 3         4 push @{ $regexp_prefix{$1} }, $dmatch;
  3         12  
309             }
310             } else {
311 0         0 push @left, $dmatch;
312 0         0 warn "optimize: leaving $regexp over\n";
313             }
314             }
315            
316 3         10 @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 22     22 1 5008 my($p) = @_;
336            
337 22         34 $p->{'deviceid_resultcode'} = '';
338            
339 22 50       33 return _a_err($p, 'no_format') if (!defined $p->{'format'});
340 22 50       31 return _a_err($p, 'no_dstcall') if (!defined $p->{'dstcallsign'});
341            
342 22 100       39 if ($p->{'format'} eq 'mice') {
343             #warn Dumper($p);
344             #warn "comment: " . $p->{'comment'} . "\n";
345 17 50       20 if (!defined $p->{'comment'}) {
346 0         0 return _a_err($p, 'mice_no_comment');
347             }
348 17 100       110 if ($p->{'comment'} =~ s/^>(.*)=$/$1/) {
    100          
    100          
    100          
    100          
    50          
349 2         5 $p->{'deviceid'} = $response{'d72'};
350             } elsif ($p->{'comment'} =~ s/^>//) {
351 2         4 $p->{'deviceid'} = $response{'d7'};
352             } elsif ($p->{'comment'} =~ s/^\](.*)=$/$1/) {
353 1         2 $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         6 $p->{'deviceid'} = $response{'vx8'};
360             } elsif ($p->{'comment'} =~ /^([`\'])(.*)(..)$/) {
361 7         15 my($b, $s, $code) = ($1, $2, $3);
362            
363 7 50       10 if (defined $mice_codes{$code}) {
364 7         8 $p->{'deviceid'} = $mice_codes{$code};
365 7         8 $p->{'comment'} = $s;
366             } else {
367 0         0 $p->{'deviceid'} = $response{'unknown'};
368 0         0 $p->{'comment'} = $s . $code;
369             }
370 7 100       14 $p->{'messaging'} = 1 if ($b eq '`');
371             }
372            
373 17 50       23 if ($p->{'deviceid'}) {
374 17 100       29 $p->{'messaging'} = 1 if ($p->{'deviceid'}->{'messaging'});
375 17         24 return 1;
376             }
377            
378 0         0 return _a_err($p, 'mice_no_deviceid');
379             }
380            
381 5 100       10 if (defined $fixed_dstcalls{$p->{'dstcallsign'}}) {
382 2         4 $p->{'deviceid'} = $fixed_dstcalls{$p->{'dstcallsign'}};
383 2         4 return 1;
384             }
385            
386 3         5 foreach my $len (5, 4, 3, 2) {
387 7         9 my $prefix = substr($p->{'dstcallsign'}, 0, $len);
388 7 100       14 if (defined $regexp_prefix{$prefix}) {
389 3         2 foreach my $dmatch (@{ $regexp_prefix{$prefix} }) {
  3         7  
390 4         8 my($regexp, $response, $compiled) = @$dmatch;
391             #warn "trying '$regexp' against " . $p->{'dstcallsign'} . "\n";
392 4 100       26 if ($p->{'dstcallsign'} =~ $compiled) {
393             #warn "match!\n";
394 3         2 my %copy = %{ $response };
  3         14  
395 3         5 $p->{'deviceid'} = \%copy;
396            
397 3 50       5 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 3         10 return 1;
404             }
405             }
406             }
407             }
408            
409 0           return _a_err($p, 'no_id');
410             }
411              
412              
413             1;
414             __END__