Python drivers and RF utilities for bench instrument automation — Siglent, Icom, Yaesu via raw TCP/SCPI and Hamlib
Project description
rf-bench
Python drivers and RF utilities for bench instrument automation. Connects to Siglent test equipment via raw TCP/SCPI (no pyvisa required) and to HF transceivers via Hamlib rigctld.
Instruments supported
Siglent (rf_bench.siglent)
| Class | Instrument family | Tested with | Protocol |
|---|---|---|---|
SSA3000X |
SSA3000X Plus series spectrum analyzers | SSA3032X Plus (9 kHz–3.2 GHz) | SCPI / TCP port 5025 |
SDG1000X |
SDG1000X series function generators | SDG1062X (2-ch, 60 MHz) | EasyWave / TCP port 5025 |
SDS2000X |
SDS2000X Plus series oscilloscopes | SDS2354X Plus (500 MHz) | SCPI / TCP port 5025 |
SDM3000X |
SDM3000 series bench multimeters | SDM3045X (4.5-digit) | SCPI / TCP port 5025 |
SPD3303X |
SPD3303X series triple-output PSUs | SPD3303X-E (2×32 V/3.2 A + fixed) | SCPI / TCP port 5025 |
Icom (rf_bench.icom)
| Class | Instrument | Protocol |
|---|---|---|
IC7300 |
IC-7300 HF/6m transceiver | Hamlib rigctld / TCP port 4532 |
Yaesu (rf_bench.yaesu)
| Class | Instrument | Protocol |
|---|---|---|
FT891 |
FT-891 HF/6m transceiver | Hamlib rigctld / TCP port 4532 |
Utilities (rf_bench.utils)
rf_utils — pure-Python RF math library. Power conversions, impedance and
reflection math, noise figure, IP3, frequency formatting. No instruments,
no side effects; safe to import anywhere.
Installation
pip install rf-bench
Or from source:
git clone https://github.com/jfrancis42/rf-bench
cd rf-bench
pip install -e .
Dependency: NumPy (for rf_bench.utils and the
SDS2000X waveform decoder).
For radio control: Hamlib must be installed
and rigctld must be running before using IC7300 or FT891.
# IC-7300 (CI-V baud set to 115200 in radio menu)
rigctld -m 3073 -r /dev/ttyUSB0 -s 115200 &
# FT-891 (CAT baud set to 38400 in Menu 031)
rigctld -m 1036 -r /dev/ttyUSB0 -s 38400 &
Quick start
from rf_bench import SDG1000X, IC7300, dbm_to_vpp, format_freq
# Function generator — two-tone test signal
with SDG1000X("10.1.1.61") as sdg:
sdg.set_sine(1, freq_hz=14_001_000, level_dbm=-30)
sdg.set_sine(2, freq_hz=14_001_500, level_dbm=-30)
sdg.output_on(1)
sdg.output_on(2)
# ... run test ...
# IC-7300 S-meter reading
with IC7300() as rig:
rig.set_frequency(14_200_000)
rig.set_mode("usb")
rig.set_agc("off")
strength = rig.get_strength_settled(settle_s=0.5)
print(f"Signal: {strength:.1f} STRENGTH units")
# RF math
dbm_to_vpp(-20) # → 0.0632 Vpp (P = Vpp²/8R, 50 Ω)
format_freq(14_200_000) # → '14.2000 MHz'
Or import from subpackages:
from rf_bench.siglent import SSA3000X, SDG1000X
from rf_bench.icom import IC7300
from rf_bench.yaesu import FT891, PREAMP_OFF, PREAMP_AMP1
from rf_bench.utils import thermal_noise_floor, ip3_from_imd, rl_to_vswr
Siglent drivers
SSA3000X
Tested with: Siglent SSA3032X Plus
from rf_bench.siglent import SSA3000X
with SSA3000X("10.1.1.60") as ssa:
ssa.enable_tracking_generator(dbm=0)
rbw = ssa.setup_band(14_000_000, 14_350_000, points=1001)
ssa.single_sweep() # blocks until sweep completes
trace = ssa.get_trace() # → np.ndarray of dBm values (length = points)
SDG1000X
Tested with: Siglent SDG1062X
from rf_bench.siglent import SDG1000X
with SDG1000X("10.1.1.61") as sdg:
sdg.set_sine(1, freq_hz=14_001_000, level_dbm=-30)
sdg.output_on(1)
sdg.set_level(1, level_dbm=-40) # change level only, preserve frequency
info = sdg.query_channel(1) # → {freq_hz, amp_vpp, amp_dbm, ...}
Amplitude range: ≈ −50 dBm (2 mVpp) to +24 dBm (10 Vpp) into 50 Ω.
SDS2000X
Tested with: Siglent SDS2354X Plus
from rf_bench.siglent import SDS2000X
with SDS2000X("10.1.1.62") as scope:
voltages, sample_rate = scope.capture_audio(channel=1, duration_s=2.0)
rms = scope.measure_rms(channel=1)
vdiv = scope.autoscale_vdiv(channel=1)
SDM3000X
Tested with: Siglent SDM3045X (4.5-digit) Compatible with: SDM3045X, SDM3055 (5.5-digit), SDM3065X (6.5-digit)
All measurement functions return SI units (V, A, Ω, Hz, F, °C). MEAS commands
are one-shot; use configure_*() + read_multiple() for repeated measurements.
from rf_bench.siglent import SDM3000X
with SDM3000X("10.1.1.63") as dmm:
v = dmm.measure_vdc() # DC voltage, auto-range → V
v = dmm.measure_vdc(range_v=20) # DC voltage, 20 V range → V
i = dmm.measure_idc() # DC current → A
r = dmm.measure_resistance() # 2-wire resistance → Ω
r = dmm.measure_resistance(four_wire=True) # 4-wire (Kelvin) → Ω
f = dmm.measure_frequency() # frequency → Hz
dmm.measure_continuity() # resistance; beeps if < ~30 Ω
dmm.measure_diode() # forward voltage → V
# SDM3055 / SDM3065X only:
c = dmm.measure_capacitance() # → F
t = dmm.measure_temperature() # → °C (FRTD probe default)
# Multi-sample: configure once, read many
dmm.configure_vdc(range_v=5)
samples = dmm.read_multiple(20) # → [float, ...] 20 samples
SPD3303X
Tested with: Siglent SPD3303X-E (2× 0–32 V / 0–3.2 A + fixed CH3) Compatible with: SPD3303C, SPD3303X, SPD3303X-E
CH1 and CH2 are fully programmable CC/CV channels. CH3 is a fixed-voltage output (2.5 V, 3.3 V, or 5 V selected by front-panel switch); its voltage cannot be set via SCPI but its output can be enabled/disabled and measured.
from rf_bench.siglent import SPD3303X, TRACKING_INDEPENDENT, TRACKING_SERIES
with SPD3303X("10.1.1.64") as psu:
# Basic CH1 setup
psu.set_voltage(1, 5.0) # 5 V setpoint
psu.set_current(1, 0.5) # 500 mA current limit
psu.enable(1)
v = psu.measure_voltage(1) # actual output voltage → V
i = psu.measure_current(1) # actual output current → A
p = psu.measure_power(1) # actual output power → W
mode = psu.get_mode(1) # 'CV' or 'CC'
state = psu.measure_all(1) # {'voltage_v', 'current_a', 'power_w'}
# CH3 (fixed voltage — 2.5/3.3/5 V set by front-panel switch)
psu.enable(3)
psu.measure_voltage(3) # reads actual CH3 output voltage
psu.disable_all()
# Series tracking: CH1+CH2 in series for up to 64 V
psu.set_tracking(TRACKING_SERIES)
psu.set_voltage(1, 24.0) # CH2 mirrors CH1 automatically
psu.enable(1)
psu.enable(2)
psu.get_status() # → {'ch1_mode': 'CV', 'ch2_mode': 'CV', 'track_mode': 'SER'}
Radio drivers
IC7300 and FT891 share an identical core interface and are drop-in
substitutable.
from rf_bench.icom import IC7300
from rf_bench.yaesu import FT891, PREAMP_OFF, PREAMP_AMP1
# Shared interface
for RigClass in (IC7300, FT891):
with RigClass() as rig:
rig.set_frequency(14_200_000)
rig.set_mode("usb", passband_hz=2400)
rig.set_agc("slow")
rig.set_rf_gain(1.0)
strength = rig.get_strength_settled()
# FT-891 additions: preamp / attenuator
with FT891() as rig:
rig.set_preamp(PREAMP_OFF) # IPO — bypass preamp for large-signal tests
rig.set_preamp(PREAMP_AMP1) # AMP1 — ~10 dB gain for sensitivity tests
rig.set_att(6) # 0, 6, or 12 dB front-end attenuation
AGC note: set_agc("off") is a true hardware bypass on the IC-7300.
On the FT-891 it maps to the slowest AGC constant — not a true bypass.
If both radios are in use simultaneously, run each rigctld on a separate port:
rigctld -m 3073 -r /dev/ttyUSB0 -s 115200 -T localhost -t 4532 &
rigctld -m 1036 -r /dev/ttyUSB1 -s 38400 -T localhost -t 4533 &
ic = IC7300("localhost", 4532)
ft = FT891("localhost", 4533)
RF utilities
rf_bench.utils is a pure-Python RF math library. No instruments, no side effects;
safe to import anywhere.
from rf_bench.utils import (
# Constants
SPEED_OF_LIGHT, # 299 792 458 m/s (exact)
S9_HF_DBM, S9_VHF_DBM, # −73 / −93 dBm (ITU S-meter references)
# Power / voltage (50 Ω default; pass impedance= to override)
dbm_to_vpp, vpp_to_dbm, # dBm ↔ Vpp (sine: P = Vpp²/8R; 0 dBm → 0.6325 Vpp)
dbm_to_vrms, vrms_to_dbm, # dBm ↔ Vrms
dbm_to_watts, watts_to_dbm, # dBm ↔ Watts
dbm_to_uv, uv_to_dbm, # dBm ↔ µVrms
# Power ratio / extended dB units
db_to_linear, linear_to_db, # power ratio ↔ dB
db_to_voltage_ratio, # voltage ratio from dB (10^(dB/20))
voltage_ratio_to_db, # dB from voltage ratio (20·log10)
dbm_to_dbw, dbw_to_dbm, # dBm ↔ dBW
dbm_to_dbuv, dbuv_to_dbm, # dBm ↔ dBµV (0 dBm at 50 Ω = 106.99 dBµV)
# Impedance / reflection
rl_to_vswr, vswr_to_rl, # return loss ↔ VSWR
gamma_to_vswr, vswr_to_gamma, # reflection coeff ↔ VSWR
rl_to_gamma, gamma_to_rl,
rl_to_vswr_v, vswr_to_rl_v, # vectorized (numpy array) versions
gamma_to_vswr_v,
# Noise and dynamic range
thermal_noise_floor, # kTB in dBm (exact Boltzmann constant)
noise_figure_from_mds, # NF from measured MDS and bandwidth
mds_from_noise_figure, # MDS from NF and bandwidth
ip3_from_imd, # OIP3 or IIP3 from two-tone IMD levels
ip3_to_dynamic_range, # SFDR = (2/3)(IP3 − noise floor)
cascaded_noise_figure, # Friis formula for cascade of (gain_db, nf_db) stages
noise_temp_to_nf, # noise temperature (K) → NF (dB)
nf_to_noise_temp, # NF (dB) → noise temperature (K)
# Propagation / antenna
wavelength, quarter_wave, # λ, λ/4 in metres; optional velocity_factor
half_wave, # λ/2 in metres
freespace_path_loss, # FSPL = 20·log10(4πdf/c) in dB
# Passive components
capacitive_reactance, # Xc = 1/(2πfC) Ω
inductive_reactance, # Xl = 2πfL Ω
lc_resonant_freq, # f = 1/(2π√(LC)) Hz
l_from_resonant, c_from_resonant,# compute L or C from resonant frequency
q_factor, bw_from_q, # Q = f0/BW ↔ BW = f0/Q
parallel_resistance, # 1/Σ(1/Rᵢ) — 2 or more values
voltage_divider, # Vout = Vin · R2 / (R1+R2)
skin_depth, # δ in metres (copper default: 5.8×10⁷ S/m)
# Attenuator design
pi_attenuator, # π-pad: {'r_shunt': Ω, 'r_series': Ω}
t_attenuator, # T-pad: {'r_series': Ω, 'r_shunt': Ω}
# IM products
intermod_products, # two-tone near-carrier IM products, odd orders
# S-meter
s_unit_to_dbm, dbm_to_s_unit, # ITU S-unit ↔ dBm (HF default; vhf=True for VHF)
# Formatting
format_freq, # 14200000 → '14.2000 MHz'; also GHz and Hz
format_freq_short, # 14200000 → '14.2 MHz' (trailing zeros trimmed)
nearest_rbw, # nearest Siglent RBW step
nearest_value, # nearest value in any list (E-series, RBW, etc.)
# Standard value series
SIGLENT_RBW_SERIES,
E12_SERIES, E24_SERIES, E48_SERIES, E96_SERIES,
)
Default instrument addresses
| Driver class | Tested instrument | Default IP | Port |
|---|---|---|---|
SSA3000X |
SSA3032X Plus | 10.1.1.60 | 5025 |
SDG1000X |
SDG1062X | 10.1.1.61 | 5025 |
SDS2000X |
SDS2354X Plus | 10.1.1.62 | 5025 |
SDM3000X |
SDM3045X | 10.1.1.63 (suggested) | 5025 |
SPD3303X |
SPD3303X-E | 10.1.1.64 (suggested) | 5025 |
IC7300 / FT891 |
IC-7300 / FT-891 | localhost | 4532 |
All drivers accept host and port constructor arguments to override defaults.
License
GPL-3.0-or-later — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rf_bench-0.2.0.tar.gz.
File metadata
- Download URL: rf_bench-0.2.0.tar.gz
- Upload date:
- Size: 53.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d80e9beeb6140a496b1279743e2b4a629f06baea39c818b04463d86e8fe61681
|
|
| MD5 |
cee28ec754f45ec31114ec13b6ba1195
|
|
| BLAKE2b-256 |
d74c5f47c5b738258fa42180fd798b9e6234335a92ad2e588d1cad9d5c159aae
|
File details
Details for the file rf_bench-0.2.0-py3-none-any.whl.
File metadata
- Download URL: rf_bench-0.2.0-py3-none-any.whl
- Upload date:
- Size: 55.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ecce9f7f352f9f3fbba1bbd40c4b7ade018d07f011365fe4f6d878fbf37e7f35
|
|
| MD5 |
e984d3b49f7b151b636e54b508133cc5
|
|
| BLAKE2b-256 |
9c9ac1519866b0bc53a58e296217adf16b2883590d0c851d194f1364bc5136b3
|