1 |
#!/opt/bin/perl |
2 |
|
3 |
# http://www.beepworld.de/members81/frequenzen/flughaefen.htm |
4 |
# http://www.dia.unisa.it/professori/ads/corso-security/www/CORSO-9900/a5/Netsec/netsec.html |
5 |
# http://www.scnt01426.pwp.blueyonder.co.uk/Articles/RS-232/AR-8000.htm |
6 |
|
7 |
use List::Util; |
8 |
use Array::Heap2; |
9 |
|
10 |
use Fcntl; |
11 |
use POSIX; |
12 |
use Time::HiRes; |
13 |
|
14 |
use Event; |
15 |
|
16 |
use Coro; |
17 |
use Coro::Event; |
18 |
use Coro::Timer qw(sleep); |
19 |
use Coro::Signal; |
20 |
|
21 |
use PDL (); |
22 |
#use PDL::FFTW; |
23 |
#use PDL::Audio; |
24 |
#use PDL::IO::Pic; |
25 |
|
26 |
use PApp::SQL; |
27 |
|
28 |
use strict; |
29 |
|
30 |
sub BAUD() { 9600 } |
31 |
sub RESOLUTION() { 50 } # in Hz |
32 |
sub round($) { int ($_[0] / RESOLUTION + 0.5) * RESOLUTION } |
33 |
|
34 |
sub SEARCH_INTERVAL_MIN() { 555 } |
35 |
sub AFT_INTERVAL_MIN() { 3333 } |
36 |
|
37 |
$PApp::SQL::DBH = PApp::SQL::connect_cached __FILE__, "DBI:mysql:database=aor;mysql_read_default_file=/root/.my.cnf" |
38 |
or die "unable to open mysql database aor"; |
39 |
|
40 |
my $tty = $ARGV[0] || "/dev/ttyS0"; |
41 |
|
42 |
sub AGC_MAX() { 26000 } |
43 |
|
44 |
my $record_rate = 48000; |
45 |
my $record_init = "amixer sset Mic cap"; |
46 |
my $record_set = "amixer sset Capture {}%"; |
47 |
my $record_cmd = "arecord -traw -c1 -fS16_LE -r$record_rate 2>/dev/null"; |
48 |
my $playback_on = "amixer sset Mic unmute"; |
49 |
my $playback_off = "amixer sset Mic mute"; |
50 |
my $playback_set = "amixer sset Mic {}%"; |
51 |
|
52 |
sub imag($) { |
53 |
my ($pdl) = @_; |
54 |
wpic $pdl, "/tmp/aor.pgm"; |
55 |
system "cv", "/tmp/aor.pgm"; |
56 |
} |
57 |
|
58 |
sub acmd($;$) { |
59 |
my ($cmd, $arg) = @_; |
60 |
|
61 |
$cmd =~ s/\{\}/$arg/g; |
62 |
system "$cmd >/dev/null"; |
63 |
} |
64 |
|
65 |
my $record_buf; |
66 |
my $record_vol = 100; |
67 |
|
68 |
sub record($$) { |
69 |
my ($data_cb, $done_cb) = @_; |
70 |
|
71 |
$record_buf = ""; |
72 |
|
73 |
open my $fh, "-|", $record_cmd |
74 |
or die "$record_cmd: $!"; |
75 |
|
76 |
Event->io (fd => $fh, poll => 'r', cb => sub { |
77 |
if (sysread $fh, $record_buf, 65536, length $record_buf) { |
78 |
if (4096 < length $record_buf) { |
79 |
# do the agc adjustment |
80 |
my $pdl = PDL::short (unpack "v*", substr $record_buf, -4096); |
81 |
if (AGC_MAX < $pdl->abs->max) { |
82 |
acmd $record_set, --$record_vol; |
83 |
# printf "AGC %d $record_vol\n", max abs $pdl; |
84 |
} |
85 |
} |
86 |
|
87 |
unless ($data_cb->()) { |
88 |
$_[0]->w->cancel; |
89 |
$done_cb->(); |
90 |
} |
91 |
} else { |
92 |
$_[0]->w->cancel; |
93 |
$done_cb->(); |
94 |
} |
95 |
}); |
96 |
} |
97 |
|
98 |
sub record_nsamples($) { |
99 |
my $bytes = $_[0] * 2; |
100 |
|
101 |
my $done = new Coro::Signal; |
102 |
|
103 |
record sub { |
104 |
$bytes > length $record_buf |
105 |
}, sub { |
106 |
$done->send; |
107 |
}; |
108 |
|
109 |
$done->wait; |
110 |
|
111 |
substr $record_buf, -$bytes |
112 |
} |
113 |
|
114 |
if ($ARGV[0] eq "--addrange") { |
115 |
my (undef, $mode, $lo, $hi, $step, $level) = @ARGV; |
116 |
|
117 |
my $cnt; |
118 |
while () { |
119 |
my $f = $lo + $cnt++ * $step; |
120 |
|
121 |
last if $f > $hi; |
122 |
|
123 |
eval { |
124 |
sql_exec "insert into freq (mode, freq, width, activity_level) values (?, ?, ?, ?)", |
125 |
$mode, round $f, $step, $level; |
126 |
}; |
127 |
} |
128 |
|
129 |
exit; |
130 |
} elsif ($ARGV[0] eq "--daemon") { |
131 |
# nop |
132 |
} else { |
133 |
# exit |
134 |
} |
135 |
|
136 |
my $NOW = time; |
137 |
|
138 |
Event->timer (after => 0, interval => 1, hard => 1, prio => 3, cb => sub { $NOW = time }); |
139 |
|
140 |
open my $aor, "+<:raw", $tty |
141 |
or die "$tty: $!"; |
142 |
|
143 |
{ |
144 |
my $tio = new POSIX::Termios; |
145 |
|
146 |
$tio->getattr (fileno $aor); |
147 |
|
148 |
$tio->setiflag (POSIX::IXOFF | POSIX::IXON); |
149 |
$tio->setoflag (0); |
150 |
$tio->setcflag (POSIX::CLOCAL | POSIX::CREAD | POSIX::CSTOPB | POSIX::CS8); |
151 |
$tio->setlflag (0); |
152 |
$tio->setispeed (&POSIX::B9600); |
153 |
$tio->setospeed (&POSIX::B9600); |
154 |
|
155 |
$tio->setattr (fileno $aor); |
156 |
} |
157 |
|
158 |
my %mode = ( |
159 |
0 => "WFM", |
160 |
1 => "NFM", |
161 |
2 => "AM", |
162 |
3 => "USB", |
163 |
4 => "LSB", |
164 |
5 => "CW", |
165 |
); |
166 |
|
167 |
sub cmd($) { |
168 |
# print ">$_[0]<\n";#d# |
169 |
# fcntl $aor, F_SETFL, 0; |
170 |
syswrite $aor, "$_[0]\015\012"; |
171 |
} |
172 |
|
173 |
sub delay($) { |
174 |
my $bytes = int ($_[0] * (BAUD / 11)) - 2; |
175 |
|
176 |
" " x ($bytes - 2) |
177 |
} |
178 |
|
179 |
cmd "MC1"; # mic off (0 = on, 1 = off, 2 = squelch) |
180 |
cmd "ST000050"; # minimum stepsize |
181 |
acmd $record_init; |
182 |
acmd $record_set, $record_vol; |
183 |
acmd $playback_off; |
184 |
|
185 |
my @send_jobs; |
186 |
my ($curfreq, $curmode); |
187 |
my $pipeline = 4; |
188 |
|
189 |
my $rbuf; |
190 |
my @resp_jobs; |
191 |
|
192 |
sub send_scheduler { |
193 |
while (@send_jobs |
194 |
&& @resp_jobs < $pipeline |
195 |
&& (!@resp_jobs || !$resp_jobs[-1]{exclusive}) |
196 |
) { |
197 |
my $job = (pop_heap @send_jobs)->[1]; |
198 |
|
199 |
my ($cmd, @exp); |
200 |
|
201 |
if ($curmode != $job->{mode}) { |
202 |
$cmd .= "MD$job->{mode}\015\012"; |
203 |
$curmode = $job->{mode}; |
204 |
} |
205 |
|
206 |
if ($curfreq != $job->{freq}) { |
207 |
$cmd .= "RF$job->{freq}\015\012"; |
208 |
$curfreq = $job->{freq}; |
209 |
} |
210 |
|
211 |
$job->{exp} = []; |
212 |
$job->{res} = []; |
213 |
|
214 |
for (@{ $job->{cmd} }) { |
215 |
push @exp, $1 if s/^(..)=//; |
216 |
$cmd .= "$_\015\012"; |
217 |
} |
218 |
|
219 |
$job->{exp} = \@exp; |
220 |
$job->{res} = []; |
221 |
|
222 |
syswrite $aor, $cmd; |
223 |
|
224 |
if (@exp) { |
225 |
push @resp_jobs, $job; |
226 |
} else { |
227 |
$job->{done}->send; |
228 |
} |
229 |
} |
230 |
} |
231 |
|
232 |
Event->io (fd => $aor, prio => 1, poll => 'r', cb => sub { |
233 |
sysread $aor, $rbuf, 4096, length $rbuf; |
234 |
|
235 |
while ($rbuf =~ s/^([^\015\012]*)[\012\015]+//s) { |
236 |
my $line = $1; |
237 |
$line =~ s/[\012\015]//g; |
238 |
|
239 |
next unless $line =~ /^\S/; |
240 |
|
241 |
@resp_jobs or die "out of sync: expected nothing, but got '$line'\n"; |
242 |
|
243 |
my $exp = shift @{ $resp_jobs[0]{exp} }; |
244 |
|
245 |
$exp eq substr $line, 0, 2 |
246 |
or die "sync error: expected '$exp', got '$line'\n"; |
247 |
|
248 |
push @{ $resp_jobs[0]{res} }, $line; |
249 |
|
250 |
unless (@{ $resp_jobs[0]{exp} }) { |
251 |
my $job = shift @resp_jobs; |
252 |
|
253 |
$job->{exclusive}->() if $job->{exclusive}; |
254 |
$job->{done}->send; |
255 |
|
256 |
send_scheduler; |
257 |
} |
258 |
|
259 |
next; |
260 |
} |
261 |
}); |
262 |
|
263 |
sub job::result { |
264 |
my ($self) = @_; |
265 |
|
266 |
$self->{done}->wait; |
267 |
|
268 |
wantarray ? @{ $self->{res} } : $self->{res}[0] |
269 |
} |
270 |
|
271 |
sub job { |
272 |
my ($nice, $mode, $freq, @cmd) = @_; |
273 |
|
274 |
my $job = bless { |
275 |
freq => round $freq, |
276 |
mode => $mode, |
277 |
cmd => \@cmd, |
278 |
done => new Coro::Signal, |
279 |
}, job::; |
280 |
|
281 |
if (@cmd && ref $cmd[-1]) { |
282 |
$job->{exclusive} = pop @cmd; |
283 |
} |
284 |
|
285 |
push_heap @send_jobs, [$nice, $job]; |
286 |
|
287 |
send_scheduler; |
288 |
|
289 |
$job |
290 |
} |
291 |
|
292 |
sub lm { |
293 |
map +(hex substr $_, 2) & 0x7f, @_ |
294 |
} |
295 |
|
296 |
sub aft($$$$) { |
297 |
my ($nice, $mode, $center, $radius) = @_; |
298 |
|
299 |
$radius = List::Util::max RESOLUTION, $radius * 0.1; |
300 |
|
301 |
my @level = map [$_->{freq}, (List::Util::sum lm $_->result) / 5], |
302 |
map +(job $nice, $mode, $center + $radius * $_, (delay 0.0020, "LM=LM") x 5), |
303 |
-10 .. 10; |
304 |
|
305 |
my ($tune, $weight); |
306 |
for (@level) { |
307 |
my ($f, $w) = @$_; |
308 |
|
309 |
$w **= 8; |
310 |
|
311 |
$tune += $f * $w; |
312 |
$weight += $w; |
313 |
} |
314 |
|
315 |
(int ($tune / $weight / RESOLUTION) * RESOLUTION, $weight ** 0.1 * 100) |
316 |
} |
317 |
|
318 |
sub sweep { |
319 |
my ($nice, $freqs, $cb) = @_; |
320 |
|
321 |
my $job; |
322 |
|
323 |
for (@$freqs, undef) { |
324 |
my ($mode, $freq, $width) = $_ ? @$_ : (); |
325 |
|
326 |
my $next_job = $freq && job $nice, $mode, $freq, delay 0.050, "LM=LM"; |
327 |
|
328 |
$cb->($mode, $freq, $width, lm $job->result) |
329 |
if $job; |
330 |
|
331 |
$job = $next_job; |
332 |
} |
333 |
} |
334 |
|
335 |
############################################################################# |
336 |
# raw aearch |
337 |
|
338 |
async { |
339 |
my $nice = 1000; |
340 |
|
341 |
while () { |
342 |
my @freq = sql_fetchall "select mode, freq, width from freq |
343 |
where search_time < ? |
344 |
order by search_time, mode, freq |
345 |
limit 1000", |
346 |
$NOW - SEARCH_INTERVAL_MIN; |
347 |
|
348 |
if (@freq) { |
349 |
sweep $nice, \@freq, sub { |
350 |
my ($mode, $freq, $width, $lm) = @_; |
351 |
|
352 |
sql_exec "update freq set search_time = ?, search_level = ? |
353 |
where mode = ? and freq = ?", |
354 |
$NOW, $lm, |
355 |
$mode, $freq; |
356 |
}; |
357 |
} else { |
358 |
Coro::Timer::sleep 60; |
359 |
} |
360 |
} |
361 |
}; |
362 |
|
363 |
############################################################################# |
364 |
# aft of active freqs |
365 |
|
366 |
async { |
367 |
my $nice = 500; |
368 |
|
369 |
while () { |
370 |
my $st = sql_exec \my ($aft_time, $mode, $freq, $width, $activity_level), |
371 |
"select aft_time, mode, freq, width, activity_level |
372 |
from freq |
373 |
where (search_level >= activity_level |
374 |
or aft_time > 0) |
375 |
and aft_time < ? |
376 |
order by aft_time, mode, freq |
377 |
limit 10", |
378 |
$NOW; |
379 |
|
380 |
if ($st->rows) { |
381 |
while ($st->fetch) { |
382 |
my @lm = lm +(job $nice, $mode, $freq, delay 0.050, "LM=LM")->result; |
383 |
|
384 |
if ($lm[0] >= $activity_level) { |
385 |
my ($tune, $weight) = aft $nice, $mode, $freq, $width * 0.9; |
386 |
|
387 |
if ($freq - 0.5 * $width <= $tune && $tune <= $freq + 0.5 * $width) { |
388 |
sql_exec "update freq set aft_time = ?, aft_freq = ?, aft_level = ? |
389 |
where mode = ? and freq = ?", |
390 |
$NOW + 86400 - 3600, $tune, $weight, |
391 |
$mode, $freq; |
392 |
} else { |
393 |
sql_exec "update freq set aft_time = ?, aft_level = 0 |
394 |
where mode = ? and freq = ?", |
395 |
$NOW + 86400 + 3600, |
396 |
$mode, $freq; |
397 |
} |
398 |
} else { |
399 |
# currently inactive |
400 |
sql_exec "update freq set aft_time = ? |
401 |
where mode = ? and freq = ?", |
402 |
$NOW + 3600, |
403 |
$mode, $freq; |
404 |
} |
405 |
} |
406 |
} else { |
407 |
Coro::Timer::sleep 60; |
408 |
} |
409 |
} |
410 |
}; |
411 |
|
412 |
############################################################################# |
413 |
# scan active freqs |
414 |
|
415 |
async { |
416 |
return; |
417 |
my $nice = 100; |
418 |
|
419 |
while () { |
420 |
my $st = sql_exec \my ($mode, $freq, $width, $activity_level), |
421 |
"select mode, aft_freq, width, activity_level |
422 |
from freq |
423 |
where aft_level > 0 and aft_freq > 0 |
424 |
order by freq, mode"; |
425 |
|
426 |
if ($st->rows) { |
427 |
my @jobs; |
428 |
|
429 |
while ($st->fetch) { |
430 |
push @jobs, [$activity_level, job $nice, $mode, $freq, delay 0.030, "LM=LM"]; |
431 |
} |
432 |
|
433 |
for (@jobs) { |
434 |
my ($activity_level, $job) = @$_; |
435 |
|
436 |
my @lm = lm $job->result; |
437 |
|
438 |
if ($lm[0] >= $activity_level) { |
439 |
warn "scan $job->{mode} $job->{freq} @lm\n"; |
440 |
(job $nice-1, $mode, $job->{freq}, "LM=LM", sub { |
441 |
# (job $nice-1, 0, 98400000, "LM=LM", sub { |
442 |
acmd $playback_on; |
443 |
Coro::Timer::sleep 0.5; |
444 |
# my $data = 0.5 + (1 / 65536) * float unpack "v*", record_nsamples $record_rate * 0.1; |
445 |
acmd $playback_off; |
446 |
#$data = sin +(1/480) * xvals zeroes 48000; |
447 |
# $data->reshape (100, $record_rate / 100); |
448 |
# |
449 |
# my $spectrum = 120 + cat map spectrum ($data->slice ("($_),"), "dB", "KAISER"), 0..99; |
450 |
# printf "%s %s\n", (min $spectrum), (max $spectrum); |
451 |
# imag short $spectrum; |
452 |
})->result; |
453 |
} |
454 |
} |
455 |
|
456 |
Coro::Timer::sleep 60; |
457 |
} else { |
458 |
Coro::Timer::sleep 60; |
459 |
} |
460 |
} |
461 |
}; |
462 |
|
463 |
############################################################################# |
464 |
|
465 |
Coro::Event::loop; |
466 |
|
467 |
__END__ |
468 |
mysql -e "delete from freq" aor |
469 |
./aorscan --addrange 1 26565000 27405000 10000 25 |
470 |
./aorscan --addrange 1 34360000 35800000 20000 25 |
471 |
./aorscan --addrange 1 38460000 39840000 20000 25 |
472 |
./aorscan --addrange 1 74215000 77455000 20000 25 |
473 |
./aorscan --addrange 1 84015000 87255000 20000 24 |
474 |
./aorscan --addrange 2 108000000 144000000 25000 20 |
475 |
./aorscan --addrange 1 118000000 136000000 8333.3333333333333 24 |
476 |
./aorscan --addrange 1 108000000 144000000 25000 24 |
477 |
./aorscan --addrange 4 165210000 169380000 20000 24 |
478 |
./aorscan --addrange 3 169810000 173980000 20000 24 |
479 |
./aorscan --addrange 1 230000000 328000000 8333.3333333333333 10 |
480 |
./aorscan --addrange 2 230000000 328000000 50000 20 |
481 |
./aorscan --addrange 1 438650000 439425000 25000 10 |
482 |
./aorscan --addrange 4 443600000 444962500 12500 10 |
483 |
./aorscan --addrange 3 448600000 449962500 12500 10 |
484 |
./aorscan --addrange 1 1270200000 1270700000 25000 10 |
485 |
./aorscan --addrange 1 1298200000 1298700000 25000 10 |
486 |
|
487 |
./aorscan --addrange 3 2690000 2690000 25000 10 |
488 |
./aorscan --addrange 3 3413000 3413000 25000 10 |
489 |
./aorscan --addrange 4 3413000 3413000 25000 10 |
490 |
./aorscan --addrange 3 5640000 5640000 25000 10 |
491 |
./aorscan --addrange 4 5640000 5640000 25000 10 |
492 |
./aorscan --addrange 3 8957000 8957000 25000 10 |
493 |
./aorscan --addrange 4 8957000 8957000 25000 10 |
494 |
./aorscan --addrange 3 13264000 13264000 25000 10 |
495 |
./aorscan --addrange 4 13264000 13264000 25000 10 |
496 |
|