mod acquire;
mod channel;
mod frontend;
mod labfit;
mod scene;
mod signal;
mod sweep;
mod track;
pub use signal::build_code;
#[cfg(feature = "python")]
pub(crate) use channel::{build_channel, ChannelParams};
#[cfg(feature = "python")]
pub(crate) use frontend::{build_chain, FrontendParams};
#[cfg(feature = "python")]
pub(crate) use scene::{build_broadcast_scene, build_scene, BroadcastParams, SceneParams};
use std::collections::HashMap;
pub(crate) const USAGE: &str = "usage: kshana iq scene <out> --rate <hz> --duration <s> --signal <name> --prn <list> [--doppler <list>] [--cn0 <dbhz>] [--noise-figure <db> | --no-noise] [--seed <n>] [--data] [--center <hz>] [--if <hz>] [--format <fmt>|sigmf] [--truth <path>] [--truth-format csv|jsonl] [--threads <n>]
or: kshana iq scene <out> --rate <hz> --window <s> --nav <rinex_nav> --rx-pos lat,lon,alt [--start <tow_s>] [--prn <list>] [--mask <deg>] [--cn0 <dbhz>] [--no-noise] [--format <fmt>|sigmf]
or: kshana iq acquire <recording> --signal <name> --prn <list> [--coherent <N>] [--noncoherent <M>] [--doppler-max <hz>] [--doppler-step <hz>] [--pfa <p>] [--json <out>] [--csv <out>]
or: kshana iq track <recording> --signal <name> --prn <list> [--pll-bw <hz>] [--fll-bw <hz>] [--dll-bw <hz>] [--spacing <chips>] [--coherent <N>] [--max-seconds <s>] [--acq-coherent <N> (default auto, ≈4 ms coherent)] [--acq-noncoherent <M>] [--doppler-max <hz>] [--json <out>] [--csv <out>]
or: kshana iq sweep <recording> --signal <name> --prn <list> [--pll-bw <list>] [--dll-bw <list>] [--spacing <list>] [--coherent <list>] [--max-seconds <s>] [--doppler-max <hz>] [--json <out>] [--csv <out>]
or: kshana iq labfit <scenario.toml> [--out-prefix <prefix>]
or: kshana iq frontend <in> <out> [--bandpass lo,hi] [--notch] [--blank <thr>] [--excise] [--agc] [--bits <n>] [--out-format <fmt>]
acquire/track/sweep also take the front-end flags [--bandpass lo,hi] [--notch] [--blank <thr>] [--excise] [--agc] [--bits <n>], applied before processing (to the acquisition and the tracking pass alike)
scene channel knobs: [--iono-stec <tecu> | --iono-vtec <tecu> | --iono-klobuchar] [--tropo [--tropo-doy <n>]] [--s4 <v> [--scint-tau0 <s>]] [--sigma-phi <rad>] [--multipath-height <m> [--multipath-ground dry|wet|sea]] [--land-mobile] [--nlos]
recording/raw inputs without a sidecar also take: --format <format> --rate <hz> [--center <hz>] [--if <hz>] [--header <bytes>]
signals: gps-l1ca gps-l5i gps-l5q gps-l2c galileo-e1b galileo-e1c galileo-e5a-i galileo-e5a-q beidou-b1i beidou-b1c glonass-l1of
<list> is a comma-separated list (one per --prn, or a single value applied to all)";
pub(crate) enum Fail {
Usage(String),
Run(String),
}
impl From<crate::iq::IqError> for Fail {
fn from(e: crate::iq::IqError) -> Self {
Fail::Run(e.to_string())
}
}
pub(crate) struct Args {
pub(crate) pos: Vec<String>,
pub(crate) opts: HashMap<String, String>,
switches: Vec<String>,
}
impl Args {
pub(crate) fn parse(args: &[String], switches: &[&str]) -> Result<Args, String> {
let mut a = Args {
pos: Vec::new(),
opts: HashMap::new(),
switches: Vec::new(),
};
let mut i = 0;
while i < args.len() {
let s = args[i].as_str();
if switches.contains(&s) {
a.switches.push(s.to_string());
i += 1;
} else if s.starts_with("--") {
let v = args.get(i + 1).ok_or(format!("{s} needs a value"))?;
a.opts.insert(s.to_string(), v.clone());
i += 2;
} else {
a.pos.push(s.to_string());
i += 1;
}
}
Ok(a)
}
pub(crate) fn num<T: std::str::FromStr>(&self, k: &str) -> Result<Option<T>, String> {
self.opts
.get(k)
.map(|v| {
v.parse::<T>()
.map_err(|_| format!("{k}: cannot parse {v:?}"))
})
.transpose()
}
pub(crate) fn has(&self, k: &str) -> bool {
self.switches.iter().any(|s| s == k)
}
pub(crate) fn get(&self, k: &str) -> Option<&str> {
self.opts.get(k).map(String::as_str)
}
pub(crate) fn need_pos(&self, n: usize, cmd: &str) -> Result<(), Fail> {
if self.pos.len() != n {
return Err(Fail::Usage(format!(
"iq {cmd} takes {n} positional argument(s), got {}",
self.pos.len()
)));
}
Ok(())
}
pub(crate) fn list<T: std::str::FromStr>(&self, k: &str) -> Result<Vec<T>, String> {
match self.opts.get(k) {
None => Ok(Vec::new()),
Some(v) => v
.split(',')
.map(|p| {
p.trim()
.parse::<T>()
.map_err(|_| format!("{k}: cannot parse {p:?}"))
})
.collect(),
}
}
}
pub fn signal_names() -> &'static [&'static str] {
signal::SIGNAL_NAMES
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CommandError {
Usage(String),
Run(String),
}
impl std::fmt::Display for CommandError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CommandError::Usage(m) => write!(f, "usage error: {m}"),
CommandError::Run(m) => f.write_str(m),
}
}
}
impl std::error::Error for CommandError {}
pub fn execute(args: &[String]) -> Result<String, CommandError> {
let rest = args.get(1..).unwrap_or_default();
let r = match args.first().map(String::as_str) {
Some("scene") => scene::run(rest),
Some("acquire") => acquire::run(rest),
Some("track") => track::run(rest),
Some("sweep") => sweep::run(rest),
Some("labfit") => labfit::run(rest),
Some("frontend") => frontend::run(rest),
Some(other) => Err(Fail::Usage(format!(
"unknown iq processing command {other:?}; expected scene, acquire, track, sweep, \
labfit or frontend"
))),
None => Err(Fail::Usage("no iq processing command given".into())),
};
r.map_err(|f| match f {
Fail::Usage(m) => CommandError::Usage(m),
Fail::Run(m) => CommandError::Run(m),
})
}
pub fn run(args: &[String]) -> i32 {
match args.first().map(String::as_str) {
None => {
eprintln!("{USAGE}\n{}", super::io::cli::USAGE);
2
}
Some("--help" | "-h" | "help") => {
println!("{USAGE}\n{}", super::io::cli::USAGE);
0
}
Some("scene" | "acquire" | "track" | "sweep" | "labfit" | "frontend") => {
finish(execute(args))
}
Some(_) => super::io::cli::run(args),
}
}
fn finish(r: Result<String, CommandError>) -> i32 {
match r {
Ok(msg) => {
println!("{msg}");
0
}
Err(CommandError::Usage(m)) => {
eprintln!("error: {m}\n{USAGE}");
2
}
Err(CommandError::Run(m)) => {
eprintln!("error: {m}");
1
}
}
}
pub(crate) fn raw_sidecar(a: &Args) -> Result<Option<crate::iq::io::inventory::RawSidecar>, Fail> {
let Some(format) = a.get("--format") else {
return Ok(None);
};
Ok(Some(crate::iq::io::inventory::RawSidecar {
format: format.to_string(),
sample_rate_hz: a
.num("--rate")
.map_err(Fail::Usage)?
.ok_or(Fail::Usage("--format needs --rate".into()))?,
center_hz: a.num("--center").map_err(Fail::Usage)?,
if_hz: a.num("--if").map_err(Fail::Usage)?,
header_bytes: a.num("--header").map_err(Fail::Usage)?,
datetime: None,
description: None,
}))
}