#!/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 MD_DELAY() { 0.0010 } # delay after mode switch sub RF_DELAY() { 0.0040 } # delay after freq change 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 {}%"; 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 } 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 = 7; # 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 && $record_vol) { # # 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 } 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", ); my %parse_resp = ( LM => sub { 0x7f & hex substr $_, 2 }, ); sub cmd($) { # print ">$_[0]<\n";#d# # fcntl $aor, F_SETFL, 0; syswrite $aor, "$_[0]\015\012"; } # seconds, overhead sub delay($;$) { my $bytes = int $_[0] * (BAUD / 11) - $_[1]; " " x ($bytes - 2) } sub tune($$) { ( "MD$_[0]" . (delay MD_DELAY, 4), "RF" . (round $_[1]) . (delay RF_DELAY, 4), ) } 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 $resp_buf; 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); $job->{exp} = []; $job->{res} = []; for (@{ $job->{cmd} }) { if (/^MD/) { next if $curmode eq $_; $curmode = $_; } elsif (/^RF/) { next if $curfreq eq $_; $curfreq = $_; } push @exp, $1 if s/^(..)=//; $cmd .= "$_\015\012"; } printf "\r%s %s %4d %4d ", $curmode, $curfreq, $job->{nice}, scalar @send_jobs; $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, $resp_buf, 4096, length $resp_buf; while ($resp_buf =~ s/^([^\015\012]*)[\012\015]+//s) { local $_ = $1; s/[\012\015]//g; next unless /^\S/; @resp_jobs or die "out of sync: expected nothing, but got '$_'\n"; my $job = $resp_jobs[0]; my $exp = shift @{ $job->{exp} }; $exp eq substr $_, 0, 2 or die "sync error: expected '$exp', got '$_'\n"; push @{ $job->{res} }, $parse_resp{$exp}->(); unless (@{ $job->{exp} }) { $job->{exclusive}->() if $job->{exclusive}; $job->{done}->send; shift @resp_jobs; 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 { mode => $mode, freq => round $freq, cmd => \@cmd, done => new Coro::Signal, nice => $nice, }, job::; unshift @cmd, tune $mode, $freq; if (@cmd && ref $cmd[-1]) { $job->{exclusive} = pop @cmd; } push @send_jobs, $job; $send_requeue = 1; send_scheduler; $job } sub avg_lm { my ($nice, $mode, $freq) = @_; (List::Util::sum +(job $nice, $mode, $freq, ((delay 0.010, 4), "LM=LM") x 4)->result) / 4 } sub aft($$$$) { my ($nice, $mode, $center, $radius) = @_; $radius = List::Util::max RESOLUTION, $radius * 0.1; my @level = map [$_->{freq}, (List::Util::sum $_->result) / 5], map +(job $nice, $mode, $center + $radius * $_, ((delay 0.002, 4), "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->[$_], $jobs[$_]->result) for 0 .. $#jobs; } ############################################################################# # raw search async { my $nice = 1000; while () { my @info = sql_fetchall "select mode, freq, width, activity_level from freq where check_level < activity_level and check_time < ? order by check_time, mode, freq limit 1000", $NOW; if (@info) { sweep $nice, \@info, sub { my ($info, $pre_lm) = @_; my ($mode, $freq, $width, $activity_level) = @$info; eval { $pre_lm >= $activity_level or die "pre_lm too low"; my $avg_lm = avg_lm 0, $mode, $freq; warn "$mode:$freq plm $pre_lm alm $avg_lm\n";#d# $avg_lm >= $activity_level or die "avg_lm too low"; my ($aft_freq, $aft_lm) = aft 0, $mode, $freq, $width * 0.9; $aft_lm >= $activity_level or die "aft_lm too low"; print "$mode:$aft_freq($freq) $activity_level<= $pre_lm,$aft_lm "; if ($freq - 0.5 * $width <= $aft_freq && $aft_freq <= $freq + 0.5 * $width) { print "tuned\n"; sql_exec "update freq set check_time = ?, check_freq = ?, check_level = ? where mode = ? and freq = ?", $NOW + 86400*5 - (rand 86400), $aft_freq, $aft_lm, $mode, $freq; } else { # outlier print "outlier\n"; sql_exec "update freq set check_time = ?, check_freq = 0, check_level = 0 where mode = ? and freq = ?", $NOW + 86400*7 - (rand 86400), $mode, $freq; } }; if ($@) { # currently inactive sql_exec "update freq set check_time = ? where mode = ? and freq = ?", $NOW, $mode, $freq; } }; } else { Coro::Timer::sleep 60; } } }; ############################################################################# # scan active freqs async { my $nice = 100; while () { my $st = sql_exec \my ($mode, $freq, $width, $aft_freq, $activity_level), "select mode, freq, width, check_freq, activity_level from freq where check_level >= activity_level and check_freq > 0"; if ($st->rows) { my @jobs; while ($st->fetch) { next if -e "/root/aor/$mode,$freq"; push @jobs, [$mode, $freq, $activity_level, job $nice, $mode, $aft_freq, "LM=LM"] } for (@jobs) { my ($mode, $freq, $activity_level, $job) = @$_; my $lm = $job->result; $lm >= $activity_level or next; (avg_lm 0, $nice-1, $mode, $freq) >= $activity_level or next; warn "record $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 5; } 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