# NanoVNASaver # # A python program to view and export Touchstone data from a NanoVNA # Copyright (C) 2019, 2020 Rune B. Broberg # Copyright (C) 2020 NanoVNA-Saver Authors # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import logging import platform import time from struct import pack, unpack_from from time import sleep from typing import Dict, List from .Serial import Interface from .Calibration import Calibration from .RFTools import Datapoint __all__ = ['NanoVNA_V2_H4'] if platform.system() != "Windows": import tty logger = logging.getLogger(__name__) _CMD_NOP = 0x00 _CMD_INDICATE = 0x0D _CMD_READ = 0x10 _CMD_READ2 = 0x11 _CMD_READ4 = 0x12 _CMD_READFIFO = 0x18 _CMD_WRITE = 0x20 _CMD_WRITE2 = 0x21 _CMD_WRITE4 = 0x22 _CMD_WRITE8 = 0x23 _CMD_WRITEFIFO = 0x28 WRITE_SLEEP = 0.05 _ADF4350_TXPOWER_DESC_MAP = { 0: "9dattenuation", 1: "6dB attenuation", 2: "3dB attenuation", 3: "Maximum", } _ADF4350_TXPOWER_DESC_REV_MAP = { value: key for key, value in _ADF4350_TXPOWER_DESC_MAP.items() } # _NANOVNA_V2_CMDS = { # 'NOP' : {'val': 0x, 'len':, 'resp': }, # 'INDICATE' : {'val': 0x, 'len':, 'resp': }, # 'READ' : {'val': 0x, 'len':, 'resp': }, # 'READ2' : {'val': 0x, 'len':, 'resp': }, # 'READ4' : {'val': 0x, 'len':, 'resp': }, # 'READFIFO' : {'val': 0x, 'len':, 'resp': }, # 'NOP' : {'val': 0x, 'len':, 'resp': }, # 'NOP' : {'val': 0x, 'len':, 'resp': }, # } _NANOVNA_V2_REGS = { "sweepStartHz": {"addr": 0x00, "len": 8}, "sweepStepHz": {"addr": 0x10, "len": 8}, "sweepPoints": {"addr": 0x20, "len": 2}, "valuesPerFrequency": {"addr": 0x22, "len": 2}, "rawSamplesMode": {"addr": 0x26, "len": 1}, "valuesFIFO": {"addr": 0x30, "len": 4}, "deviceVariant": {"addr": 0xF0, "len": 1}, "protocolVersion": {"addr": 0xF1, "len": 1}, "hardwareRevision": {"addr": 0xF2, "len": 1}, "firmwareMajor": {"addr": 0xF3, "len": 1}, "firmwareMinor": {"addr": 0xF4, "len": 1}, } """ NanoVNA FIFO Format (32 bytes) fwd0Re (uint32) fw0Im (uint32) rev0Re (uint32) rev0Re (uint32) rev1Re (uint32) rev1Im (uint32) freqIndex (uint16) reserved (6 bytes) """ _NANOVAN_V2_FIFO_FORMAT = " bool: self.iface.open() return self.iface.isOpen() def disconnect(self) -> bool: self.iface.close() return not self.iface.isOpen() def reconnect(self) -> bool: self.iface.close() sleep(self.wait) self.iface.open() return self.iface.isOpen() def get_info(self): reg = self.read_registers() info = Dict() def _read_register(self, reg_name: str): if reg_name not in _NANOVNA_V2_REGS: raise ValueError( f"must be name of nanoVNA v2 register: {_NANOVNA_V2_REGS.keys()}" ) size = _NANOVNA_V2_REGS[reg_name]["len"] addr = _NANOVNA_V2_REGS[reg_name]["addr"] if size == 1: packet = pack(" 0: # calculate the number of points to read (255) at a time points_to_read = min(255, points_remaining) bytes_to_read = points_to_read * 32 # TODO: Fix self.iface.write(pack(" List: reg = self.read_registers() self.freq_start = reg["sweepStartHz"] self.freq_step = reg["sweepStepHz"] self.points = reg["sweepPoints"] self.freq_end = self.freq_start + self.freq_step * (self.points - 1) self.avg = reg["valuesPerFrequency"] return (self.freq_start, self.freq_end, self.points, self.avg) def wizard_cal(self, port1_only:bool=False): # wipe old cal because I'm scared self._cal = Calibration() # short input("connect Short to port 1...") fs, s11_point, _ = self.measure(skip_calibration=True) s11_dp = [] for (f, s11_point) in zip(fs, s11_point): s11_dp.append(Datapoint(f, s11_point.real, s11_point.imag)) self._cal.insert("short", s11_dp) # open input("connect Open to port 1...") fs, s11, _ = self.measure(skip_calibration=True) s11_dp = [] for (f, s11_point) in zip(fs, s11): s11_dp.append(Datapoint(f, s11_point.real, s11_point.imag)) self._cal.insert("open", s11_dp) # load input("connect Load to port 1...") fs, s11, _ = self.measure(skip_calibration=True) s11_dp = [] for (f, s11_point) in zip(fs, s11): s11_dp.append(Datapoint(f, s11_point.real, s11_point.imag)) self._cal.insert("load", s11_dp) # through input("connect port 1 to port 2...") fs, _, s21 = self.measure(skip_calibration=True) s21_dp = [] for (f, s21_point) in zip(fs, s21): s21_dp.append(Datapoint(f, s21_point.real, s21_point.imag)) self._cal.insert("through", s21_dp) # isolation input("connect load to both ports...") fs, _, s21 = self.measure(skip_calibration=True) s21_dp = [] for (f, s21_point) in zip(fs, s21): s21_dp.append(Datapoint(f, s21_point.real, s21_point.imag)) self._cal.insert("isolation", s21_dp) # calculate corrections self._cal.calc_corrections() if self._cal.isValid1Port(): self._cal_valid = True else: self._cal_valid = False # Store Notes notes = dict() notes['Date'] = time.ctime() start, stop, points, avg = self.get_sweep() notes['Sweep Start'] = start notes['Sweep Stop'] = stop notes['Sweep Points'] = points notes['Point Average'] = avg notes['Device Name'] = self.name notes['Device Settings'] = self.read_registers() self._cal.notes.append(notes) # date return self._cal_valid def save_cal(self, filename: str): if self._cal_valid: self._cal.save(filename) def load_cal(self, filename: str) -> bool: self._cal = Calibration() self._cal.load(filename) self._cal.calc_corrections() if self._cal.isValid1Port(): self._cal_valid = True else: self._cal_valid = False return self._cal_valid def measure(self, skip_calibration=False): # retrive data from fifo fifo_bytes = self._read_fifo(self.points) if fifo_bytes == None: return None # unpack data and convert to complex for n_point in range(self.points): # TODO: clean this up ( fwd_real, fwd_imag, rev0_real, rev0_imag, rev1_real, rev1_imag, freq_index, ) = unpack_from(_NANOVAN_V2_FIFO_FORMAT, fifo_bytes, n_point * 32) fwd = complex(fwd_real, fwd_imag) refl = complex(rev0_real, rev0_imag) thru = complex(rev1_real, rev1_imag) # store internally and calculate s11, s21 self._fwd[freq_index] = fwd self._refl[freq_index] = refl self._thru[freq_index] = thru self._f[freq_index] = self.freq_start + self.freq_step * freq_index self._s11[freq_index] = refl / fwd self._s21[freq_index] = thru / fwd # skip calibration if requested if skip_calibration==True: return self._f, self._s11, self._s21 # apply calibration # TODO: Implement this better (skrf or custom modifications to calibration import) cal_s11 = None cal_s21 = None if self._cal.isValid1Port(): cal_s11 = [] for f, s11_point in zip(self._f, self._s11): dp = Datapoint(f, s11_point.real, s11_point.imag) cal_s11.append(self._cal.correct11(dp).z) if self._cal.isValid2Port(): cal_s21 = [] for f, s21_point in zip(self._f, self._s21): dp = Datapoint(f, s21_point.real, s21_point.imag) cal_s21.append(self._cal.correct21(dp).z) if cal_s11 == None: return self._f, None, None if cal_s21 == None: return self._f, cal_s11, None return self._f, cal_s11, cal_s21 # def setTXPower(self, freq_range, power_desc): # if freq_range[0] != 140e6: # raise ValueError('Invalid TX power frequency range') # # 140MHz..max => ADF4350 # self._set_register(0x42, _ADF4350_TXPOWER_DESC_REV_MAP[power_desc], 1)