Skip to content

Commit 7efdf5b

Browse files
committed
Refactor command line argument parsing and enhance GNSS simulation capabilities
1 parent 88a73da commit 7efdf5b

1 file changed

Lines changed: 253 additions & 101 deletions

File tree

core/src/main.rs

Lines changed: 253 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,278 @@
1-
use clap::Parser;
1+
use clap::{Args, Parser, Subcommand, ValueEnum};
22
use csv::ReaderBuilder;
33
use std::error::Error;
4-
use std::path::PathBuf;
4+
use std::path::{Path, PathBuf};
5+
use strapdown::filter::UKF;
6+
use strapdown::messages::{
7+
GnssDegradationConfig, GnssFaultModel, GnssScheduler, build_event_stream,
8+
};
59
use strapdown::sim::{
610
NavigationResult, TestDataRecord, closed_loop, dead_reckoning, degrade_measurements,
11+
initialize_ukf,
712
};
813

14+
const LONG_ABOUT: &str = "STRAPDOWN: A simulation and analysis tool for strapdown inertial navigation systems.
15+
16+
This program can operate in two modes: open-loop and closed-loop. In open loop mode, the system relies solely on inertial measurements (IMU) and an initial position estimate and performs simple dead reckoning. This mode is only particularly useful for high-accuracy low noise IMUs such as those for aerospace or marine applications (i.e. have a drift rate <=1nm per 24 hours). In closed-loop mode, the system incorporates GNSS measurements to correct for IMU drift and improve overall navigation accuracy. The closed-loop mode can simulate various GNSS degradation scenarios, including jamming (signal dropouts, reduced update rates, and measurement corruption) and a limited form of spoofing (bias introduction, signal hijack).
17+
18+
This program is designed to work with tabular comma-separated value datasets that contain IMU and GNSS measurements of the form:
19+
* time - ISO UTC timestamp of the form YYYY-MM-DD HH:mm:ss.ssss+HH:MM (where the +HH:MM is the timezone offset)
20+
* speed - Speed measurement in meters per second
21+
* bearing - Bearing measurement in degrees
22+
* altitude - Altitude measurement in meters
23+
* longitude - Longitude measurement in degrees
24+
* latitude - Latitude measurement in degrees
25+
* qz - Quaternion component
26+
* qy - Quaternion component
27+
* qx - Quaternion component
28+
* qw - Quaternion component
29+
* roll - Roll angle in degrees
30+
* pitch - Pitch angle in degrees
31+
* yaw - Yaw angle in degrees
32+
* acc_z - Acceleration in the Z direction (meters per second squared)
33+
* acc_y - Acceleration in the Y direction (meters per second squared)
34+
* acc_x - Acceleration in the X direction (meters per second squared)
35+
* gyro_z - Angular velocity around the Z axis (radians per second)
36+
* gyro_y - Angular velocity around the Y axis (radians per second)
37+
* gyro_x - Angular velocity around the X axis (radians per second)
38+
* mag_z - Magnetic field strength in the Z direction (micro teslas)
39+
* mag_y - Magnetic field strength in the Y direction (micro teslas)
40+
* mag_x - Magnetic field strength in the X direction (micro teslas)
41+
* relativeAltitude - Relative altitude measurement (meters)
42+
* pressure - Atmospheric pressure measurement (milli bar)
43+
* grav_z - Gravitational acceleration in the Z direction (meters per second squared)
44+
* grav_y - Gravitational acceleration in the Y direction (meters per second squared)
45+
* grav_x - Gravitational acceleration in the X direction (meters per second squared)";
46+
947
/// Command line arguments
10-
#[derive(Parser, Debug)]
11-
#[clap(author, version, about, long_about = None)]
12-
struct Args {
48+
#[derive(Parser)]
49+
#[command(author, version, about, long_about = LONG_ABOUT)]
50+
struct Cli {
1351
/// Mode of operation, either open-loop or closed-loop
14-
#[clap(
15-
short,
16-
long,
17-
value_parser,
18-
default_value = "open-loop",
19-
help = "Mode of operation: 'open-loop' or 'closed-loop'"
20-
)]
21-
mode: String,
22-
/// Input CSV file path
23-
#[clap(
24-
short,
25-
long,
26-
value_parser,
27-
help = "Path to the input CSV file containing IMU and (optionally) GPS data"
28-
)]
52+
#[command(subcommand)]
53+
mode: SimMode,
54+
/// Input file path
55+
#[arg(short, long, value_parser)]
2956
input: PathBuf,
30-
/// Output CSV file path
31-
#[clap(
32-
short,
33-
long,
34-
value_parser,
35-
help = "Path to the output CSV file for navigation results"
36-
)]
57+
/// Output file path
58+
#[arg(short, long, value_parser)]
3759
output: PathBuf,
38-
/// Optional: GPS measurement interval in seconds (for simulating intermittent GPS outages)
39-
#[clap(
40-
long,
41-
value_parser,
42-
help = "Interval (in seconds) between GPS measurements, used to simulate GPS outages. Default is 0.0 which updates at every iteration."
43-
)]
44-
gps_interval: Option<f64>,
45-
/// Optional: GPS degradation factor
46-
#[clap(
47-
long,
48-
value_parser,
49-
default_value = "1.0",
50-
help = "Factor by which GPS accuracy is degraded. 1.0 is no degradation. >= 1.0 is a degradation factor. This factor is applied as a scalar multiplier to the recorded GPS accuracy."
51-
)]
52-
gps_degradation: Option<f64>,
53-
/// Optional: GPS spoofing offset
54-
#[clap(
55-
long,
56-
value_parser,
57-
default_value = "0.0",
58-
help = "Offset to apply to GPS coordinates (in meters)"
59-
)]
60-
gps_spoofing_offset: Option<f64>,
6160
}
62-
fn main() -> Result<(), Box<dyn Error>> {
63-
let args = Args::parse();
64-
// Validate the mode
65-
if args.mode != "open-loop" && args.mode != "closed-loop" {
66-
return Err("Invalid mode specified. Use 'open-loop' or 'closed-loop'.".into());
61+
#[derive(Subcommand, Clone)]
62+
enum SimMode {
63+
#[command(name = "open-loop", about = "Run the simulation in open-loop mode")]
64+
OpenLoop,
65+
#[command(name = "closed-loop", about = "Run the simulation in closed-loop mode")]
66+
ClosedLoop(ClosedLoopArgs),
67+
}
68+
/* -------------------- COMPARTMENTALIZED GROUPS -------------------- */
69+
#[derive(Args, Clone, Debug)]
70+
struct ClosedLoopArgs {
71+
/// RNG seed (applies to any stochastic options)
72+
#[arg(long, default_value_t = 42)]
73+
seed: u64,
74+
75+
/// Scheduler settings (dropouts / reduced rate)
76+
#[command(flatten)]
77+
scheduler: SchedulerArgs,
78+
79+
/// Fault model settings (corrupt measurement content)
80+
#[command(flatten)]
81+
fault: FaultArgs,
82+
}
83+
84+
// Scheduler group
85+
#[derive(Copy, Clone, Debug, ValueEnum)]
86+
enum SchedKind {
87+
Passthrough,
88+
Fixed,
89+
Duty,
90+
}
91+
92+
#[derive(Args, Clone, Debug)]
93+
struct SchedulerArgs {
94+
/// Scheduler kind: passthrough | fixed | duty
95+
#[arg(long, value_enum, default_value_t = SchedKind::Passthrough)]
96+
sched: SchedKind,
97+
98+
/// Fixed-interval seconds (sched=fixed)
99+
#[arg(long, default_value_t = 1.0)]
100+
interval_s: f64,
101+
102+
/// Initial phase seconds (sched=fixed)
103+
#[arg(long, default_value_t = 0.0)]
104+
phase_s: f64,
105+
106+
/// Duty-cycle ON seconds (sched=duty)
107+
#[arg(long, default_value_t = 10.0)]
108+
on_s: f64,
109+
110+
/// Duty-cycle OFF seconds (sched=duty)
111+
#[arg(long, default_value_t = 10.0)]
112+
off_s: f64,
113+
114+
/// Duty-cycle start phase seconds (sched=duty)
115+
#[arg(long, default_value_t = 0.0)]
116+
duty_phase_s: f64,
117+
}
118+
/// Fault group
119+
#[derive(Args, Clone, Debug)]
120+
struct FaultArgs {
121+
/// Fault kind: none | degraded | slowbias | hijack
122+
#[arg(long, value_enum, default_value_t = FaultKind::None)]
123+
fault: FaultKind,
124+
125+
/// Degraded (AR(1))
126+
#[arg(long, default_value_t = 0.99)]
127+
rho_pos: f64,
128+
#[arg(long, default_value_t = 3.0)]
129+
sigma_pos_m: f64,
130+
#[arg(long, default_value_t = 0.95)]
131+
rho_vel: f64,
132+
#[arg(long, default_value_t = 0.3)]
133+
sigma_vel_mps: f64,
134+
#[arg(long, default_value_t = 5.0)]
135+
r_scale: f64,
136+
137+
/// Slow bias
138+
#[arg(long, default_value_t = 0.02)]
139+
drift_n_mps: f64,
140+
#[arg(long, default_value_t = 0.0)]
141+
drift_e_mps: f64,
142+
#[arg(long, default_value_t = 1e-6)]
143+
q_bias: f64,
144+
#[arg(long, default_value_t = 0.0)]
145+
rotate_omega_rps: f64,
146+
147+
/// Hijack
148+
#[arg(long, default_value_t = 50.0)]
149+
hijack_offset_n_m: f64,
150+
#[arg(long, default_value_t = 0.0)]
151+
hijack_offset_e_m: f64,
152+
#[arg(long, default_value_t = 120.0)]
153+
hijack_start_s: f64,
154+
#[arg(long, default_value_t = 60.0)]
155+
hijack_duration_s: f64,
156+
}
157+
158+
#[derive(Copy, Clone, Debug, ValueEnum)]
159+
enum FaultKind {
160+
None,
161+
Degraded,
162+
Slowbias,
163+
Hijack,
164+
}
165+
166+
/* -------------------- BUILDERS FROM GROUPS -------------------- */
167+
168+
fn build_scheduler(a: &SchedulerArgs) -> GnssScheduler {
169+
match a.sched {
170+
SchedKind::Passthrough => GnssScheduler::PassThrough,
171+
SchedKind::Fixed => GnssScheduler::FixedInterval {
172+
interval_s: a.interval_s,
173+
phase_s: a.phase_s,
174+
},
175+
SchedKind::Duty => GnssScheduler::DutyCycle {
176+
on_s: a.on_s,
177+
off_s: a.off_s,
178+
start_phase_s: a.duty_phase_s,
179+
},
67180
}
68-
// Validate the GPS measurement interval is positive
69-
match args.gps_interval {
70-
Some(interval) if interval < 0.0 => {
71-
return Err("GPS measurement interval must be a non-negative value.".into());
72-
}
73-
_ => {}
181+
}
182+
183+
fn build_fault(a: &FaultArgs) -> GnssFaultModel {
184+
match a.fault {
185+
FaultKind::None => GnssFaultModel::None,
186+
FaultKind::Degraded => GnssFaultModel::Degraded {
187+
rho_pos: a.rho_pos,
188+
sigma_pos_m: a.sigma_pos_m,
189+
rho_vel: a.rho_vel,
190+
sigma_vel_mps: a.sigma_vel_mps,
191+
r_scale: a.r_scale,
192+
},
193+
FaultKind::Slowbias => GnssFaultModel::SlowBias {
194+
drift_n_mps: a.drift_n_mps,
195+
drift_e_mps: a.drift_e_mps,
196+
q_bias: a.q_bias,
197+
rotate_omega_rps: a.rotate_omega_rps,
198+
},
199+
FaultKind::Hijack => GnssFaultModel::Hijack {
200+
offset_n_m: a.hijack_offset_n_m,
201+
offset_e_m: a.hijack_offset_e_m,
202+
start_s: a.hijack_start_s,
203+
duration_s: a.hijack_duration_s,
204+
},
74205
}
206+
}
207+
208+
fn main() -> Result<(), Box<dyn Error>> {
209+
let cli = Cli::parse();
210+
// Validate the mode
211+
//if args.mode != "open-loop" && args.mode != "closed-loop" {
212+
// return Err("Invalid mode specified. Use 'open-loop' or 'closed-loop'.".into());
213+
//}
75214
// Read the input CSV file
76215
// Validate that the input file exists and is readable
77-
if !args.input.exists() {
78-
return Err(format!("Input file '{}' does not exist.", args.input.display()).into());
216+
if !cli.input.exists() {
217+
return Err(format!("Input file '{}' does not exist.", cli.input.display()).into());
79218
}
80-
if !args.input.is_file() {
81-
return Err(format!("Input path '{}' is not a file.", args.input.display()).into());
219+
if !cli.input.is_file() {
220+
return Err(format!("Input path '{}' is not a file.", cli.input.display()).into());
82221
}
83222
// Validate that the output file is writable
84-
if let Some(parent) = args.output.parent() {
223+
if let Some(parent) = cli.output.parent() {
85224
if !parent.exists() && parent.is_dir() {
86225
return Err(format!("Output directory '{}' does not exist.", parent.display()).into());
87226
}
88-
// if !parent.is_dir() {
89-
// return Err(format!("Output path '{}' is not a directory.", parent.display()).into());
90-
// }
91227
}
92-
let mut rdr = ReaderBuilder::new().from_path(&args.input)?;
93-
let mut records: Vec<TestDataRecord> = rdr.deserialize().collect::<Result<_, _>>()?;
94-
println!(
95-
"Read {} records from {}",
96-
records.len(),
97-
&args.input.display()
98-
);
99-
records = match args.gps_degradation {
100-
Some(value) => degrade_measurements(records, value),
101-
None => records,
102-
};
103-
let results: Vec<NavigationResult>;
104-
if args.mode == "closed-loop" {
105-
println!(
106-
"Running in closed-loop mode with GPS interval: {:?}",
107-
args.gps_interval
108-
);
109-
results = closed_loop(&records, args.gps_interval);
110-
//match write_results_to_csv(&results, &args.output) {
111-
match NavigationResult::to_csv(&results, &args.output) {
112-
Ok(_) => println!("Results written to {}", args.output.display()),
113-
Err(e) => eprintln!("Error writing results: {}", e),
114-
};
115-
} else if args.mode == "open-loop" {
116-
results = dead_reckoning(&records);
117-
// match write_results_to_csv(&results, &args.output) {
118-
match NavigationResult::to_csv(&results, &args.output) {
119-
Ok(_) => println!("Results written to {}", args.output.display()),
120-
Err(e) => eprintln!("Error writing results: {}", e),
121-
};
122-
} else {
123-
return Err("Invalid mode specified. Use 'open-loop' or 'closed-loop'.".into());
228+
match cli.mode {
229+
SimMode::OpenLoop => println!("Running in open-loop mode"),
230+
SimMode::ClosedLoop(ref args) => {
231+
let mut rdr = ReaderBuilder::new().from_path(&cli.input)?;
232+
let records: Vec<TestDataRecord> = rdr.deserialize().collect::<Result<_, _>>()?;
233+
println!(
234+
"Read {} records from {}",
235+
records.len(),
236+
&cli.input.display()
237+
);
238+
let cfg = GnssDegradationConfig {
239+
scheduler: build_scheduler(&args.scheduler),
240+
fault: build_fault(&args.fault),
241+
seed: args.seed,
242+
};
243+
244+
let events = build_event_stream(&records, &cfg);
245+
let ukf = initialize_ukf(records[0].clone(), None, None);
246+
// let results = closed_loop_events(ukf, events);
247+
// sim::write_results_csv(&cli.output, &results)?;
248+
}
124249
}
250+
251+
// records = match args.gps_degradation {
252+
// Some(value) => degrade_measurements(records, value),
253+
// None => records,
254+
// };
255+
// let results: Vec<NavigationResult>;
256+
// if args.mode == "closed-loop" {
257+
// println!(
258+
// "Running in closed-loop mode with GPS interval: {:?}",
259+
// args.gps_interval
260+
// );
261+
// results = closed_loop(&records, args.gps_interval);
262+
// //match write_results_to_csv(&results, &args.output) {
263+
// match NavigationResult::to_csv(&results, &args.output) {
264+
// Ok(_) => println!("Results written to {}", args.output.display()),
265+
// Err(e) => eprintln!("Error writing results: {}", e),
266+
// };
267+
// } else if args.mode == "open-loop" {
268+
// results = dead_reckoning(&records);
269+
// // match write_results_to_csv(&results, &args.output) {
270+
// match NavigationResult::to_csv(&results, &args.output) {
271+
// Ok(_) => println!("Results written to {}", args.output.display()),
272+
// Err(e) => eprintln!("Error writing results: {}", e),
273+
// };
274+
// } else {
275+
// return Err("Invalid mode specified. Use 'open-loop' or 'closed-loop'.".into());
276+
// }
125277
Ok(())
126278
}

0 commit comments

Comments
 (0)