Compare commits
2 Commits
5c9e50fa48
...
2e0378ff9d
| Author | SHA1 | Date | |
|---|---|---|---|
|
Aash
|
2e0378ff9d | ||
|
Aash
|
3f8506f222 |
45
scripts/convert_pyrf_to_python3.sh
Executable file
45
scripts/convert_pyrf_to_python3.sh
Executable file
|
|
@ -0,0 +1,45 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Fix pyrf Python 3 compatibility
|
||||||
|
# Run this after: pip install pyrf
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
VENV_DIR="${1:-venv}"
|
||||||
|
PYRF_BASE="$VENV_DIR/lib/python3.12/site-packages/pyrf"
|
||||||
|
|
||||||
|
if [ ! -d "$PYRF_BASE" ]; then
|
||||||
|
echo "❌ pyrf not found at $PYRF_BASE"
|
||||||
|
echo "Usage: $0 [venv_directory]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔧 Fixing pyrf for Python 3..."
|
||||||
|
|
||||||
|
# Backup originals
|
||||||
|
cp "$PYRF_BASE/devices/thinkrf.py" "$PYRF_BASE/devices/thinkrf.py.bak" 2>/dev/null || true
|
||||||
|
cp "$PYRF_BASE/devices/thinkrf_properties.py" "$PYRF_BASE/devices/thinkrf_properties.py.bak" 2>/dev/null || true
|
||||||
|
cp "$PYRF_BASE/connectors/blocking.py" "$PYRF_BASE/connectors/blocking.py.bak" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Fix thinkrf.py
|
||||||
|
echo " Fixing thinkrf.py..."
|
||||||
|
sed -i 's/\.iteritems()/.items()/g' "$PYRF_BASE/devices/thinkrf.py"
|
||||||
|
sed -i 's/raw_input/input/g' "$PYRF_BASE/devices/thinkrf.py"
|
||||||
|
# Fix print statements (carefully to handle the format string)
|
||||||
|
sed -i '884s/.*/ print(fmt % (index, wsa["HOST"], modelstring, wsa["SERIAL"]))/' "$PYRF_BASE/devices/thinkrf.py"
|
||||||
|
sed -i 's/print "r) Refresh"/print("r) Refresh")/g' "$PYRF_BASE/devices/thinkrf.py"
|
||||||
|
sed -i 's/print "q) Abort"/print("q) Abort")/g' "$PYRF_BASE/devices/thinkrf.py"
|
||||||
|
sed -i 's/print "error: invalid selection: '\''%s'\''" % choice/print("error: invalid selection: '\''%s'\''" % choice)/g' "$PYRF_BASE/devices/thinkrf.py"
|
||||||
|
|
||||||
|
# Fix thinkrf_properties.py
|
||||||
|
echo " Fixing thinkrf_properties.py..."
|
||||||
|
sed -i 's/\.iteritems()/.items()/g' "$PYRF_BASE/devices/thinkrf_properties.py"
|
||||||
|
|
||||||
|
# Fix blocking.py (socket bytes issue)
|
||||||
|
echo " Fixing blocking.py..."
|
||||||
|
sed -i '29s/self._sock_scpi.send(cmd)/self._sock_scpi.send(cmd.encode())/' "$PYRF_BASE/connectors/blocking.py"
|
||||||
|
sed -i '34s/self._sock_scpi.send(cmd)/self._sock_scpi.send(cmd.encode())/' "$PYRF_BASE/connectors/blocking.py"
|
||||||
|
# Fix line 37 - replace entire line to avoid double decode
|
||||||
|
sed -i '37s/.*/ return buf.decode()/' "$PYRF_BASE/connectors/blocking.py"
|
||||||
|
|
||||||
|
echo "✅ pyrf fixed for Python 3!"
|
||||||
|
echo " Backups saved with .bak extension"
|
||||||
|
|
@ -44,6 +44,8 @@ class ThinkRF(SDR):
|
||||||
|
|
||||||
BASE_SAMPLE_RATE = 125_000_000
|
BASE_SAMPLE_RATE = 125_000_000
|
||||||
SUPPORTED_DECIMATIONS = (1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024)
|
SUPPORTED_DECIMATIONS = (1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024)
|
||||||
|
MAX_ONBOARD_SAMPLES = 33_500_000 # Confirmed: 512 packets @ dec 1 = 33.5M samples (268ms)
|
||||||
|
DEFAULT_SPP = 65504 # VRT packet size (samples per packet)
|
||||||
|
|
||||||
def __init__(self, identifier: Optional[str] = None):
|
def __init__(self, identifier: Optional[str] = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
@ -108,22 +110,27 @@ class ThinkRF(SDR):
|
||||||
gain: int,
|
gain: int,
|
||||||
channel: int,
|
channel: int,
|
||||||
gain_mode: Optional[str] = "absolute",
|
gain_mode: Optional[str] = "absolute",
|
||||||
|
decimation: Optional[int] = None,
|
||||||
):
|
):
|
||||||
if channel not in (0, None):
|
if channel not in (0, None):
|
||||||
raise ValueError("ThinkRF devices expose a single receive channel")
|
raise ValueError("ThinkRF devices expose a single receive channel")
|
||||||
|
|
||||||
stream_mode = getattr(self, "_capture_mode", "block") == "stream"
|
stream_mode = getattr(self, "_capture_mode", "block") == "stream"
|
||||||
|
|
||||||
decimation = self._derive_decimation(sample_rate)
|
# Enforce sample rate / decimation
|
||||||
if stream_mode and decimation < self._min_stream_decimation:
|
# Note: decimation parameter takes precedence if provided
|
||||||
|
actual_decimation, actual_sample_rate = self.enforce_sample_rate(sample_rate, decimation)
|
||||||
|
|
||||||
|
if stream_mode and actual_decimation < self._min_stream_decimation:
|
||||||
enforced = self._min_stream_decimation
|
enforced = self._min_stream_decimation
|
||||||
print(
|
print(
|
||||||
"Requested ThinkRF sample rate exceeds typical GigE throughput; "
|
"Requested ThinkRF sample rate exceeds typical GigE throughput; "
|
||||||
f"enforcing decimation {enforced} for streaming."
|
f"enforcing decimation {enforced} for streaming."
|
||||||
)
|
)
|
||||||
decimation = enforced
|
actual_decimation = enforced
|
||||||
actual_sample_rate = self.BASE_SAMPLE_RATE / decimation
|
actual_sample_rate = self.BASE_SAMPLE_RATE / actual_decimation
|
||||||
self._decimation = decimation
|
|
||||||
|
self._decimation = actual_decimation
|
||||||
|
|
||||||
self.radio.reset()
|
self.radio.reset()
|
||||||
self.radio.scpiset(":SYSTEM:FLUSH")
|
self.radio.scpiset(":SYSTEM:FLUSH")
|
||||||
|
|
@ -140,11 +147,11 @@ class ThinkRF(SDR):
|
||||||
gain_profile = self._gain_profile
|
gain_profile = self._gain_profile
|
||||||
if gain_mode and isinstance(gain_mode, str) and gain_mode.upper() in {"LOW", "MEDIUM", "HIGH", "VLOW"}:
|
if gain_mode and isinstance(gain_mode, str) and gain_mode.upper() in {"LOW", "MEDIUM", "HIGH", "VLOW"}:
|
||||||
gain_profile = gain_mode.upper()
|
gain_profile = gain_mode.upper()
|
||||||
self.radio.psfm_gain(gain_profile)
|
self.radio.gain(gain_profile.lower()) # WSA.gain() expects lowercase
|
||||||
|
|
||||||
self.radio.decimation(decimation)
|
self.radio.decimation(actual_decimation)
|
||||||
if stream_mode:
|
if stream_mode:
|
||||||
self.radio.scpiset(f":SENSE:DECIMATION {decimation}")
|
self.radio.scpiset(f":SENSE:DECIMATION {actual_decimation}")
|
||||||
trigger = self._trigger_config or self._default_trigger(center_frequency)
|
trigger = self._trigger_config or self._default_trigger(center_frequency)
|
||||||
self.radio.trigger(trigger)
|
self.radio.trigger(trigger)
|
||||||
|
|
||||||
|
|
@ -152,12 +159,20 @@ class ThinkRF(SDR):
|
||||||
if stream_mode:
|
if stream_mode:
|
||||||
self._streaming_active = False
|
self._streaming_active = False
|
||||||
else:
|
else:
|
||||||
|
print(f"ThinkRF: Configuring block capture - SPP={self._samples_per_packet}, PPB={self._packets_per_block}")
|
||||||
self.radio.scpiset(f":TRACE:BLOCK:PACKETS {self._packets_per_block}")
|
self.radio.scpiset(f":TRACE:BLOCK:PACKETS {self._packets_per_block}")
|
||||||
self.radio.scpiset(":TRACE:BLOCK:DATA?")
|
self.radio.scpiset(":TRACE:BLOCK:DATA?")
|
||||||
|
|
||||||
self.rx_sample_rate = actual_sample_rate
|
self.rx_sample_rate = actual_sample_rate
|
||||||
self.rx_center_frequency = center_frequency
|
self.rx_center_frequency = center_frequency
|
||||||
self.rx_gain = {"attenuation_dB": attenuation, "profile": gain_profile}
|
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_buffer_size = self._samples_per_packet
|
||||||
self.rx_channel = 0
|
self.rx_channel = 0
|
||||||
|
|
||||||
|
|
@ -195,10 +210,20 @@ class ThinkRF(SDR):
|
||||||
try:
|
try:
|
||||||
packet = self.radio.read()
|
packet = self.radio.read()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
# In block mode, reaching end of block can cause exceptions
|
||||||
|
# This is normal - just stop reading
|
||||||
|
if not stream_mode and packets_processed > 0:
|
||||||
|
# Got some packets in block mode, finish gracefully
|
||||||
|
print(f"ThinkRF: Block read complete ({packets_processed} packets received)")
|
||||||
|
break
|
||||||
print(f"ThinkRF read error: {exc}")
|
print(f"ThinkRF read error: {exc}")
|
||||||
break
|
break
|
||||||
|
|
||||||
if packet is None:
|
if packet is None:
|
||||||
|
# No more packets available
|
||||||
|
if not stream_mode and packets_processed >= self._packets_per_block:
|
||||||
|
# Finished reading block
|
||||||
|
break
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if packet.is_context_packet():
|
if packet.is_context_packet():
|
||||||
|
|
@ -206,14 +231,25 @@ class ThinkRF(SDR):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not packet.is_data_packet():
|
if not packet.is_data_packet():
|
||||||
|
# Unknown packet type - skip
|
||||||
continue
|
continue
|
||||||
|
|
||||||
iq_data = np.asarray(packet.data, dtype=np.float32)
|
# packet.data is an iterable IQData object that yields (I, Q) tuples
|
||||||
if iq_data.ndim != 2 or iq_data.shape[1] != 2:
|
# Convert to numpy array: collect all [I, Q] pairs
|
||||||
print("Unexpected ThinkRF packet format; skipping packet")
|
try:
|
||||||
continue
|
# Iterate through packet.data to get all IQ pairs
|
||||||
|
iq_pairs = list(packet.data) # List of (I, Q) tuples
|
||||||
|
if not iq_pairs:
|
||||||
|
continue
|
||||||
|
|
||||||
complex_buffer = (iq_data[:, 0] + 1j * iq_data[:, 1]).astype(np.complex64, copy=False)
|
# Convert to numpy array [N, 2]
|
||||||
|
iq_array = np.array(iq_pairs, dtype=np.float32)
|
||||||
|
|
||||||
|
# Extract I and Q channels and create complex buffer
|
||||||
|
complex_buffer = (iq_array[:, 0] + 1j * iq_array[:, 1]).astype(np.complex64)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error extracting IQ from packet.data: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
metadata = None
|
metadata = None
|
||||||
if hasattr(packet, "fields"):
|
if hasattr(packet, "fields"):
|
||||||
|
|
@ -221,16 +257,15 @@ class ThinkRF(SDR):
|
||||||
if metadata.get("sample_loss"):
|
if metadata.get("sample_loss"):
|
||||||
print("\033[93mWarning: ThinkRF sample overflow detected\033[0m")
|
print("\033[93mWarning: ThinkRF sample overflow detected\033[0m")
|
||||||
|
|
||||||
|
# Send packet data to callback (accumulation handled by parent)
|
||||||
callback(buffer=complex_buffer, metadata=metadata)
|
callback(buffer=complex_buffer, metadata=metadata)
|
||||||
|
|
||||||
if stream_mode:
|
packets_processed += 1
|
||||||
packets_processed += 1
|
|
||||||
else:
|
# In block mode, stop after receiving all packets in the block
|
||||||
packets_processed += 1
|
if not stream_mode and packets_processed >= self._packets_per_block:
|
||||||
if packets_processed >= self._packets_per_block:
|
# Got all packets for this block
|
||||||
packets_processed = 0
|
break
|
||||||
if self._enable_rx:
|
|
||||||
self.radio.scpiset(":TRACE:BLOCK:DATA?")
|
|
||||||
|
|
||||||
print("ThinkRF RX Completed.")
|
print("ThinkRF RX Completed.")
|
||||||
if stream_mode and self._streaming_active:
|
if stream_mode and self._streaming_active:
|
||||||
|
|
@ -271,15 +306,109 @@ class ThinkRF(SDR):
|
||||||
raise NotImplementedError("ThinkRF radios do not expose a controllable bias-tee")
|
raise NotImplementedError("ThinkRF radios do not expose a controllable bias-tee")
|
||||||
|
|
||||||
def _derive_decimation(self, target_sample_rate: int | float) -> int:
|
def _derive_decimation(self, target_sample_rate: int | float) -> int:
|
||||||
|
"""
|
||||||
|
Derive decimation from target sample rate.
|
||||||
|
Always rounds DOWN decimation (UP sample rate) to meet or exceed user's requested rate.
|
||||||
|
|
||||||
|
Example: 30 MS/s requested → dec 4 (31.25 MS/s), NOT dec 8 (15.625 MS/s)
|
||||||
|
"""
|
||||||
if not target_sample_rate:
|
if not target_sample_rate:
|
||||||
return 1
|
return 1
|
||||||
requested = float(target_sample_rate)
|
requested = float(target_sample_rate)
|
||||||
if requested >= self.BASE_SAMPLE_RATE:
|
if requested >= self.BASE_SAMPLE_RATE:
|
||||||
return 1
|
return 1
|
||||||
desired = self.BASE_SAMPLE_RATE / requested
|
|
||||||
best = min(self.SUPPORTED_DECIMATIONS, key=lambda dec: abs(dec - desired))
|
desired_decimation = self.BASE_SAMPLE_RATE / requested
|
||||||
|
|
||||||
|
# Round DOWN decimation (UP sample rate) to meet or exceed requested rate
|
||||||
|
# Find largest decimation that gives sample rate >= requested
|
||||||
|
valid_decimations = [d for d in self.SUPPORTED_DECIMATIONS if d <= desired_decimation]
|
||||||
|
|
||||||
|
if valid_decimations:
|
||||||
|
# Use largest valid decimation (gives sample rate >= requested)
|
||||||
|
best = max(valid_decimations)
|
||||||
|
else:
|
||||||
|
# Requested rate too low, use minimum decimation (max sample rate)
|
||||||
|
best = self.SUPPORTED_DECIMATIONS[0]
|
||||||
|
|
||||||
return int(best)
|
return int(best)
|
||||||
|
|
||||||
|
def enforce_sample_rate(self, requested_sample_rate: int | float, decimation: Optional[int] = None) -> tuple[int, float]:
|
||||||
|
"""
|
||||||
|
Enforce valid sample rate and decimation.
|
||||||
|
|
||||||
|
If decimation is provided, it takes precedence.
|
||||||
|
Otherwise, derive decimation from requested sample rate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(decimation, actual_sample_rate)
|
||||||
|
"""
|
||||||
|
if decimation is not None:
|
||||||
|
# Decimation provided - validate and use it
|
||||||
|
if decimation not in self.SUPPORTED_DECIMATIONS:
|
||||||
|
# Round to nearest supported
|
||||||
|
decimation = min(self.SUPPORTED_DECIMATIONS, key=lambda d: abs(d - decimation))
|
||||||
|
print(f"ThinkRF: Requested decimation not supported. Using decimation={decimation}")
|
||||||
|
else:
|
||||||
|
# Derive from sample rate
|
||||||
|
decimation = self._derive_decimation(requested_sample_rate)
|
||||||
|
|
||||||
|
actual_sample_rate = self.BASE_SAMPLE_RATE / decimation
|
||||||
|
|
||||||
|
if abs(actual_sample_rate - requested_sample_rate) > 1e3: # More than 1 kHz difference
|
||||||
|
print(f"ThinkRF: Requested {requested_sample_rate/1e6:.2f} MS/s → Using decimation={decimation} ({actual_sample_rate/1e6:.2f} MS/s)")
|
||||||
|
|
||||||
|
return decimation, actual_sample_rate
|
||||||
|
|
||||||
|
def calculate_spp_ppb(self, num_samples: int, spp: Optional[int] = None) -> tuple[int, int]:
|
||||||
|
"""
|
||||||
|
Calculate optimal SPP (samples per packet) and PPB (packets per block).
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
- Maximize SPP (use DEFAULT_SPP) unless num_samples < DEFAULT_SPP
|
||||||
|
- Calculate PPB to get as close as possible to num_samples
|
||||||
|
- Actual captured samples = SPP * PPB (may exceed num_samples slightly)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_samples: Desired number of samples
|
||||||
|
spp: Override SPP (for advanced users, not recommended)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(spp, ppb)
|
||||||
|
"""
|
||||||
|
if spp is not None:
|
||||||
|
# User override - use as-is
|
||||||
|
actual_spp = max(1, int(spp))
|
||||||
|
else:
|
||||||
|
# Maximize SPP unless samples requested is smaller
|
||||||
|
if num_samples < self.DEFAULT_SPP:
|
||||||
|
actual_spp = num_samples
|
||||||
|
else:
|
||||||
|
actual_spp = self.DEFAULT_SPP
|
||||||
|
|
||||||
|
# Calculate PPB to get close to num_samples
|
||||||
|
ppb = max(1, int(np.ceil(num_samples / actual_spp)))
|
||||||
|
|
||||||
|
actual_samples = actual_spp * ppb
|
||||||
|
if actual_samples != num_samples:
|
||||||
|
print(f"ThinkRF: Requested {num_samples} samples → Capturing {actual_samples} (SPP={actual_spp}, PPB={ppb})")
|
||||||
|
|
||||||
|
return actual_spp, ppb
|
||||||
|
|
||||||
|
def check_ram_limit(self, num_samples: int, decimation: int) -> None:
|
||||||
|
"""
|
||||||
|
Check if requested capture exceeds onboard RAM limits.
|
||||||
|
|
||||||
|
Raises warning if exceeds MAX_ONBOARD_SAMPLES at low decimations.
|
||||||
|
For decimation 1 or 2, block captures are limited by onboard RAM.
|
||||||
|
"""
|
||||||
|
if decimation <= 2 and num_samples > self.MAX_ONBOARD_SAMPLES:
|
||||||
|
raise ValueError(
|
||||||
|
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)."
|
||||||
|
)
|
||||||
|
|
||||||
def _default_trigger(self, center_frequency: int | float) -> Dict[str, Any]:
|
def _default_trigger(self, center_frequency: int | float) -> Dict[str, Any]:
|
||||||
span = 40_000_000
|
span = 40_000_000
|
||||||
half = span // 2
|
half = span // 2
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user