1417 lines
53 KiB
Python
Executable File
1417 lines
53 KiB
Python
Executable File
#! /usr/bin/python
|
|
|
|
# All QUISK software is Copyright (C) 2006-2018 by James C. Ahlstrom.
|
|
# This free software is licensed for use under the GNU General Public
|
|
# License (GPL), see http://www.opensource.org.
|
|
# Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!!
|
|
|
|
"""The main program for Quisk VNA, a vector network analyzer.
|
|
|
|
Usage: python quisk_vns.py [-c | --config config_file_path]
|
|
This can also be installed as a package and run as quisk_vna.main().
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
|
|
import sys, os
|
|
os.chdir(os.path.normpath(os.path.dirname(__file__))) # change directory to the location of this script
|
|
if sys.path[0] != '': # Make sure the current working directory is on path
|
|
sys.path.insert(0, '')
|
|
|
|
import wx, wx.html, wx.lib.stattext, wx.lib.colourdb
|
|
import math, cmath, time, traceback, string, pickle
|
|
import threading, webbrowser
|
|
import _quisk as QS
|
|
from quisk_widgets import *
|
|
import configure
|
|
|
|
DEBUG = 0
|
|
|
|
# Command line parsing: be able to specify the config file.
|
|
from optparse import OptionParser
|
|
parser = OptionParser()
|
|
parser.add_option('-c', '--config', dest='config_file_path',
|
|
help='Specify the configuration file path')
|
|
parser.add_option('', '--config2', dest='config_file_path2', default='',
|
|
help='Specify a second configuration file to read after the first')
|
|
parser.add_option('-a', '--ask', action="store_true", dest='AskMe', default=False,
|
|
help='Ask which radio to use when starting')
|
|
argv_options = parser.parse_args()[0]
|
|
ConfigPath = argv_options.config_file_path # Get config file path
|
|
ConfigPath2 = argv_options.config_file_path2
|
|
if sys.platform == 'win32':
|
|
path = os.getenv('HOMEDRIVE', '') + os.getenv('HOMEPATH', '')
|
|
for dir in ("Documents", "My Documents", "Eigene Dateien", "Documenti", "Mine Dokumenter"):
|
|
config_dir = os.path.join(path, dir)
|
|
if os.path.isdir(config_dir):
|
|
break
|
|
else:
|
|
config_dir = os.path.join(path, "My Documents")
|
|
try:
|
|
try:
|
|
import winreg as Qwinreg
|
|
except ImportError:
|
|
import _winreg as Qwinreg
|
|
key = Qwinreg.OpenKey(Qwinreg.HKEY_CURRENT_USER,
|
|
r"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders")
|
|
val = Qwinreg.QueryValueEx(key, "Personal")
|
|
val = Qwinreg.ExpandEnvironmentStrings(val[0])
|
|
Qwinreg.CloseKey(key)
|
|
if os.path.isdir(val):
|
|
DefaultConfigDir = val
|
|
else:
|
|
DefaultConfigDir = config_dir
|
|
except:
|
|
traceback.print_exc()
|
|
DefaultConfigDir = config_dir
|
|
if not ConfigPath:
|
|
ConfigPath = os.path.join(DefaultConfigDir, "quisk_conf.py")
|
|
if not os.path.isfile(ConfigPath):
|
|
path = os.path.join(config_dir, "quisk_conf.py")
|
|
if os.path.isfile(path):
|
|
ConfigPath = path
|
|
del config_dir
|
|
else:
|
|
DefaultConfigDir = os.path.expanduser('~')
|
|
if not ConfigPath:
|
|
ConfigPath = os.path.join(DefaultConfigDir, ".quisk_conf.py")
|
|
|
|
|
|
if not ConfigPath: # Use default path
|
|
if sys.platform == 'win32':
|
|
path = os.getenv('HOMEDRIVE', '') + os.getenv('HOMEPATH', '')
|
|
for dir in ("Documents", "My Documents", "Eigene Dateien", "Documenti"):
|
|
ConfigPath = os.path.join(path, dir)
|
|
if os.path.isdir(ConfigPath):
|
|
break
|
|
else:
|
|
ConfigPath = os.path.join(path, "My Documents")
|
|
ConfigPath = os.path.join(ConfigPath, "quisk_conf.py")
|
|
if not os.path.isfile(ConfigPath): # See if the user has a config file
|
|
try:
|
|
import shutil # Try to create an initial default config file
|
|
shutil.copyfile('quisk_conf_win.py', ConfigPath)
|
|
except:
|
|
pass
|
|
else:
|
|
ConfigPath = os.path.expanduser('~/.quisk_conf.py')
|
|
|
|
class SoundThread(threading.Thread):
|
|
"""Create a second (non-GUI) thread to read samples."""
|
|
def __init__(self):
|
|
self.do_init = 1
|
|
threading.Thread.__init__(self)
|
|
self.doQuit = threading.Event()
|
|
self.doQuit.clear()
|
|
def run(self):
|
|
"""Read, process, play sound; then notify the GUI thread to check for FFT data."""
|
|
if self.do_init: # Open sound using this thread
|
|
self.do_init = 0
|
|
QS.start_sound()
|
|
wx.CallAfter(application.PostStartup)
|
|
while not self.doQuit.isSet():
|
|
QS.read_sound()
|
|
wx.CallAfter(application.OnReadSound)
|
|
QS.close_sound()
|
|
def stop(self):
|
|
"""Set a flag to indicate that the sound thread should end."""
|
|
self.doQuit.set()
|
|
|
|
class GraphDisplay(wx.Window):
|
|
"""Display the graph within the graph screen."""
|
|
def __init__(self, parent, x, y, graph_width, height, chary):
|
|
wx.Window.__init__(self, parent,
|
|
pos = (x, y),
|
|
size = (graph_width, height),
|
|
style = wx.NO_BORDER)
|
|
self.parent = parent
|
|
self.chary = chary
|
|
self.graph_width = graph_width
|
|
self.line_mag = []
|
|
self.line_phase = []
|
|
self.line_swr = []
|
|
self.display_text = ""
|
|
self.SetBackgroundColour(conf.color_graph)
|
|
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
|
self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown)
|
|
self.Bind(wx.EVT_LEFT_UP, parent.OnLeftUp)
|
|
self.Bind(wx.EVT_MOTION, parent.OnMotion)
|
|
self.Bind(wx.EVT_MOUSEWHEEL, parent.OnWheel)
|
|
self.tune_tx = graph_width // 2 # Current X position of the Tx tuning line
|
|
self.height = 10
|
|
self.y_min = 1000
|
|
self.y_max = 0
|
|
self.y_ticks = []
|
|
self.max_height = application.screen_height
|
|
self.tuningPenTx = wx.Pen('Red', 1)
|
|
self.magnPen = wx.Pen('Black', 1)
|
|
self.phasePen = wx.Pen((0, 180, 0), 1)
|
|
self.swrPen = wx.Pen('Blue', 1)
|
|
self.backgroundPen = wx.Pen(self.GetBackgroundColour(), 1)
|
|
self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID)
|
|
self.font = wx.Font(24, wx.FONTFAMILY_SWISS, wx.NORMAL,
|
|
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
|
|
if sys.platform == 'win32':
|
|
self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter)
|
|
def OnEnter(self, event):
|
|
self.SetFocus() # Set focus so we get mouse wheel events
|
|
def OnPaint(self, event):
|
|
dc = wx.PaintDC(self)
|
|
x = self.tune_tx
|
|
dc.SetPen(self.tuningPenTx)
|
|
dc.DrawLine(x, 0, x, self.max_height)
|
|
dc.SetPen(self.horizPen)
|
|
for y in self.y_ticks:
|
|
dc.DrawLine(0, y, self.graph_width, y)
|
|
# Magnitude
|
|
t = 'Magnitude, '
|
|
x = self.chary
|
|
y = self.height - self.chary
|
|
dc.SetTextForeground(self.magnPen.GetColour())
|
|
dc.DrawText(t, x, y)
|
|
w, h = dc.GetTextExtent(t)
|
|
x += w + self.chary
|
|
# Phase
|
|
t = 'Phase, '
|
|
dc.SetTextForeground(self.phasePen.GetColour())
|
|
dc.DrawText(t, x, y)
|
|
w, h = dc.GetTextExtent(t)
|
|
x += w + self.chary
|
|
# SWR
|
|
t = 'SWR'
|
|
dc.SetTextForeground(self.swrPen.GetColour())
|
|
dc.DrawText(t, x, y)
|
|
w, h = dc.GetTextExtent(t)
|
|
x += w + self.chary
|
|
# Draw graph
|
|
if self.line_phase: # Phase line
|
|
# Try to avoid drawing vertical lines when the phase goes from +180 to -180
|
|
dc.SetPen(self.phasePen)
|
|
top = self.y_ticks[0]
|
|
high = self.y_ticks[1]
|
|
low = self.y_ticks[-2]
|
|
bottom = self.y_ticks[-1]
|
|
old_phase = self.line_phase[0]
|
|
line = [(0, old_phase)]
|
|
for x in range(1, self.graph_width):
|
|
phase = self.line_phase[x]
|
|
if phase < high and old_phase > low:
|
|
line.append((x-1, bottom))
|
|
dc.DrawLines(line)
|
|
line = [(x, top), (x, phase)]
|
|
elif phase > low and old_phase < high:
|
|
line.append((x-1, top))
|
|
dc.DrawLines(line)
|
|
line = [(x, bottom), (x, phase)]
|
|
else:
|
|
line.append((x, phase))
|
|
old_phase = phase
|
|
dc.DrawLines(line)
|
|
if self.line_mag: # Magnitude line
|
|
dc.SetPen(self.magnPen)
|
|
dc.DrawLines(self.line_mag)
|
|
if self.line_swr: # SWR line
|
|
dc.SetPen(self.swrPen)
|
|
dc.DrawLines(self.line_swr)
|
|
if self.display_text:
|
|
dc.SetFont(self.font)
|
|
dc.SetTextBackground(conf.color_graph)
|
|
dc.SetTextForeground('red')
|
|
dc.SetBackgroundMode(wx.SOLID)
|
|
dc.DrawText(self.display_text, 10, 50)
|
|
def SetHeight(self, height):
|
|
self.height = height
|
|
self.SetSize((self.graph_width, height))
|
|
def SetTuningLine(self, tune_tx):
|
|
dc = wx.ClientDC(self)
|
|
dc.SetPen(self.backgroundPen)
|
|
dc.DrawLine(self.tune_tx, 0, self.tune_tx, self.max_height)
|
|
dc.SetPen(self.tuningPenTx)
|
|
dc.DrawLine(tune_tx, 0, tune_tx, self.max_height)
|
|
self.tune_tx = tune_tx
|
|
self.Refresh()
|
|
|
|
class GraphScreen(wx.Window):
|
|
"""Display the graph screen X and Y axis, and create a graph display."""
|
|
def __init__(self, parent, data_width, graph_width, correct_width, correct_delta, in_splitter=0):
|
|
wx.Window.__init__(self, parent, pos = (0, 0))
|
|
self.in_splitter = in_splitter # Are we in the top of a splitter window?
|
|
self.data_width = data_width
|
|
self.graph_width = graph_width
|
|
self.correct_width = correct_width
|
|
self.correct_delta = correct_delta
|
|
self.started = False
|
|
self.doResize = False
|
|
self.pen_tick = wx.Pen("Black", 1, wx.SOLID)
|
|
self.font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
|
|
self.SetFont(self.font)
|
|
w = self.GetCharWidth() * 14 // 10
|
|
h = self.GetCharHeight()
|
|
self.freq_start = 1000000
|
|
self.freq_stop = 2000000
|
|
self.charx = w
|
|
self.chary = h
|
|
self.mode = ''
|
|
self.data_mag = []
|
|
self.data_phase = []
|
|
self.data_impedance = []
|
|
self.data_reflect = []
|
|
self.data_freq = [0] * data_width
|
|
self.tick = max(2, h * 3 // 10)
|
|
self.originX = w * 5
|
|
self.offsetY = h + self.tick
|
|
self.width = self.originX * 2 + self.graph_width + self.tick
|
|
self.height = application.screen_height * 3 // 10
|
|
self.x0 = self.originX + self.graph_width // 2 # center of graph
|
|
self.originY = 10
|
|
self.num_ticks = 8 # number of Y lines above the X axis
|
|
self.dy_ticks = 10
|
|
# The pixel = slope * value + zero_pixel
|
|
# The value = (pixel - zero_pixel) / slope
|
|
self.leftZero = 10 # y location of left zero value
|
|
self.rightZero = 10 # y location of right zero value
|
|
self.leftSlope = 10 # slope of left scale times 360
|
|
self.rightSlope = 10 # slope of right scale times 360
|
|
self.SetSize((self.width, self.height))
|
|
self.SetSizeHints(self.width, 1, self.width)
|
|
self.SetBackgroundColour(conf.color_graph)
|
|
self.Bind(wx.EVT_SIZE, self.OnSize)
|
|
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
|
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
|
|
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
|
|
self.Bind(wx.EVT_MOTION, self.OnMotion)
|
|
self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel)
|
|
self.display = GraphDisplay(self, self.originX, 0, self.graph_width, 5, self.chary)
|
|
def OnPaint(self, event):
|
|
dc = wx.PaintDC(self)
|
|
if self.started and not self.in_splitter:
|
|
dc.SetFont(self.font)
|
|
self.MakeYTicks(dc)
|
|
self.MakeXTicks(dc)
|
|
def OnIdle(self, event):
|
|
if self.doResize:
|
|
self.ResizeGraph()
|
|
def OnSize(self, event):
|
|
self.doResize = True
|
|
self.ClearGraph()
|
|
event.Skip()
|
|
def ResizeGraph(self):
|
|
"""Change the height of the graph.
|
|
|
|
Changing the width interactively is not allowed.
|
|
Call after changing the zero or scale to recalculate the X and Y axis marks.
|
|
"""
|
|
w, h = self.GetClientSize()
|
|
if self.in_splitter: # Splitter window has no X axis scale
|
|
self.height = h
|
|
self.originY = h
|
|
else:
|
|
self.height = h - self.chary # Leave space for X scale
|
|
self.originY = self.height - self.offsetY
|
|
self.MakeYScale()
|
|
self.display.SetHeight(self.originY)
|
|
self.doResize = False
|
|
self.started = True
|
|
self.Refresh()
|
|
def MakeYScale(self):
|
|
chary = self.chary
|
|
dy = self.dy_ticks = (self.originY - chary * 2) // self.num_ticks # pixels per tick
|
|
ytot = dy * self.num_ticks
|
|
# Voltage dB scale
|
|
dbs = 80 # Number of dB to display
|
|
self.leftZero = self.originY - ytot - chary
|
|
self.leftSlope = - ytot * 360 // dbs # pixels per dB times 360
|
|
# Phase scale
|
|
self.rightSlope = - ytot # pixels per degree times 360
|
|
self.rightZero = self.originY - ytot // 2 - chary
|
|
# SWR scale
|
|
swrs = 9 # display range 1.0 to swrs
|
|
self.swrSlope = - ytot * 360 // (swrs - 1) # pixels per SWR unit times 360
|
|
self.swrZero = self.originY - self.swrSlope // 360 - chary
|
|
def MakeYTicks(self, dc):
|
|
charx = self.charx
|
|
chary = self.chary
|
|
x1 = self.originX - self.tick * 3 # left of tick mark
|
|
x2 = self.originX - 1 # x location of left y axis
|
|
x3 = self.originX + self.graph_width # end of graph data
|
|
x4 = x3 + 1 # right y axis
|
|
x5 = x3 + self.tick * 3 # right tick mark
|
|
dc.SetPen(self.pen_tick)
|
|
dc.DrawLine(x2, 0, x2, self.originY + 1) # y axis
|
|
dc.DrawLine(x4, 0, x4, self.originY + 1) # y axis
|
|
del self.display.y_ticks[:]
|
|
y = self.leftZero
|
|
dc.SetTextForeground(self.display.magnPen.GetColour())
|
|
for i in range(self.num_ticks + 1):
|
|
# Create the dB scale
|
|
val = (y - self.leftZero) * 360 // self.leftSlope
|
|
t = str(val)
|
|
dc.DrawLine(x1, y, x2, y)
|
|
self.display.y_ticks.append(y)
|
|
w, h = dc.GetTextExtent(t)
|
|
dc.DrawText(t, x1 - w, y - h // 2)
|
|
y += self.dy_ticks
|
|
y = self.leftZero
|
|
dc.SetTextForeground(self.display.phasePen.GetColour())
|
|
for i in range(self.num_ticks + 1):
|
|
# Create the scale on the right
|
|
val = (y - self.rightZero) * 360 // self.rightSlope
|
|
t = str(val)
|
|
dc.DrawLine(x4, y, x5, y)
|
|
w, h = dc.GetTextExtent(t)
|
|
dc.DrawText(t, self.width - w - charx, y - h // 2 + 3) # right text
|
|
y += self.dy_ticks
|
|
# Create the SWR scale
|
|
if self.mode == 'Reflection':
|
|
y = self.leftZero
|
|
dc.SetTextForeground(self.display.swrPen.GetColour())
|
|
for i in range(self.num_ticks + 1):
|
|
val = (y - self.swrZero) * 360 // self.swrSlope
|
|
t = str(val)
|
|
w, h = dc.GetTextExtent(t)
|
|
dc.DrawText(t, w//2, y - h // 2)
|
|
y += self.dy_ticks
|
|
def MakeXTicks(self, dc):
|
|
originY = self.originY
|
|
x3 = self.originX + self.graph_width # end of fft data
|
|
charx , z = dc.GetTextExtent('-30000XX')
|
|
tick0 = self.tick
|
|
tick1 = tick0 * 2
|
|
tick2 = tick0 * 3
|
|
dc.SetTextForeground(self.display.magnPen.GetColour())
|
|
# Draw the X axis
|
|
dc.SetPen(self.pen_tick)
|
|
dc.DrawLine(self.originX, originY, x3, originY)
|
|
sample_rate = int(self.freq_stop - self.freq_start)
|
|
if sample_rate < 12000:
|
|
return
|
|
VFO = int((self.freq_start + self.freq_stop) / 2)
|
|
# Draw the band plan colors below the X axis
|
|
x = self.originX
|
|
f = float(x - self.x0) * sample_rate / self.data_width
|
|
c = None
|
|
y = originY + 1
|
|
for freq, color in conf.BandPlan:
|
|
freq -= VFO
|
|
if f < freq:
|
|
xend = int(self.x0 + float(freq) * self.data_width / sample_rate + 0.5)
|
|
if c is not None:
|
|
dc.SetPen(wx.TRANSPARENT_PEN)
|
|
dc.SetBrush(wx.Brush(c))
|
|
dc.DrawRectangle(x, y, min(x3, xend) - x, tick0) # x axis
|
|
if xend >= x3:
|
|
break
|
|
x = xend
|
|
f = freq
|
|
c = color
|
|
stick = 1000 # small tick in Hertz
|
|
mtick = 5000 # medium tick
|
|
ltick = 10000 # large tick
|
|
# check the width of the frequency label versus frequency span
|
|
df = float(charx) * sample_rate / self.data_width # max label freq in Hertz
|
|
df *= 2.0
|
|
df = math.log10(df)
|
|
expn = int(df)
|
|
mant = df - expn
|
|
if mant < 0.3: # label every 10
|
|
tfreq = 10 ** expn
|
|
ltick = tfreq
|
|
mtick = ltick // 2
|
|
stick = ltick // 10
|
|
elif mant < 0.69: # label every 20
|
|
tfreq = 2 * 10 ** expn
|
|
ltick = tfreq // 2
|
|
mtick = ltick // 2
|
|
stick = ltick // 10
|
|
else: # label every 50
|
|
tfreq = 5 * 10 ** expn
|
|
ltick = tfreq
|
|
mtick = ltick // 5
|
|
stick = ltick // 10
|
|
# Draw the X axis ticks and frequency in kHz
|
|
dc.SetPen(self.pen_tick)
|
|
freq1 = VFO - sample_rate // 2
|
|
freq1 = (freq1 // stick) * stick
|
|
freq2 = freq1 + sample_rate + stick + 1
|
|
y_end = 0
|
|
for f in range (freq1, freq2, stick):
|
|
x = self.x0 + int(float(f - VFO) / sample_rate * self.data_width)
|
|
if self.originX <= x <= x3:
|
|
if f % ltick is 0: # large tick
|
|
dc.DrawLine(x, originY, x, originY + tick2)
|
|
elif f % mtick is 0: # medium tick
|
|
dc.DrawLine(x, originY, x, originY + tick1)
|
|
else: # small tick
|
|
dc.DrawLine(x, originY, x, originY + tick0)
|
|
if f % tfreq is 0: # place frequency label
|
|
t = str(f//1000)
|
|
w, h = dc.GetTextExtent(t)
|
|
dc.DrawText(t, x - w // 2, originY + tick2)
|
|
y_end = originY + tick2 + h
|
|
if y_end: # mark the center of the display
|
|
dc.DrawLine(self.x0, y_end, self.x0, application.screen_height)
|
|
def ClearGraph(self):
|
|
del self.display.line_mag[:]
|
|
del self.display.line_phase[:]
|
|
del self.display.line_swr[:]
|
|
del self.data_mag[:]
|
|
del self.data_phase[:]
|
|
del self.data_impedance[:]
|
|
del self.data_reflect[:]
|
|
self.display.Refresh()
|
|
def SetDisplayMsg(self, text=''):
|
|
self.display.display_text = text
|
|
self.display.Refresh()
|
|
def SetMode(self, mode):
|
|
self.mode = mode
|
|
def OnGraphData(self, volts):
|
|
# SWR = (1 + rho) / (1 - rho)
|
|
# Create graph lines
|
|
mode = self.mode
|
|
del self.display.line_mag[:]
|
|
del self.display.line_phase[:]
|
|
del self.display.line_swr[:]
|
|
del self.data_mag[:]
|
|
del self.data_phase[:]
|
|
del self.data_impedance[:]
|
|
del self.data_reflect[:]
|
|
if mode == 'Calibrate':
|
|
for x in range(application.correct_width):
|
|
self.calibrate_tmp[x] += volts[x]
|
|
self.calibrate_count += 1
|
|
for x in range(self.graph_width):
|
|
self.data_impedance.append(50)
|
|
self.data_reflect.append(0)
|
|
i = x * self.correct_width // self.data_width
|
|
magn = abs(volts[i])
|
|
phase = cmath.phase(volts[i]) * 360. / (2.0 * math.pi)
|
|
if magn < 1e-6:
|
|
db = -120.0
|
|
else:
|
|
db = 20.0 * math.log10(magn)
|
|
self.data_mag.append(db)
|
|
y = self.leftZero - int( - db * self.leftSlope / 360.0 + 0.5)
|
|
self.display.line_mag.append((x, y))
|
|
self.data_phase.append(phase)
|
|
y = self.rightZero - int( - phase * self.rightSlope / 360.0 + 0.5)
|
|
y = int(y)
|
|
self.display.line_phase.append(y)
|
|
elif mode == 'Reflection':
|
|
for x in range(self.graph_width):
|
|
delta = self.correct_delta
|
|
# Find the frequency for this pixel
|
|
freq = self.data_freq[x]
|
|
# Find the corresponding index into the correction array
|
|
i = int(freq / delta)
|
|
if i > self.correct_width - 2:
|
|
i = self.correct_width - 2
|
|
dd = float(freq - i * delta) / delta # fractional part of next index for linear interpolation
|
|
Vx = volts[x]
|
|
# linear interpolation
|
|
if application.reflection_short is not None and application.reflection_open is not None and application.reflection_load is not None:
|
|
Vs = application.reflection_short[i] + (application.reflection_short[i+1] - application.reflection_short[i]) * dd
|
|
Vo = application.reflection_open[i] + (application.reflection_open[i+1] - application.reflection_open[i]) * dd
|
|
Vl = application.reflection_load[i] + (application.reflection_load[i+1] - application.reflection_load[i]) * dd
|
|
S11 = Vl
|
|
VVop = Vo - S11
|
|
VVsh = Vs - S11
|
|
try:
|
|
S12S21 = 2.0 * VVop * VVsh / (VVsh - VVop)
|
|
S22 = (VVop + VVsh) / (VVop - VVsh)
|
|
reflect = (Vx - S11) / (S12S21 + S22 * (Vx - S11))
|
|
Z = 50.0 * (1.0 + reflect) / (1.0 - reflect)
|
|
except:
|
|
Z = 50E3
|
|
reflect = (Z - 50) / (Z + 50)
|
|
#print ('Vs Vo Vl', abs(Vs), abs(Vo), abs(Vl), 'S22', abs(S22), 'S1221', abs(S12S21))
|
|
else:
|
|
if application.reflection_open is not None:
|
|
correct = application.reflection_open[i] + (application.reflection_open[i+1] - application.reflection_open[i]) * dd
|
|
if application.reflection_short is not None:
|
|
correct = (correct - (application.reflection_short[i] + (application.reflection_short[i+1] - application.reflection_short[i]) * dd)) / 2.0
|
|
else: # Use Short
|
|
correct = - (application.reflection_short[i] + (application.reflection_short[i+1] - application.reflection_short[i]) * dd)
|
|
try:
|
|
reflect = volts[x] / correct
|
|
Z = 50.0 * (1.0 + reflect) / (1.0 - reflect)
|
|
except:
|
|
Z = 50E3
|
|
reflect = (Z - 50) / (Z + 50)
|
|
self.data_reflect.append(reflect)
|
|
self.data_impedance.append(Z)
|
|
magn = abs(reflect)
|
|
swr = (1.0 + magn) / (1.0 - magn)
|
|
if not 0.999 <= swr <= 99:
|
|
swr = 99.0
|
|
if magn < 1e-6:
|
|
db = -120.0
|
|
else:
|
|
db = 20.0 * math.log10(magn)
|
|
self.data_mag.append(db)
|
|
y = self.leftZero - int( - db * self.leftSlope / 360.0 + 0.5)
|
|
self.display.line_mag.append((x, y))
|
|
phase = cmath.phase(reflect) * 360. / (2.0 * math.pi)
|
|
self.data_phase.append(phase)
|
|
y = self.rightZero - int( - phase * self.rightSlope / 360.0 + 0.5)
|
|
y = int(y)
|
|
self.display.line_phase.append(y)
|
|
y = self.swrZero - int( - swr * self.swrSlope / 360.0 + 0.5)
|
|
self.display.line_swr.append((x,y))
|
|
else: # Mode is transmission
|
|
for x in range(self.graph_width):
|
|
delta = self.correct_delta
|
|
# Find the frequency for this pixel
|
|
freq = self.data_freq[x]
|
|
# Find the corresponding index into the correction array
|
|
i = int(freq / delta)
|
|
if i > self.correct_width - 2:
|
|
i = self.correct_width - 2
|
|
dd = float(freq - i * delta) / delta # fractional part of next index for linear interpolation
|
|
trans = volts[x]
|
|
if application.transmission_open is not None:
|
|
trans -= application.transmission_open[i] + (application.transmission_open[i+1] - application.transmission_open[i]) * dd
|
|
trans /= application.transmission_short[i] + (application.transmission_short[i+1] - application.transmission_short[i]) * dd
|
|
self.data_reflect.append(trans)
|
|
self.data_impedance.append(50)
|
|
magn = abs(trans)
|
|
if magn < 1e-6:
|
|
db = -120.0
|
|
else:
|
|
db = 20.0 * math.log10(magn)
|
|
self.data_mag.append(db)
|
|
y = self.leftZero - int( - db * self.leftSlope / 360.0 + 0.5)
|
|
self.display.line_mag.append((x, y))
|
|
phase = cmath.phase(trans) * 360. / (2.0 * math.pi)
|
|
self.data_phase.append(phase)
|
|
y = self.rightZero - int( - phase * self.rightSlope / 360.0 + 0.5)
|
|
y = int(y)
|
|
self.display.line_phase.append(y)
|
|
self.display.Refresh()
|
|
def NewFreq(self, start, stop):
|
|
if self.freq_start != start or self.freq_stop != stop:
|
|
self.ClearGraph()
|
|
self.freq_start = start
|
|
self.freq_stop = stop
|
|
for i in range(self.data_width): # The frequency in Hertz for every graph pixel
|
|
self.data_freq[i] = int(start + float(stop - start) * i / (self.data_width - 1) + 0.5)
|
|
self.SetTxFreq(index=self.display.tune_tx)
|
|
self.doResize = True
|
|
def SetTxFreq(self, freq=None, index=None):
|
|
if index is None:
|
|
index = int(float(freq - self.freq_start) * (self.data_width - 1) / (self.freq_stop - self.freq_start) + 0.5)
|
|
if index < 0:
|
|
index = 0
|
|
elif index >= self.data_width:
|
|
index = self.data_width - 1
|
|
if freq is None:
|
|
freq = self.data_freq[index]
|
|
self.display.SetTuningLine(index)
|
|
application.ShowFreq(freq, index)
|
|
def GetMousePosition(self, event):
|
|
"""For mouse clicks in our display, translate to our screen coordinates."""
|
|
mouse_x, mouse_y = event.GetPosition()
|
|
win = event.GetEventObject()
|
|
if win is not self:
|
|
x, y = win.GetPosition().Get()
|
|
mouse_x += x
|
|
mouse_y += y
|
|
return mouse_x, mouse_y
|
|
def OnLeftDown(self, event):
|
|
mouse_x, mouse_y = self.GetMousePosition(event)
|
|
self.SetTxFreq(index=mouse_x - self.originX)
|
|
self.CaptureMouse()
|
|
def OnLeftUp(self, event):
|
|
if self.HasCapture():
|
|
self.ReleaseMouse()
|
|
def OnMotion(self, event):
|
|
if event.Dragging() and event.LeftIsDown():
|
|
mouse_x, mouse_y = self.GetMousePosition(event)
|
|
self.SetTxFreq(index=mouse_x - self.originX)
|
|
def OnWheel(self, event):
|
|
tune = self.display.tune_tx + event.GetWheelRotation() // event.GetWheelDelta()
|
|
self.SetTxFreq(index=tune)
|
|
|
|
class HelpScreen(wx.html.HtmlWindow):
|
|
"""Create the screen for the Help button."""
|
|
def __init__(self, parent, width, height):
|
|
wx.html.HtmlWindow.__init__(self, parent, -1, size=(width, height))
|
|
if "gtk2" in wx.PlatformInfo:
|
|
self.SetStandardFonts()
|
|
self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22])
|
|
# read in text from file help.html in the directory of this module
|
|
self.LoadFile('help_vna.html')
|
|
def OnLinkClicked(self, link):
|
|
webbrowser.open(link.GetHref(), new=2)
|
|
|
|
class QMainFrame(wx.Frame):
|
|
"""Create the main top-level window."""
|
|
def __init__(self, width, height):
|
|
fp = open('__init__.py') # Read in the title
|
|
self.title = fp.readline().strip()
|
|
fp.close()
|
|
self.title = 'Quisk Vector Network Analyzer ' + self.title[7:]
|
|
wx.Frame.__init__(self, None, -1, self.title, wx.DefaultPosition,
|
|
(width, height), wx.DEFAULT_FRAME_STYLE, 'MainFrame')
|
|
self.SetBackgroundColour(conf.color_bg)
|
|
self.Bind(wx.EVT_CLOSE, self.OnBtnClose)
|
|
def OnBtnClose(self, event):
|
|
application.OnBtnClose(event)
|
|
self.Destroy()
|
|
def SetConfigText(self, text):
|
|
if len(text) > 100:
|
|
text = text[0:80] + '|||' + text[-17:]
|
|
self.SetTitle("Radio %s %s %s" % (configure.Settings[1], self.title, text))
|
|
|
|
class Spacer(wx.Window):
|
|
"""Create a bar between the graph screen and the controls"""
|
|
def __init__(self, parent):
|
|
wx.Window.__init__(self, parent, pos = (0, 0),
|
|
size=(-1, 6), style = wx.NO_BORDER)
|
|
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
|
r, g, b = parent.GetBackgroundColour().Get(False)
|
|
dark = (r * 7 // 10, g * 7 // 10, b * 7 // 10)
|
|
light = (r + (255 - r) * 5 // 10, g + (255 - g) * 5 // 10, b + (255 - b) * 5 // 10)
|
|
self.dark_pen = wx.Pen(dark, 1, wx.SOLID)
|
|
self.light_pen = wx.Pen(light, 1, wx.SOLID)
|
|
self.width = application.screen_width
|
|
def OnPaint(self, event):
|
|
dc = wx.PaintDC(self)
|
|
w = self.width
|
|
dc.SetPen(self.dark_pen)
|
|
dc.DrawLine(0, 0, w, 0)
|
|
dc.DrawLine(0, 1, w, 1)
|
|
dc.DrawLine(0, 2, w, 2)
|
|
dc.SetPen(self.light_pen)
|
|
dc.DrawLine(0, 3, w, 3)
|
|
dc.DrawLine(0, 4, w, 4)
|
|
dc.DrawLine(0, 5, w, 5)
|
|
|
|
class CalibrateDialog(wx.Dialog):
|
|
def __init__(self, app):
|
|
self.app = app
|
|
self.correct_open = None
|
|
self.correct_short = None
|
|
self.correct_load = None
|
|
w, h = app.main_frame.GetSize().Get()
|
|
width = w // 2
|
|
if app.screen_name == "Reflection":
|
|
title = "Calibrate for Reflection Mode"
|
|
t = ''
|
|
if app.reflection_short is not None:
|
|
t += "Short"
|
|
if app.reflection_open is not None:
|
|
t += "Open"
|
|
if app.reflection_load is not None:
|
|
t += "Load"
|
|
if t:
|
|
t = "Reflection mode calibration is %s from %s" % (t, app.calibrate_time)
|
|
else:
|
|
t = "Reflection mode is Uncalibrated"
|
|
else:
|
|
title = "Calibrate for Transmission Mode"
|
|
t = ''
|
|
if app.transmission_short is not None:
|
|
t += "Short"
|
|
if app.transmission_open is not None:
|
|
t += "Open"
|
|
if t:
|
|
t = "Transmission mode calibration is %s from %s" % (t, app.calibrate_time)
|
|
else:
|
|
t = "Transmission mode is Uncalibrated"
|
|
wx.Dialog.__init__(self, None, -1, title, size=(width, h))
|
|
tab = self.GetCharHeight() * 2
|
|
y = tab
|
|
txt = wx.StaticText(self, -1, t, pos=(tab, y))
|
|
z, chary = txt.GetSize().Get()
|
|
y += chary * 3 // 2
|
|
if app.screen_name == "Reflection":
|
|
t = "To calibrate the VNA for reflection mode, connect the standard Short, Open and Load connectors to the unknown port, and press the button."
|
|
t += " Reflection mode requires at least an Open or Short calibration, but using all three is highly recommended."
|
|
else:
|
|
t = "To calibrate the VNA for transmission mode, connect the cables together for Short, or leave them unconnected for Open, and press the button."
|
|
t += " The Short calibration is required, but the Open calibration is optional."
|
|
t += " The calibration will be saved for use the next time the program starts."
|
|
txt = wx.StaticText(self, -1, t, pos=(tab, y))
|
|
txt.Wrap(width - tab * 2)
|
|
w, h = txt.GetSize().Get()
|
|
y += h + chary
|
|
# Calibrate buttons
|
|
t1 = wx.StaticText(self, -1, "Connect the Short connector and press", pos=(tab, y))
|
|
tw, th = t1.GetSize().Get()
|
|
bx = tab + tw + tab // 2
|
|
b1 = QuiskPushbutton(self, self.OnBtnShort, " Short ")
|
|
b1.SetColorGray()
|
|
bw, bh = b1.GetSize().Get()
|
|
by = y + (th - bh) // 2
|
|
b1.Move(wx.Point(bx, by))
|
|
self.txt_short = wx.StaticText(self, -1, "Not done", pos=(bx + bw + tab // 2, y))
|
|
y = by + bh * 15 // 10
|
|
by = y + (th - bh) // 2
|
|
t2 = wx.StaticText(self, -1, "Connect the Open connector and press", pos=(tab, y), size = (tw, th))
|
|
b2 = QuiskPushbutton(self, self.OnBtnOpen, "Open")
|
|
b2.SetColorGray()
|
|
b2.SetPosition((bx, by))
|
|
b2.SetSize((bw, bh))
|
|
self.txt_open = wx.StaticText(self, -1, "Not done", pos=(bx + bw + tab // 2, y))
|
|
y = by + bh * 15 // 10
|
|
by = y + (th - bh) // 2
|
|
if app.screen_name == "Reflection":
|
|
t3 = wx.StaticText(self, -1, "Connect the Load connector and press", pos=(tab, y), size = (tw, th))
|
|
b3 = QuiskPushbutton(self, self.OnBtnLoad, "Load")
|
|
b3.SetColorGray()
|
|
b3.SetPosition((bx, by))
|
|
b3.SetSize((bw, bh))
|
|
self.txt_load = wx.StaticText(self, -1, "Not done", pos=(bx + bw + tab // 2, y))
|
|
y = by + bh * 15 // 10
|
|
# Calibrate buttons
|
|
b1 = QuiskPushbutton(self, self.OnBtnCalibrate, " Calibrate ")
|
|
b1.SetColorGray()
|
|
b1.Enable(False)
|
|
w, h = b1.GetSize().Get()
|
|
b2 = QuiskPushbutton(self, self.OnBtnCancel, "Cancel")
|
|
b2.SetColorGray()
|
|
b2.SetSize((w, h))
|
|
ww = (width - w * 2 - 40) // 3
|
|
b1.Move(wx.Point(ww, y))
|
|
b2.Move(wx.Point(width - w - ww, y))
|
|
y += h * 3 // 2
|
|
self.SetClientSize(wx.Size(width, y))
|
|
self.btns = [b1, b2]
|
|
# timer for calibrate buttons
|
|
self.calibrate_timer = wx.Timer(self)
|
|
self.Bind(wx.EVT_TIMER, self.OnCalibrateTimer, self.calibrate_timer)
|
|
def OnBtnCalibrate(self, event):
|
|
app = self.app
|
|
if app.screen_name == "Reflection":
|
|
app.reflection_short = self.correct_short
|
|
app.reflection_open = self.correct_open
|
|
app.reflection_load = self.correct_load
|
|
elif app.screen_name == "Transmission":
|
|
app.transmission_short = self.correct_short
|
|
app.transmission_open = self.correct_open
|
|
app.calibrate_time = time.asctime()
|
|
app.EnableButtons()
|
|
app.SetCalText()
|
|
app.SaveState()
|
|
self.EndModal(4)
|
|
def OnBtnCancel(self, event):
|
|
self.EndModal(5)
|
|
def Calibrate(self):
|
|
for b in self.btns:
|
|
b.Enable(False)
|
|
self.app.Calibrate()
|
|
self.calibrate_timer.Start(3000, oneShot=True)
|
|
def OnBtnShort(self, event):
|
|
self.txt_short.SetLabel("Wait")
|
|
self.mode = "Short"
|
|
self.Calibrate()
|
|
def OnBtnOpen(self, event):
|
|
self.txt_open.SetLabel("Wait")
|
|
self.mode = "Open"
|
|
self.Calibrate()
|
|
def OnBtnLoad(self, event):
|
|
self.txt_load.SetLabel("Wait")
|
|
self.mode = "Load"
|
|
self.Calibrate()
|
|
def OnCalibrateTimer(self, event):
|
|
self.app.running = False
|
|
if self.app.has_SetVNA:
|
|
Hardware.SetVNA(key_down=0)
|
|
for b in self.btns:
|
|
b.Enable(True)
|
|
data = self.app.graph.calibrate_tmp
|
|
count = self.app.graph.calibrate_count
|
|
if count == 0:
|
|
if self.mode == "Short":
|
|
self.txt_short.SetLabel("Not done")
|
|
elif self.mode == "Open":
|
|
self.txt_open.SetLabel("Not done")
|
|
elif self.mode == "Load":
|
|
self.txt_load.SetLabel("Not done")
|
|
return
|
|
for i in range(application.correct_width):
|
|
data[i] /= count
|
|
if self.mode == "Short":
|
|
self.txt_short.SetLabel("Done")
|
|
self.correct_short = data
|
|
elif self.mode == "Open":
|
|
self.txt_open.SetLabel("Done")
|
|
self.correct_open = data
|
|
elif self.mode == "Load":
|
|
self.txt_load.SetLabel("Done")
|
|
self.correct_load = data
|
|
|
|
class App(wx.App):
|
|
"""Class representing the application."""
|
|
StateNames = ['transmission_open', 'transmission_short', 'reflection_open', 'reflection_short', 'reflection_load', 'calibrate_time',
|
|
'calibrate_version']
|
|
def __init__(self):
|
|
global application
|
|
application = self
|
|
self.bottom_widgets = None
|
|
self.is_vna_program = None
|
|
if sys.stdout.isatty():
|
|
wx.App.__init__(self, redirect=False)
|
|
else:
|
|
wx.App.__init__(self, redirect=True)
|
|
def OnInit(self):
|
|
"""Perform most initialization of the app here (called by wxPython on startup)."""
|
|
wx.lib.colourdb.updateColourDB() # Add additional color names
|
|
import quisk_widgets # quisk_widgets needs the application object
|
|
quisk_widgets.application = self
|
|
del quisk_widgets
|
|
global conf # conf is the module for all configuration data
|
|
import quisk_conf_defaults as conf
|
|
setattr(conf, 'config_file_path', ConfigPath)
|
|
setattr(conf, 'DefaultConfigDir', DefaultConfigDir)
|
|
if os.path.isfile(ConfigPath): # See if the user has a config file
|
|
setattr(conf, 'config_file_exists', True)
|
|
d = {}
|
|
d.update(conf.__dict__) # make items from conf available
|
|
exec(compile(open(ConfigPath).read(), ConfigPath, 'exec'), d) # execute the user's config file
|
|
if os.path.isfile(ConfigPath2): # See if the user has a second config file
|
|
exec(compile(open(ConfigPath2).read(), ConfigPath2, 'exec'), d) # execute the user's second config file
|
|
for k in d: # add user's config items to conf
|
|
v = d[k]
|
|
if k[0] != '_': # omit items starting with '_'
|
|
setattr(conf, k, v)
|
|
else:
|
|
setattr(conf, 'config_file_exists', False)
|
|
# Read in configuration from the selected radio
|
|
if configure: self.local_conf = configure.Configuration(self, argv_options.AskMe)
|
|
if configure: self.local_conf.UpdateConf()
|
|
# Choose whether to use Unicode or text symbols
|
|
for k in ('sym_stat_mem', 'sym_stat_fav', 'sym_stat_dx',
|
|
'btn_text_range_dn', 'btn_text_range_up', 'btn_text_play', 'btn_text_rec', 'btn_text_file_rec',
|
|
'btn_text_file_play', 'btn_text_fav_add',
|
|
'btn_text_fav_recall', 'btn_text_mem_add', 'btn_text_mem_next', 'btn_text_mem_del'):
|
|
if conf.use_unicode_symbols:
|
|
setattr(conf, 'X' + k, getattr(conf, 'U' + k))
|
|
else:
|
|
setattr(conf, 'X' + k, getattr(conf, 'T' + k))
|
|
MakeWidgetGlobals()
|
|
self.BtnRfGain = None
|
|
self.graph_freq = 7e6
|
|
self.graph_index = 50
|
|
self.transmission_open = None
|
|
self.transmission_short = None
|
|
self.reflection_open = None
|
|
self.reflection_short = None
|
|
self.reflection_load = None
|
|
self.reflection_cal = "Cal x"
|
|
self.transmission_cal = "Cal x"
|
|
self.calibrate_time = time.asctime()
|
|
self.calibrate_version = 1
|
|
QS.set_params(quisk_is_vna=1) # Call this only if we are the VNA program
|
|
# Open hardware file
|
|
self.firmware_version = None
|
|
global Hardware
|
|
if configure and self.local_conf.GetHardware():
|
|
pass
|
|
else:
|
|
if hasattr(conf, "Hardware"): # Hardware defined in config file
|
|
self.Hardware = conf.Hardware(self, conf)
|
|
hname = ConfigPath
|
|
else:
|
|
self.Hardware = conf.quisk_hardware.Hardware(self, conf)
|
|
hname = conf.quisk_hardware.__file__
|
|
if hname[-3:] == 'pyc':
|
|
hname = hname[0:-1]
|
|
setattr(conf, 'hardware_file_name', hname)
|
|
if conf.quisk_widgets:
|
|
hname = conf.quisk_widgets.__file__
|
|
if hname[-3:] == 'pyc':
|
|
hname = hname[0:-1]
|
|
setattr(conf, 'widgets_file_name', hname)
|
|
else:
|
|
setattr(conf, 'widgets_file_name', '')
|
|
Hardware = self.Hardware
|
|
# Initialization
|
|
if configure: self.local_conf.Initialize()
|
|
# get the screen size
|
|
x, y, self.screen_width, self.screen_height = wx.Display().GetGeometry()
|
|
self.Bind(wx.EVT_QUERY_END_SESSION, self.OnEndSession)
|
|
self.sample_rate = 48000
|
|
self.timer = time.time() # A seconds clock
|
|
self.time0 = 0 # timer to display fields
|
|
self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow
|
|
self.heart_time0 = self.timer # timer to call HeartBeat at intervals
|
|
self.running = False
|
|
self.startup = True
|
|
self.save_data = []
|
|
self.frequency = 0
|
|
self.main_frame = frame = QMainFrame(10, 10)
|
|
self.SetTopWindow(frame)
|
|
# Find the data width, the width of returned graph data.
|
|
width = self.screen_width * conf.graph_width
|
|
width = int(width)
|
|
self.data_width = width
|
|
# correct_delta is the spacing of correction points in Hertz
|
|
if conf.use_rx_udp == 10: # Hermes UDP protocol
|
|
self.max_freq = 30000000 # maximum calculation frequency
|
|
self.correct_width = self.data_width # number of data points in the correct arrays
|
|
else:
|
|
self.max_freq = 60000000
|
|
self.correct_width = self.max_freq // 15000 + 4
|
|
if hasattr(Hardware, 'SetVNA'):
|
|
self.has_SetVNA = True
|
|
start, stop = Hardware.SetVNA(vna_start=0, vna_stop=self.max_freq, vna_count=self.correct_width)
|
|
self.correct_delta = float(stop - start) / (self.correct_width - 1)
|
|
Hardware.SetVNA(vna_count=self.data_width)
|
|
else:
|
|
self.has_SetVNA = False
|
|
self.correct_delta = 1
|
|
# Restore persistent program state
|
|
self.init_path = os.path.join(os.path.dirname(ConfigPath), '.quisk_vna_init.pkl')
|
|
try:
|
|
fp = open(self.init_path, "r")
|
|
d = pickle.load(fp)
|
|
fp.close()
|
|
for k in d:
|
|
v = d[k]
|
|
if k in self.StateNames:
|
|
setattr(self, k, v)
|
|
except:
|
|
pass #traceback.print_exc()
|
|
# Record the basic application parameters
|
|
if sys.platform == 'win32':
|
|
h = self.main_frame.GetHandle()
|
|
else:
|
|
h = 0
|
|
QS.set_enable_bandscope(0)
|
|
# FFT size must equal the data_width so that all data points are returned!
|
|
QS.record_app(self, conf, self.data_width, self.data_width, self.data_width,
|
|
1, self.sample_rate, h)
|
|
# Make all the screens and hide all but one
|
|
self.graph = GraphScreen(frame, self.data_width, self.data_width, self.correct_width, self.correct_delta)
|
|
self.screen = self.graph
|
|
width = self.graph.width
|
|
self.help_screen = HelpScreen(frame, width, self.screen_height // 10)
|
|
self.help_screen.Hide()
|
|
# Make a vertical box to hold all the screens and the bottom rows
|
|
vertBox = self.vertBox = wx.BoxSizer(wx.VERTICAL)
|
|
frame.SetSizer(vertBox)
|
|
# Add the screens
|
|
vertBox.Add(self.graph, 1)
|
|
vertBox.Add(self.help_screen, 1)
|
|
# Add the spacer
|
|
vertBox.Add(Spacer(frame), 0, wx.EXPAND)
|
|
# Add the sizer for the buttons
|
|
szr1 = wx.BoxSizer(wx.HORIZONTAL)
|
|
vertBox.Add(szr1, 0, wx.EXPAND, 0)
|
|
# Make the buttons in row 1
|
|
self.buttons1 = buttons1 = []
|
|
self.screen_name = "Reflection"
|
|
self.graph.SetMode(self.screen_name)
|
|
b = RadioButtonGroup(frame, self.OnBtnScreen, (' Transmission ', 'Reflection', 'Help'), self.screen_name)
|
|
buttons1 += b.buttons
|
|
self.btn_run = b = QuiskCheckbutton(frame, self.OnBtnRun, 'Run')
|
|
buttons1.append(b)
|
|
self.btn_calibrate = b = QuiskPushbutton(frame, self.OnBtnCal, 'Calibrate..')
|
|
buttons1.append(b)
|
|
width = 0
|
|
for b in buttons1:
|
|
w, height = b.GetMinSize()
|
|
if width < w:
|
|
width = w
|
|
for i in range(24, 8, -2):
|
|
font = wx.Font(i, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
|
|
frame.SetFont(font)
|
|
w, h = frame.GetTextExtent('Start ')
|
|
if h < height * 9 // 10:
|
|
break
|
|
for b in buttons1:
|
|
b.SetMinSize((width, height))
|
|
# Frequency entry start and stop
|
|
t = wx.lib.stattext.GenStaticText(frame, -1, 'Start ')
|
|
t.SetFont(font)
|
|
t.SetBackgroundColour(conf.color_bg)
|
|
gap = max(2, height//8)
|
|
freq0 = t
|
|
e = wx.TextCtrl(frame, -1, '1', style=wx.TE_PROCESS_ENTER)
|
|
e.SetFont(font)
|
|
tw, z = e.GetTextExtent("xx30.333xxxxx")
|
|
e.SetMinSize((tw, height))
|
|
e.SetBackgroundColour(conf.color_entry)
|
|
self.freq_start_ctrl = e
|
|
frame.Bind(wx.EVT_TEXT_ENTER, self.OnNewFreq, source=e)
|
|
frame.Bind(wx.EVT_TEXT, self.OnNewFreq, source=e)
|
|
t = wx.lib.stattext.GenStaticText(frame, -1, 'Stop ')
|
|
t.SetFont(font)
|
|
t.SetBackgroundColour(conf.color_bg)
|
|
freq2 = t
|
|
e = wx.TextCtrl(frame, -1, '30', style=wx.TE_PROCESS_ENTER)
|
|
e.SetFont(font)
|
|
e.SetMinSize((tw, height))
|
|
e.SetBackgroundColour(conf.color_entry)
|
|
self.freq_stop_ctrl = e
|
|
frame.Bind(wx.EVT_TEXT_ENTER, self.OnNewFreq, source=e)
|
|
frame.Bind(wx.EVT_TEXT, self.OnNewFreq, source=e)
|
|
# Band buttons
|
|
ilst = []
|
|
slst = []
|
|
for l in conf.BandEdge: # Sort keys
|
|
if not (l in conf.bandLabels or l == '60'):
|
|
continue
|
|
try:
|
|
ilst.append((int(l), conf.BandEdge[l]))
|
|
except ValueError: # item is a string, not an integer
|
|
slst.append((l, conf.BandEdge[l]))
|
|
ilst.sort()
|
|
ilst.reverse()
|
|
slst.sort()
|
|
band = []
|
|
width = 0
|
|
for l in ilst + slst:
|
|
b = QuiskPushbutton(frame, self.OnBtnBand, str(l[0]))
|
|
b.bandEdge = l[1]
|
|
band.append(b)
|
|
w, h= b.GetMinSize()
|
|
if width < w:
|
|
width = w
|
|
# make a list of all buttons
|
|
self.buttons = buttons1 + band
|
|
# Add button row to sizer
|
|
gap = max(2, height // 8)
|
|
gap2 = max(2, height // 4)
|
|
szr1.Add(buttons1[0], 0, wx.RIGHT|wx.LEFT, gap)
|
|
szr1.Add(buttons1[1], 0, wx.RIGHT, gap)
|
|
szr1.Add(buttons1[2], 0, wx.RIGHT, gap)
|
|
szr1.Add(buttons1[3], 0, wx.RIGHT|wx.LEFT, gap2)
|
|
szr1.Add(buttons1[4], 0, wx.RIGHT|wx.LEFT, gap)
|
|
szr1.Add(freq0, 0, wx.ALIGN_CENTER_VERTICAL)
|
|
szr1.Add(self.freq_start_ctrl, 0, wx.RIGHT, gap)
|
|
szr1.Add(freq2, 0, wx.ALIGN_CENTER_VERTICAL)
|
|
szr1.Add(self.freq_stop_ctrl, 0, wx.RIGHT, gap)
|
|
for x in band:
|
|
szr1.Add(x, 1, wx.RIGHT, gap)
|
|
self.statusbar = self.main_frame.CreateStatusBar()
|
|
# Set top window size
|
|
self.main_frame.SetClientSize(wx.Size(self.graph.width, self.screen_height * 5 // 10))
|
|
w, h = self.main_frame.GetSize().Get()
|
|
self.main_frame.SetSizeHints(w, 1, w)
|
|
if hasattr(Hardware, 'pre_open'): # pre_open() is called before open()
|
|
Hardware.pre_open()
|
|
if conf.use_rx_udp == 10: # Hermes UDP protocol
|
|
self.add_version = False
|
|
conf.tx_ip = Hardware.hermes_ip
|
|
conf.tx_audio_port = conf.rx_udp_port
|
|
elif conf.use_rx_udp:
|
|
self.add_version = True # Add firmware version to config text
|
|
conf.rx_udp_decimation = 8 * 8 * 8
|
|
if not conf.tx_ip:
|
|
conf.tx_ip = conf.rx_udp_ip
|
|
if not conf.tx_audio_port:
|
|
conf.tx_audio_port = conf.rx_udp_port + 2
|
|
else:
|
|
self.add_version = False
|
|
# Open the hardware. This must be called before open_sound().
|
|
self.config_text = Hardware.open()
|
|
self.status_error = "No hardware response" # possible error messages
|
|
if self.config_text:
|
|
self.main_frame.SetConfigText(self.config_text)
|
|
if conf.use_rx_udp == 10: # Hermes UDP protocol
|
|
if self.config_text[0:12] == "Capture from":
|
|
self.status_error = ''
|
|
else:
|
|
self.config_text = "Missing config_text"
|
|
# Note: Subsequent calls to set channels must not name a higher channel number.
|
|
# Normally, these calls are only used to reverse the channels.
|
|
QS.open_sound(conf.name_of_sound_capt, '', self.sample_rate,
|
|
conf.data_poll_usec, conf.latency_millisecs,
|
|
'', conf.tx_ip, conf.tx_audio_port,
|
|
48000, 0, 0, 1.0, '', 48000)
|
|
self.Bind(wx.EVT_IDLE, self.graph.OnIdle)
|
|
frame.Show()
|
|
self.NewFreq(1000000, 30000000)
|
|
self.SetCalText()
|
|
self.WriteFields()
|
|
self.EnableButtons()
|
|
QS.set_fdx(1)
|
|
QS.set_rx_mode(0)
|
|
self.sound_thread = SoundThread()
|
|
self.sound_thread.start()
|
|
return True
|
|
def OnExit(self):
|
|
QS.close_rx_udp()
|
|
##self.local_conf.SaveState() # to save default radio selection
|
|
return 0
|
|
def SaveState(self):
|
|
if self.init_path: # save current program state
|
|
d = {}
|
|
for n in self.StateNames:
|
|
d[n] = getattr(self, n)
|
|
try:
|
|
fp = open(self.init_path, "w")
|
|
pickle.dump(d, fp)
|
|
fp.close()
|
|
except:
|
|
pass #traceback.print_exc()
|
|
def OnEndSession(self, event):
|
|
event.Skip()
|
|
self.OnBtnClose(event)
|
|
def OnBtnClose(self, event):
|
|
if self.has_SetVNA:
|
|
Hardware.SetVNA(key_down=0, do_tx=True)
|
|
time.sleep(0.5)
|
|
if self.sound_thread:
|
|
self.sound_thread.stop()
|
|
for i in range(0, 20):
|
|
if threading.activeCount() == 1:
|
|
break
|
|
time.sleep(0.1)
|
|
Hardware.close()
|
|
def OnBtnBand(self, event):
|
|
btn = event.GetEventObject()
|
|
start, stop = btn.bandEdge
|
|
start = float(start) * 1e-6
|
|
stop = float(stop) * 1e-6
|
|
self.freq_start_ctrl.SetValue(str(start))
|
|
self.freq_stop_ctrl.SetValue(str(stop))
|
|
def Calibrate(self):
|
|
self.graph.calibrate_tmp = [0] * self.correct_width
|
|
self.graph.calibrate_count = 0
|
|
self.graph.SetMode("Calibrate")
|
|
self.NewFreq(0, self.max_freq)
|
|
if self.has_SetVNA:
|
|
Hardware.SetVNA(key_down=1)
|
|
self.running = True
|
|
self.startup = True
|
|
def OnBtnCal(self, event):
|
|
if self.has_SetVNA:
|
|
Hardware.SetVNA(key_down=0, vna_start=0, vna_stop=self.max_freq, vna_count=self.correct_width)
|
|
dlg = CalibrateDialog(self)
|
|
dlg.ShowModal()
|
|
dlg.Destroy()
|
|
if application.has_SetVNA:
|
|
Hardware.SetVNA(key_down=0, vna_count=self.data_width)
|
|
def OnBtnScreen(self, event):
|
|
btn = event.GetEventObject()
|
|
self.screen_name = btn.GetLabel().strip()
|
|
if self.screen_name == 'Help':
|
|
self.help_screen.Show()
|
|
self.graph.Hide()
|
|
else:
|
|
self.help_screen.Hide()
|
|
self.graph.Show()
|
|
self.graph.SetMode(self.screen_name)
|
|
self.vertBox.Layout()
|
|
self.EnableButtons()
|
|
def OnBtnRun(self, event):
|
|
btn = event.GetEventObject()
|
|
run = btn.GetValue()
|
|
if run:
|
|
for b in self.buttons1:
|
|
if b != btn:
|
|
b.Enable(False)
|
|
else:
|
|
for b in self.buttons1:
|
|
b.Enable(True)
|
|
self.graph.SetMode(self.screen_name)
|
|
if not self.running and not self.OnNewFreq():
|
|
return
|
|
if self.has_SetVNA:
|
|
if run:
|
|
self.running = True
|
|
self.startup = True
|
|
Hardware.SetVNA(key_down=1)
|
|
else:
|
|
self.running = False
|
|
Hardware.SetVNA(key_down=0)
|
|
def EnableButtons(self):
|
|
if self.screen_name == 'Transmission':
|
|
if self.transmission_short is not None and len(self.transmission_short) == self.correct_width:
|
|
self.btn_run.Enable(1)
|
|
else:
|
|
self.btn_run.Enable(0)
|
|
elif self.screen_name == 'Reflection':
|
|
if (self.reflection_short is not None or self.reflection_open is not None) and len(self.reflection_short) == self.correct_width:
|
|
self.btn_run.Enable(1)
|
|
else:
|
|
self.btn_run.Enable(0)
|
|
else: # Help
|
|
self.btn_run.Enable(0)
|
|
def ShowFreq(self, freq, index):
|
|
self.frequency = freq
|
|
if hasattr(Hardware, 'ChangeFilterFrequency'):
|
|
Hardware.ChangeFilterFrequency(freq)
|
|
self.graph_freq = freq
|
|
self.graph_index = index
|
|
self.WriteFields()
|
|
def OnNewFreq(self, event=None):
|
|
if self.status_error and self.status_error[0:15] != "Error in Start ":
|
|
return False
|
|
try:
|
|
start = self.freq_start_ctrl.GetValue()
|
|
start = float(start) * 1e6
|
|
stop = self.freq_stop_ctrl.GetValue()
|
|
stop = float(stop) * 1e6
|
|
except:
|
|
self.status_error = "Error in Start or Stop freq"
|
|
#traceback.print_exc()
|
|
return False
|
|
start = int(start + 0.5)
|
|
stop = int(stop + 0.5)
|
|
if start > stop:
|
|
self.status_error = "Error in Start or Stop freq"
|
|
return False
|
|
if stop > self.max_freq:
|
|
stop = self.max_freq
|
|
self.freq_stop_ctrl.SetValue("%.6f" % (stop * 1.E-6))
|
|
self.status_error = ''
|
|
self.NewFreq(start, stop)
|
|
return True
|
|
def NewFreq(self, start, stop):
|
|
if application.has_SetVNA:
|
|
start, stop = Hardware.SetVNA(vna_start=start, vna_stop=stop)
|
|
self.graph.NewFreq(start, stop)
|
|
def SetCalText(self):
|
|
text = ''
|
|
if self.reflection_short is not None:
|
|
text += "S"
|
|
if self.reflection_open is not None:
|
|
text += "O"
|
|
if self.reflection_load is not None:
|
|
text += "L"
|
|
if text:
|
|
text = "Cal " + text
|
|
else:
|
|
text = "Cal x"
|
|
self.reflection_cal = text
|
|
text = ''
|
|
if self.transmission_short is not None:
|
|
text += "S"
|
|
if self.transmission_open is not None:
|
|
text += "O"
|
|
if text:
|
|
text = "Cal " + text
|
|
else:
|
|
text = "Cal x"
|
|
self.transmission_cal = text
|
|
def WriteFields(self):
|
|
index = self.graph_index
|
|
if index < 0:
|
|
index = 0
|
|
elif index >= self.data_width:
|
|
index = self.data_width - 1
|
|
freq = "Freq %.6f" % (self.frequency * 1E-6)
|
|
mode = self.graph.mode
|
|
if self.status_error:
|
|
text = self.status_error
|
|
elif not self.graph.data_mag:
|
|
if mode == 'Transmission':
|
|
text = u" %s %s" % (self.transmission_cal, freq)
|
|
elif mode == 'Reflection':
|
|
text = u" %s %s" % (self.reflection_cal, freq)
|
|
else:
|
|
text = ''
|
|
elif mode == 'Calibrate':
|
|
db = self.graph.data_mag[index]
|
|
phase = self.graph.data_phase[index]
|
|
text = u" %s Calibrate %.2f dB %.1f\u00B0" % (freq, db, phase)
|
|
elif mode == 'Transmission':
|
|
db = self.graph.data_mag[index]
|
|
phase = self.graph.data_phase[index]
|
|
text = u" %s %s Transmission %.2f dB %.1f\u00B0" % (self.transmission_cal, freq, db, phase)
|
|
elif mode == 'Reflection':
|
|
db = self.graph.data_mag[index]
|
|
phase = self.graph.data_phase[index]
|
|
aref = abs(self.graph.data_reflect[index])
|
|
swr = (1.0 + aref) / (1.0 - aref)
|
|
if not 0.999 <= swr <= 99:
|
|
swr = 99.0
|
|
text = u" %s %s Reflect ( %.2f dB %.1f\u00B0 ) SWR %.1f" % (self.reflection_cal, freq, db, phase, swr)
|
|
Z = self.graph.data_impedance[index]
|
|
mag = abs(Z)
|
|
phase = cmath.phase(Z) * 360. / (2.0 * math.pi)
|
|
freq = self.graph.data_freq[index]
|
|
z_real = Z.real
|
|
z_imag = Z.imag
|
|
if z_imag < 0:
|
|
text += u" Z \u03A9 ( %.1f - %.1fJ ) = ( %.1f %.1f\u00B0 )" % (z_real, abs(z_imag), mag, phase)
|
|
else:
|
|
text += u" Z \u03A9 ( %.1f + %.1fJ ) = ( %.1f %.1f\u00B0 )" % (z_real, z_imag, mag, phase)
|
|
if z_imag >= 0.5:
|
|
L = z_imag / (2.0 * math.pi * freq) * 1e9
|
|
Xp = (z_imag ** 2 + z_real ** 2) / z_imag
|
|
Lp = Xp / (2.0 * math.pi * freq) * 1e9
|
|
text += ' L %.0f nH' % L
|
|
if z_real > 0.01:
|
|
Rp = (z_imag ** 2 + z_real ** 2) / z_real
|
|
text += " ( %.1f || %.0f nH )" % (Rp, Lp)
|
|
elif z_imag < -0.5:
|
|
C = -1.0 / (2.0 * math.pi * freq * z_imag) * 1e9
|
|
Xp = (z_imag ** 2 + z_real ** 2) / z_imag
|
|
Cp = -1.0 / (2.0 * math.pi * freq * Xp) * 1e9
|
|
text += ' C %.3f nF' % C
|
|
if z_real > 0.01:
|
|
Rp = (z_imag ** 2 + z_real ** 2) / z_real
|
|
text += " ( %.1f || %.3f nF )" % (Rp, Cp)
|
|
self.statusbar.SetStatusText(text)
|
|
def PostStartup(self): # called once after sound attempts to start
|
|
pass
|
|
def OnReadSound(self): # called at frequent intervals
|
|
self.timer = time.time()
|
|
dat = QS.get_graph(0, 1.0, 0)
|
|
if dat and self.running:
|
|
dat = list(dat)
|
|
try:
|
|
start = dat.index(0)
|
|
except ValueError:
|
|
self.save_data += dat
|
|
return
|
|
data = self.save_data + dat[0:start]
|
|
self.save_data = dat[start+1:]
|
|
if self.graph.mode == 'Calibrate':
|
|
if len(data) != self.correct_width:
|
|
if DEBUG: print(' bad calibrate array', len(data), self.correct_width)
|
|
return
|
|
else:
|
|
if len(data) != self.data_width:
|
|
if DEBUG: print(' bad data array', len(data), self.data_width)
|
|
return
|
|
for i in range(len(data)):
|
|
data[i] /= 2147483647.0
|
|
if self.startup: # always skip the first block of data
|
|
self.startup = False
|
|
else:
|
|
self.graph.OnGraphData(data)
|
|
if QS.get_overrange() and self.running:
|
|
self.clip_time0 = self.timer
|
|
self.status_error = " *** CLIP ***"
|
|
self.graph.SetDisplayMsg("Clip")
|
|
if self.clip_time0:
|
|
if self.timer - self.clip_time0 > 1.0:
|
|
self.clip_time0 = 0
|
|
self.status_error = ''
|
|
self.graph.SetDisplayMsg()
|
|
if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks
|
|
self.heart_time0 = self.timer
|
|
Hardware.HeartBeat()
|
|
if self.add_version and self.firmware_version is None:
|
|
self.firmware_version = Hardware.GetFirmwareVersion()
|
|
if self.firmware_version is not None:
|
|
if self.firmware_version < 3:
|
|
self.status_error = "Need firmware ver 3"
|
|
else:
|
|
self.status_error = ''
|
|
# Set text fields
|
|
if self.timer - self.time0 > 0.5:
|
|
self.time0 = self.timer
|
|
#print "len %5d re %9.6f im %9.6f mag %9.6f phase %7.2f" % (len(data),
|
|
# volts.real, volts.imag, abs(volts), phase)
|
|
#print "Z re %12.2f im %12.2f mag %12.2f phase %7.2f" % (zzz.real, zzz.imag,
|
|
# abs(zzz), cmath.phase(zzz) * 360. / (2.0 * math.pi))
|
|
self.WriteFields()
|
|
|
|
def main():
|
|
"""If quisk is installed as a package, you can run it with quisk.main()."""
|
|
App()
|
|
application.MainLoop()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|