diff --git a/src/ria_toolkit_oss/sdr/thinkrf.py b/src/ria_toolkit_oss/sdr/thinkrf.py index 67d425d..7d108ab 100644 --- a/src/ria_toolkit_oss/sdr/thinkrf.py +++ b/src/ria_toolkit_oss/sdr/thinkrf.py @@ -36,7 +36,7 @@ except SyntaxError as exc: # pragma: no cover - Python 2/3 compatibility issue print("Manual fix: Run `python scripts/fix_pyrf_python3.py` from ria-toolkit-oss directory") raise exc -from ria_toolkit_oss.sdr.sdr import SDR +from ria_toolkit_oss.sdr.sdr import SDR, SDRParameterError class ThinkRF(SDR): @@ -51,7 +51,7 @@ class ThinkRF(SDR): super().__init__() if identifier is None: - raise ValueError("ThinkRF requires an IP address or hostname identifier") + raise SDRParameterError("ThinkRF requires an IP address or hostname identifier") self.identifier = identifier try: @@ -90,7 +90,7 @@ class ThinkRF(SDR): mode = capture_mode.lower() if mode not in {"block", "stream"}: - raise ValueError("capture_mode must be either 'block' or 'stream'") + raise SDRParameterError("capture_mode must be either 'block' or 'stream'") self._rfe_mode = rfe_mode self._attenuation = int(max(0, min(attenuation, 30))) @@ -113,10 +113,12 @@ class ThinkRF(SDR): decimation: Optional[int] = None, ): if channel not in (0, None): - raise ValueError("ThinkRF devices expose a single receive channel") + raise SDRParameterError("ThinkRF supports only channel 0 for RX.") stream_mode = getattr(self, "_capture_mode", "block") == "stream" - actual_decimation, actual_sample_rate = self.set_rx_sample_rate(sample_rate=sample_rate, decimation=decimation) + actual_decimation, _ = self.set_rx_sample_rate( + sample_rate=sample_rate, decimation=decimation, stream_mode=stream_mode + ) self.radio.reset() self.radio.scpiset(":SYSTEM:FLUSH") @@ -127,15 +129,7 @@ class ThinkRF(SDR): self.radio.rfe_mode(self._rfe_mode) self.set_rx_center_frequency(center_frequency=center_frequency) - - attenuation = self._attenuation if gain is None else int(gain) # gain - attenuation = max(0, min(attenuation, 30)) - self.radio.attenuator(attenuation) - - gain_profile = self._gain_profile - if gain_mode and isinstance(gain_mode, str) and gain_mode.upper() in {"LOW", "MEDIUM", "HIGH", "VLOW"}: - gain_profile = gain_mode.upper() - self.radio.gain(gain_profile.lower()) # WSA.gain() expects lowercase + self.set_rx_gain(gain=gain, gain_mode=gain_mode, actual_decimation=actual_decimation) self.radio.decimation(actual_decimation) if stream_mode: @@ -153,14 +147,6 @@ class ThinkRF(SDR): self.radio.scpiset(f":TRACE:BLOCK:PACKETS {self._packets_per_block}") self.radio.scpiset(":TRACE:BLOCK:DATA?") - self.rx_gain = { - "attenuation_dB": attenuation, - "profile": gain_profile, - "decimation": actual_decimation, - "rfe_mode": self._rfe_mode, - "spp": self._samples_per_packet, - "ppb": self._packets_per_block, - } self.rx_buffer_size = self._samples_per_packet self.rx_channel = 0 @@ -168,6 +154,10 @@ class ThinkRF(SDR): self._tx_initialized = False def set_rx_sample_rate(self, sample_rate, decimation, stream_mode): + """ + Set the sample rate of the receiver. + Not callable during recording; ThinkRF requires stream stop/restart to change sample rate. + """ # Enforce sample rate / decimation # Note: decimation parameter takes precedence if provided actual_decimation, actual_sample_rate = self.enforce_sample_rate(sample_rate, decimation) @@ -188,9 +178,32 @@ class ThinkRF(SDR): return actual_decimation, actual_sample_rate def set_rx_center_frequency(self, center_frequency): - self.radio.freq(int(center_frequency)) - self.rx_center_frequency = self.radio.freq - print(f"ThinkRF RX Center Frequency = {self.radio.freq}") + """ + Set the center frequency of the receiver. Callable during streaming. + """ + with self._param_lock: + self.radio.freq(int(center_frequency)) + self.rx_center_frequency = self.radio.freq + print(f"ThinkRF RX Center Frequency = {self.radio.freq}") + + def set_rx_gain(self, gain, gain_mode, actual_decimation): + attenuation = self._attenuation if gain is None else int(gain) # gain + attenuation = max(0, min(attenuation, 30)) + self.radio.attenuator(attenuation) + + gain_profile = self._gain_profile + if gain_mode and isinstance(gain_mode, str) and gain_mode.upper() in {"LOW", "MEDIUM", "HIGH", "VLOW"}: + gain_profile = gain_mode.upper() + self.radio.gain(gain_profile.lower()) # WSA.gain() expects lowercase + + self.rx_gain = { + "attenuation_dB": attenuation, + "profile": gain_profile, + "decimation": actual_decimation, + "rfe_mode": self._rfe_mode, + "spp": self._samples_per_packet, + "ppb": self._packets_per_block, + } def _stream_rx(self, callback): if not self._rx_initialized: @@ -431,7 +444,7 @@ class ThinkRF(SDR): For decimation 1 or 2, block captures are limited by onboard RAM. """ if decimation <= 2 and num_samples > self.MAX_ONBOARD_SAMPLES: - raise ValueError( + raise SDRParameterError( f"ThinkRF: Cannot capture {num_samples} samples at decimation {decimation}. " f"Onboard RAM limit is ~{self.MAX_ONBOARD_SAMPLES} samples for dec 1/2. " f"Either reduce num_samples or use stream mode (increase decimation to >=4)." @@ -446,3 +459,6 @@ class ThinkRF(SDR): "fstop": int(center_frequency) + half, "amplitude": -100, } + + def supports_dynamic_updates(self) -> dict: + return {"center_frequency": True, "sample_rate": False, "gain": False}