Compare commits
6 Commits
26682f1741
...
30fb1190bb
Author | SHA1 | Date | |
---|---|---|---|
30fb1190bb | |||
fa80af8447 | |||
926a6abf1f | |||
2285bb78c1 | |||
894d980a64 | |||
9a922762fa |
34
README.md
34
README.md
|
@ -12,22 +12,31 @@ On Ubuntu 22.04 just run `sudo apt-get install -y libiio-dev`
|
||||||
## Hardware Setup
|
## Hardware Setup
|
||||||
|
|
||||||
You need a few things:
|
You need a few things:
|
||||||
- [Analog Devices Pluto SDR](https://www.analog.com/en/resources/evaluation-hardware-and-software/evaluation-boards-kits/adalm-pluto.html).
|
- [Analog Devices Pluto SDR](https://www.analog.com/en/resources/evaluation-hardware-and-software/evaluation-boards-kits/adalm-pluto.html)
|
||||||
Any variant of the Pluto *should* work too such as the [Pluto+](https://github.com/plutoplus/plutoplus?tab=readme-ov-file) however I have only tested with the basic flavor.
|
- Any variant of the Pluto *should* work too such as the [Pluto+](https://github.com/plutoplus/plutoplus?tab=readme-ov-file) however I have only tested with the basic flavor
|
||||||
- Directional couplers (1 per port up to 4 ports).
|
- Note that you _must_ have two receive ports which means revision C or later of the basic Pluto
|
||||||
I have been using [AAMCS-UDC-0.5G-18G-10dB-Sf](http://www.aa-mcs.com/wp-content/uploads/documents/AAMCS-UDC-0.5G-18G-10dB-Sf.pdf)
|
- Directional couplers (1 per port up to 4 ports)
|
||||||
- Charon switch board - coming soon.
|
- I have been using [AAMCS-UDC-0.5G-18G-10dB-Sf](http://www.aa-mcs.com/wp-content/uploads/documents/AAMCS-UDC-0.5G-18G-10dB-Sf.pdf)
|
||||||
Without this, you'll be limited to S11 and uncalibrated S21 measurements (with required re-cabling).
|
- Charon switch board - coming soon.
|
||||||
There's nothing special about this particular board, if you want more than 4 ports you can make your own pretty easily. You just need 3 SPxT switches. Note that these switches will see tons of cycles so avoid mechanical switches.
|
- Optional. Without this you'll be limited to S11 and uncalibrated S21 measurements (with required re-cabling)
|
||||||
|
- There's nothing special about this particular board, if you want more than 4 ports you can make your own pretty easily. You just need 3 SPxT switches. Note that these switches will see tons of cycles so avoid mechanical switches
|
||||||
- SMA cables
|
- SMA cables
|
||||||
|
- Calibration standard
|
||||||
|
- Ideally something with s-parameters measured on a better VNA
|
||||||
|
- I have used a basic SMA load and two modified SMA jacks with decent results
|
||||||
|
|
||||||
### Pluto Configuration
|
### Pluto Configuration
|
||||||
|
|
||||||
Most of my testing is with Pluto firmware [v0.39](https://github.com/analogdevicesinc/plutosdr-fw/releases/tag/v0.39) though this may work with other firmware versions. Instructions for upgrading firmware are on the [Analog Devices wiki](https://wiki.analog.com/university/tools/pluto/users/firmware).
|
Most of my testing is with Pluto firmware [v0.39](https://github.com/analogdevicesinc/plutosdr-fw/releases/tag/v0.39) though this may work with other firmware versions. I had issues with the Pluto sometimes seeing no signal which resolved when I upgraded from v0.35. Instructions for upgrading firmware are on the [Analog Devices wiki](https://wiki.analog.com/university/tools/pluto/users/firmware).
|
||||||
|
|
||||||
We need two receive channels on the SDR. If you have a Pluto+ that should already be configured and you can skip this step.
|
We need two receive channels on the SDR. If you have a Pluto+ that should already be configured and you can skip this step.
|
||||||
|
|
||||||
Analog devices has a [guide](https://wiki.analog.com/university/tools/pluto/users/customizing#updating_to_the_ad9364) for enabling the second channel. Ideally this should be set as `ad9361` to enable a wider band of operation in addition to the second channel, however the critical setting is enabling 2r2t.
|
Analog devices has a [guide](https://wiki.analog.com/university/tools/pluto/users/customizing#updating_to_the_ad9364) for enabling the second channel. Ideally this should be set as `ad9361` to enable a wider band of operation in addition to the second channel, however the critical setting is enabling 2r2t. SSH into the Pluto and run the following:
|
||||||
|
```bash
|
||||||
|
fw_setenv attr_name compatible
|
||||||
|
fw_setenv attr_val ad9361
|
||||||
|
fw_setenv mode 2r2t
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
@ -46,3 +55,10 @@ Absolute output power is generally not well calibrated for VNAs anyway and has n
|
||||||
If you have an RF power meter you can generate your own power calibration.
|
If you have an RF power meter you can generate your own power calibration.
|
||||||
|
|
||||||
Note that unlike the main calibration, power calibration frequencies do not need to match the measurement frequencies. Values are interpolated.
|
Note that unlike the main calibration, power calibration frequencies do not need to match the measurement frequencies. Values are interpolated.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
#### Pluto Default Connection Settings
|
||||||
|
user: `root`
|
||||||
|
password: `analog`
|
||||||
|
ip: `192.168.2.1`
|
16
charon_vna/config_default.json
Normal file
16
charon_vna/config_default.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"frequency": [
|
||||||
|
1000000000.0,
|
||||||
|
1100000000.0,
|
||||||
|
1200000000.0,
|
||||||
|
1300000000.0,
|
||||||
|
1400000000.0,
|
||||||
|
1500000000.0,
|
||||||
|
1600000000.0,
|
||||||
|
1700000000.0,
|
||||||
|
1800000000.0,
|
||||||
|
1900000000.0,
|
||||||
|
2000000000.0
|
||||||
|
],
|
||||||
|
"power": -5
|
||||||
|
}
|
25
charon_vna/config_default.py
Normal file
25
charon_vna/config_default.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from charon_vna.gui import DEFAULT_CONFIG
|
||||||
|
|
||||||
|
config = dict(
|
||||||
|
frequency=np.linspace(1e9, 2e9, 11).tolist(),
|
||||||
|
power=-5,
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(DEFAULT_CONFIG, "w") as f:
|
||||||
|
json.dump(config, f)
|
||||||
|
|
||||||
|
# autoformat
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"python",
|
||||||
|
"-m",
|
||||||
|
"json.tool",
|
||||||
|
DEFAULT_CONFIG.resolve().as_posix(),
|
||||||
|
DEFAULT_CONFIG.resolve().as_posix(),
|
||||||
|
]
|
||||||
|
)
|
|
@ -1,167 +1,55 @@
|
||||||
# %% imports
|
# %% imports
|
||||||
|
import json
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, List, Literal, Tuple
|
from typing import List
|
||||||
|
|
||||||
import matplotlib as mpl
|
import matplotlib as mpl
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import xarray as xr
|
import xarray as xr
|
||||||
from matplotlib import pyplot as plt
|
|
||||||
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
|
|
||||||
from matplotlib.lines import Line2D
|
|
||||||
from matplotlib.ticker import EngFormatter
|
|
||||||
from numpy import typing as npt
|
from numpy import typing as npt
|
||||||
from PySide6.QtGui import QAction, QKeySequence
|
from PySide6.QtGui import QAction, QKeySequence
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
QInputDialog,
|
QInputDialog,
|
||||||
|
QLineEdit,
|
||||||
QMainWindow,
|
QMainWindow,
|
||||||
QMenu,
|
QMenu,
|
||||||
QProgressBar,
|
QProgressBar,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
from skrf import plotting as rf_plt
|
|
||||||
from vna import Charon
|
from vna import Charon
|
||||||
|
|
||||||
from charon_vna.util import db20, s2vswr
|
from charon_vna.plots import PlotWidget
|
||||||
|
|
||||||
# %%
|
# %%
|
||||||
DEFAULT_CONFIG = dict(
|
DEFAULT_CONFIG = Path(__file__).parent / "config_default.json"
|
||||||
frequency=np.arange(1e9, 2e9, 11), # Hz
|
|
||||||
power=-5, # dB
|
|
||||||
)
|
|
||||||
|
|
||||||
CONFIG_SUFFIX = ".json"
|
CONFIG_SUFFIX = ".json"
|
||||||
|
|
||||||
|
|
||||||
class PlotWidget(QWidget):
|
|
||||||
traces: List[Tuple[int | str]]
|
|
||||||
lines: List[Line2D]
|
|
||||||
|
|
||||||
def __init__(self, type_: str = "logmag"):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.traces = [(1, 1)]
|
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
self.fig = plt.Figure(figsize=(5, 4), dpi=100, tight_layout=True)
|
|
||||||
self.ax = self.fig.add_subplot(111)
|
|
||||||
self.set_plot_type(type_)
|
|
||||||
self.lines = [
|
|
||||||
self.ax.plot([np.nan], [np.nan], label="$S_{" + str(m) + str(n) + "}$")[0] for m, n in self.traces
|
|
||||||
]
|
|
||||||
self.ax.legend(loc="upper right")
|
|
||||||
|
|
||||||
canvas = FigureCanvasQTAgg(self.fig)
|
|
||||||
layout.addWidget(canvas)
|
|
||||||
|
|
||||||
# toolbar = QToolBar("Toolbar")
|
|
||||||
# toolbar.addAction("blah")
|
|
||||||
# self.addToolBar(toolbar)
|
|
||||||
|
|
||||||
def set_plot_type(
|
|
||||||
self,
|
|
||||||
type_: Literal["logmag", "phase", "vswr", "smith"],
|
|
||||||
sweep_type: Literal["frequency", "time"] = "frequency",
|
|
||||||
) -> None:
|
|
||||||
if sweep_type != "frequency":
|
|
||||||
raise NotImplementedError("Only frequency sweeps are currently supported")
|
|
||||||
|
|
||||||
if type_ == "logmag":
|
|
||||||
self.setup_logmag()
|
|
||||||
elif type_ == "phase":
|
|
||||||
self.setup_phase()
|
|
||||||
elif type_ == "vswr":
|
|
||||||
self.setup_vswr()
|
|
||||||
elif type_ == "smith":
|
|
||||||
self.setup_smith()
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown plot type: {type_}")
|
|
||||||
|
|
||||||
self._plot_type = type_
|
|
||||||
|
|
||||||
def update_plot(self, data: xr.DataArray):
|
|
||||||
if self._plot_type == "logmag":
|
|
||||||
self.update_logmag(data)
|
|
||||||
elif self._plot_type == "phase":
|
|
||||||
self.update_phase(data)
|
|
||||||
elif self._plot_type == "vswr":
|
|
||||||
self.update_vswr(data)
|
|
||||||
elif self._plot_type == "smith":
|
|
||||||
self.update_smith(data)
|
|
||||||
|
|
||||||
def setup_rect(self) -> None:
|
|
||||||
self.ax.grid(True)
|
|
||||||
self.ax.xaxis.set_major_formatter(EngFormatter())
|
|
||||||
self.ax.set_xlabel("Frequency [Hz]")
|
|
||||||
|
|
||||||
def update_rect(self, data: xr.DataArray, func: Callable[[npt.ArrayLike], npt.ArrayLike]) -> None:
|
|
||||||
self.ax.set_xlim(data["frequency"].min().data, data["frequency"].max().data)
|
|
||||||
for ii, (m, n) in enumerate(self.traces):
|
|
||||||
self.lines[ii].set_xdata(data["frequency"])
|
|
||||||
self.lines[ii].set_ydata(func(data.sel(m=m, n=n)))
|
|
||||||
|
|
||||||
self.fig.canvas.draw()
|
|
||||||
|
|
||||||
def setup_logmag(self, ylim: List[float] = [-30, 30]) -> None:
|
|
||||||
self.setup_rect()
|
|
||||||
self.ax.set_ylim(ylim)
|
|
||||||
self.ax.set_ylabel("Amplitude [dB]")
|
|
||||||
|
|
||||||
def update_logmag(self, data: xr.DataArray) -> None:
|
|
||||||
self.update_rect(data, db20)
|
|
||||||
|
|
||||||
def setup_phase(self) -> None:
|
|
||||||
self.setup_rect()
|
|
||||||
self.ax.set_ylim(-200, 200)
|
|
||||||
self.ax.set_ylabel("Phase [deg]")
|
|
||||||
|
|
||||||
def update_phase(self, data: xr.DataArray):
|
|
||||||
self.update_rect(data, lambda s: np.angle(s, deg=True))
|
|
||||||
|
|
||||||
def setup_vswr(self) -> None:
|
|
||||||
self.setup_rect()
|
|
||||||
self.ax.set_yticks(np.arange(1, 11))
|
|
||||||
self.ax.set_ylim(1, 10)
|
|
||||||
self.ax.set_ylabel("VSWR")
|
|
||||||
|
|
||||||
def update_vswr(self, data: xr.DataArray) -> None:
|
|
||||||
self.update_rect(data, s2vswr)
|
|
||||||
|
|
||||||
def setup_smith(self) -> None:
|
|
||||||
self.ax.grid(False)
|
|
||||||
self.ax.set_xlim(-1, 1)
|
|
||||||
self.ax.set_ylim(-1, 1)
|
|
||||||
self.ax.set_aspect("equal")
|
|
||||||
rf_plt.smith(ax=self.ax, smithR=1, chart_type="z", draw_vswr=None)
|
|
||||||
|
|
||||||
def update_smith(self, data: xr.DataArray) -> None:
|
|
||||||
for ii, (m, n) in enumerate(self.traces):
|
|
||||||
sel = data.sel(m=m, n=n)
|
|
||||||
self.lines[ii].set_xdata(sel.real)
|
|
||||||
self.lines[ii].set_ydata(sel.imag)
|
|
||||||
|
|
||||||
self.fig.canvas.draw()
|
|
||||||
|
|
||||||
|
|
||||||
# Subclass QMainWindow to customize your application's main window
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
config_path: Path | None
|
config_path: Path | None
|
||||||
# device: Charon
|
# device: Charon
|
||||||
|
|
||||||
plots: List[PlotWidget]
|
plots: List[PlotWidget]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, ip: str | None = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.config_path = None
|
self.config_path = DEFAULT_CONFIG
|
||||||
self._frequency = np.linspace(1e9, 2e9, 101) # TODO: read frequency from config
|
with open(self.config_path, "r") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
self._frequency = config["frequency"]
|
||||||
|
|
||||||
self.vna = Charon("ip:192.168.3.1", frequency=DEFAULT_CONFIG["frequency"])
|
vna_kwargs = dict(
|
||||||
|
frequency=self._frequency,
|
||||||
|
)
|
||||||
|
if ip is not None:
|
||||||
|
vna_kwargs["ip"] = ip
|
||||||
|
self.vna = Charon(**vna_kwargs)
|
||||||
|
|
||||||
mpl.use("QtAgg")
|
mpl.use("QtAgg")
|
||||||
|
|
||||||
|
@ -236,11 +124,12 @@ class MainWindow(QMainWindow):
|
||||||
dialog.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)
|
dialog.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)
|
||||||
if dialog.exec():
|
if dialog.exec():
|
||||||
config_path = Path(dialog.selectedFiles()[0])
|
config_path = Path(dialog.selectedFiles()[0])
|
||||||
print(config_path)
|
|
||||||
if config_path.suffix != CONFIG_SUFFIX:
|
if config_path.suffix != CONFIG_SUFFIX:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"{config_path.name} is not a valid configuration file. Must have extension {CONFIG_SUFFIX}"
|
f"{config_path.name} is not a valid configuration file. Must have extension {CONFIG_SUFFIX}"
|
||||||
)
|
)
|
||||||
|
if config_path == DEFAULT_CONFIG:
|
||||||
|
raise ValueError(f"Cannot overwrite default configuration file at {DEFAULT_CONFIG}")
|
||||||
self.config_path = config_path
|
self.config_path = config_path
|
||||||
print(f"Config path is now {self.config_path.resolve()}")
|
print(f"Config path is now {self.config_path.resolve()}")
|
||||||
|
|
||||||
|
@ -264,7 +153,7 @@ class MainWindow(QMainWindow):
|
||||||
self.load_config(self.config_path)
|
self.load_config(self.config_path)
|
||||||
|
|
||||||
def save_config(self) -> None:
|
def save_config(self) -> None:
|
||||||
if self.config_path is None:
|
if self.config_path == DEFAULT_CONFIG:
|
||||||
self.saveas_config()
|
self.saveas_config()
|
||||||
else:
|
else:
|
||||||
print(f"Saving config to {self.config_path.resolve()}")
|
print(f"Saving config to {self.config_path.resolve()}")
|
||||||
|
@ -312,7 +201,25 @@ class MainWindow(QMainWindow):
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
window = MainWindow()
|
try:
|
||||||
|
window = MainWindow()
|
||||||
|
except Exception as e:
|
||||||
|
if e.args[0] == "No device found":
|
||||||
|
dialog = QInputDialog()
|
||||||
|
text, ok = dialog.getText(
|
||||||
|
None,
|
||||||
|
"Pluto IP Address",
|
||||||
|
"Enter Pluto IP Address",
|
||||||
|
QLineEdit.Normal,
|
||||||
|
"192.168.2.1",
|
||||||
|
)
|
||||||
|
match = re.match(r"(\d{1,3}\.){3}\d{1,3}", text)
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f"Invalid IP address: {text}")
|
||||||
|
window = MainWindow(ip=text)
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
window.show()
|
window.show()
|
||||||
|
|
||||||
app.exec()
|
app.exec()
|
||||||
|
|
129
charon_vna/plots.py
Normal file
129
charon_vna/plots.py
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
# %% imports
|
||||||
|
from typing import Callable, List, Literal, Tuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import xarray as xr
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
|
||||||
|
from matplotlib.lines import Line2D
|
||||||
|
from matplotlib.ticker import EngFormatter
|
||||||
|
from numpy import typing as npt
|
||||||
|
from PySide6.QtWidgets import QVBoxLayout, QWidget
|
||||||
|
from skrf import plotting as rf_plt
|
||||||
|
|
||||||
|
from charon_vna.util import db20, s2vswr
|
||||||
|
|
||||||
|
__all__ = ("PlotWidget",)
|
||||||
|
|
||||||
|
|
||||||
|
# %%
|
||||||
|
class PlotWidget(QWidget):
|
||||||
|
traces: List[Tuple[int | str]]
|
||||||
|
lines: List[Line2D]
|
||||||
|
|
||||||
|
def __init__(self, type_: str = "logmag"):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.traces = [(1, 1)]
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
self.fig = plt.Figure(figsize=(5, 4), dpi=100, tight_layout=True)
|
||||||
|
self.ax = self.fig.add_subplot(111)
|
||||||
|
self.set_plot_type(type_)
|
||||||
|
self.lines = [
|
||||||
|
self.ax.plot([np.nan], [np.nan], label="$S_{" + str(m) + str(n) + "}$")[0] for m, n in self.traces
|
||||||
|
]
|
||||||
|
self.ax.legend(loc="upper right")
|
||||||
|
|
||||||
|
canvas = FigureCanvasQTAgg(self.fig)
|
||||||
|
layout.addWidget(canvas)
|
||||||
|
|
||||||
|
# toolbar = QToolBar("Toolbar")
|
||||||
|
# toolbar.addAction("blah")
|
||||||
|
# self.addToolBar(toolbar)
|
||||||
|
|
||||||
|
def set_plot_type(
|
||||||
|
self,
|
||||||
|
type_: Literal["logmag", "phase", "vswr", "smith"],
|
||||||
|
sweep_type: Literal["frequency", "time"] = "frequency",
|
||||||
|
) -> None:
|
||||||
|
if sweep_type != "frequency":
|
||||||
|
raise NotImplementedError("Only frequency sweeps are currently supported")
|
||||||
|
|
||||||
|
if type_ == "logmag":
|
||||||
|
self.setup_logmag()
|
||||||
|
elif type_ == "phase":
|
||||||
|
self.setup_phase()
|
||||||
|
elif type_ == "vswr":
|
||||||
|
self.setup_vswr()
|
||||||
|
elif type_ == "smith":
|
||||||
|
self.setup_smith()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown plot type: {type_}")
|
||||||
|
|
||||||
|
self._plot_type = type_
|
||||||
|
|
||||||
|
def update_plot(self, data: xr.DataArray):
|
||||||
|
if self._plot_type == "logmag":
|
||||||
|
self.update_logmag(data)
|
||||||
|
elif self._plot_type == "phase":
|
||||||
|
self.update_phase(data)
|
||||||
|
elif self._plot_type == "vswr":
|
||||||
|
self.update_vswr(data)
|
||||||
|
elif self._plot_type == "smith":
|
||||||
|
self.update_smith(data)
|
||||||
|
|
||||||
|
def setup_rect(self) -> None:
|
||||||
|
self.ax.grid(True)
|
||||||
|
self.ax.xaxis.set_major_formatter(EngFormatter())
|
||||||
|
self.ax.set_xlabel("Frequency [Hz]")
|
||||||
|
|
||||||
|
def update_rect(self, data: xr.DataArray, func: Callable[[npt.ArrayLike], npt.ArrayLike]) -> None:
|
||||||
|
self.ax.set_xlim(data["frequency"].min().data, data["frequency"].max().data)
|
||||||
|
for ii, (m, n) in enumerate(self.traces):
|
||||||
|
self.lines[ii].set_xdata(data["frequency"])
|
||||||
|
self.lines[ii].set_ydata(func(data.sel(m=m, n=n)))
|
||||||
|
|
||||||
|
self.fig.canvas.draw()
|
||||||
|
|
||||||
|
def setup_logmag(self, ylim: List[float] = [-30, 30]) -> None:
|
||||||
|
self.setup_rect()
|
||||||
|
self.ax.set_ylim(ylim)
|
||||||
|
self.ax.set_ylabel("Amplitude [dB]")
|
||||||
|
|
||||||
|
def update_logmag(self, data: xr.DataArray) -> None:
|
||||||
|
self.update_rect(data, db20)
|
||||||
|
|
||||||
|
def setup_phase(self) -> None:
|
||||||
|
self.setup_rect()
|
||||||
|
self.ax.set_ylim(-200, 200)
|
||||||
|
self.ax.set_ylabel("Phase [deg]")
|
||||||
|
|
||||||
|
def update_phase(self, data: xr.DataArray):
|
||||||
|
self.update_rect(data, lambda s: np.angle(s, deg=True))
|
||||||
|
|
||||||
|
def setup_vswr(self) -> None:
|
||||||
|
self.setup_rect()
|
||||||
|
self.ax.set_yticks(np.arange(1, 11))
|
||||||
|
self.ax.set_ylim(1, 10)
|
||||||
|
self.ax.set_ylabel("VSWR")
|
||||||
|
|
||||||
|
def update_vswr(self, data: xr.DataArray) -> None:
|
||||||
|
self.update_rect(data, s2vswr)
|
||||||
|
|
||||||
|
def setup_smith(self) -> None:
|
||||||
|
self.ax.grid(False)
|
||||||
|
self.ax.set_xlim(-1, 1)
|
||||||
|
self.ax.set_ylim(-1, 1)
|
||||||
|
self.ax.set_aspect("equal")
|
||||||
|
rf_plt.smith(ax=self.ax, smithR=1, chart_type="z", draw_vswr=None)
|
||||||
|
|
||||||
|
def update_smith(self, data: xr.DataArray) -> None:
|
||||||
|
for ii, (m, n) in enumerate(self.traces):
|
||||||
|
sel = data.sel(m=m, n=n)
|
||||||
|
self.lines[ii].set_xdata(sel.real)
|
||||||
|
self.lines[ii].set_ydata(sel.imag)
|
||||||
|
|
||||||
|
self.fig.canvas.draw()
|
|
@ -36,9 +36,11 @@ def generate_tone(f: float, fs: float, N: int = 1024, scale: int = 2**14):
|
||||||
class Charon:
|
class Charon:
|
||||||
FREQUENCY_OFFSET = 1e6
|
FREQUENCY_OFFSET = 1e6
|
||||||
|
|
||||||
|
calibration: rf.calibration.Calibration | None = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
uri: str = "192.168.2.1",
|
ip: str = "192.168.2.1",
|
||||||
frequency: npt.ArrayLike = np.linspace(1e9, 2e9, 3),
|
frequency: npt.ArrayLike = np.linspace(1e9, 2e9, 3),
|
||||||
ports: Tuple[int] = (1,),
|
ports: Tuple[int] = (1,),
|
||||||
):
|
):
|
||||||
|
@ -46,7 +48,7 @@ class Charon:
|
||||||
self.frequency = frequency
|
self.frequency = frequency
|
||||||
|
|
||||||
# everything RF
|
# everything RF
|
||||||
self.sdr = adi.ad9361(uri=uri)
|
self.sdr = adi.ad9361(uri=f"ip:{ip}")
|
||||||
for attr, expected in [
|
for attr, expected in [
|
||||||
("adi,2rx-2tx-mode-enable", True),
|
("adi,2rx-2tx-mode-enable", True),
|
||||||
# ("adi,gpo-manual-mode-enable", True),
|
# ("adi,gpo-manual-mode-enable", True),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user