File Coverage

blib/lib/Ham/Packet/DXSpider.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             #!usr/bin/perl
2             #
3             # Class : Ham::Packet::DXSpider
4             # Purpose: Provides a remote interface to the DXSpider DX Cluster software.
5             # Author : Bruce James (custard@cpan.org)
6             # Date : 7th April 2012
7             #
8             package Ham::Packet::DXSpider;
9 1     1   29756 use strict;
  1         3  
  1         42  
10 1     1   5 use warnings;
  1         2  
  1         30  
11 1     1   1077 use IO::Handle;
  1         8358  
  1         59  
12 1     1   1085 use IO::Socket;
  1         19423  
  1         4  
13 1     1   1755 use POSIX;
  1         8207  
  1         9  
14 1     1   4280 use Moose;
  0            
  0            
15              
16             our $VERSION="0.04";
17              
18             =pod
19              
20             =head1 NAME
21              
22             Ham::Packet::DXSpider - Receives DX Spots from the DXCluster
23              
24             =head1 SYNOPSIS
25              
26             # Construct object using address and optional port
27             my $dx=Ham::Packet::DXSpider->new(
28             callsign => 'your callsign',
29             address => 'dxcluster address',
30             port => 'port',
31             );
32              
33             # Construct object using supplied IO::Handle
34             my $dx=Ham::Packet::DXSpider->new(
35             callsign => 'your callsign',
36             handle => IO::Handle
37             );
38              
39             # Set a handler for received private messages
40             $dx->addPrivateMessageHandler( sub {
41             my %args=@_;
42             my $from= $args{from} || '';
43             my $to= $args{to} || '';
44             my $body= $args{body} || 'no message';
45             my $subject= $args{subject} || 'no subject';
46             my $time= $args{time} || gmtime(time());
47             } );
48              
49             # Set a handler for received DX messages
50             $dx->addDXMessageHandler( sub {
51             my %args=@_;
52             my $from= $args{from};
53             my $frequency= $args{frequency};
54             my $message= $args{message};
55             my $heard= $args{heard};
56             my $time= $args{time};
57             my $locator= $args{locator};
58             } );
59              
60             # Add a handler for collecting statistics on DX spots received.
61             $dx->addStatsHandler( sub {
62             my %args=@_;
63             my $from= $args{from};
64             my $freq= $args{frequency};
65             my $message= $args{message};
66             my $heard= $args{heard};
67             my $time= $args{time};
68             my $locator= $args{locator};
69             } );
70              
71             # Send a message
72             $dx->sendPrivate( $to, $subject, $message );
73              
74             $dx->start();
75              
76             =head1 DESCRIPTION
77              
78             =head2 CONSTRUCTOR
79              
80             new( callsign => 'your callsign', address => 'dxcluster address', port => 'port', handle => IO::Handle );
81              
82             Create a new DXSpider object for the specified callsign. If address and optionally port are
83             specified, this will also open the connection to the DXSPIDER server.
84              
85             Address can also be an already open IO::Handle object, in which case port becomes meaningless.
86              
87             =head2 METHODS
88              
89             =cut
90              
91              
92             has 'callsign' => (is=>'ro');
93             has 'address' => (is=>'ro');
94             has 'handle' => (is=>'rw');
95             has 'port' => (is=>'ro');
96              
97             has 'private_message_handler' => (is=>'rw', default => sub { [] } );
98             has 'dx_message_handler' => (is=>'rw', default => sub { [] } );
99             has 'stats_handler' => (is=>'rw', default => sub { [] } );
100              
101             has 'pending_messages' => (is=>'rw', default => sub { [] } );
102              
103              
104             =head2 BUILD()
105              
106             Moose builder. Called after construction. Opens the handle if necessary.
107              
108             =cut
109             sub BUILD {
110             my $self = shift;
111            
112             $self->open();
113             }
114              
115              
116             =head2 open()
117              
118             Opens a connection to a DXSPIDER server located at the address and port specified.
119             Address can also be an already open IO::Handle object, in which case port becomes meaningless.
120              
121             =cut
122             sub open {
123             my $self=shift;
124              
125             return if ( ref($self->handle) && ($self->handle->isa( 'IO::Handle' )));
126             if ($self->address) {
127             $self->handle( IO::Socket::INET->new(
128             PeerAddr => $self->address,
129             PeerPort => $self->port
130             ));
131             }
132             }
133              
134              
135             =head2 addStatsHandler( $codeRef )
136              
137             Adds a code reference to a function that can be used to collect statistics of the
138             received DX spot messages. Only DX spot messages will be sent to this handler.
139              
140             Handlers are added to a list and will be called in turn when a new DX spot message arrives.
141              
142             =cut
143             sub addStatsHandler {
144             my $self= shift;
145             my $handler= shift;
146              
147             return unless( ref($handler) eq 'CODE' );
148              
149             push( @{$self->stats_handler}, $handler );
150              
151             return $handler;
152             }
153              
154             =head2 addDXMessageHandler( $codeRef )
155              
156             Adds a code reference to a function that handles DX spot messages.
157             Handlers are added to a list and will be called in turn when a DX spot message arrives.
158              
159             =cut
160             sub addDXMessageHandler {
161             my $self= shift;
162             my $handler= shift;
163             return unless( ref($handler) eq 'CODE' );
164              
165             push( @{$self->dx_message_handler}, $handler );
166              
167             return $handler;
168             }
169              
170             =head2 addPrivateMessageHandler( $codeRef )
171              
172             Adds a code reference to a function that handles Private messages directed to the logged
173             in callsign.
174             Handlers are added to a list and will be called in turn when a new message arrives.
175              
176             =cut
177             sub addPrivateMessageHandler {
178             my $self= shift;
179             my $handler= shift;
180             return unless( ref($handler) eq 'CODE' );
181              
182             push( @{$self->private_message_handler}, $handler );
183              
184             return $handler;
185             }
186              
187             =head2 start()
188              
189             Continuously polls the DXSPIDER for new events. Returns if the handle for the connection
190             closes or becomes undefined for whatever reason.
191              
192             =cut
193             sub start {
194             # Continuously poll the handle and process responses
195             my $self= shift;
196              
197             while ( $self->{handle} ) {
198             $self->process( $self->{handle} );
199             }
200              
201             return $self->{handle};
202             }
203              
204             =head2 poll()
205              
206             Polls the DXSPIDER once for a new event. This will block until something is received and the
207             current transaction is completed.
208              
209             TODO: Probably would be a candidate for a timeout when I get time.
210              
211             =cut
212             sub poll {
213             # Poll the handle once, and process only that response.
214             my $self= shift;
215              
216             if ( $self->{handle} ) {
217             $self->process( $self->{handle} );
218             }
219            
220             return $self->{handle};
221             }
222              
223             =head2 sendPrivate( $to, $subject, $body )
224              
225             Sends a private message to the callsign specified.
226              
227             =cut
228             sub sendPrivate {
229             # Queues a private message for sending.
230             my $self= shift;
231             my $to= shift;
232             my $subject= shift;
233             my $body= shift;
234              
235             push @{$self->pending_messages}, {
236             to => $to,
237             subject => $subject,
238             body => $body,
239             };
240              
241             return scalar @{$self->pending_messages};
242             }
243              
244              
245              
246             =head2 FUNCTIONS
247              
248             Three functions are available for use as default handlers for testing and debugging purposes.
249              
250             =over
251              
252             =item defaultDXMessageHandler()
253              
254             =item defaultStatsHandler()
255              
256             =item defaultPrivateMessageHandler()
257              
258             =back
259              
260             =cut
261              
262             sub defaultStatsHandler {
263             my %args=@_;
264             my $from= $args{from};
265             my $freq= $args{frequency};
266             my $message= $args{message};
267             my $heard= $args{heard};
268             my $time= $args{time};
269             my $locator= $args{locator};
270              
271             our $BAND={
272             80 => [3000,4000],
273             40 => [7000,7200],
274             20 => [14000,14300],
275             10 => [28000,29000]
276             } unless $BAND;
277             our $COUNT;
278              
279             for my $key (keys %{$BAND}) {
280             my $span=$BAND->{$key};
281             my ($min,$max)=@{$span};
282             if ($freq >= $min and $freq <= $max) {
283             $COUNT->{$key}++;
284             }
285             }
286             for my $key (keys %{$COUNT}) {
287             print( $key." ".$COUNT->{$key}."\n" );
288             }
289             }
290              
291             sub defaultDXMessageHandler {
292             my %args=@_;
293             my $from= $args{from};
294             my $freq= $args{frequency};
295             my $message= $args{message};
296             my $heard= $args{heard};
297             my $time= $args{time};
298             my $locator= $args{locator};
299              
300             print( "Heard: $heard on $freq by $from at $time location $locator $message\n" );
301             }
302              
303             sub defaultPrivateMessageHandler {
304             my %args=@_;
305             my $from= $args{from} || '';
306             my $to= $args{to} || '';
307             my $body= $args{body} || 'no message';
308             my $subject= $args{subject} || 'no subject';
309             my $time= $args{time} || gmtime(time());
310              
311             print( "Private to $to from $from at $time subject $subject message $body\n" );
312             }
313              
314             ##############
315             ## PRIVATES ##
316             ##############
317              
318             =head1 Private Methods
319              
320             =over
321              
322             =item dispatchStats()
323              
324             =item dispatchDXMessage()
325              
326             =item dispatchPrivateMessage()
327              
328             =item _sendPrivate()
329              
330             =item processPending()
331              
332             =item process()
333              
334             =back
335              
336             =cut
337              
338             sub dispatchStats {
339             # PRIVATE: Dispatches the message to all the handlers
340             my $self= shift;
341             my %args=@_;
342             # Fire each handler in turn.
343             foreach my $handler (@{$self->stats_handler}) {
344             &{$handler}( %args );
345             }
346             }
347              
348             sub dispatchDXMessage {
349             # PRIVATE: Dispatches the message to all the handlers
350             my $self= shift;
351             my %args=@_;
352             my $from= $args{from};
353             my $frequency= $args{frequency};
354             my $message= $args{message};
355             my $heard= $args{heard};
356             my $time= $args{time};
357             my $locator= $args{locator};
358              
359             # Fire each handler in turn.
360             foreach my $handler (@{$self->dx_message_handler}) {
361             &{$handler}( %args );
362             }
363             }
364              
365             sub dispatchPrivateMessage {
366             # PRIVATE: Dispatches the message to all the handlers
367             my $self= shift;
368             my %args=@_;
369             my $time= $args{time};
370             my $from= $args{from};
371             my $to= $args{to};
372             my $subject= $args{subject};
373             my $body= $args{body};
374              
375             foreach my $handler (@{$self->private_message_handler}) {
376             &{$handler}( %args );
377             }
378             }
379              
380             sub _sendPrivate {
381             # Sends a message using the supplied hashRef
382             my $self=shift;
383             my $hashRef = shift; # hashRef
384             return unless ref($hashRef);
385             my $to = $hashRef->{to};
386             my $subject = $hashRef->{subject};
387             my $body = $hashRef->{body};
388              
389             if ( $self->{handle} ) {
390             my $fd=$self->{handle};
391              
392             print( $fd "sp $to\n" );
393             print( $fd "$subject\n" );
394             print( $fd "$body\n" );
395             print( $fd '/ex'."\n" );
396             }
397              
398             return $self->{handle};
399             }
400              
401             sub processPending {
402             # Sends any pending messages from the queue
403             my $self=shift;
404              
405             while( my $message = shift( @{$self->pending_messages}) ) {
406             $self->_sendPrivate( $message );
407             }
408             }
409              
410             sub process {
411             # PRIVATE: Watches the connection, and parses dx spider results.
412             # Returns when any messages have been read and the spider
413             # has returned to a prompt.
414             # returns 1 if idle, Or undef if a timeout occurs (not yet implemented)
415             my $self=shift;
416             my $fd=shift;
417              
418             $fd->blocking(0);
419             my %message=( type=>'start poll' );
420             while ( %message ) {
421             last unless $fd;
422             $_ = <$fd>;
423             unless($_) {
424             #Wait for more input if none available.
425             sleep 1;
426             next;
427             }
428             chomp;
429             s/(\r|\n)//g;
430              
431             (/^Enter your \*real\* Callsign to continue|login:/i) && do {
432             # Login
433             sleep 1;
434             print( $fd $self->{callsign}."\n" );
435             next;
436             };
437              
438             (/^DX de ([^:]+):\s+([\d.]+)\s+(\S+)\s+(.*)$/) && do {
439             #DX de I5WEA: 7058.0 IV3/IK3SSW/P DCI-UD036 Op. TONY
440             #DX de PA3GDY: 50108.6 UX2KA/P 599 KO31<es>JO21 1350Z JO21
441             my ($from,$freq,$heard,$comment)=($1,$2,$3,$4);
442             my ($locator,$time)=('',gmtime(time));
443             $comment=~s/([A-Z]{2}\d{2})\s*/$locator=$1,''/e;
444             $comment=~s/(\d{4}[A-Z]{1})\s*/$time=$1,''/e;
445              
446             my %dx = (
447             time => $time,
448             from => $from,
449             frequency => $freq,
450             heard => $heard,
451             locator => $locator,
452             message => $comment
453             );
454             $self->dispatchStats( %dx );
455             $self->dispatchDXMessage( %dx );
456             $self->processPending(); # Process any mesages to send
457             next;
458             };
459              
460             (/dxspider >$/i) && do {
461             #M1BSC de GB7EDX 16-Jun-2005 0857Z dxspider >
462             # If we're back at a prompt, then no messages are being handled!
463             if (%message && ($message{type} eq 'private')) {
464             # Got a private message
465             $self->dispatchPrivateMessage( %message );
466             };
467              
468             $self->processPending(); # Process any mesages to send
469              
470             %message=(); # Clear the received message
471             next;
472             };
473              
474             (/^New mail has arrived for you/i) && do {
475             #New mail has arrived for you
476             print( $fd "read\n" );
477             next;
478             };
479              
480             (/^Msg: (\d+) From: (\S+) Date:\s+(\S+)\s+(\S+) Subj: (.*)$/) && do {
481             # Read subject and start body reading
482             #Msg: 3960 From: M1BSC Date: 3-Jun 1022Z Subj: m1bsc 145.450 13:00 5/9
483             my ($id,$from,$date,$time,$subject)=($1,$2,$3,$4,$5);
484             %message=(
485             type => 'private',
486             from => $from,
487             to => $self->{callsign},
488             subject => $subject,
489             body => '',
490             time => $date,
491             );
492             next;
493             };
494              
495             (%message && $message{type} =~ /(private|public)/) && do {
496             # Read body text
497             $message{body}.=$_."\n";
498             next;
499             };
500              
501              
502             (/^To ([\w\d]+) de /) && do {
503             #To ALL de 9A8A: LZ2HM: FB..here band closed, for now..?
504             next;
505             };
506              
507             }
508              
509             return 1;
510             }
511              
512             =head1 PREREQUISITES
513              
514             =over
515              
516             =item IO::Handle
517              
518             =item IO::Socket
519              
520             =item IO::Socket::INET
521              
522             =item Moose
523              
524             =item POSIX
525              
526             =item Test::More
527              
528             =back
529              
530             =head1 OSNAMES
531              
532             Unix or Unix-likes.
533              
534             =head1 AUTHOR
535              
536             Bruce James - custard@cpan.org
537              
538             =head1 VERSION
539              
540             0.04
541              
542             =head1 COPYRIGHT
543              
544             Copyright 2012, Bruce James
545              
546             =head1 LICENSE
547              
548             This library is free software; you can redistribute it and/or modify
549             it under the same terms as Perl itself.
550              
551             =cut
552              
553             1;
554