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
8 changes: 8 additions & 0 deletions src/app/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ impl App {
cfg: AppConfig,
config_path: Option<PathBuf>,
device: Arc<dyn hardware::SdrDevice>,
device_kind: hardware::DeviceKind,
) -> anyhow::Result<Self> {
let info = device.info();
let caps = Arc::new(device.capabilities().clone());
Expand Down Expand Up @@ -180,6 +181,7 @@ impl App {
Some(Arc::clone(&device)),
Some(Arc::clone(&rx_ctx)),
None,
device_kind,
)?;

match caps.acquisition {
Expand Down Expand Up @@ -222,6 +224,7 @@ impl App {
config_path: Option<PathBuf>,
sysinfo: hardware::sysfs::HackRfSysInfo,
profile: hardware::discovery::ObserverProfile,
device_kind: hardware::DeviceKind,
) -> anyhow::Result<Self> {
let state = Arc::new(Mutex::new(initial_metrics(
&cfg,
Expand All @@ -245,6 +248,7 @@ impl App {
None,
None,
Some("observer"),
device_kind,
)?;
tasks::spawn_observer_task(Arc::clone(&state), sysinfo.bus, sysinfo.dev, profile);
tasks::spawn_sys_resource_task(Arc::clone(&state));
Expand All @@ -265,6 +269,7 @@ impl App {
device: Option<Arc<dyn hardware::SdrDevice>>,
rx_ctx: Option<Arc<hardware::RxContext>>,
preset_override: Option<&str>,
device_kind: hardware::DeviceKind,
) -> anyhow::Result<Self> {
let themes_dir = config_path
.as_deref()
Expand Down Expand Up @@ -341,6 +346,9 @@ impl App {
theme,
focus_keys,
theme_config: cfg.theme.clone(),
tinysa_config: cfg.tinysa.clone(),
tinysa_basic_input: None,
device_kind,
user_presets: cfg.presets,
})
}
Expand Down
47 changes: 32 additions & 15 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ pub struct App {
/// and never touches again - so without a copy of them there is nothing left
/// to write back.
pub(super) theme_config: crate::config::ThemeConfig,
pub(super) tinysa_config: crate::config::TinySaSettings,
pub(super) tinysa_basic_input: Option<crate::hardware::tinysa::BasicInput>,
pub(super) device_kind: hardware::DeviceKind,
}

impl App {
Expand All @@ -55,8 +58,11 @@ impl App {
config_path: Option<PathBuf>,
listing: &hardware::DeviceListing,
) -> anyhow::Result<Self> {
match hardware::open_device(listing) {
Ok(device) => Self::new_normal(cfg, config_path, device),
let tinysa_basic_input = (listing.kind == hardware::DeviceKind::TinySa).then(|| {
hardware::tinysa::resolve_basic_input(listing.tiny_sa_input, cfg.tinysa.basic_input)
});
let mut app = match hardware::open_device(listing, &cfg.tinysa) {
Ok(device) => Self::new_normal(cfg, config_path, device, listing.kind),
Err(open_err) => {
// Device is present but couldn't be opened (e.g. busy) - fall back
// to read-only observer mode via the matching backend's sysfs
Expand All @@ -68,9 +74,11 @@ impl App {
let Some(sysinfo) = (profile.scan)() else {
return Err(open_err);
};
Self::new_observer(cfg, config_path, sysinfo, profile)
Self::new_observer(cfg, config_path, sysinfo, profile, listing.kind)
}
}
}?;
app.tinysa_basic_input = tinysa_basic_input;
Ok(app)
}

pub fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> io::Result<()> {
Expand Down Expand Up @@ -104,7 +112,7 @@ impl App {
self.state.lock().unwrap_or_else(|error| error.into_inner());
m.ui.quit_after_device_option = true;
} else {
self.finish_session();
self.finish_session()?;
return Ok(());
}
}
Expand All @@ -119,7 +127,7 @@ impl App {
AppEvent::DeviceOptionComplete(completion) => {
input::complete_device_option(&self.state, completion);
if quit_requested && !self.device_option_pending() {
self.finish_session();
self.finish_session()?;
return Ok(());
}
true
Expand Down Expand Up @@ -163,10 +171,10 @@ impl App {
)
}

fn finish_session(&self) {
fn finish_session(&self) -> io::Result<()> {
self.restore_noise_sweep();
self.restore_sweep_tuning();
self.save_config();
self.save_config().map_err(io::Error::other)
}

fn draw<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> io::Result<()> {
Expand Down Expand Up @@ -368,12 +376,21 @@ impl App {
let _ = device.set_frequency(hz);
}

fn save_config(&self) {
if self.device.is_none() {
return;
}
fn save_config(&self) -> anyhow::Result<()> {
let Some(device) = self.device.as_ref() else {
return Ok(());
};
let Some(path) = &self.config_path else {
return;
return Ok(());
};
let tinysa = if self.device_kind == hardware::DeviceKind::TinySa {
crate::hardware::tinysa::persisted_settings(
&self.tinysa_config,
&device.options(),
self.tinysa_basic_input,
)?
} else {
self.tinysa_config.clone()
};
let (freq, rate, gains, amp, wf_rows, wf_palette, spec_style, markers, sweep_cfg, recall) = {
let m = self.state.lock().unwrap_or_else(|e| e.into_inner());
Expand Down Expand Up @@ -422,9 +439,10 @@ impl App {
stop_hz: sweep_cfg.stop_hz,
dwell_ms: sweep_cfg.dwell_ms,
},
tinysa,
presets: self.user_presets.clone(),
};
let _ = cfg.save(path);
cfg.save(path)
}
}

Expand All @@ -434,7 +452,6 @@ mod tests {
use crate::state::DeviceOptionUpdate;
use std::cell::Cell;
use std::sync::{mpsc, Arc, Mutex};

/// Quitting must give the tuner back before the config is written.
///
/// `save_config` persists `radio.frequency`, and while a sweep is running
Expand Down
97 changes: 97 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,56 @@ impl Default for SweepSettings {
}
}

fn default_tinysa_points() -> u32 {
450
}

fn default_auto() -> String {
"auto".into()
}

/// Settings applied when a tinySA backend opens.
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
pub struct TinySaSettings {
#[serde(default)]
pub basic_input: crate::hardware::tinysa::BasicInput,
#[serde(default = "default_tinysa_points")]
pub points: u32,
#[serde(default = "default_auto")]
pub rbw: String,
#[serde(default = "default_auto")]
pub attenuation: String,
#[serde(default)]
pub high_attenuation: bool,
#[serde(default)]
pub lna: bool,
#[serde(default = "default_auto")]
pub lna2: String,
#[serde(default = "default_auto")]
pub agc: String,
#[serde(default = "default_auto")]
pub spur: String,
#[serde(default)]
pub ext_gain_db: i32,
}

impl Default for TinySaSettings {
fn default() -> Self {
Self {
basic_input: crate::hardware::tinysa::BasicInput::default(),
points: default_tinysa_points(),
rbw: default_auto(),
attenuation: default_auto(),
high_attenuation: false,
lna: false,
lna2: default_auto(),
agc: default_auto(),
spur: default_auto(),
ext_gain_db: 0,
}
}
}

#[derive(Deserialize, Serialize, Clone, Debug, Default)]
pub struct AppConfig {
#[serde(default)]
Expand All @@ -171,6 +221,8 @@ pub struct AppConfig {
pub theme: ThemeConfig,
#[serde(default)]
pub sweep: SweepSettings,
#[serde(default)]
pub tinysa: TinySaSettings,
/// User-defined layout presets, merged into the built-in set at startup.
/// A preset here with the same name as a built-in overrides it. Preserved
/// verbatim across save so hand-written presets survive a quit.
Expand Down Expand Up @@ -718,6 +770,51 @@ panels = [
assert_eq!(restored.display.active_preset, "spectrum");
}

#[test]
fn tinysa_settings_default_and_round_trip() {
let defaults: AppConfig = toml::from_str("").unwrap();
assert_eq!(defaults.tinysa, TinySaSettings::default());

let source = r#"
[tinysa]
basic_input = "high"
points = 900
rbw = "0.2"
attenuation = "12"
high_attenuation = true
lna = true
lna2 = "3"
agc = "7"
spur = "off"
ext_gain_db = -8
"#;
let config: AppConfig = toml::from_str(source).unwrap();
let serialized = toml::to_string_pretty(&config).unwrap();
let restored: AppConfig = toml::from_str(&serialized).unwrap();
assert_eq!(restored.tinysa, config.tinysa);
assert!(serialized.contains("[tinysa]"));
assert!(serialized.contains("basic_input = \"high\""));
assert!(restored.tinysa.high_attenuation);
}

#[test]
fn partial_tinysa_settings_fill_field_defaults() {
let config: AppConfig = toml::from_str("[tinysa]\npoints = 64\n").unwrap();
assert_eq!(config.tinysa.points, 64);
assert_eq!(
config.tinysa.basic_input,
crate::hardware::tinysa::BasicInput::Low
);
assert_eq!(config.tinysa.rbw, "auto");
assert_eq!(config.tinysa.attenuation, "auto");
assert!(!config.tinysa.high_attenuation);
assert!(!config.tinysa.lna);
assert_eq!(config.tinysa.lna2, "auto");
assert_eq!(config.tinysa.agc, "auto");
assert_eq!(config.tinysa.spur, "auto");
assert_eq!(config.tinysa.ext_gain_db, 0);
}

#[test]
fn spectrum_style_round_trips_and_defaults_braille() {
let cfg: AppConfig = toml::from_str("[display]\nactive_preset = \"spectrum\"\n").unwrap();
Expand Down
11 changes: 9 additions & 2 deletions src/hardware/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,10 @@ fn offer_soapy(
}

/// Opens the device a listing points at, as a trait object.
pub fn open_device(listing: &DeviceListing) -> anyhow::Result<Arc<dyn SdrDevice>> {
pub fn open_device(
listing: &DeviceListing,
tinysa_settings: &crate::config::TinySaSettings,
) -> anyhow::Result<Arc<dyn SdrDevice>> {
match listing.kind {
DeviceKind::HackRf => Ok(Arc::new(hackrf::HackRfDevice::open(listing.index)?)),
DeviceKind::RtlSdr => Ok(Arc::new(rtlsdr::RtlDevice::open(listing.index)?)),
Expand All @@ -260,9 +263,13 @@ pub fn open_device(listing: &DeviceListing) -> anyhow::Result<Arc<dyn SdrDevice>
let Some(path) = listing.path.as_deref() else {
anyhow::bail!("a tinySA listing with no serial port cannot be opened");
};
let basic_input =
tinysa::resolve_basic_input(listing.tiny_sa_input, tinysa_settings.basic_input);
Ok(Arc::new(tinysa::TinySaDevice::open(
path,
listing.tiny_sa_input.unwrap_or_default(),
basic_input,
listing.tiny_sa_input,
tinysa_settings,
)?))
}
}
Expand Down
Loading
Loading