File Coverage

blib/lib/Ham/Packet/DXSpider.pm
Criterion Covered Total %
statement 18 185 9.7
branch 0 40 0.0
condition 0 23 0.0
subroutine 6 23 26.0
pod 16 16 100.0
total 40 287 13.9


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