#!/opt/bin/perl # http://www.beepworld.de/members81/frequenzen/flughaefen.htm # http://www.dia.unisa.it/professori/ads/corso-security/www/CORSO-9900/a5/Netsec/netsec.html # http://www.scnt01426.pwp.blueyonder.co.uk/Articles/RS-232/AR-8000.htm use List::Util; use Array::Heap2; use Fcntl; use POSIX; use Time::HiRes; use Event; use Coro; use Coro::Event; use Coro::Timer qw(sleep); use Coro::Signal; use PDL (); use PDL::FFTW; use PDL::Audio; use PDL::IO::Pic; use PApp::SQL; use strict; sub BAUD() { 9600 } sub RESOLUTION() { 50 } # in Hz sub round($) { int ($_[0] / RESOLUTION + 0.5) * RESOLUTION } sub AFT_INTERVAL_MIN() { 3333 } $PApp::SQL::DBH = PApp::SQL::connect_cached __FILE__, "DBI:mysql:database=aor;mysql_read_default_file=/root/.my.cnf" or die "unable to open mysql database aor"; my $tty = $ARGV[0] || "/dev/ttyS0"; sub AGC_MAX() { 26000 } my $record_rate = 48000; my $record_init = "amixer sset Mic cap"; my $record_set = "amixer sset Capture {}%"; my $record_cmd = "arecord -traw -c1 -fS16_LE -r$record_rate 2>/dev/null"; my $playback_on = "amixer sset Mic unmute"; my $playback_off = "amixer sset Mic mute"; my $playback_set = "amixer sset Mic {}%"; sub imag($) { my ($pdl) = @_; wpic $pdl, "/tmp/aor.pgm"; system "cv", "/tmp/aor.pgm"; } sub acmd($;$) { my ($cmd, $arg) = @_; $cmd =~ s/\{\}/$arg/g; system "$cmd >/dev/null"; } my $record_buf; my $record_vol = 100; sub record($$) { my ($data_cb, $done_cb) = @_; $record_buf = ""; open my $fh, "-|", $record_cmd or die "$record_cmd: $!"; Event->io (fd => $fh, poll => 'r', cb => sub { if (sysread $fh, $record_buf, 65536, length $record_buf) { if (4096 < length $record_buf) { # do the agc adjustment my $pdl = PDL::Core::short (unpack "v*", substr $record_buf, -4096); if (AGC_MAX < $pdl->abs->max) { acmd $record_set, --$record_vol; # printf "AGC %d $record_vol\n", max abs $pdl; } } unless ($data_cb->()) { $_[0]->w->cancel; $done_cb->(); } } else { $_[0]->w->cancel; $done_cb->(); } }); } sub record_nsamples($) { my $bytes = $_[0] * 2; my $done = new Coro::Signal; record sub { $bytes > length $record_buf }, sub { $done->send; }; $done->wait; substr $record_buf, -$bytes } if ($ARGV[0] eq "--addrange") { my (undef, $mode, $lo, $hi, $step, $level) = @ARGV; my $cnt; while () { my $f = $lo + $cnt++ * $step; last if $f > $hi; eval { sql_exec "insert into freq (mode, freq, width, activity_level) values (?, ?, ?, ?)", $mode, round $f, $step, $level; }; } exit; } elsif ($ARGV[0] eq "--daemon") { # nop } else { # exit } my $NOW = time; Event->timer (after => 0, interval => 1, hard => 1, prio => 3, cb => sub { $NOW = time }); open my $aor, "+<:raw", $tty or die "$tty: $!"; { my $tio = new POSIX::Termios; $tio->getattr (fileno $aor); $tio->setiflag (POSIX::IXOFF | POSIX::IXON); $tio->setoflag (0); $tio->setcflag (POSIX::CLOCAL | POSIX::CREAD | POSIX::CSTOPB | POSIX::CS8); $tio->setlflag (0); $tio->setispeed (&POSIX::B9600); $tio->setospeed (&POSIX::B9600); $tio->setattr (fileno $aor); } my %mode = ( 0 => "WFM", 1 => "NFM", 2 => "AM", 3 => "USB", 4 => "LSB", 5 => "CW", ); sub cmd($) { # print ">$_[0]<\n";#d# # fcntl $aor, F_SETFL, 0; syswrite $aor, "$_[0]\015\012"; } sub delay($) { my $bytes = int ($_[0] * (BAUD / 11)) - 2; " " x ($bytes - 2) } cmd "MC1"; # mic off (0 = on, 1 = off, 2 = squelch) cmd "ST000050"; # minimum stepsize acmd $record_init; acmd $record_set, $record_vol; acmd $playback_off; my @send_jobs; my $send_requeue; my ($curfreq, $curmode); my $pipeline = 4; my $rbuf; my @resp_jobs; sub send_scheduler { while (@send_jobs && @resp_jobs < $pipeline && (!@resp_jobs || !$resp_jobs[-1]{exclusive}) ) { if ($send_requeue) { # re-sort queue $send_requeue = 0; @send_jobs = sort { $b->{nice} <=> $a->{nice} || $b->{mode} <=> $a->{mode} || $b->{freq} <=> $a->{freq}; } @send_jobs; } my $job = pop @send_jobs; my ($cmd, @exp); if ($curmode != $job->{mode}) { $cmd .= "MD$job->{mode}\015\012"; $curmode = $job->{mode}; } if ($curfreq != $job->{freq}) { $cmd .= "RF$job->{freq}\015\012"; $curfreq = $job->{freq}; } printf "\r%d:%010d %4d %4d ", $curmode, $curfreq, $job->{nice}, scalar @send_jobs; $job->{exp} = []; $job->{res} = []; for (@{ $job->{cmd} }) { push @exp, $1 if s/^(..)=//; $cmd .= "$_\015\012"; } $job->{exp} = \@exp; $job->{res} = []; syswrite $aor, $cmd; if (@exp) { push @resp_jobs, $job; } else { $job->{done}->send; } } } Event->io (fd => $aor, prio => 1, poll => 'r', cb => sub { sysread $aor, $rbuf, 4096, length $rbuf; while ($rbuf =~ s/^([^\015\012]*)[\012\015]+//s) { my $line = $1; $line =~ s/[\012\015]//g; next unless $line =~ /^\S/; @resp_jobs or die "out of sync: expected nothing, but got '$line'\n"; my $exp = shift @{ $resp_jobs[0]{exp} }; $exp eq substr $line, 0, 2 or die "sync error: expected '$exp', got '$line'\n"; push @{ $resp_jobs[0]{res} }, $line; unless (@{ $resp_jobs[0]{exp} }) { my $job = shift @resp_jobs; $job->{exclusive}->() if $job->{exclusive}; $job->{done}->send; send_scheduler; } next; } }); sub job::result { my ($self) = @_; $self->{done}->wait; wantarray ? @{ $self->{res} } : $self->{res}[0] } sub job { my ($nice, $mode, $freq, @cmd) = @_; my $job = bless { freq => round $freq, mode => $mode, cmd => \@cmd, done => new Coro::Signal, nice => $nice, }, job::; if (@cmd && ref $cmd[-1]) { $job->{exclusive} = pop @cmd; } push @send_jobs, $job; $send_requeue = 1; send_scheduler; $job } sub lm { map +(hex substr $_, 2) & 0x7f, @_ } sub aft($$$$) { my ($nice, $mode, $center, $radius) = @_; $radius = List::Util::max RESOLUTION, $radius * 0.1; my @level = map [$_->{freq}, (List::Util::sum lm $_->result) / 5], map +(job $nice, $mode, $center + $radius * $_, (delay 0.002, "LM=LM") x 5), -10 .. 10; my ($tune, $weight); for (@level) { my ($f, $w) = @$_; $w **= 8; $tune += $f * $w; $weight += $w; } ( int ($tune / $weight / RESOLUTION) * RESOLUTION, int 0.5 + List::Util::max map $_->[1], @level, ) } sub sweep { my ($nice, $freqs, $cb) = @_; my @jobs = map { job $nice, $_->[0], $_->[1], "LM=LM"; } @$freqs; $cb->($freqs->[$_], lm $jobs[$_]->result) for 0 .. $#jobs; } ############################################################################# # raw search async { my $nice = 1000; while () { my @info = sql_fetchall "select mode, freq, activity_level from freq where search_level < activity_level order by search_time, mode, freq limit 1000"; if (@info) { sweep $nice, \@info, sub { my ($info, $lm) = @_; print "found new frequency $info->[0]:$info->[1] $lm>=$info->[2]\n" if $lm >= $info->[2]; sql_exec "update freq set search_time = ?, search_level = ? where mode = ? and freq = ?", $NOW, $lm, $info->[0], $info->[1]; }; } else { Coro::Timer::sleep 60; } } }; ############################################################################# # aft of active freqs async { my $nice = 500; while () { my @info = sql_fetchall "select mode, freq, width, activity_level from freq where ((search_level >= activity_level and aft_time = 0) or aft_time > 0) and aft_time < ? order by mode, freq limit 100", $NOW; if (@info) { sweep $nice, \@info, sub { my ($info, $lm) = @_; my ($mode, $freq, $width, $activity_level) = @$info; if ($lm >= $activity_level) { my ($tune, $weight) = aft 0, $mode, $freq, $width * 0.9; print "$mode:$tune($freq) $lm>=$activity_level $weight "; if ($freq - 0.5 * $width <= $tune && $tune <= $freq + 0.5 * $width) { print "tuned\n"; sql_exec "update freq set aft_time = ?, aft_freq = ?, aft_level = ? where mode = ? and freq = ?", $NOW + 86400*5 - (rand 86400), $tune, $weight, $mode, $freq; } else { # outlier print "outlier\n"; sql_exec "update freq set aft_time = ?, aft_level = 0 where mode = ? and freq = ?", $NOW + 86400*7 - (rand 86400), $mode, $freq; } } else { # currently inactive sql_exec "update freq set aft_time = ? where mode = ? and freq = ?", $NOW + 10, $mode, $freq; } }; Coro::Timer::sleep 1; } else { Coro::Timer::sleep 10; } } }; ############################################################################# # scan active freqs async { my $nice = 100; while () { my $st = sql_exec \my ($mode, $freq, $width, $aft_freq, $activity_level), "select mode, freq, width, aft_freq, activity_level from freq where aft_level > 0 and aft_freq > 0 order by freq, mode"; if ($st->rows) { my @jobs; while ($st->fetch) { push @jobs, [$activity_level, $freq, job $nice, $mode, $aft_freq, delay 0.040, "LM=LM"] unless -e "/root/aor/$mode,$freq"; } for (@jobs) { my ($activity_level, $freq, $job) = @$_; my @lm = lm $job->result; if ($lm[0] >= $activity_level) { warn "scan $job->{mode} $job->{freq} @lm\n"; (job $nice-1, $mode, $job->{freq}, "LM=LM", sub { # (job $nice-1, 0, 98400000, "LM=LM", sub { # acmd $playback_on; my $data = record_nsamples $record_rate * 10; # acmd $playback_off; open my $fh, ">:raw", "/root/aor/$mode,$freq" or die "/root/aor/$mode,$freq: $!"; print $fh $data; close $fh; # my $data = 0.5 + (1 / 65536) * float unpack "v*", record_nsamples $record_rate * 0.1; #$data = sin +(1/480) * xvals zeroes 48000; # $data->reshape (100, $record_rate / 100); # # my $spectrum = 120 + cat map spectrum ($data->slice ("($_),"), "dB", "KAISER"), 0..99; # printf "%s %s\n", (min $spectrum), (max $spectrum); # imag short $spectrum; })->result; Coro::Timer::sleep 1; } } Coro::Timer::sleep 300; } else { Coro::Timer::sleep 300; } } }; ############################################################################# $| = 1; Coro::Event::loop; __END__ mysql -e "delete from freq" aor ./aorscan --addrange 1 26565000 27405000 10000 25 ./aorscan --addrange 1 34360000 35800000 20000 25 ./aorscan --addrange 1 38460000 39840000 20000 25 ./aorscan --addrange 1 74215000 77455000 20000 25 ./aorscan --addrange 1 84015000 87255000 20000 24 ./aorscan --addrange 2 108000000 144000000 25000 20 ./aorscan --addrange 1 118000000 136000000 8333.3333333333333 24 ./aorscan --addrange 1 108000000 144000000 25000 24 ./aorscan --addrange 4 165210000 169380000 20000 24 ./aorscan --addrange 3 169810000 173980000 20000 24 ./aorscan --addrange 1 230000000 328000000 8333.3333333333333 10 ./aorscan --addrange 2 230000000 328000000 50000 20 ./aorscan --addrange 1 438650000 439425000 25000 10 ./aorscan --addrange 4 443600000 444962500 12500 10 ./aorscan --addrange 3 448600000 449962500 12500 10 ./aorscan --addrange 1 1270200000 1270700000 25000 10 ./aorscan --addrange 1 1298200000 1298700000 25000 10 ./aorscan --addrange 3 2690000 2690000 25000 10 ./aorscan --addrange 3 3413000 3413000 25000 10 ./aorscan --addrange 4 3413000 3413000 25000 10 ./aorscan --addrange 3 5640000 5640000 25000 10 ./aorscan --addrange 4 5640000 5640000 25000 10 ./aorscan --addrange 3 8957000 8957000 25000 10 ./aorscan --addrange 4 8957000 8957000 25000 10 ./aorscan --addrange 3 13264000 13264000 25000 10 ./aorscan --addrange 4 13264000 13264000 25000 10