#!/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 SEARCH_INTERVAL_MIN() { 555 } 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::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 ($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}) ) { my $job = (pop_heap @send_jobs)->[1]; 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}; } $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, }, job::; if (@cmd && ref $cmd[-1]) { $job->{exclusive} = pop @cmd; } push_heap @send_jobs, [$nice, $job]; 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.0020, "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, $weight ** 0.1 * 100) } sub sweep { my ($nice, $freqs, $cb) = @_; my $job; for (@$freqs, undef) { my ($mode, $freq, $width) = $_ ? @$_ : (); my $next_job = $freq && job $nice, $mode, $freq, delay 0.050, "LM=LM"; $cb->($mode, $freq, $width, lm $job->result) if $job; $job = $next_job; } } ############################################################################# # raw aearch async { my $nice = 1000; while () { my @freq = sql_fetchall "select mode, freq, width from freq where search_time < ? order by search_time, mode, freq limit 1000", $NOW - SEARCH_INTERVAL_MIN; if (@freq) { sweep $nice, \@freq, sub { my ($mode, $freq, $width, $lm) = @_; sql_exec "update freq set search_time = ?, search_level = ? where mode = ? and freq = ?", $NOW, $lm, $mode, $freq; }; } else { Coro::Timer::sleep 60; } } }; ############################################################################# # aft of active freqs async { my $nice = 500; while () { my $st = sql_exec \my ($aft_time, $mode, $freq, $width, $activity_level), "select aft_time, mode, freq, width, activity_level from freq where (search_level >= activity_level or aft_time > 0) and aft_time < ? order by aft_time, mode, freq limit 10", $NOW; if ($st->rows) { while ($st->fetch) { my @lm = lm +(job $nice, $mode, $freq, delay 0.050, "LM=LM")->result; if ($lm[0] >= $activity_level) { my ($tune, $weight) = aft $nice, $mode, $freq, $width * 0.9; if ($freq - 0.5 * $width <= $tune && $tune <= $freq + 0.5 * $width) { sql_exec "update freq set aft_time = ?, aft_freq = ?, aft_level = ? where mode = ? and freq = ?", $NOW + 86400 - 3600, $tune, $weight, $mode, $freq; } else { sql_exec "update freq set aft_time = ?, aft_level = 0 where mode = ? and freq = ?", $NOW + 86400 + 3600, $mode, $freq; } } else { # currently inactive sql_exec "update freq set aft_time = ? where mode = ? and freq = ?", $NOW + 3600, $mode, $freq; } } } else { Coro::Timer::sleep 60; } } }; ############################################################################# # scan active freqs async { return; my $nice = 100; while () { my $st = sql_exec \my ($mode, $freq, $width, $activity_level), "select mode, aft_freq, width, 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, job $nice, $mode, $freq, delay 0.030, "LM=LM"]; } for (@jobs) { my ($activity_level, $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; Coro::Timer::sleep 0.5; # my $data = 0.5 + (1 / 65536) * float unpack "v*", record_nsamples $record_rate * 0.1; acmd $playback_off; #$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 60; } else { Coro::Timer::sleep 60; } } }; ############################################################################# 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