Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rf_bench-0.2.0.tar.gz (53.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rf_bench-0.2.0-py3-none-any.whl (55.5 kB view details)

Uploaded Python 3

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

Hashes for rf_bench-0.2.0.tar.gz
Algorithm Hash digest
SHA256 d80e9beeb6140a496b1279743e2b4a629f06baea39c818b04463d86e8fe61681
MD5 cee28ec754f45ec31114ec13b6ba1195
BLAKE2b-256 d74c5f47c5b738258fa42180fd798b9e6234335a92ad2e588d1cad9d5c159aae

See more details on using hashes here.

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

Hashes for rf_bench-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ecce9f7f352f9f3fbba1bbd40c4b7ade018d07f011365fe4f6d878fbf37e7f35
MD5 e984d3b49f7b151b636e54b508133cc5
BLAKE2b-256 9c9ac1519866b0bc53a58e296217adf16b2883590d0c851d194f1364bc5136b3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page