use crate::error::RtlSdrError;
use crate::reg::TunerType;
use crate::usb;
const OFFSET_TUNING_MULTIPLIER_NUM: u32 = 170;
const OFFSET_TUNING_MULTIPLIER_DEN: u32 = 100;
use super::RtlSdrDevice;
impl RtlSdrDevice {
pub fn set_sample_rate(&mut self, samp_rate: u32) -> Result<(), RtlSdrError> {
if (samp_rate <= 225_000)
|| (samp_rate > 3_200_000)
|| (samp_rate > 300_000 && samp_rate <= 900_000)
{
return Err(RtlSdrError::InvalidSampleRate { rate_hz: samp_rate });
}
let rsamp_ratio =
((f64::from(self.rtl_xtal) * (1u64 << 22) as f64) / f64::from(samp_rate)) as u32;
let rsamp_ratio = rsamp_ratio & 0x0fff_fffc;
let real_rsamp_ratio = rsamp_ratio | ((rsamp_ratio & 0x0800_0000) << 1);
let real_rate =
(f64::from(self.rtl_xtal) * (1u64 << 22) as f64 / f64::from(real_rsamp_ratio)) as u32;
if samp_rate != real_rate {
tracing::debug!("Exact sample rate: {} Hz", real_rate);
}
if self.offs_freq > 0 && self.freq > 0 {
let new_floor = offset_tuning_floor(real_rate);
if self.freq <= new_floor {
return Err(RtlSdrError::InvalidParameter(format!(
"cannot set sample rate to {real_rate} Hz: current freq {} Hz would fall \
at or below the new offset-tuning floor {new_floor} Hz \
(≈ 0.85 × sample_rate); disable offset tuning or tune higher first",
self.freq,
)));
}
}
self.rate = real_rate;
if let Some(tuner) = &mut self.tuner {
usb::set_i2c_repeater(&self.handle, true)?;
let bw = if self.bw > 0 { self.bw } else { self.rate };
match tuner.set_bw(&self.handle, bw, self.rate) {
Ok(if_freq) => {
if let Err(e) = self.set_if_freq(if_freq) {
tracing::warn!("set_sample_rate: IF freq update failed: {e}");
}
if self.freq > 0 {
if let Ok(adjusted) = freq_minus_offset(self.freq, self.offs_freq) {
if let Some(tuner) = &mut self.tuner {
if let Err(e) = tuner.set_freq(&self.handle, adjusted) {
tracing::warn!(
"set_sample_rate: tuner retune failed: {e}; \
resetting cached freq to 0 (parity with \
set_center_freq's audit-fix-#11)"
);
self.freq = 0;
}
}
}
}
}
Err(e) => {
tracing::warn!("set_sample_rate: tuner set_bw failed: {e}");
}
}
usb::set_i2c_repeater(&self.handle, false)?;
}
let tmp = (rsamp_ratio >> 16) as u16;
usb::demod_write_reg(&self.handle, 1, 0x9f, tmp, 2)?;
let tmp = (rsamp_ratio & 0xffff) as u16;
usb::demod_write_reg(&self.handle, 1, 0xa1, tmp, 2)?;
self.set_sample_freq_correction(self.corr)?;
usb::demod_write_reg(&self.handle, 1, 0x01, 0x14, 1)?;
usb::demod_write_reg(&self.handle, 1, 0x01, 0x10, 1)?;
if self.offs_freq > 0 {
self.set_offset_tuning(true)?;
}
Ok(())
}
pub fn set_center_freq(&mut self, freq: u32) -> Result<(), RtlSdrError> {
let mut r = Err(RtlSdrError::NoTuner);
if self.direct_sampling != 0 {
r = self.set_if_freq(freq);
} else if let Some(tuner) = &mut self.tuner {
usb::set_i2c_repeater(&self.handle, true)?;
r = freq_minus_offset(freq, self.offs_freq)
.and_then(|adjusted| tuner.set_freq(&self.handle, adjusted));
usb::set_i2c_repeater(&self.handle, false)?;
}
match r {
Ok(()) => {
self.freq = freq;
}
Err(ref _e) => {
self.freq = 0;
}
}
r
}
pub fn set_freq_correction(&mut self, ppm: i32) -> Result<(), RtlSdrError> {
if self.corr == ppm {
return Ok(());
}
tracing::info!("set_freq_correction: {ppm} ppm");
self.corr = ppm;
self.set_sample_freq_correction(ppm)?;
let corrected_xtal = self.get_tuner_xtal();
if let Some(tuner) = &mut self.tuner {
tuner.set_xtal(corrected_xtal);
}
if self.freq > 0 {
self.set_center_freq(self.freq)?;
}
Ok(())
}
pub fn set_offset_tuning(&mut self, on: bool) -> Result<(), RtlSdrError> {
if self.tuner_type == TunerType::R820T || self.tuner_type == TunerType::R828D {
return Err(RtlSdrError::InvalidParameter(
"offset tuning not supported for R82XX tuners".to_string(),
));
}
if self.direct_sampling != 0 {
return Err(RtlSdrError::InvalidParameter(
"offset tuning not available in direct sampling mode".to_string(),
));
}
let new_offs_freq = if on {
offset_tuning_floor(self.rate)
} else {
0
};
if on && self.freq > 0 && self.freq <= new_offs_freq {
return Err(RtlSdrError::InvalidParameter(format!(
"cannot enable offset tuning: current freq {} Hz is at or below the \
computed floor {} Hz (≈ 0.85 × sample_rate); tune above the floor first",
self.freq, new_offs_freq,
)));
}
tracing::info!(
"set_offset_tuning: {} (offs_freq = {new_offs_freq} Hz)",
if on { "on" } else { "off" },
);
self.offs_freq = new_offs_freq;
self.set_if_freq(self.offs_freq)?;
if let Some(tuner) = &mut self.tuner {
usb::set_i2c_repeater(&self.handle, true)?;
let bw = if on {
2 * self.offs_freq
} else if self.bw > 0 {
self.bw
} else {
self.rate
};
if let Err(e) = tuner.set_bw(&self.handle, bw, self.rate) {
tracing::warn!("set_offset_tuning: tuner set_bw failed: {e}");
}
usb::set_i2c_repeater(&self.handle, false)?;
}
if self.freq > self.offs_freq {
self.set_center_freq(self.freq)?;
}
Ok(())
}
pub fn set_tuner_bandwidth(&mut self, bw: u32) -> Result<(), RtlSdrError> {
let actual_bw = if bw > 0 { bw } else { self.rate };
let rate = self.rate;
let Some(tuner) = self.tuner.as_mut() else {
return Ok(());
};
usb::set_i2c_repeater(&self.handle, true)?;
let bw_result = tuner.set_bw(&self.handle, actual_bw, rate);
let if_freq = match bw_result {
Ok(f) => f,
Err(e) => {
if let Err(close_err) = usb::set_i2c_repeater(&self.handle, false) {
tracing::warn!(
"set_tuner_bandwidth: I2C repeater restore failed after set_bw error: {close_err}"
);
}
return Err(e);
}
};
if let Err(e) = self.set_if_freq(if_freq) {
tracing::warn!("set_tuner_bandwidth: IF freq update failed: {e}");
}
if self.freq > 0 {
if let Ok(adjusted) = freq_minus_offset(self.freq, self.offs_freq) {
if let Some(tuner) = self.tuner.as_mut() {
if let Err(e) = tuner.set_freq(&self.handle, adjusted) {
tracing::warn!(
"set_tuner_bandwidth: tuner retune failed: {e}; \
resetting cached freq to 0 (parity with set_sample_rate \
and set_center_freq's audit-fix-#11)"
);
self.freq = 0;
}
}
}
}
usb::set_i2c_repeater(&self.handle, false)?;
self.bw = bw;
Ok(())
}
}
fn freq_minus_offset(freq: u32, offs_freq: u32) -> Result<u32, RtlSdrError> {
freq.checked_sub(offs_freq).ok_or_else(|| {
RtlSdrError::InvalidParameter(format!(
"freq {freq} Hz is below the offset-tuning floor {offs_freq} Hz"
))
})
}
fn offset_tuning_floor(rate: u32) -> u32 {
(rate / 2) * OFFSET_TUNING_MULTIPLIER_NUM / OFFSET_TUNING_MULTIPLIER_DEN
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn freq_minus_offset_above_floor_subtracts() {
assert_eq!(
freq_minus_offset(100_000_000, 2_720_000).ok(),
Some(97_280_000)
);
}
#[test]
fn freq_minus_offset_at_floor_returns_zero() {
assert_eq!(freq_minus_offset(2_720_000, 2_720_000).ok(), Some(0));
}
#[test]
fn freq_minus_offset_with_zero_offset_is_identity() {
assert_eq!(freq_minus_offset(100_000_000, 0).ok(), Some(100_000_000));
}
#[test]
fn freq_minus_offset_below_floor_returns_invalid_parameter() {
let result = freq_minus_offset(100_000, 2_720_000);
assert!(
matches!(
&result,
Err(RtlSdrError::InvalidParameter(msg))
if msg.contains("100000") && msg.contains("2720000")
),
"expected InvalidParameter naming both values, got {result:?}",
);
}
#[test]
fn offset_tuning_floor_at_2_4msps_is_2_04mhz() {
assert_eq!(offset_tuning_floor(2_400_000), 2_040_000);
}
#[test]
fn offset_tuning_floor_at_2_048msps_is_1_7408mhz() {
assert_eq!(offset_tuning_floor(2_048_000), 1_740_800);
}
#[test]
fn offset_tuning_floor_zero_rate_is_zero() {
assert_eq!(offset_tuning_floor(0), 0);
}
}