#!/usr/bin/env python3 # Must use Python 3 # Copyright (C) 2022 Analog Devices, Inc. # # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # - Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # - Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # - Neither the name of Analog Devices, Inc. nor the names of its # contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # - The use of this software may or may not infringe the patent rights # of one or more patent holders. This license does not release you # from the requirement that you obtain separate licenses from these # patent holders to use this software. # - Use of the software either in source or binary form, must be run # on or directly connected to an Analog Devices Inc. component. # # THIS SOFTWARE IS PROVIDED BY ANALOG DEVICES "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, # INCLUDING, BUT NOT LIMITED TO, NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A # PARTICULAR PURPOSE ARE DISCLAIMED. # # IN NO EVENT SHALL ANALOG DEVICES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, INTELLECTUAL PROPERTY # RIGHTS, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. '''CW Radar Demo with Phaser (CN0566) Jon Kraft, Jan 19 2024''' # Imports import adi import sys import time import matplotlib.pyplot as plt import numpy as np import pyqtgraph as pg from PyQt5.QtCore import Qt from PyQt5.QtWidgets import * from pyqtgraph.Qt import QtCore, QtGui # Instantiate all the Devices rpi_ip = "ip:phaser.local" # IP address of the Raspberry Pi sdr_ip = "ip:192.168.2.1" # "192.168.2.1, or pluto.local" # IP address of the Transceiver Block my_sdr = adi.ad9361(uri=sdr_ip) my_phaser = adi.CN0566(uri=rpi_ip, sdr=my_sdr) # Initialize both ADAR1000s, set gains to max, and all phases to 0 my_phaser.configure(device_mode="rx") my_phaser.load_gain_cal() my_phaser.load_phase_cal() for i in range(0, 8): my_phaser.set_chan_phase(i, 0) gain_list = [8, 34, 84, 127, 127, 84, 34, 8] # Blackman taper for i in range(0, len(gain_list)): my_phaser.set_chan_gain(i, gain_list[i], apply_cal=True) # Setup Raspberry Pi GPIO states try: my_phaser._gpios.gpio_tx_sw = 0 # 0 = TX_OUT_2, 1 = TX_OUT_1 my_phaser._gpios.gpio_vctrl_1 = 1 # 1=Use onboard PLL/LO source (0=disable PLL and VCO, and set switch to use external LO input) my_phaser._gpios.gpio_vctrl_2 = 1 # 1=Send LO to transmit circuitry (0=disable Tx path, and send LO to LO_OUT) except: my_phaser.gpios.gpio_tx_sw = 0 # 0 = TX_OUT_2, 1 = TX_OUT_1 my_phaser.gpios.gpio_vctrl_1 = 1 # 1=Use onboard PLL/LO source (0=disable PLL and VCO, and set switch to use external LO input) my_phaser.gpios.gpio_vctrl_2 = 1 # 1=Send LO to transmit circuitry (0=disable Tx path, and send LO to LO_OUT) sample_rate = 0.6e6 center_freq = 2.2e9 signal_freq = 100e3 num_slices = 50 fft_size = 1024 * 64 img_array = np.ones((num_slices, fft_size))*(-100) # Configure SDR Rx my_sdr.sample_rate = int(sample_rate) my_sdr.rx_lo = int(center_freq) # set this to output_freq - (the freq of the HB100) my_sdr.rx_enabled_channels = [0, 1] # enable Rx1 (voltage0) and Rx2 (voltage1) my_sdr.rx_buffer_size = int(fft_size) my_sdr.gain_control_mode_chan0 = "manual" # manual or slow_attack my_sdr.gain_control_mode_chan1 = "manual" # manual or slow_attack my_sdr.rx_hardwaregain_chan0 = int(30) # must be between -3 and 70 my_sdr.rx_hardwaregain_chan1 = int(30) # must be between -3 and 70 # Configure SDR Tx my_sdr.tx_lo = int(center_freq) my_sdr.tx_enabled_channels = [0, 1] my_sdr.tx_cyclic_buffer = True # must set cyclic buffer to true for the tdd burst mode. Otherwise Tx will turn on and off randomly my_sdr.tx_hardwaregain_chan0 = -88 # must be between 0 and -88 my_sdr.tx_hardwaregain_chan1 = -0 # must be between 0 and -88 # Configure the ADF4159 Rampling PLL output_freq = 12.2e9 my_phaser.frequency = int(output_freq / 4) # Output frequency divided by 4 my_phaser.ramp_mode = "disabled" # ramp_mode can be: "disabled", "continuous_sawtooth", "continuous_triangular", "single_sawtooth_burst", "single_ramp_burst" my_phaser.enable = 0 # 0 = PLL enable. Write this last to update all the registers # Create a sinewave waveform fs = int(my_sdr.sample_rate) N = int(my_sdr.rx_buffer_size) fc = int(signal_freq / (fs / N)) * (fs / N) ts = 1 / float(fs) t = np.arange(0, N * ts, ts) i = np.cos(2 * np.pi * t * fc) * 2 ** 14 q = np.sin(2 * np.pi * t * fc) * 2 ** 14 iq = 1 * (i + 1j * q) # Send data my_sdr._ctx.set_timeout(0) my_sdr.tx([iq * 0.5, iq]) # only send data to the 2nd channel (that's all we need) c = 3e8 N_frame = fft_size freq = np.linspace(-fs / 2, fs / 2, int(N_frame)) class Window(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Interactive FFT") self.setGeometry(100, 100, 800, 800) # (x,y, width, height) self.setFixedWidth(1600) self.num_rows = 12 self.setWindowFlag(QtCore.Qt.WindowCloseButtonHint, False) #remove the window's close button self.UiComponents() self.show() # method for components def UiComponents(self): widget = QWidget() global layout layout = QGridLayout() # Control Panel control_label = QLabel("PHASER Simple CW Radar") font = control_label.font() font.setPointSize(24) control_label.setFont(font) font.setPointSize(12) control_label.setAlignment(Qt.AlignHCenter) # | Qt.AlignVCenter) layout.addWidget(control_label, 0, 0, 1, 2) # Buttons self.quit_button = QPushButton("Quit") self.quit_button.pressed.connect(self.end_program) layout.addWidget(self.quit_button, 30, 0, 4, 4) # waterfall level slider self.low_slider = QSlider(Qt.Horizontal) self.low_slider.setMinimum(-100) self.low_slider.setMaximum(0) self.low_slider.setValue(-66) self.low_slider.setTickInterval(20) self.low_slider.setMaximumWidth(200) self.low_slider.setTickPosition(QSlider.TicksBelow) self.low_slider.valueChanged.connect(self.get_water_levels) layout.addWidget(self.low_slider, 8, 0) self.high_slider = QSlider(Qt.Horizontal) self.high_slider.setMinimum(-100) self.high_slider.setMaximum(0) self.high_slider.setValue(-42) self.high_slider.setTickInterval(20) self.high_slider.setMaximumWidth(200) self.high_slider.setTickPosition(QSlider.TicksBelow) self.high_slider.valueChanged.connect(self.get_water_levels) layout.addWidget(self.high_slider, 10, 0) self.water_label = QLabel("Waterfall Intensity Levels") self.water_label.setFont(font) self.water_label.setAlignment(Qt.AlignCenter) self.water_label.setMinimumWidth(300) layout.addWidget(self.water_label, 7, 0) self.low_label = QLabel("LOW LEVEL: %0.0f" % (self.low_slider.value())) self.low_label.setFont(font) self.low_label.setAlignment(Qt.AlignLeft) self.low_label.setMinimumWidth(100) layout.addWidget(self.low_label, 8, 1) self.high_label = QLabel("HIGH LEVEL: %0.0f" % (self.high_slider.value())) self.high_label.setFont(font) self.high_label.setAlignment(Qt.AlignLeft) self.high_label.setMinimumWidth(100) layout.addWidget(self.high_label, 10, 1) # FFT plot self.fft_plot = pg.plot() self.fft_plot.setMinimumWidth(600) self.fft_curve = self.fft_plot.plot(freq, pen={'color':'y', 'width':2}) title_style = {"size": "20pt"} label_style = {"color": "#FFF", "font-size": "14pt"} self.fft_plot.setLabel("bottom", text="Frequency", units="Hz", **label_style) self.fft_plot.setLabel("left", text="Magnitude", units="dB", **label_style) self.fft_plot.setTitle("Received Signal - Frequency Spectrum", **title_style) layout.addWidget(self.fft_plot, 0, 2, self.num_rows, 1) self.fft_plot.setYRange(-60, 0) self.fft_plot.setXRange(99e3, 101e3) # Waterfall plot self.waterfall = pg.PlotWidget() self.imageitem = pg.ImageItem() self.waterfall.addItem(self.imageitem) # Use a viridis colormap pos = np.array([0.0, 0.25, 0.5, 0.75, 1.0]) color = np.array([[68, 1, 84,255], [59, 82, 139,255], [33, 145, 140,255], [94, 201, 98,255], [253, 231, 37,255]], dtype=np.ubyte) lut = pg.ColorMap(pos, color).getLookupTable(0.0, 1.0, 256) self.imageitem.setLookupTable(lut) self.imageitem.setLevels([0,1]) # self.imageitem.scale(0.35, sample_rate / (N)) # this is deprecated -- we have to use setTransform instead tr = QtGui.QTransform() tr.translate(0,-sample_rate/2) tr.scale(0.35, sample_rate / (N)) self.imageitem.setTransform(tr) zoom_freq = 0.3e3 self.waterfall.setRange(yRange=(signal_freq - zoom_freq, signal_freq + zoom_freq)) self.waterfall.setTitle("Waterfall Spectrum", **title_style) self.waterfall.setLabel("left", "Frequency", units="Hz", **label_style) self.waterfall.setLabel("bottom", "Time", units="sec", **label_style) layout.addWidget(self.waterfall, 0 + self.num_rows + 1, 2, self.num_rows, 1) self.img_array = np.ones((num_slices, fft_size))*(-100) widget.setLayout(layout) # setting this widget as central widget of the main window self.setCentralWidget(widget) def get_water_levels(self): """ Updates the waterfall intensity levels Returns: None """ if self.low_slider.value() > self.high_slider.value(): self.low_slider.setValue(self.high_slider.value()) self.low_label.setText("LOW LEVEL: %0.0f" % (self.low_slider.value())) self.high_label.setText("HIGH LEVEL: %0.0f" % (self.high_slider.value())) def end_program(self): """ Gracefully shutsdown the program and Pluto Returns: None """ my_sdr.tx_destroy_buffer() self.close() # create pyqt5 app App = QApplication(sys.argv) # create the instance of our Window win = Window() win.setWindowState(QtCore.Qt.WindowMaximized) index = 0 def update(): """ Updates the FFT in the window Returns: None """ global index, freq, dist label_style = {"color": "#FFF", "font-size": "14pt"} data = my_sdr.rx() data = data[0] + data[1] win_funct = np.blackman(len(data)) y = data * win_funct sp = np.absolute(np.fft.fft(y)) sp = np.fft.fftshift(sp) s_mag = np.abs(sp) / np.sum(win_funct) s_mag = np.maximum(s_mag, 10 ** (-15)) s_dbfs = 20 * np.log10(s_mag / (2 ** 11)) win.fft_curve.setData(freq, s_dbfs) win.fft_plot.setLabel("bottom", text="Frequency", units="Hz", **label_style) win.img_array = np.roll(win.img_array, 1, axis=0) win.img_array[0] = s_dbfs win.imageitem.setLevels([win.low_slider.value(), win.high_slider.value()]) win.imageitem.setImage(win.img_array, autoLevels=False) if index == 1: win.fft_plot.enableAutoRange("xy", False) index = index + 1 timer = QtCore.QTimer() timer.timeout.connect(update) timer.start(0) # start the app sys.exit(App.exec())