mod acquire;
mod campaign;
mod channel;
mod frontend;
mod labfit;
mod monitor;
mod scene;
mod signal;
mod sweep;
mod track;
pub use signal::build_code;
#[cfg(feature = "python")]
pub(crate) use channel::{build_channel, ChannelParams};
pub(crate) use frontend::{apply_chain, build_chain, FrontendParams};
#[cfg(feature = "python")]
pub(crate) use monitor::{parse_args as monitor_parse_args, report_from_args as monitor_report};
#[cfg(feature = "python")]
pub(crate) use scene::{build_broadcast_scene, build_scene, BroadcastParams, SceneParams};
#[cfg(feature = "python")]
pub(crate) use track::sampling_warnings;
#[cfg(feature = "python")]
pub(crate) use Fail as CliFail;
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>] [--surface <path> [--surface-format csv|json|bin]]
or: kshana iq track <recording> --signal <name> --prn <list> [--design <file.toml> [--design-name <n>]] [--pll-bw <hz>] [--fll-bw <hz>] [--dll-bw <hz>] [--spacing <chips>] [--extra-taps <chips,...>] [--coherent <N>] [--reacquire] [--threads <N|auto>] [--max-seconds <s>] [--acq-coherent <N> (default auto, ≈4 ms coherent)] [--acq-noncoherent <M>] [--doppler-max <hz>] [--epochs <path> [--epochs-format csv|jsonl|bin]] [--events <path>] [--summary <path>] [--json <out>] [--csv <out>]
or: kshana iq sweep <recording> --signal <name> --prn <list> (--design <file.toml> | [--pll-bw <list>] [--dll-bw <list>] [--spacing <list>] [--coherent <list>]) [--reacquire] [--threads <N|auto>] [--max-seconds <s>] [--doppler-max <hz>] [--epochs <path>] [--events <path>] [--json <out>] [--csv <out>]
or: kshana iq labfit <scenario.toml> [--out-prefix <prefix>]
or: kshana iq campaign <campaign.toml> [--out <dir>] [--workers <n>] [--no-resume] [--max-cells <n>] [--dry-run] [--json <out>]
or: kshana iq campaign report <out-dir>
or: kshana iq conditions <test-conditions.toml|json>
or: kshana iq frontend <in> <out> [--bandpass lo,hi] [--notch] [--blank <thr>] [--excise] [--agc] [--bits <n>] [--out-format <fmt>]
or: kshana iq monitor <recording> [--power] [--spectral] [--settings <toml|json>] [--baseline <s>] [--signal <name> --prn <list> [--spacing <chips>] [--pll-bw <hz>] [--fll-bw <hz>] [--dll-bw <hz>] [--coherent <N>] [--cn0-windows <M>] [--doppler-max <hz>]] [--max-seconds <s>] [--json <out>] [--csv <prefix>]
loop designs: a kshana.loop-design/1 TOML file (docs/design/LOOP-DESIGN-TOML.md); explicit loop/acquisition flags override the selected design
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]
scene C/N0 profile: [--cn0-profile <toml>] time-varying C/N0 offsets per satellite (step, ramp, points, fade), applied over the channel; the truth's cn0_dbhz follows it
recording/raw inputs without a sidecar also take: --format <format> --rate <hz> [--center <hz>] [--if <hz>] [--header <bytes>] [--channels <n>]; multi-stream inputs take --channel <k>
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("monitor") => monitor::run(rest),
Some("campaign") => campaign::run(rest),
Some("conditions") => campaign::conditions(rest),
Some(other) => Err(Fail::Usage(format!(
"unknown iq processing command {other:?}; expected scene, acquire, track, sweep, \
labfit, frontend, monitor, campaign or conditions"
))),
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" | "monitor"
| "campaign" | "conditions",
) => 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)?,
channels: a.num("--channels").map_err(Fail::Usage)?,
channel: None,
datetime: None,
description: None,
}))
}
pub(crate) fn open_input(
a: &Args,
pos: usize,
) -> Result<crate::iq::io::inventory::OpenedRecording, Fail> {
let path = a
.pos
.get(pos)
.ok_or_else(|| Fail::Usage("missing recording path".into()))?;
Ok(crate::iq::io::inventory::open_recording_with(
std::path::Path::new(path),
raw_sidecar(a)?,
a.num("--channel").map_err(Fail::Usage)?,
)?)
}