use std::io;
use byteorder::{NativeEndian, ReadBytesExt};
use clap::{App, Arg, ArgMatches};
use log::{info, LevelFilter};
use sameold::SameReceiverBuilder;
mod app;
mod spawner;
const STDIN_FILE: &str = "-";
const USAGE: &str = r#"
A simple decoder for Specific Area Message Encoding (SAME). This
program accepts raw PCM samples in signed 16-bit (i16) format,
at the given sampling --rate, and decodes any SAME headers that
are present. Decoded headers are printed in their ASCII
representation.
You can pipe in an audio file with sox
sox input.wav -t raw -r 22.5k -e signed -b 16 -c 1 - \
| samedec -r 22050
Arguments which follow "--" will be used to spawn a child
process. The child process will have the input audio signal
piped to its standard input. You can use this to play or store
SAME messages.
parec --channels 1 --format s16ne \
--rate 22050 --latency-msec 500 \
| samedec -r 22050 -- pacat \
--channels 1 --format s16ne \
--rate 22050 --latency-msec 500
The child process receives the following additional environment
variables which describe the message:
SAMEDEC_MSG="ZCZC-EAS-RWT-012057-012081+0030-2780415-WTSP/TV-"
SAMEDEC_ORIGINATOR="EAS Participant"
SAMEDEC_EVT="RWT"
SAMEDEC_EVENT="Required Weekly Test"
SAMEDEC_SIGNIFICANCE="T" (or M,S,E,A,W)
SAMEDEC_LOCATIONS="012057 012081"
SAMEDEC_ISSUETIME="1616883240" (UTC UNIX timesamp)
SAMEDEC_PURGETIME="1616886840" (UTC UNIX timesamp)
Child processes MUST read or close standard input.
Child processes MUST exit when their standard input is closed.
ALWAYS TEST YOUR DECODING SETUP!
"#;
fn main() {
let matches = App::new(env!("CARGO_PKG_NAME"))
.version(env!("CARGO_PKG_VERSION"))
.author("Colin S. <https://crates.io/crates/samedec>")
.about("SAME/EAS decoder")
.after_help(USAGE)
.arg(
Arg::with_name("v")
.long("verbose")
.short("v")
.multiple(true)
.help("Verbosity level (-vvv for more)")
.display_order(1),
)
.arg(
Arg::with_name("quiet")
.long("quiet")
.short("q")
.help("Print NOTHING, not even SAME headers")
.display_order(2),
)
.arg(
Arg::with_name("rate")
.long("rate")
.short("r")
.takes_value(true)
.default_value("22050")
.help("Sampling rate (Hz)")
.display_order(3),
)
.arg(
Arg::with_name("file")
.long("file")
.help("Input file (or \"-\" for stdin)")
.required(false)
.takes_value(true)
.default_value(STDIN_FILE),
)
.arg(
Arg::with_name("demo")
.long("demo")
.takes_value(false)
.help("Issue demo warning (DMO) and exit"),
)
.arg(
Arg::with_name("dc-blocker-len")
.long("dc-blocker-len")
.help("DC Blocker filter length (fsym)")
.takes_value(true)
.default_value("0.38")
.hidden_short_help(true),
)
.arg(
Arg::with_name("agc-bw")
.long("agc-bw")
.help("AGC bandwidth (fsym)")
.takes_value(true)
.default_value("0.01")
.hidden_short_help(true),
)
.arg(
Arg::with_name("timing-bw-unlocked")
.long("timing-bw-unlocked")
.help("Timing loop bandwidth, searching (fsym)")
.takes_value(true)
.default_value("0.125")
.hidden_short_help(true),
)
.arg(
Arg::with_name("timing-bw-locked")
.long("timing-bw-locked")
.help("Timing loop bandwidth, tracking (fsym)")
.takes_value(true)
.default_value("0.05")
.hidden_short_help(true),
)
.arg(
Arg::with_name("timing-max-dev")
.long("timing-max-dev")
.help("Timing maximum deviation (fsym)")
.takes_value(true)
.default_value("0.01")
.hidden_short_help(true),
)
.arg(
Arg::with_name("squelch-pwr-open")
.long("squelch-pwr-open")
.help("Power req'd to start receiving (0.0 ≤ PWR ≤ 1.0)")
.takes_value(true)
.default_value("0.10")
.hidden_short_help(true),
)
.arg(
Arg::with_name("squelch-pwr-close")
.long("squelch-pwr-close")
.help("Power req'd to keep receiving (0.0 ≤ PWR ≤ 1.0)")
.takes_value(true)
.default_value("0.05")
.hidden_short_help(true),
)
.arg(
Arg::with_name("preamble-max-errors")
.long("preamble-max-errors")
.help("Permitted bit errors in sync pattern (<7)")
.takes_value(true)
.default_value("2")
.hidden_short_help(true),
)
.arg(
Arg::with_name("CHILD")
.takes_value(true)
.multiple(true)
.last(true)
.help("Spawn child process to handle message audio."),
)
.get_matches();
log_setup(&matches);
let rate = str::parse::<u32>(matches.value_of("rate").unwrap())
.expect("invalid sampling --rate: expect integer");
let mut rx = SameReceiverBuilder::new(rate)
.with_agc_gain_limits(1.0f32 / (i16::MAX as f32), 1.0 / 200.0)
.with_agc_bandwidth(
str::parse(matches.value_of("agc-bw").unwrap()).expect("--agc-bw: expect float"),
)
.with_dc_blocker_length(
str::parse(matches.value_of("dc-blocker-len").unwrap())
.expect("--dc-blocker-len: expect float"),
)
.with_timing_bandwidth(
str::parse(matches.value_of("timing-bw-unlocked").unwrap())
.expect("--timing-bw-unlocked: expect float"),
str::parse(matches.value_of("timing-bw-locked").unwrap())
.expect("--timing-bw-locked: expect float"),
)
.with_timing_max_deviation(
str::parse(matches.value_of("timing-max-dev").unwrap())
.expect("--timing-max-dev: expect float"),
)
.with_squelch_power(
str::parse(matches.value_of("squelch-pwr-open").unwrap())
.expect("--squelch-pwr-open: expect float"),
str::parse(matches.value_of("squelch-pwr-close").unwrap())
.expect("--squelch-pwr-close: expect float"),
)
.with_preamble_max_errors(
str::parse(matches.value_of("preamble-max-errors").unwrap())
.expect("--preamble-max-errors: expect integer"),
)
.build();
let stdin = io::stdin();
let stdin_handle = stdin.lock();
let mut inbuf = file_setup(&matches, stdin_handle);
app::run(
&matches,
&mut rx,
std::iter::from_fn(|| Some(inbuf.read_i16::<NativeEndian>().ok()?)),
);
match rx.flush() {
Some(lastmsg) => {
if matches.occurrences_of("quiet") == 0 {
println!("{}", lastmsg)
}
}
None => {}
}
}
fn log_setup(args: &ArgMatches) {
if args.occurrences_of("quiet") > 0 {
return;
} else if std::env::var_os("RUST_LOG").is_none() {
let log_filter = match args.occurrences_of("v") {
0 => LevelFilter::Warn,
1 => LevelFilter::Info,
2 => LevelFilter::Debug,
3 | _ => LevelFilter::Trace,
};
pretty_env_logger::formatted_builder()
.filter_module("sameold", log_filter)
.filter_module("samedec", log_filter)
.init();
} else {
pretty_env_logger::init();
}
}
fn file_setup<'stdin>(
args: &ArgMatches,
stdin: std::io::StdinLock<'stdin>,
) -> Box<dyn io::BufRead + 'stdin> {
let filename = args.value_of("file").unwrap();
if filename == STDIN_FILE {
info!("SAME decoder reading standard input");
Box::new(io::BufReader::new(stdin))
} else {
info!("SAME decoder reading file");
Box::new(io::BufReader::new(
std::fs::File::open(filename).expect("Unable to open requested FILE"),
))
}
}