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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ The whole story, in order: [What's new](user_docs/whats-new.md).
|---|---|---|
| HackRF One | ✅ Full support | All diagnostics, gain stages, ADC metrics |
| RTL-SDR (R820T, E4000, R828D) | ✅ Full support | Single tuner gain + AGC; no VGA, no BB filter, no Friis NF |
| tinySA / Ultra / Ultra+ | ✅ Spectrum support | ZS405 verified; calibrated dBm spectrum and waterfall |
| tinySA / Ultra / Ultra+ | ✅ Spectrum support | ZS405 verified; calibrated dBm spectrum, waterfall and native band sweeps |
| **Anything with a SoapySDR driver** | 🧪 **Beta** | Airspy, SDRplay, Pluto, Lime, bladeRF, USRP, SoapyRemote. Unconfirmed on anything but a HackRF |
| PortaPack H4M (Mayhem) | ✅ Full support | HackRF mode: all HackRF diagnostics apply |
| HackRF Pro | 🔲 Planned | Needs hardware |
Expand Down
2 changes: 1 addition & 1 deletion src/app/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ impl App {
std::thread::spawn(move || NetWorker::new(net_rx, net_state, geometry).run());

tasks::spawn_rx_task(Arc::clone(&state), Arc::clone(&device), Arc::clone(&rx_ctx));
tasks::spawn_sweep_task(Arc::clone(&state), Arc::clone(&device));
tasks::spawn_net_survey_task(Arc::clone(&state), Arc::clone(&device));
}
hardware::AcquisitionKind::PowerTrace => {
Expand All @@ -207,6 +206,7 @@ impl App {
);
}
}
tasks::spawn_sweep_task(Arc::clone(&state), Arc::clone(&device));
tasks::spawn_sys_resource_task(Arc::clone(&state));

Ok(app)
Expand Down
72 changes: 68 additions & 4 deletions src/app/builder/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ impl App {
acquisition: crate::hardware::AcquisitionKind,
) -> anyhow::Result<Vec<String>> {
let mut warnings = Vec::new();
if acquisition == crate::hardware::AcquisitionKind::PowerTrace {
if let Some(preset) = config.presets.get_mut("lab_sweep") {
preset.panels.retain(|panel| panel.name != "signal_metrics");
}
}
config.presets.retain(|name, preset| {
if preset.panels.is_empty() {
warnings.push(format!("Preset '{name}' is unavailable because it has no panels"));
Expand Down Expand Up @@ -526,8 +531,40 @@ mod tests {
..Default::default()
},
);
user.insert(
"my_sweep".to_string(),
crate::config::PresetConfig {
panels: vec![
crate::config::PanelSpec {
name: "header_slim".into(),
position: crate::config::Position::Top,
height: None,
width_pct: None,
},
crate::config::PanelSpec {
name: "sweep_panel".into(),
position: crate::config::Position::Body,
height: None,
width_pct: None,
},
crate::config::PanelSpec {
name: "system_resources".into(),
position: crate::config::Position::Right,
height: None,
width_pct: None,
},
crate::config::PanelSpec {
name: "footer".into(),
position: crate::config::Position::Bottom,
height: None,
width_pct: None,
},
],
..Default::default()
},
);

let (engine, _) = App::build_ui_for(
let (mut engine, _) = App::build_ui_for(
"my_trace",
&user,
None,
Expand All @@ -539,8 +576,11 @@ mod tests {
"spectrum",
"waterfall",
"spectrum_waterfall",
"lab_sweep",
"micro_sweep",
"my_trace",
"my_status",
"my_sweep",
] {
assert!(engine.has_preset(available), "{available} was hidden");
}
Expand All @@ -550,13 +590,31 @@ mod tests {
"lab_rf",
"lab_timing",
"lab_signal",
"lab_sweep",
"micro_sweep",
"my_iq",
] {
assert!(!engine.has_preset(unavailable), "{unavailable} survived");
}
assert_eq!(engine.active_preset(), "my_trace");
engine.set_preset("my_sweep");
assert!(engine.is_panel_visible("sweep_panel"));
assert!(engine.is_panel_visible("system_resources"));
engine.set_preset("lab_sweep");
assert!(engine.is_panel_visible("sweep_panel"));
assert!(!engine.is_panel_visible("signal_metrics"));
}

#[test]
fn an_iq_device_keeps_signal_metrics_in_the_lab_sweep_layout() {
let (engine, _) = App::build_ui_for(
"lab_sweep",
&HashMap::new(),
None,
false,
crate::hardware::AcquisitionKind::IqSamples,
)
.unwrap();
assert_eq!(engine.active_preset(), "lab_sweep");
assert!(engine.is_panel_visible("signal_metrics"));
}

#[test]
Expand Down Expand Up @@ -615,7 +673,13 @@ mod tests {
#[test]
fn incompatible_overrides_cannot_leave_a_power_device_without_a_layout() {
let mut user = HashMap::new();
for name in ["spectrum", "waterfall", "spectrum_waterfall"] {
for name in [
"spectrum",
"waterfall",
"spectrum_waterfall",
"lab_sweep",
"micro_sweep",
] {
user.insert(
name.to_string(),
crate::config::PresetConfig {
Expand Down
5 changes: 3 additions & 2 deletions src/app/input/sweep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub(super) fn sweep_panel(key: KeyEvent, ctx: &mut InputCtx<'_>) -> KeyAction {
}
KeyCode::Char('c') => {
let m = metrics(state);
let unit = m.caps.level_unit.label();
let msg = if let Some(frame) = m.sweep.current_frame.as_ref() {
let curve = if m.sweep.show_peak {
&frame.peak_dbfs
Expand All @@ -62,7 +63,7 @@ pub(super) fn sweep_panel(key: KeyEvent, ctx: &mut InputCtx<'_>) -> KeyAction {
.min_by_key(|(_, &f)| f.abs_diff(hz))
.and_then(|(i, _)| curve.get(i).copied().filter(|v| v.is_finite()));
let db_str = level
.map(|v| format!("{:.1} dBFS", v))
.map(|v| format!("{v:.1} {unit}"))
.unwrap_or_else(|| "\u{2014}".into());
format!("cursor {:.3} MHz {} · ", hz as f64 / 1e6, db_str)
} else {
Expand All @@ -72,7 +73,7 @@ pub(super) fn sweep_panel(key: KeyEvent, ctx: &mut InputCtx<'_>) -> KeyAction {
.top_peaks(1, 500_000)
.into_iter()
.next()
.map(|(f, v)| format!("top {:.3} MHz {:.1} dBFS", f as f64 / 1e6, v))
.map(|(f, v)| format!("top {:.3} MHz {v:.1} {unit}", f as f64 / 1e6))
.unwrap_or_else(|| "no data".into());
format!(
"Sweep snapshot — {}{} · {:.1}–{:.1} MHz ({:.1}s/cycle)",
Expand Down
4 changes: 4 additions & 0 deletions src/app/input/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,15 @@ pub(super) fn sweep_range(key: KeyEvent, state: &Arc<Mutex<SdrMetrics>>, is_star
let (start, stop) = (m.sweep.config.start_hz, m.sweep.config.stop_hz);
let ordered = if is_start { hz < stop } else { hz > start };
if ordered {
let changed = if is_start { hz != start } else { hz != stop };
if is_start {
m.sweep.config.start_hz = hz;
} else {
m.sweep.config.stop_hz = hz;
}
if changed {
m.sweep.generation = m.sweep.generation.wrapping_add(1);
}
m.sweep.cycle_count = 0;
m.sweep.positions_done = 0;
m.sweep.current_frame = None;
Expand Down
6 changes: 2 additions & 4 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,9 @@ impl App {
}

fn draw<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> io::Result<()> {
// Sweep mode is owned by the `lab_sweep` preset: keep the real state's
// `sweep.active` in sync with the active preset so the sweep_task starts
// and stops with it, then take the render snapshot.
let active_preset = self.engine.active_preset().to_string();
let sweep_active = active_preset == "lab_sweep" || active_preset == "micro_sweep";
let sweep_active = self.engine.is_panel_visible("sweep_panel")
|| self.engine.is_panel_visible("micro_sweep_panel");
// The demod is gated on its panel being on screen, not on the preset being
// called `lab_signal`: presets are data, and a user preset that lists
// `fm_demod` used to get a panel that never received a block - it sat at
Expand Down
6 changes: 3 additions & 3 deletions src/hardware/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub use discovery::{list_all_devices, open_device, DeviceKind, DeviceListing};
#[cfg(test)]
pub(crate) use traits::RateSet;
pub use traits::{
AcquisitionKind, Boost, DeliveryModel, DeviceCapabilities, DeviceInfo, FeedHealth, GainModel,
LevelUnit, PowerTrace, RxContext, SampleFormat, SampleGeometry, SdrDevice, SoftwareStack,
StageSpec, StreamBlock, IQ_TRACE_STALE_MS,
AcquisitionKind, Boost, DeliveryModel, DeviceCapabilities, DeviceInfo, DirectSweepConfig,
FeedHealth, GainModel, LevelUnit, PowerTrace, PowerTraceTarget, RxContext, SampleFormat,
SampleGeometry, SdrDevice, SoftwareStack, StageSpec, StreamBlock, IQ_TRACE_STALE_MS,
};
Loading
Loading