use std::io::{Read, Write};
use anyhow::Result;
use clap::Parser;
use log::info;
use rustradio::Float;
use rustradio::blockchain;
use rustradio::blocks::*;
use rustradio::graph::GraphRunner;
use rustradio::mtgraph::MTGraph;
use rustradio::stream::ReadStream;
use rustradio::{parse_frequency, parse_verbosity};
#[derive(clap::Parser, Debug)]
#[command(version, about)]
struct Opt {
#[arg(short)]
driver: String,
#[arg(short, value_parser=parse_verbosity, default_value = "info")]
verbose: usize,
#[arg(long, value_parser=parse_frequency)]
freq: f64,
#[arg(long, value_parser=parse_frequency, default_value = "300k")]
sample_rate: f64,
#[arg(long, default_value_t = 0.0)]
ogain: f64,
#[arg(long, default_value_t = 0.2)]
igain: f64,
#[arg(long, default_value = "0.1")]
symbol_max_deviation: Float,
#[arg(
long = "symbol_taps",
//default_value = "0.0001,0.99999999",
default_value = "1",
use_value_delimiter = true
)]
symbol_taps: Vec<Float>,
#[arg(long)]
tcp_listen: Option<String>,
#[cfg(feature = "nix")]
#[arg(long)]
tty: Option<std::path::PathBuf>,
#[arg(long)]
wpcr: bool,
}
fn get_kiss_stream(opt: &Opt) -> Result<(Box<dyn Write + Send>, Box<dyn Read + Send>)> {
if let Some(addr) = &opt.tcp_listen {
let listener = std::net::TcpListener::bind(addr)?;
info!("Awaiting connection");
let (tcp, addr) = listener.accept()?;
drop(listener);
info!("Connect from {addr}");
info!("Setting up device");
let rx = tcp.try_clone()?;
return Ok((Box::new(tcp), Box::new(rx)));
}
#[cfg(feature = "nix")]
{
if opt.tcp_listen.is_some() && opt.tty.is_some() {
return Err(anyhow::Error::msg("-tcp and -tty are mutually exclusive"));
}
use nix::pty::{OpenptyResult, openpty};
use std::ffi::CStr;
use std::fs::File;
use std::os::fd::AsRawFd;
if let Some(path) = &opt.tty {
let OpenptyResult { master, slave } = openpty(None, None)?;
let ptr = unsafe { libc::ptsname(master.as_raw_fd()) };
if ptr.is_null() {
return Err(anyhow::Error::msg(
"ptsname() on newly created TTY returned NULL",
));
}
let slave_name = unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() };
info!("Slave tty device: {slave_name}");
if let Err(e) = std::os::unix::fs::symlink(&slave_name, path) {
if e.kind() != std::io::ErrorKind::AlreadyExists
|| !path.symlink_metadata()?.is_symlink()
{
Err(rustradio::Error::file_io(e, path))?;
}
log::trace!(
"Symlink {} already exists. Attempting to delete it",
path.display()
);
std::fs::remove_file(path).map_err(|e| rustradio::Error::file_io(e, path))?;
std::os::unix::fs::symlink(slave_name, path)
.map_err(|e| rustradio::Error::file_io(e, path))?;
}
let rx = master.try_clone()?;
std::mem::forget(slave);
return Ok((Box::new(File::from(master)), Box::new(File::from(rx))));
}
}
Err(anyhow::Error::msg("-tcp or -tty is mandatory"))
}
fn receiver_traditional(
opt: &Opt,
g: &mut MTGraph,
prev: ReadStream<rustradio::Complex>,
) -> Result<ReadStream<Float>> {
let samp_rate_2 = 50_000.0;
let baud = 9600.0;
Ok(blockchain![
g,
prev,
RationalResampler::builder()
.deci(opt.sample_rate as usize)
.interp(samp_rate_2 as usize)
.build(prev)?,
QuadratureDemod::new(prev, 1.0),
SymbolSync::new(
prev,
samp_rate_2 / baud,
opt.symbol_max_deviation,
Box::new(rustradio::symbol_sync::TedZeroCrossing::new()),
Box::new(rustradio::iir_filter::IirFilter::new(&opt.symbol_taps)),
),
])
}
fn receiver_wpcr(
opt: &Opt,
g: &mut MTGraph,
prev: ReadStream<rustradio::Complex>,
) -> Result<ReadStream<Float>> {
let samp_rate_2 = 50_000.0;
let prev = blockchain![
g,
prev,
RationalResampler::builder()
.deci(opt.sample_rate as usize)
.interp(samp_rate_2 as usize)
.build(prev)?,
];
let iir_alpha = 0.01;
let threshold = 0.1;
let (b, prev, burst_tee) = Tee::new(prev);
g.add(Box::new(b));
let burst_tee = blockchain![
g,
burst_tee,
ComplexToMag2::new(burst_tee),
SinglePoleIirFilter::new(burst_tee, iir_alpha)
.ok_or(anyhow::Error::msg("bad IIR parameters"))?,
];
Ok(blockchain![
g,
prev,
QuadratureDemod::new(prev, 1.0),
BurstTagger::new(prev, burst_tee, threshold, "burst".to_string()),
StreamToPdu::new(prev, "burst".to_string(), samp_rate_2 as usize, 11000),
Midpointer::new(prev),
Wpcr::builder(prev).samp_rate(samp_rate_2).build(),
VecToStream::new(prev),
])
}
pub fn main() -> Result<()> {
println!("bell202 modem");
let opt = Opt::parse();
stderrlog::new()
.module(module_path!())
.module("rustradio")
.module("soapysdr")
.quiet(false)
.verbosity(opt.verbose)
.timestamp(stderrlog::Timestamp::Second)
.init()?;
soapysdr::configure_logging();
let mut g = MTGraph::new();
let (kiss, rx) = get_kiss_stream(&opt)?;
let dev = soapysdr::Device::new(&*opt.driver)?;
{
info!("Setting up transmitter");
let cancel = g.cancel_token();
let baud = 9600.0;
let if_rate = 48000.0;
let prev = blockchain![
g,
prev,
ReaderSource::new(rx)?,
KissFrame::new(prev),
KissDecode::new(prev),
FcsAdder::new(prev),
HdlcFramer::new(prev),
PduToStream::new(prev),
Scrambler::g3ruh(prev),
NrziEncode::new(prev),
RationalResampler::builder()
.deci(baud as usize)
.interp(if_rate as usize)
.build(prev)?,
Map::keep_tags(prev, "bits_to_pn", |s| if s > 0 {
3000.0 as Float
} else {
-3000.0
}),
Vco::new(prev, 2.0 * std::f64::consts::PI / if_rate),
MultiplyConst::new(prev, 0.5.into()),
RationalResampler::builder()
.deci(if_rate as usize)
.interp(opt.sample_rate as usize)
.build(prev)?,
FftFilter::new(
prev,
rustradio::fir::low_pass_complex(
opt.sample_rate as Float,
8_800.0,
1000.0,
&rustradio::window::WindowType::Hamming,
)
),
Canary::new(prev, move || cancel.cancel()),
];
g.add(Box::new(
SoapySdrSink::builder(&dev, opt.freq, opt.sample_rate)
.ogain(opt.ogain)
.build(prev)?,
));
}
{
info!("Setting up receiver");
let prev = blockchain![
g,
prev,
SoapySdrSource::builder(&dev, opt.freq, opt.sample_rate)
.igain(opt.igain)
.build()?,
FftFilter::new(
prev,
rustradio::fir::low_pass_complex(
opt.sample_rate as Float,
12_500.0,
100.0,
&rustradio::window::WindowType::Hamming,
)
),
];
let prev = if opt.wpcr {
receiver_wpcr(&opt, &mut g, prev)?
} else {
receiver_traditional(&opt, &mut g, prev)?
};
let prev = blockchain![
g,
prev,
BinarySlicer::new(prev),
NrziDecode::new(prev),
Descrambler::g3ruh(prev),
HdlcDeframer::new(prev, 10, 1500),
KissEncode::new(prev),
PduToStream::new(prev),
];
g.add(Box::new(WriterSink::new(prev, kiss)));
}
let cancel = g.cancel_token();
ctrlc::set_handler(move || {
eprintln!("Received Ctrl+C!");
cancel.cancel();
})
.expect("Error setting Ctrl-C handler");
g.run()?;
println!("{}", g.generate_stats().expect("failed to generate stats"));
Ok(())
}