Skip to content

Commit

Permalink
Merge pull request #3 from sdgtt/tfcollins/dwta
Browse files Browse the repository at this point in the history
Add Keysight DWTA CLI tools
  • Loading branch information
tfcollins authored Oct 3, 2024
2 parents 38ae2fb + cef400d commit c22157b
Show file tree
Hide file tree
Showing 25 changed files with 2,673 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/doc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
./.github/scripts/install_libiio.sh
pip install -r requirements_dev.txt
pip install -r doc/requirements.txt
pip install ".[cli,web]"
- name: Build doc
run: |
Expand All @@ -41,6 +42,7 @@ jobs:
./.github/scripts/install_libiio.sh
pip install -r requirements_dev.txt
pip install -r doc/requirements.txt
pip install ".[cli,web]"
- name: Check doc build
run: |
Expand Down Expand Up @@ -80,6 +82,7 @@ jobs:
./.github/scripts/install_libiio.sh
pip install -r requirements_dev.txt
pip install -r doc/requirements.txt
pip install ".[cli,web]"
- name: Build doc and release
run: |
Expand Down Expand Up @@ -111,6 +114,7 @@ jobs:
./.github/scripts/install_libiio.sh
pip install -r requirements_dev.txt
pip install -r doc/requirements.txt
pip install ".[cli,web]"
- name: Build doc and release
run: |
Expand Down Expand Up @@ -151,6 +155,7 @@ jobs:
pip install -r requirements_dev.txt
pip install -r doc/requirements.txt
pip install setuptools wheel twine build
pip install ".[cli,web]"
- name: Build doc and release
run: |
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/pyinstallers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generate pyinstaller executables for Windows, macOS, and Linux
name: Generate PyInstaller

on: [push, pull_request]

jobs:
Windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: conda-incubator/setup-miniconda@v2
with:
auto-update-conda: true
python-version: "3.8"
- name: Conda info
shell: bash -l {0}
run: conda info
- name: Conda list
shell: pwsh
run: conda list
- name: Dependencies
run: |
conda activate test
python --version
conda install -c conda-forge pylibiio
pip install -r requirements_dev.txt
pip install -r doc/requirements.txt
pip install pyinstaller
- name: Generate Windows executable
run: |
pyinstaller --onefile --name=pybenchiio cli.py
- name: Upload Windows executable
uses: actions/upload-artifact@v4
with:
name: pybenchiio
path: dist/pybenchiio.exe
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ repos:
- id: codespell
entry: codespell --ignore-words=.codespell-whitelist --skip="*.pyc,*.xml"
- repo: https://github.com/pre-commit/mirrors-isort
rev: v4.3.20
rev: v5.5.4
hooks:
- id: isort
additional_dependencies: ["toml"]
args: ["--profile", "black"]
#- repo: https://github.com/pre-commit/mirrors-mypy
# rev: v0.720
# hooks:
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
# pybench
Interfaces to control instruments in python

## Web backend development

```bash
fastapi dev bench/web/app.py
```
243 changes: 233 additions & 10 deletions bench/cli/iiotools.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import os
import subprocess
import time

import click
import requests

from bench.keysight.dwta.data_capture import capture_iq_datafile

try:
import iio
Expand All @@ -9,15 +16,11 @@

@click.group()
@click.option("--uri", "-u", help="URI of target device/board")
@click.option("--device", "-d", help="Device driver to use")
@click.option("--complex", "-x", is_flag=True, help="Use complex mode")
@click.pass_context
def cli(ctx, uri, device, complex):
def cli(ctx, uri):
"""Command line interface for pybench IIO based boards"""
ctx.obj = {}
ctx.obj["uri"] = uri
ctx.obj["device"] = device
ctx.obj["complex"] = complex


@cli.command()
Expand All @@ -27,17 +30,19 @@ def cli(ctx, uri, device, complex):
@click.option(
"--amplitude", "-a", help="Set the amplitude of the DDS in 0->1", required=True
)
@click.option("--device", "-d", help="IIO device driver name to use")
@click.option("--channel", "-c", help="Set the channel of the DDS", required=True)
@click.option("--complex", "-x", is_flag=True, help="Use complex mode")
@click.pass_context
def set_dds(ctx, frequency, amplitude, channel):
def set_dds(ctx, frequency, amplitude, device, channel, complex):
"""Configure DDS"""
iioctx = iio.Context(ctx.obj["uri"])
if not iioctx:
click.echo("No context")
return
dev = iioctx.find_device(ctx.obj["device"])
dev = iioctx.find_device(device)
if not dev:
click.echo(f"Device {ctx.obj['device']} not found")
click.echo(f"Device {device} not found")
return
dds_channels = [ch.id for ch in dev.channels if "altvoltage" in ch.id]
# Set all the DDS scales to 0
Expand All @@ -49,7 +54,7 @@ def set_dds(ctx, frequency, amplitude, channel):
return
chan.attrs["scale"].value = "0"
# Set the desired DDS scale
if ctx.obj["complex"]:
if complex:
# Channels are groups of 4
ch = int(channel) * 4
channels = [ch, ch + 1]
Expand All @@ -64,11 +69,229 @@ def set_dds(ctx, frequency, amplitude, channel):
return
chan.attrs["frequency"].value = frequency
chan.attrs["scale"].value = amplitude
if ctx.obj["complex"]:
if complex:
# i is odd
if i % 2:
chan.attrs["phase"].value = "90000"
else:
chan.attrs["phase"].value = "0"

click.echo(f"Set DDS of channel {channel} to {frequency}Hz and {amplitude} scale")


@cli.command()
@click.option("--filename", "-f", help="Name of file to write data to", required=True)
@click.option("--device", "-d", help="Name of device to configure", required=True)
@click.option(
"--channel",
"-c",
help="Channel index to capture data from. Starts from 0",
required=True,
)
@click.option("--samples", "-s", help="Number of samples to capture", required=True)
@click.argument(
"props", nargs=-1, required=False,
)
@click.pass_context
def capture_data(ctx, filename, device, channel, samples, props):
"""Capture IQ data to a file in DWTA format
PROPS is a list of property=value pairs to set device properties. These are
the properties available in the pyadi-iio class interface for the device.
Example usage with ADALM-PLUTO:
pybenchiio -u ip:analog.local capture_data -f data.csv -d Pluto -c 0 -s 1024 sample_rate=1000000
"""
# Checks
samples = int(samples)
channel = int(channel)

# Parse properties
if props:
oprops = {}
for prop in props:
if "=" not in prop:
raise ValueError(
f"Invalid property: {prop}. Must be in the form key=value"
)
k, v = prop.split("=")
if v.isdigit():
v = int(v)
oprops[k] = v
props = oprops

capture_iq_datafile(
filename, device, channel, samples, ctx.obj["uri"], **dict(props)
)


@cli.command()
@click.option("--filename", "-f", help="Name of file to write data to", required=True)
@click.option("--device", "-d", help="Name of device to configure", required=True)
@click.option("--channel", "-c", help="Channel to capture data from", required=True)
@click.option(
"--server-ip",
"-s",
help="IP address of the server",
required=False,
default="localhost",
)
@click.option(
"--server-port", "-p", help="Port of the server", required=False, default=12345
)
@click.argument("props", nargs=-1)
@click.pass_context
def transmit_data(ctx, filename, device, channel, server_ip, server_port, props):
"""Transmit IQ data file to device through backend server
File must be in DWTA format, where the first two lines are sample rate and
center frequency, and the rest of the lines are IQ data in the format
I, Q per line.
Example usage with ADALM-PLUTO:
pybenchiio -u ip:analog.local transmit_data -f data.csv -d Pluto -c 0 sample_rate=1000000
"""

# Checks
channel = int(channel)

# Parse properties
# if props:
# oprops = {}
# for prop in props:
# if "=" not in prop:
# raise ValueError(
# f"Invalid property: {prop}. Must be in the form key=value"
# )
# k, v = prop.split("=")
# if v.isdigit():
# v = int(v)
# oprops[k] = v
# props = oprops

# transmit_iq_datafile(
# filename, device, channel, ctx.obj["uri"], **dict(props)
# )

import os

import requests

# Send post request to localhost
url = f"http://{server_ip}:{server_port}/writebuffer"
json_data = {
"uri": ctx.obj["uri"],
"device": device,
"channel": channel,
"data_filename": filename,
"do_scaling": False,
"cycle": True,
"data_complex": True,
"properties": props,
}
r = requests.post(url, json=json_data)
assert r.status_code == 200, f"Failed to send data: {r.json()}"


@cli.command()
@click.option(
"--server-ip",
"-s",
help="IP address of the server",
required=False,
default="localhost",
)
@click.option(
"--server-port", "-p", help="Port of the server", required=False, default=12345
)
def transmit_data_clear(server_ip, server_port):
"""Clear the transmit buffer on the server"""

url = f"http://{server_ip}:{server_port}/clearbuffer"
r = requests.post(url)
assert r.status_code == 200, f"Failed to clear buffer: {r.json()}"


@cli.command()
@click.option(
"--host",
"-h",
help="Host to start the server on",
required=False,
default="localhost",
)
@click.option(
"--port", "-p", help="Port to start the server on", required=False, default=12345
)
def start_server(host, port):

# Start the server as a subprocess
import os
import subprocess

loc = os.path.dirname(os.path.realpath(__file__))
web_app = os.path.join(loc, "..", "web", "app.py")
web_app = os.path.abspath(web_app)

# Get location of python executable
python = "python"
if "CONDA_PREFIX" in os.environ:
python = os.path.join(os.environ["CONDA_PREFIX"], "bin", "python")
elif "VIRTUAL_ENV" in os.environ:
python = os.path.join(os.environ["VIRTUAL_ENV"], "bin", "python")
elif "PYTHON" in os.environ:
python = os.environ["PYTHON"]

command = [
python,
"-m",
"fastapi",
"run",
web_app,
"--host",
str(host),
"--port",
str(port),
]
# Start process and verify running after 5 seconds
p = subprocess.Popen(command)
time.sleep(5)
assert p.poll() is None, "Failed to start server"
print(f"Server started on {host}:{port} with PID {p.pid}")


# Stop server
@cli.command()
@click.option(
"--server-port", "-p", help="Port of the server", required=False, default=12345
)
def stop_server(server_port):
"""Stop the server"""
# Find the process using the port and kill it
if os.name == "nt":
# Query for the process ID
pid = subprocess.run(
f"netstat -aon | findstr {server_port}", shell=True, stdout=subprocess.PIPE
)
if pid.returncode != 0:
print("Server not running")
return
pid = pid.stdout.decode().split(" ")[-1]
# Kill the process
subprocess.run(f"taskkill /F /PID {pid}", shell=True)
else:
# Query for the process ID
pid = subprocess.run(
f"lsof -t -i:{server_port}", shell=True, stdout=subprocess.PIPE
)
if pid.returncode != 0:
print("Server not running")
return
pid = pid.stdout.decode().strip()
print(f"Server found on port {server_port} with PID {pid}")
# Kill the process
subprocess.run(f"kill -9 {pid}", shell=True)

print(f"Server on port {server_port} stopped")
1 change: 1 addition & 0 deletions bench/hittite/hmct2220.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

import pyvisa

from bench.common import Common, check_connected

hmct2220_logger = logging.getLogger(__name__)
Expand Down
Loading

0 comments on commit c22157b

Please sign in to comment.