Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 172 additions & 9 deletions src/app/builder/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ pub fn resolve_gains(radio: &RadioConfig, gm: &GainModel) -> (Vec<f64>, Vec<Stri
/// Everything not reached through `boot` is the same on both startups by
/// definition: history ring buffers sized but empty, every measurement at its
/// no-measurement value.
pub(super) fn initial_metrics(cfg: &AppConfig, boot: Boot) -> SdrMetrics {
pub(super) fn initial_metrics(cfg: &AppConfig, boot: Boot) -> anyhow::Result<SdrMetrics> {
let Boot {
caps,
tuning,
Expand All @@ -311,7 +311,9 @@ pub(super) fn initial_metrics(cfg: &AppConfig, boot: Boot) -> SdrMetrics {
recall,
} = boot;

SdrMetrics {
caps.validate_display()?;

Ok(SdrMetrics {
radio: RadioState {
frequency: tuning.frequency_hz,
config_sample_rate: tuning.sample_rate,
Expand Down Expand Up @@ -361,8 +363,8 @@ pub(super) fn initial_metrics(cfg: &AppConfig, boot: Boot) -> SdrMetrics {
observer,
spectrum: SpectrumState {
step_hz: 100_000,
y_min: -120.0,
y_max: 0.0,
y_min: caps.level_min_db,
y_max: caps.level_max_db,
hold: None,
cursor_freq: None,
markers,
Expand All @@ -375,6 +377,8 @@ pub(super) fn initial_metrics(cfg: &AppConfig, boot: Boot) -> SdrMetrics {
waterfall: WaterfallState::new(
cfg.display.waterfall_max_rows.max(WATERFALL_MIN_ROWS),
cfg.display.waterfall_palette,
caps.level_min_db,
caps.level_max_db,
),
system: SystemState {
board_name: Arc::from(identity.board_name.as_str()),
Expand Down Expand Up @@ -406,7 +410,7 @@ pub(super) fn initial_metrics(cfg: &AppConfig, boot: Boot) -> SdrMetrics {
net: crate::state::NetState::default(),
caps,
acc: Accumulators::default(),
}
})
}

#[cfg(test)]
Expand Down Expand Up @@ -564,6 +568,7 @@ mod tests {
),
),
] {
let m = m.unwrap();
assert!(!m.radio.rx_enabled);
assert!(!m.radio.hw_streaming);
assert_eq!(m.radio.actual_sample_rate, 0);
Expand All @@ -576,6 +581,133 @@ mod tests {
}
}

#[test]
fn iq_devices_keep_the_existing_startup_defaults() {
let cfg = AppConfig::default();
for caps in [
hardware::native::hackrf::caps(),
hardware::native::rtlsdr::observer_caps(),
] {
let tuning = resolve_tuning(&cfg.radio, &caps);
let metrics = initial_metrics(
&cfg,
Boot::normal(
&cfg,
Arc::new(caps),
tuning,
&hardware::DeviceInfo::default(),
),
)
.unwrap();
assert!(!metrics.radio.rx_enabled);
assert!(!metrics.radio.hw_streaming);
assert_eq!(metrics.spectrum.y_min, -120.0);
assert_eq!(metrics.spectrum.y_max, 0.0);
assert_eq!(metrics.waterfall.db_min, -120.0);
assert_eq!(metrics.waterfall.db_max, 0.0);
assert_eq!(metrics.caps.level_unit, hardware::LevelUnit::Dbfs);
assert_eq!(metrics.caps.level_unit.label(), "dBFS");
assert_eq!(metrics.caps.trace_stale_ms, 500);
}
}

#[test]
fn startup_uses_the_device_level_axis() {
let cfg = AppConfig::default();
let mut caps = hardware::native::hackrf::caps();
caps.level_min_db = -110.0;
caps.level_max_db = -10.0;
let tuning = resolve_tuning(&cfg.radio, &caps);
let metrics = initial_metrics(
&cfg,
Boot::normal(
&cfg,
Arc::new(caps),
tuning,
&hardware::DeviceInfo::default(),
),
)
.unwrap();
assert!(!metrics.radio.rx_enabled);
assert!(!metrics.radio.hw_streaming);
assert_eq!(metrics.spectrum.y_min, -110.0);
assert_eq!(metrics.spectrum.y_max, -10.0);
assert_eq!(metrics.waterfall.db_min, -110.0);
assert_eq!(metrics.waterfall.db_max, -10.0);
}

#[test]
fn startup_rejects_invalid_display_capabilities() {
let cfg = AppConfig::default();
for (min, max, budget, error) in [
(0.0, 0.0, 500, "Invalid display level bounds"),
(0.0, -10.0, 500, "Invalid display level bounds"),
(f32::NAN, 0.0, 500, "Invalid display level bounds"),
(-120.0, f32::NAN, 500, "Invalid display level bounds"),
(f32::NEG_INFINITY, 0.0, 500, "Invalid display level bounds"),
(-120.0, f32::INFINITY, 500, "Invalid display level bounds"),
(-f32::MAX, f32::MAX, 500, "Invalid display level bounds"),
(-120.0, 0.0, 0, "Invalid trace_stale_ms"),
] {
let mut caps = hardware::native::hackrf::caps();
caps.level_min_db = min;
caps.level_max_db = max;
caps.trace_stale_ms = budget;
let tuning = resolve_tuning(&cfg.radio, &caps);
let result = initial_metrics(
&cfg,
Boot::normal(
&cfg,
Arc::new(caps),
tuning,
&hardware::DeviceInfo::default(),
),
);
assert!(result
.err()
.expect("invalid caps must fail")
.to_string()
.contains(error));
}
}

#[test]
fn startup_accepts_valid_display_capabilities() {
let cfg = AppConfig::default();
for (min, max, budget) in [(-10.0, 0.0, 1), (-120.0, 20.0, 5_000)] {
let mut caps = hardware::native::hackrf::caps();
caps.level_min_db = min;
caps.level_max_db = max;
caps.trace_stale_ms = budget;
let tuning = resolve_tuning(&cfg.radio, &caps);
let m = initial_metrics(
&cfg,
Boot::normal(
&cfg,
Arc::new(caps),
tuning,
&hardware::DeviceInfo::default(),
),
)
.unwrap();
assert_eq!((m.spectrum.y_min, m.spectrum.y_max), (min, max));
assert_eq!((m.waterfall.db_min, m.waterfall.db_max), (min, max));
assert_eq!(m.caps.trace_stale_ms, budget);
}
}

#[test]
fn observer_startup_also_rejects_invalid_display_capabilities() {
let cfg = AppConfig::default();
let mut boot = Boot::observer(&cfg, &sysinfo(), profile(hardware::DeviceKind::HackRf));
Arc::make_mut(&mut boot.caps).trace_stale_ms = 0;
assert!(initial_metrics(&cfg, boot)
.err()
.expect("invalid observer caps must fail")
.to_string()
.contains("Invalid trace_stale_ms"));
}

/// A config that asks for less waterfall history than a full-height panel
/// needs is raised, not honoured.
///
Expand All @@ -589,7 +721,8 @@ mod tests {
let m = initial_metrics(
&cfg,
Boot::observer(&cfg, &sysinfo(), profile(hardware::DeviceKind::HackRf)),
);
)
.unwrap();
assert_eq!(
m.waterfall.buffer.max_rows,
crate::state::WATERFALL_MIN_ROWS
Expand All @@ -604,10 +737,38 @@ mod tests {
let m = initial_metrics(
&cfg,
Boot::observer(&cfg, &sysinfo(), profile(hardware::DeviceKind::HackRf)),
);
)
.unwrap();
assert_eq!(m.waterfall.buffer.max_rows, 4_096);
}

#[test]
fn a_power_trace_backend_uses_its_level_axis_and_starts_paused() {
let cfg = AppConfig::default();
let mut caps = hardware::native::hackrf::caps();
caps.acquisition = hardware::AcquisitionKind::PowerTrace;
caps.level_unit = hardware::LevelUnit::Dbm;
caps.level_min_db = -110.0;
caps.level_max_db = -10.0;
let tuning = resolve_tuning(&cfg.radio, &caps);
let metrics = initial_metrics(
&cfg,
Boot::normal(
&cfg,
Arc::new(caps),
tuning,
&hardware::DeviceInfo::default(),
),
)
.unwrap();
assert!(!metrics.radio.rx_enabled);
assert!(!metrics.radio.hw_streaming);
assert_eq!(metrics.spectrum.y_min, -110.0);
assert_eq!(metrics.spectrum.y_max, -10.0);
assert_eq!(metrics.waterfall.db_min, -110.0);
assert_eq!(metrics.waterfall.db_max, -10.0);
}

#[test]
fn observer_mode_starts_without_markers_or_recall() {
let mut cfg = AppConfig::default();
Expand All @@ -627,14 +788,16 @@ mod tests {
resolve_tuning(&cfg.radio, &hardware::native::hackrf::caps()),
&hardware::DeviceInfo::default(),
),
);
)
.unwrap();
assert_eq!(live.spectrum.markers.len(), 1);
assert_eq!(live.ui.recall[0], Some(100_000_000));

let obs = initial_metrics(
&cfg,
Boot::observer(&cfg, &sysinfo(), profile(hardware::DeviceKind::HackRf)),
);
)
.unwrap();
assert!(obs.spectrum.markers.is_empty());
assert!(obs.ui.recall.iter().all(|s| s.is_none()));
assert!(obs.observer.active);
Expand Down
Loading
Loading