Add latest RBN chnages and data stats
[spider.git] / perl / RBN.pm
1 #
2 # The RBN connection system
3 #
4 # Copyright (c) 2020 Dirk Koopman G1TLH
5 #
6
7 use warnings;
8 use strict;
9
10 package RBN;
11
12 use 5.10.1;
13
14 use lib qw {.};
15
16 use DXDebug;
17 use DXUtil;
18 use DXLog;
19 use DXUser;
20 use DXChannel;
21 use Math::Round qw(nearest);
22 use Date::Parse;
23 use Time::HiRes qw(gettimeofday);
24 use Spot;
25 use DXJSON;
26 use IO::File;
27
28 use constant {
29                           ROrigin => 0,
30                           RQrg => 1,
31                           RCall => 2,
32                           RMode => 3,
33                           RStrength => 4,
34                           RTime => 5,
35                           RUtz => 6,
36                           Respot => 7,
37                           RQra => 8,
38                           RSpotData => 9,
39                          };
40
41 use constant {
42                           SQrg => 0,
43                           SCall => 1,
44                           STime => 2,
45                           SComment => 3,
46                           SOrigin => 4,
47                           SZone => 11,
48                          };
49 use constant {
50                           OQual => 0,
51                           OAvediff => 1,
52                           OSpare => 2,
53                           ODiff => 3,
54                          };
55 use constant {
56                           CTime => 0,
57                           CQual => 1,
58                           CData => 2,
59                          };
60
61
62 our $DATA_VERSION = 1;
63
64 our @ISA = qw(DXChannel);
65
66 our $startup_delay = 5*60;              # don't send anything out until this timer has expired
67                                 # this is to allow the feed to "warm up" with duplicates
68                                 # so that the "big rush" doesn't happen.
69
70 our $minspottime = 30*60;               # the time between respots of a callsign - if a call is
71                                 # still being spotted (on the same freq) and it has been
72                                 # spotted before, it's spotted again after this time
73                                 # until the next minspottime has passed.
74
75 our $beacontime = 5*60;                 # same as minspottime, but for beacons (and shorter)
76
77 our $dwelltime = 10;                    # the amount of time to wait for duplicates before issuing
78                                 # a spot to the user (no doubt waiting with bated breath).
79
80 our $filterdef = $Spot::filterdef; # we use the same filter as the Spot system. Can't think why :-).
81
82 my $spots;                                              # the GLOBAL spot cache
83
84 my %runtime;                                    # how long each channel has been running
85
86 our $cachefn = localdata('rbn_cache');
87 our $cache_valid = 4*60;                # The cache file is considered valid if it is not more than this old
88
89 our $maxqrgdiff = 10;                   # the maximum
90 our $minqual = 2;                               # the minimum quality we will accept for output
91
92 my $json;
93 my $noinrush = 0;                               # override the inrushpreventor if set
94
95 sub init
96 {
97         $json = DXJSON->new;
98         if (check_cache()) {
99                 $noinrush = 1;
100         } else {
101                 $spots = {VERSION=>$DATA_VERSION};
102         }
103         if (defined $DB::VERSION) {
104                 $noinrush = 1;
105                 $json->indent(1);
106         }
107         
108 }
109
110 sub new 
111 {
112         my $self = DXChannel::alloc(@_);
113
114         # routing, this must go out here to prevent race condx
115         my $pkg = shift;
116         my $call = shift;
117
118         $self->{last} = 0;
119         $self->{noraw} = 0;
120         $self->{nospot} = 0;
121         $self->{nouser} = {};
122         $self->{norbn} = 0;
123         $self->{noraw10} = 0;
124         $self->{nospot10} = 0;
125         $self->{nouser10} = {};
126         $self->{norbn10} = 0;
127         $self->{nospothour} = 0;
128         $self->{nouserhour} = {};
129         $self->{norbnhour} = 0;
130         $self->{norawhour} = 0;
131         $self->{sort} = 'N';
132         $self->{lasttime} = $main::systime;
133         $self->{minspottime} = $minspottime;
134         $self->{beacontime} = $beacontime;
135         $self->{showstats} = 0;
136         $self->{pingint} = 0;
137         $self->{nopings} = 0;
138         $self->{queue} = {};
139
140         return $self;
141 }
142
143 sub start
144
145         my ($self, $line, $sort) = @_;
146         my $user = $self->{user};
147         my $call = $self->{call};
148         my $name = $user->{name};
149                 
150         # log it
151         my $host = $self->{conn}->peerhost;
152         $host ||= "unknown";
153         $self->{hostname} = $host;
154
155         $self->{name} = $name ? $name : $call;
156         $self->state('prompt');         # a bit of room for further expansion, passwords etc
157         $self->{lang} = $user->lang || $main::lang || 'en';
158         if ($line =~ /host=/) {
159                 my ($h) = $line =~ /host=(\d+\.\d+\.\d+\.\d+)/;
160                 $line =~ s/\s*host=\d+\.\d+\.\d+\.\d+// if $h;
161                 unless ($h) {
162                         ($h) = $line =~ /host=([\da..fA..F:]+)/;
163                         $line =~ s/\s*host=[\da..fA..F:]+// if $h;
164                 }
165                 if ($h) {
166                         $h =~ s/^::ffff://;
167                         $self->{hostname} = $h;
168                 }
169         }
170         $self->{width} = 80 unless $self->{width} && $self->{width} > 80;
171         $self->{consort} = $line;       # save the connection type
172
173         LogDbg('DXCommand', "$call connected from $self->{hostname}");
174
175         # set some necessary flags on the user if they are connecting
176         $self->{registered} = 1;
177         # sort out privilege reduction
178         $self->{priv} = 0;
179
180         # get the filters
181         my $nossid = $call;
182         $nossid =~ s/-\d+$//;
183
184         $self->{inrbnfilter} = Filter::read_in('rbn', $call, 1) 
185                 || Filter::read_in('rbn', 'node_default', 1);
186         
187         # clean up qra locators
188         my $qra = $user->qra;
189         $qra = undef if ($qra && !DXBearing::is_qra($qra));
190         unless ($qra) {
191                 my $lat = $user->lat;
192                 my $long = $user->long;
193                 $user->qra(DXBearing::lltoqra($lat, $long)) if (defined $lat && defined $long);  
194         }
195
196         # if we have been running and stopped for a while 
197         # if the cache is warm enough don't operate the inrush preventor
198         $self->{inrushpreventor} = exists $runtime{$call} && $runtime{$call} > $startup_delay || $noinrush ?  0 : $main::systime + $startup_delay;
199         dbg("RBN: noinrush: $noinrush, setting inrushpreventor on $self->{call} to $self->{inrushpreventor}");
200 }
201
202 my @queue;                                              # the queue of spots ready to send
203
204 sub normal
205 {
206         my $self = shift;
207         my $line = shift;
208         my @ans;
209 #       my $spots = $self->{spot};
210         
211         # remove leading and trailing spaces
212         chomp $line;
213         $line =~ s/^\s*//;
214         $line =~ s/\s*$//;
215
216         # add base RBN
217
218         my $now = $main::systime;
219
220         # parse line
221         dbg "RBN:RAW,$line" if isdbg('rbnraw');
222         return unless $line=~/^DX\s+de/;
223
224         my (undef, undef, $origin, $qrg, $call, $mode, $s, $m, $spd, $u, $sort, $t, $tx) = split /[:\s]+/, $line;
225
226         # fix up FT8 spots from 7001
227         $t = $u, $u = '' if !$t && is_ztime($u);
228         $t = $sort, $sort = '' if !$t && is_ztime($sort);
229         my $qra = $spd, $spd = '' if is_qra($spd);
230         $u = $qra if $qra;
231
232         # is this anything like a callsign?
233         unless (is_callsign($call)) {
234                 dbg("RBN: ERROR $call from $origin on $qrg is invalid, dumped");
235                 return;
236         }
237
238         $origin =~ s/\-(?:\d{1,2}\-)?\#$//; # get rid of all the crap we aren't interested in
239
240
241         $sort ||= '';
242         $tx ||= '';
243         $qra ||= '';
244     dbg qq{RBN:input decode or:$origin qr:$qrg ca:$call mo:$mode s:$s m:$m sp:$spd u:$u sort:$sort t:$t tx:$tx qra:$qra} if isdbg('rbn');
245
246         ++$self->{noraw};
247         ++$self->{noraw10};
248         ++$self->{norawhour};
249         
250         my $b;
251         
252         if ($t || $tx) {
253
254                 # fix up times for things like 'NXDXF B' etc
255                 if ($tx && is_ztime($t)) {
256                         if (is_ztime($tx)) {
257                                 $b = $t;
258                                 $t = $tx;
259                         } else {
260                                 dbg "RBN:ERR,$line";
261                                 return (0);
262                         }
263                 }
264                 if ($sort && $sort eq 'NCDXF') {
265                         $mode = 'DXF';
266                         $t = $tx;
267                 }
268                 if ($sort && $sort eq 'BEACON') {
269                         $mode = 'BCN';
270                 }
271                 if ($mode =~ /^PSK/) {
272                         $mode = 'PSK';
273                 }
274                 if ($mode eq 'RTTY') {
275                         $mode = 'RTT';
276                 }
277
278                 # The main de-duping key is [call, $frequency], but we probe a bit around that frequency to find a
279                 # range of concurrent frequencies that might be in play. 
280
281                 # The key to this is deducing the true callsign by "majority voting" (the greater the number of spotters
282         # the more effective this is) together with some lexical analsys probably in conjuction with DXSpider
283                 # data sources (for singleton spots) to then generate a "centre" from and to zone (whatever that will mean if it isn't the usual one)
284                 # and some heuristical "Kwalitee" rating given distance from the zone centres of spotter, recipient user
285         # and spotted. A map can be generated once per user and spotter as they are essentially mostly static. 
286                 # The spotted will only get a coarse position unless other info is available. Programs that parse 
287                 # DX bulletins and the online data online databases could be be used and then cached. 
288
289                 # Obviously users have to opt in to receiving RBN spots and other users will simply be passed over and
290                 # ignored.
291
292                 # Clearly this will only work in the 'mojo' branch of DXSpider where it is possible to pass off external
293                 # data requests to ephemeral or semi resident forked processes that do any grunt work and the main
294                 # process to just the standard "message passing" which has been shown to be able to sustain over 5000 
295                 # per second (limited by the test program's output and network speed, rather than DXSpider's handling).
296
297                 my $nearest = 1;
298                 my $search = 5;
299                 my $mult = 10;
300                 my $tqrg = $qrg * $mult; 
301                 my $nqrg = nearest($nearest, $tqrg);  # normalised to nearest Khz
302 #               my $nqrg = nearest_even($qrg);  # normalised to nearest Khz
303                 my $sp = "$call|$nqrg";           # hopefully the skimmers will be calibrated at least this well!
304
305                 # find it?
306                 my $cand = $spots->{$sp};
307                 unless ($cand) {
308                         my ($i, $new);
309                         for ($i = $tqrg; !$cand && $i <= $tqrg+$search; $i += 1) {
310                                 $new = "$call|$i";
311                                 $cand = $spots->{$new}, last if exists $spots->{$new};
312                         }
313                         if ($cand) {
314                                 my $diff = $i - $tqrg;
315                                 dbg(qq{RBN: QRG Diff using $new (+$diff) for $sp for qrg $qrg}) if (isdbg('rbnqrg') || isdbg('rbn'));
316                                 $sp = $new;
317                         }
318                 }
319                 unless ($cand) {
320                         my ($i, $new);
321                         for ($i = $tqrg; !$cand && $i >= $tqrg-$search; $i -= 1) {
322                                 $new = "$call|$i";
323                                 $cand = $spots->{$new}, last if exists $spots->{$new};
324                         }
325                         if ($cand) {
326                                 my $diff = $tqrg - $i;
327                                 dbg(qq{RBN: QRG Diff using $new (-$diff) for $sp for qrg $qrg}) if (isdbg('rbnqrg') || isdbg('rbn'));
328                                 $sp = $new;
329                         }
330                 }
331                 
332                 # if we have one and there is only one slot and that slot's time isn't expired for respot then return
333                 my $respot = 0;
334                 if ($cand && ref $cand) {
335                         if (@$cand <= CData) {
336                                 unless ($self->{minspottime} > 0 && $now - $cand->[CTime] >= $self->{minspottime}) {
337                                         dbg("RBN: key: '$sp' call: $call qrg: $qrg DUPE \@ ". atime(int $cand->[CTime])) if isdbg('rbn');
338                                         return;
339                                 }
340                                 
341                                 dbg("RBN: key: '$sp' RESPOTTING call: $call qrg: $qrg last seen \@ ". atime(int $cand->[CTime])) if isdbg('rbn');
342                                 $cand->[CTime] = $now;
343                                 ++$respot;
344                         }
345
346                         # otherwise we have a spot being built up at the moment
347                 } elsif ($cand) {
348                         dbg("RBN: key '$sp' = '$cand' not ref");
349                         return;
350                 }
351
352                 # here we either have an existing spot record buildup on the go, or we need to create the first one
353                 unless ($cand) {
354                         $spots->{$sp} = $cand = [$now, 0];
355                         dbg("RBN: key: '$sp' call: $call qrg: $qrg NEW" . ($respot ? ' RESPOT' : '')) if isdbg('rbn');
356                 }
357
358                 # add me to the display queue unless we are waiting for initial in rush to finish
359                 return unless $noinrush || $self->{inrushpreventor} < $main::systime;
360
361                 # build up a new record and store it in the buildup
362                 # deal with the unix time
363                 my ($hh,$mm) = $t =~ /(\d\d)(\d\d)Z$/;
364                 my $utz = $hh*3600 + $mm*60 + $main::systime_daystart; # possible issue with late spot from previous day
365                 $utz -= 86400 if $utz > $now+3600;                                         # too far ahead, drag it back one day
366
367                 # create record and add into the buildup
368                 my $r = [$origin, nearest(.1, $qrg), $call, $mode, $s, $t, $utz, $respot, $u];
369                 my @s =  Spot::prepare($r->[RQrg], $r->[RCall], $r->[RUtz], '', $r->[ROrigin]);
370                 if ($s[5] == 666) {
371                         dbg("RBN: ERROR invalid prefix/callsign $call from $origin-# on $qrg, dumped");
372                         return;
373                 }
374                 
375                 if ($self->{inrbnfilter}) {
376                         my ($want, undef) = $self->{inrbnfilter}->it($s);
377                         return unless $want;    
378                 }
379                 $r->[RSpotData] = \@s;
380
381                 ++$self->{queue}->{$sp};# unless @$cand>= CData; # queue the KEY (not the record)
382
383                 dbg("RBN: key: '$sp' ADD RECORD call: $call qrg: $qrg origin: $origin") if isdbg('rbn');
384
385                 push @$cand, $r;
386
387         } else {
388                 dbg "RBN:DATA,$line" if isdbg('rbn');
389         }
390 }
391
392 # we should get the spot record minus the time, so just an array of record (arrays)
393 sub send_dx_spot
394 {
395         my $self = shift;
396         my $quality = shift;
397         my $cand = shift;
398
399         ++$self->{norbn};
400         ++$self->{norbn10};
401         ++$self->{norbnhour};
402         
403         # $r = [$origin, $qrg, $call, $mode, $s, $utz, $respot];
404
405         my $mode = $cand->[CData]->[RMode]; # as all the modes will be the same;
406         
407         my @dxchan = DXChannel::get_all();
408
409         foreach my $dxchan (@dxchan) {
410                 next unless $dxchan->is_user;
411                 my $user = $dxchan->{user};
412                 next unless $user &&  $user->wantrbn;
413
414                 # does this user want this sort of spot at all?
415                 my $want = 0;
416                 ++$want if $user->wantbeacon && $mode =~ /^BCN|DXF/;
417                 ++$want if $user->wantcw && $mode =~ /^CW/;
418                 ++$want if $user->wantrtty && $mode =~ /^RTT/;
419                 ++$want if $user->wantpsk && $mode =~ /^PSK|FSK|MSK/;
420                 ++$want if $user->wantft && $mode =~ /^FT/;
421
422                 dbg(sprintf("RBN: spot selection for $dxchan->{call} mode: '$mode' want: $want flags rbn:%d ft:%d bcn:%d cw:%d psk:%d rtty:%d",
423                                         $user->wantrbn,
424                                         $user->wantft,
425                                         $user->wantbeacon,
426                                         $user->wantcw,
427                                         $user->wantpsk,
428                                         $user->wantrtty,
429                                    )) if isdbg('rbnll');
430
431                 # send one spot to one user out of the ones that we have
432                 $self->dx_spot($dxchan, $quality, $cand) if $want;
433         }
434 }
435
436 sub dx_spot
437 {
438         my $self = shift;
439         my $dxchan = shift;
440         my $quality = shift;
441         my $cand = shift;
442         my $call = $dxchan->{call};
443         my $strength = 100;             # because it could if we talk about FTx
444         my $saver;
445         my %zone;
446         my $respot;
447         my $qra;
448
449         ++$self->{nousers}->{$call};
450         ++$self->{nousers10}->{$call};
451         ++$self->{nousershour}->{$call};
452
453         my $filtered;
454         my $rf = $dxchan->{rbnfilter} || $dxchan->{spotsfilter};
455         my $comment;
456         
457         foreach my $r (@$cand) {
458                 # $r = [$origin, $qrg, $call, $mode, $s, $t, $utz, $respot, $qra];
459                 # Spot::prepare($qrg, $call, $utz, $comment, $origin);
460                 next unless ref $r;
461
462                 $qra = $r->[RQra] if !$qra && $r->[RQra] && is_qra($r->[RQra]);
463
464                 $comment = sprintf "%-3s %2ddB $quality", $r->[RMode], $r->[RStrength];
465                 my $s = $r->[RSpotData];                # the prepared spot
466                 $s->[SComment] = $comment;              # apply new generated comment
467                 
468                 ++$zone{$s->[SZone]};           # save the spotter's zone
469  
470                 # save the lowest strength one
471                 if ($r->[RStrength] < $strength) {
472                         $strength = $r->[RStrength];
473                         $saver = $s;
474                         dbg("RBN: STRENGTH spot: $s->[SCall] qrg: $s->[SQrg] origin: $s->[SOrigin] dB: $r->[RStrength] < $strength") if isdbg 'rbnll';
475                 }
476
477                 if ($rf) {
478                         my ($want, undef) = $rf->it($s);
479                         dbg("RBN: FILTERING for $call spot: $s->[SCall] qrg: $s->[SQrg] origin: $s->[SOrigin] dB: $r->[RStrength] com: '$s->[SComment]' want: " . ($want ? 'YES':'NO')) if isdbg 'rbnll';
480                         next unless $want;
481                         $filtered = $s;
482 #                       last;
483                 }
484         }
485
486         if ($rf) {
487                 $saver = $filtered;             # if nothing passed the filter's lips then $saver == $filtered == undef !
488         }
489         
490         if ($saver) {
491                 my $buf;
492                 # create a zone list of spotters
493                 delete $zone{$saver->[SZone]};  # remove this spotter's zone (leaving all the other zones)
494                 my $z = join ',', sort {$a <=> $b} keys %zone;
495
496                 # alter spot data accordingly
497                 $saver->[SComment] .= " Z:$z" if $z;
498                 
499                 dbg("RBN: SENDING to $call spot: $saver->[SCall] qrg: $saver->[SQrg] origin: $saver->[SOrigin] $saver->[SComment]") if isdbg 'rbnll';
500                 if ($dxchan->{ve7cc}) {
501                         my $call = $saver->[SOrigin];
502                         $saver->[SOrigin] .= '-#';
503                         $buf = VE7CC::dx_spot($dxchan, @$saver);
504                         $saver->[SOrigin] = $call;
505                 } else {
506                         my $call = $saver->[SOrigin];
507                         $saver->[SOrigin] = substr($call, 0, 6);
508                         $saver->[SOrigin] .= '-#';
509                         $buf = $dxchan->format_dx_spot(@$saver);
510                         $saver->[SOrigin] = $call;
511                 }
512 #               $buf =~ s/^DX/RB/;
513                 $dxchan->local_send('N', $buf);
514
515                 ++$self->{nospot};
516                 ++$self->{nospot10};
517                 ++$self->{nospothour};
518                 
519                 if ($qra) {
520                         my $user = DXUser::get_current($saver->[SCall]) || DXUser->new($saver->[SCall]);
521                         unless ($user->qra && is_qra($user->qra)) {
522                                 $user->qra($qra);
523                                 dbg("RBN: update qra on $saver->[SCall] to $qra");
524                                 $user->put;
525                         }
526                 }
527         }
528 }
529
530 # per second
531 sub process
532 {
533         foreach my $dxchan (DXChannel::get_all()) {
534                 next unless $dxchan->is_rbn;
535                 
536                 # At this point we run the queue to see if anything can be sent onwards to the punter
537                 my $now = $main::systime;
538
539                 # now run the waiting queue which just contains KEYS ($call|$qrg)
540                 foreach my $sp (keys %{$dxchan->{queue}}) {
541                         my $cand = $spots->{$sp};
542                         unless ($cand && $cand->[CTime]) {
543                                 dbg "RBN Cand $sp " . ($cand ? 'def' : 'undef') . " [CTime] " . ($cand->[CTime] ? 'def' : 'undef') . " dwell $dwelltime";
544                                 next;
545                         } 
546                         if ($now >= $cand->[CTime] + $dwelltime ) {
547                                 # we have a candidate, create qualitee value(s);
548                                 unless (@$cand > CData) {
549                                         dbg "RBN: QUEUE key '$sp' MISSING RECORDS, IGNORED" . dd($cand) if isdbg 'rbn';
550                                         next;
551                                 }
552                                 dbg "RBN: QUEUE PROCESSING key: '$sp' $now >= $cand->[CTime]" if isdbg 'rbnqueue'; 
553                                 my $quality = @$cand - CData;
554                                 $quality = 9 if $quality > 9;
555                                 $cand->[CQual] = $quality if $quality > $cand->[CQual];
556
557                                 my $r;
558                                 my %qrg;
559                                 foreach $r (@$cand) {
560                                         next unless ref $r;
561                                         ++$qrg{$r->[RQrg]};
562                                 }
563                                 # determine the most likely qrg and then set it
564                                 my @deviant;
565                                 my $c = 0;
566                                 my $mv = 0;
567                                 my $qrg;
568                                 while (my ($k, $votes) = each %qrg) {
569                                         $qrg = $k, $mv = $votes if $votes > $mv;
570                                         ++$c;
571                                 }
572                                 # spit out the deviants
573                                 if ($c > 1) {
574                                         foreach $r (@$cand) {
575                                                 next unless ref $r;
576                                                 my $diff = nearest(.1, $qrg - $r->[RQrg]);
577                                                 push @deviant, sprintf("$r->[ROrigin]:%+.1f", $diff) if $diff != 0;
578                                                 $r->[RSpotData]->[SQrg] = $qrg; # set all the QRGs to the agreed value
579                                         }
580                                 }
581
582                                 $qrg = sprintf "%.1f",  $qrg;
583                                 $r = $cand->[CData];
584                                 $r->[RQrg] = $qrg;
585                                 my $squality = "Q:$cand->[CQual]";
586                                 $squality .= '*' if $c > 1; 
587                                 $squality .= '+' if $r->[Respot];
588
589                                 if ($cand->[CQual] >= $minqual) {
590                                         if (isdbg('progress')) {
591                                                 my $s = "RBN: SPOT key: '$sp' = $r->[RCall] on $r->[RQrg] by $r->[ROrigin] \@ $r->[RTime] $squality route: $dxchan->{call}";
592                                                 $s .= " Deviants: " . join(', ', sort @deviant) if @deviant;
593                                                 dbg($s);
594                                         }
595                                         send_dx_spot($dxchan, $squality, $cand);
596                                 } elsif (isdbg('rbn')) {
597                                         my $s = "RBN: SPOT IGNORED(Q $cand->[CQual] < $minqual) key: '$sp' = $r->[RCall] on $r->[RQrg] by $r->[ROrigin] \@ $r->[RTime] $squality route: $dxchan->{call}";
598                                         dbg($s);
599                                 }
600                                 
601                                 # clear out the data and make this now just "spotted", but no further action required until respot time
602                                 dbg "RBN: QUEUE key '$sp' cleared" if isdbg 'rbn';
603                                 
604                                 $spots->{$sp} = [$now, $cand->[CQual]];
605                                 delete $dxchan->{queue}->{$sp};
606                         }
607                         else {
608                                 dbg sprintf("RBN: QUEUE key: '$sp' SEND time not yet reached %.1f secs left", $cand->[CTime] + $dwelltime - $now) if isdbg 'rbnqueue'; 
609                         }
610                 }
611         }
612         
613 }
614
615 sub per_minute
616 {
617         foreach my $dxchan (DXChannel::get_all()) {
618                 next unless $dxchan->is_rbn;
619                 dbg "RBN:STATS minute $dxchan->{call} raw: $dxchan->{noraw} retrieved spots: $dxchan->{norbn} delivered: $dxchan->{nospot} after filtering to users: " . scalar keys %{$dxchan->{nousers}} if isdbg('rbnstats');
620                 if ($dxchan->{noraw} == 0 && $dxchan->{lasttime} > 60) {
621                         LogDbg('RBN', "RBN: no input from $dxchan->{call}, disconnecting");
622                         $dxchan->disconnect;
623                 }
624                 $dxchan->{noraw} = $dxchan->{norbn} = $dxchan->{nospot} = 0; $dxchan->{nousers} = {};
625                 $runtime{$dxchan->{call}} += 60;
626         }
627
628         # save the spot cache
629         write_cache() unless $main::systime + $startup_delay < $main::systime;;
630 }
631
632 sub per_10_minute
633 {
634         my $count = 0;
635         my $removed = 0;
636         while (my ($k,$cand) = each %{$spots}) {
637                 next if $k eq 'VERSION';
638                 next if $k =~ /^O\|/;
639                 
640                 if ($main::systime - $cand->[CTime] > $minspottime*2) {
641                         delete $spots->{$k};
642                         ++$removed;
643                 }
644                 else {
645                         ++$count;
646                 }
647         }
648         dbg "RBN:STATS spot cache remain: $count removed: $removed"; # if isdbg('rbn');
649         foreach my $dxchan (DXChannel::get_all()) {
650                 next unless $dxchan->is_rbn;
651                 my $nq = keys %{$dxchan->{queue}};
652                 dbg "RBN:STATS 10-minute $dxchan->{call} queue: $nq raw: $dxchan->{noraw10} retrieved spots: $dxchan->{norbn10} delivered: $dxchan->{nospot10} after filtering to  users: " . scalar keys %{$dxchan->{nousers10}};
653                 $dxchan->{noraw10} = $dxchan->{norbn10} = $dxchan->{nospot10} = 0; $dxchan->{nousers10} = {};
654         }
655 }
656
657 sub per_hour
658 {
659         foreach my $dxchan (DXChannel::get_all()) {
660                 next unless $dxchan->is_rbn;
661                 my $nq = keys %{$dxchan->{queue}};
662                 dbg "RBN:STATS hour $dxchan->{call} queue: $nq raw: $dxchan->{norawhour} retrieved spots: $dxchan->{norbnhour} delivered: $dxchan->{nospothour} after filtering to users: " . scalar keys %{$dxchan->{nousershour}};
663                 $dxchan->{norawhour} = $dxchan->{norbnhour} = $dxchan->{nospothour} = 0; $dxchan->{nousershour} = {};
664         }
665 }
666
667 sub finish
668 {
669         write_cache();
670 }
671
672 sub write_cache
673 {
674         my $fh = IO::File->new(">$cachefn") or confess("writing $cachefn $!");
675         my $s = $json->encode($spots);
676         $fh->print($s);
677         $fh->close;
678 }
679
680 sub check_cache
681 {
682         if (-e $cachefn) {
683                 my $mt = (stat($cachefn))[9];
684                 my $t = $main::systime - $mt || 1;
685                 my $p = difft($mt, 2);
686                 if ($t < $cache_valid) {
687                         dbg("RBN:check_cache '$cachefn' spot cache exists, created $p ago and not too old");
688                         my $fh = IO::File->new($cachefn);
689                         my $s;
690                         if ($fh) {
691                                 local $/ = undef;
692                                 $s = <$fh>;
693                                 dbg("RBN:check_cache cache read size " . length $s);
694                                 $fh->close;
695                         } else {
696                                 dbg("RBN:check_cache file read error $!");
697                                 return undef;
698                         }
699                         if ($s) {
700                                 eval {$spots = $json->decode($s)};
701                                 if ($spots && ref $spots) {     
702                                         if (exists $spots->{VERSION} && $spots->{VERSION} == $DATA_VERSION) {
703                                                 # now clean out anything that is current
704                                                 while (my ($k, $cand) = each %$spots) {
705                                                         next if $k eq 'VERSION';
706                                                         next if $k =~ /^O\|/;
707                                                         if (@$cand > CData) {
708                                                                 $spots->{$k} = [$cand->[CTime], $cand->[CQual]];
709                                                         }
710                                                 }
711                                                 dbg("RBN:check_cache spot cache restored");
712                                                 return 1;
713                                         } 
714                                 }
715                         }
716                         dbg("RBN::checkcache error decoding $@");
717                 } else {
718                         my $d = difft($main::systime-$cache_valid);
719                         dbg("RBN::checkcache '$cachefn' created $p ago is too old (> $d), ignored");
720                 }
721         } else {
722                 dbg("RBN:check_cache '$cachefn' spot cache not present");
723         }
724         
725         return undef;
726 }
727
728 1;