Automate an RF attenuation sweep in Python

Use the POE-ATTEN-6G REST API to program signal-level sequences from Python. The device accepts setpoints from 0 to 95.25 dB in 0.25 dB increments. Python's standard library is sufficient for HTTP control; a vendor-specific desktop application is not required.

This example logs commanded and reported setpoints. It does not measure attenuation accuracy, RF power, or RF settling time. The software is checked against the documented API using a simulated endpoint; physical-device and RF verification are separate steps.

Connect and confirm the address

Power the instrument with IEEE 802.3af PoE, or connect a USB-C data cable to a host computer. USB-C exposes an Ethernet interface as well as supplying power. At factory defaults, the device is reachable at http://192.168.42.95 over USB-C. On the Ethernet network, use its DHCP address; the default fallback is 192.168.0.95 after 30 seconds without a DHCP reply. Your computer must have a route to that network.

Open the address in a browser and confirm the connected model. Then make a read-only request:

curl --fail --max-time 5 http://192.168.42.95/api/attenuator

The JSON response includes a setpoint value in dB. The getting-started guide describes the network defaults and available ports.

Choose the sweep for your DUT

Reducing attenuation raises the signal delivered to the DUT. Establish a suitable source level and permissible DUT input before running a sequence. Account for the attenuator's baseline insertion loss, cables, and adapters. A 0 dB setpoint is not a lossless path, and a 0.25 dB command increment is not an accuracy specification.

For example, a 30-to-10 dB sweep changes the nominal attenuation by 20 dB. It does not by itself establish an absolute level at the DUT. See the characterization procedure to measure that path.

Run the Python example

Save the following as attenuation_sweep.py. It uses only the Python standard library. Dry-run mode validates and prints the complete sequence without connecting to the instrument.

#!/usr/bin/env python3
"""Log commanded/read-back attenuation, not measured RF attenuation.

Dry-run by default. --apply changes a real instrument and restores its initial
setpoint on normal completion or a handled failure. A lost connection or power
failure can prevent restoration. The persistent startup value is never changed.
"""

import argparse
import csv
import json
import math
import sys
import time
from decimal import Decimal, InvalidOperation
from urllib.request import Request, urlopen


def setpoint(value):
    try:
        number = Decimal(str(value))
    except InvalidOperation as error:
        raise ValueError("Setpoints must be finite numbers") from error
    if not number.is_finite() or not 0 <= number <= Decimal("95.25") or number % Decimal("0.25"):
        raise ValueError("Setpoints must be 0–95.25 dB in 0.25 dB steps")
    return number


def sweep_values(start, stop, step):
    start, stop, step = setpoint(start), setpoint(stop), setpoint(step)
    if step == 0 or abs(stop - start) % step:
        raise ValueError("Step must be positive and reach the stop setpoint exactly")
    direction = 1 if stop >= start else -1
    return [start + direction * step * i for i in range(int(abs(stop - start) / step) + 1)]


def request_setpoint(base_url, value=None, timeout=3.0):
    body = None if value is None else json.dumps({"setpoint": float(value)}).encode("utf-8")
    request = Request(base_url.rstrip("/") + "/api/attenuator", data=body,
                      headers={"Content-Type": "application/json"})
    with urlopen(request, timeout=timeout) as response:
        payload = json.load(response)
    return setpoint(payload["setpoint"])


def sweep(base_url, values, dwell=0.1, timeout=3.0):
    # Validate the entire sequence before making any network request.
    values = [setpoint(value) for value in values]
    if not values:
        raise ValueError("Provide at least one setpoint")
    if not math.isfinite(dwell) or dwell < 0 or not math.isfinite(timeout) or timeout <= 0:
        raise ValueError("Dwell must be finite and nonnegative; timeout must be finite and positive")
    initial = request_setpoint(base_url, timeout=timeout)
    try:
        for target in values:
            started = time.monotonic()
            request_setpoint(base_url, target, timeout)
            readback = request_setpoint(base_url, timeout=timeout)
            elapsed_ms = (time.monotonic() - started) * 1000
            if readback != target:
                raise RuntimeError(f"Setpoint mismatch: requested {target}, read back {readback}")
            time.sleep(dwell)
            yield {"requested_db": str(target), "readback_db": str(readback),
                   "post_and_readback_ms": f"{elapsed_ms:.3f}"}
    finally:
        request_setpoint(base_url, initial, timeout)
        if request_setpoint(base_url, timeout=timeout) != initial:
            raise RuntimeError("Could not verify restoration of the initial setpoint")


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("base_url", help="Instrument URL, for example http://192.168.42.95")
    parser.add_argument("--start", default="30")
    parser.add_argument("--stop", default="10")
    parser.add_argument("--step", default="1")
    parser.add_argument("--dwell", type=float, default=0.1, help="Seconds after verified readback")
    parser.add_argument("--timeout", type=float, default=3.0)
    parser.add_argument("--apply", action="store_true", help="Actually change the instrument setpoint")
    args = parser.parse_args()
    try:
        values = sweep_values(args.start, args.stop, args.step)
        if not args.apply:
            print("Dry run; no network requests. Setpoints (dB): " + ", ".join(map(str, values)))
            return
        writer = csv.DictWriter(sys.stdout, fieldnames=["requested_db", "readback_db", "post_and_readback_ms"])
        writer.writeheader()
        for row in sweep(args.base_url, values, args.dwell, args.timeout):
            writer.writerow(row)
    except (ValueError, RuntimeError, OSError, KeyError) as error:
        parser.exit(1, f"Sweep failed: {error}\n")


if __name__ == "__main__":
    main()

Preview the sequence:

python3 attenuation_sweep.py http://192.168.42.95 --start 30 --stop 10 --step 1

After checking your RF setup, explicitly apply it and capture the CSV:

python3 attenuation_sweep.py http://192.168.42.95 --start 30 --stop 10 --step 1 --dwell 0.1 --apply > control-log.csv

The script reads the initial setpoint, verifies each requested setting, and attempts to restore the initial value when it finishes or encounters a handled error. A lost connection, process termination, or loss of power can prevent restoration; verify the actual device state before resuming measurements. It does not call /api/attenuator/startup or change the saved boot setting.

Interpret the log correctly

Field Meaning
requested_db Attenuation command submitted to the device
readback_db Setpoint returned by the API after the command
post_and_readback_ms Host-observed elapsed time for the POST and subsequent GET, including network and software overhead

The elapsed time excludes the additional dwell. It is not an RF transition or settling measurement. The dwell is a host-side wait after readback; choose it for your instruments and measurement procedure. To capture RF power or S-parameters, add acquisition from the relevant calibrated instrument after that wait, and record its configuration alongside the control log.

Use SCPI when it suits the test system

Existing SCPI systems can query :ATT? and write :SETATT 22.25. Raw SCPI uses TCP port 5025; HiSLIP uses port 4880 with a compatible VISA client. There is no need to install VISA to use the HTTP example above. Avoid *RST as a connection test: it changes instrument state.

References: attenuator REST API, SCPI commands, and product specifications. Return to RF test automation guides.