/*! AX.25 9600bps receiver.
Can be used to receive APRS over the air with RTL-SDR or from
complex I/Q saved to a file.
```no_run
$ mkdir captured
$ ./ax25-9600-rx -r captured.c32 --samp_rate 50000 -o captured
[…]
$ ./ax25-9600-rx --rtlsdr -o captured -v 2
[…]
```
*
*/
use std::path::PathBuf;
use anyhow::Result;
use structopt::StructOpt;
use rustradio::blocks::*;
use rustradio::graph::Graph;
use rustradio::graph::GraphRunner;
use rustradio::{Complex, Float};
#[derive(StructOpt, Debug)]
#[structopt()]
struct Opt {
#[structopt(long = "audio", short = "a")]
audio: bool,
#[structopt(long = "out", short = "o")]
output: PathBuf,
#[cfg(feature = "rtlsdr")]
#[structopt(long = "freq", default_value = "144800000")]
freq: u64,
#[cfg(feature = "rtlsdr")]
#[structopt(long = "gain", default_value = "20")]
gain: i32,
#[structopt(short = "v", default_value = "0")]
verbose: usize,
#[structopt(long = "rtlsdr")]
rtlsdr: bool,
#[structopt(long = "clock-file", help = "File to write clock sync data to")]
clock_file: Option,
#[structopt(long = "sample_rate", default_value = "300000")]
samp_rate: u32,
#[structopt(short = "r")]
read: Option,
#[structopt(
long = "symbol_taps",
default_value = "0.0001,0.99999999",
use_delimiter = true
)]
symbol_taps: Vec,
#[structopt(long, default_value = "0.1")]
symbol_max_deviation: Float,
}
macro_rules! add_block {
($g:ident, $cons:expr) => {{
let block = Box::new($cons);
let prev = block.out();
$g.add(block);
prev
}};
}
fn main() -> Result<()> {
let opt = Opt::from_args();
stderrlog::new()
.module(module_path!())
.module("rustradio")
.quiet(false)
.verbosity(opt.verbose)
.timestamp(stderrlog::Timestamp::Second)
.init()?;
let mut g = Graph::new();
// TODO: this is a complete mess.
let (prev, samp_rate) = if opt.audio {
if let Some(read) = opt.read {
let prev = add_block![g, FileSource::new(&read, false)?];
let prev = add_block![g, AuDecode::new(prev, opt.samp_rate)];
/*
let (prev, b) = add_block![g, Tee::new(prev)];
g.add(Box::new(FileSink::new(
b,
"debug/00-audio.f32",
rustradio::file_sink::Mode::Overwrite,
)?));
*/
(prev, opt.samp_rate as Float)
} else {
panic!("Audio can only be read from file")
}
} else {
let prev = if let Some(read) = opt.read {
add_block![g, FileSource::::new(&read, false)?]
} else if opt.rtlsdr {
#[cfg(feature = "rtlsdr")]
{
// Source.
let prev = add_block![g, RtlSdrSource::new(opt.freq, opt.samp_rate, opt.gain)?];
// Decode.
let prev = add_block![g, RtlSdrDecode::new(prev)];
prev
}
#[cfg(not(feature = "rtlsdr"))]
panic!("rtlsdr feature not enabled")
} else {
panic!("Need to provide either --rtlsdr or -r")
};
let samp_rate = opt.samp_rate as Float;
/*
let (prev, b) = add_block![g, Tee::new(prev)];
g.add(Box::new(FileSink::new(
b,
"debug/00-unfiltered.c32",
rustradio::file_sink::Mode::Overwrite,
)?));
*/
// Filter RF.
let taps = rustradio::fir::low_pass_complex(
samp_rate,
12_500.0,
100.0,
&rustradio::window::WindowType::Hamming,
);
let prev = add_block![g, FftFilter::new(prev, &taps)];
// Resample RF.
let new_samp_rate = 50_000.0;
let prev = add_block![
g,
RationalResampler::new(prev, new_samp_rate as usize, samp_rate as usize)?
];
let samp_rate = new_samp_rate;
let prev = add_block![g, QuadratureDemod::new(prev, 1.0)];
(prev, samp_rate)
};
//let taps = rustradio::fir::low_pass(samp_rate, 20_000.0, 100.0);
//let prev = add_block![g, FftFilterFloat::new(prev, &taps)];
let baud = 9600.0;
let (prev, mut block) = {
let clock_filter = rustradio::iir_filter::IIRFilter::new(&opt.symbol_taps);
let block = SymbolSync::new(
prev,
samp_rate / baud,
opt.symbol_max_deviation,
Box::new(rustradio::symbol_sync::TEDZeroCrossing::new()),
Box::new(clock_filter),
);
(block.out(), block)
};
// Optional clock output.
let prev = if let Some(clockfile) = opt.clock_file {
let clock = block.out_clock();
let (a, prev) = add_block![g, Tee::new(prev)];
let clock = add_block![g, AddConst::new(clock, -samp_rate / baud)];
let clock = add_block![g, ToText::new(vec![a, clock])];
g.add(Box::new(FileSink::new(
clock,
clockfile,
rustradio::file_sink::Mode::Overwrite,
)?));
prev
} else {
prev
};
g.add(Box::new(block));
let prev = add_block![g, BinarySlicer::new(prev)];
// Delay xor, aka NRZI decode.
let prev = add_block![g, NrziDecode::new(prev)];
// G3RUH descramble.
let prev = add_block![g, Descrambler::new(prev, 0x21, 0, 16)];
// Decode.
let prev = add_block![g, HdlcDeframer::new(prev, 10, 1500)];
g.add(Box::new(PduWriter::new(prev, opt.output)));
let cancel = g.cancel_token();
ctrlc::set_handler(move || {
eprintln!("Received Ctrl+C!");
cancel.cancel();
})
.expect("Error setting Ctrl-C handler");
// Run.
eprintln!("Running…");
let st = std::time::Instant::now();
g.run()?;
eprintln!("{}", g.generate_stats(st.elapsed()));
Ok(())
}
/* ---- Emacs variables ----
* Local variables:
* compile-command: "cargo run --example ax25-9600-rx -- -r ../aprs-9600-50k.c32 --sample_rate 50000 -o ../packets"
* End:
*/