Skip to content

Instantly share code, notes, and snippets.

@csete
Created February 22, 2011 16:44
Show Gist options
  • Save csete/838950 to your computer and use it in GitHub Desktop.
Save csete/838950 to your computer and use it in GitHub Desktop.
Modified Quisk 3.5.1 to follow desktop theme instead of ugly custom colors
#! /usr/bin/python
# All QUISK software is Copyright (C) 2006-2010 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, a software defined radio.
Usage: python quisk.py [-c | --config config_file_path]
This can also be installed as a package and run as quisk.main().
"""
# Change to the directory of quisk.py. This is necessary to import Quisk packages
# and to load other extension modules that link against _quisk.so. It also helps to
# find ./__init__.py and ./help.html.
import sys, os
os.chdir(os.path.normpath(os.path.dirname(__file__)))
if sys.path[0] != "'.'": # Make sure the current working directory is on path
sys.path.insert(0, '.')
import wx, wx.html, wx.lib.buttons, wx.lib.stattext, wx.lib.colourdb
import math, cmath, time, traceback
import threading, pickle, webbrowser
import _quisk as QS
from types import *
# 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')
argv_options = parser.parse_args()[0]
ConfigPath = argv_options.config_file_path # Get config file path
if not ConfigPath: # Use default path
if sys.platform == 'win32':
path = os.getenv('HOMEDRIVE', '') + os.getenv('HOMEPATH', '')
for dir in ("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')
# These FFT sizes have multiple small factors, and are prefered for efficiency:
fftPreferedSizes = (416, 448, 480, 512, 576, 640, 672, 704, 768, 800, 832,
864, 896, 960, 1024, 1056, 1120, 1152, 1248, 1280, 1344, 1408, 1440, 1536,
1568, 1600, 1664, 1728, 1760, 1792, 1920, 2016, 2048, 2080, 2112, 2240, 2304,
2400, 2464, 2496, 2560, 2592, 2688, 2816, 2880, 2912)
def round(x): # round float to nearest integer
if x >= 0:
return int(x + 0.5)
else:
return - int(-x + 0.5)
class Timer:
"""Debug: measure and print times every ptime seconds.
Call with msg == '' to start timer, then with a msg to record the time.
"""
def __init__(self, ptime = 1.0):
self.ptime = ptime # frequency to print in seconds
self.time0 = 0 # time zero; measure from this time
self.time_print = 0 # last time data was printed
self.timers = {} # one timer for each msg
self.names = [] # ordered list of msg
self.heading = 1 # print heading on first use
def __call__(self, msg):
tm = time.time()
if msg:
if not self.time0: # Not recording data
return
if self.timers.has_key(msg):
count, average, highest = self.timers[msg]
else:
self.names.append(msg)
count = 0
average = highest = 0.0
count += 1
delta = tm - self.time0
average += delta
if highest < delta:
highest = delta
self.timers[msg] = (count, average, highest)
if tm - self.time_print > self.ptime: # time to print results
self.time0 = 0 # end data recording, wait for reset
self.time_print = tm
if self.heading:
self.heading = 0
print "count, msg, avg, max (msec)"
print "%4d" % count,
for msg in self.names: # keep names in order
count, average, highest = self.timers[msg]
if not count:
continue
average /= count
print " %s %7.3f %7.3f" % (msg, average * 1e3, highest * 1e3),
self.timers[msg] = (0, 0.0, 0.0)
print
else: # reset the time to zero
self.time0 = tm # Start timer
if not self.time_print:
self.time_print = tm
## T = Timer() # Make a timer instance
class SoundThread(threading.Thread):
"""Create a second (non-GUI) thread to read, process and play sound."""
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 FrequencyDisplay(wx.lib.stattext.GenStaticText):
"""Create a frequency display widget."""
def __init__(self, frame, gbs, width, height):
wx.lib.stattext.GenStaticText.__init__(self, frame, -1, '3',
style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE)
border = 4
for points in range(30, 6, -1):
font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL)
self.SetFont(font)
w, h = self.GetTextExtent('333 444 555 Hz')
if w < width and h < height - border * 2:
break
self.SetSizeHints(w, h, w * 5, h)
self.height = h
self.points = points
border = self.border = (height - self.height) / 2
self.height_and_border = h + border * 2
self.SetBackgroundColour(conf.color_freq)
gbs.Add(self, (0, 0), (1, 3),
flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=border)
def Clip(self, clip):
"""Change color to indicate clipping."""
if clip:
self.SetBackgroundColour('deep pink')
else:
self.SetBackgroundColour(conf.color_freq)
self.Refresh()
def Display(self, freq):
"""Set the frequency to be displayed."""
freq = int(freq)
if freq >= 0:
t = str(freq)
minus = ''
else:
t = str(-freq)
minus = '- '
l = len(t)
if l > 9:
txt = "%s%s %s %s %s" % (minus, t[0:-9], t[-9:-6], t[-6:-3], t[-3:])
elif l > 6:
txt = "%s%s %s %s" % (minus, t[0:-6], t[-6:-3], t[-3:])
elif l > 3:
txt = "%s%s %s" % (minus, t[0:-3], t[-3:])
else:
txt = minus + t
self.SetLabel('%s Hz' % txt)
class SliderBoxV(wx.BoxSizer):
"""A vertical box containing a slider and a text heading"""
# Note: A vertical wx slider has the max value at the bottom. This is
# reversed for this control.
def __init__(self, parent, text, init, themax, handler, display=False):
wx.BoxSizer.__init__(self, wx.VERTICAL)
self.slider = wx.Slider(parent, -1, init, 0, themax, style=wx.SL_VERTICAL)
self.slider.Bind(wx.EVT_SCROLL, handler)
sw, sh = self.slider.GetSize()
self.text = text
self.themax = themax
if display: # Display the slider value when it is thumb'd
self.text_ctrl = wx.StaticText(parent, -1, str(themax), style=wx.ALIGN_CENTER)
w1, h1 = self.text_ctrl.GetSize() # Measure size with max number
self.text_ctrl.SetLabel(text)
w2, h2 = self.text_ctrl.GetSize() # Measure size with text
self.width = max(w1, w2, sw)
self.text_ctrl.SetSizeHints(self.width, -1, self.width)
self.slider.Bind(wx.EVT_SCROLL_THUMBTRACK, self.Change)
self.slider.Bind(wx.EVT_SCROLL_THUMBRELEASE, self.ChangeDone)
else:
self.text_ctrl = wx.StaticText(parent, -1, text)
w2, h2 = self.text_ctrl.GetSize() # Measure size with text
self.width = max(w2, sw)
self.Add(self.text_ctrl, 0, wx.ALIGN_CENTER)
self.Add(self.slider, 1, wx.ALIGN_CENTER)
def Change(self, event):
event.Skip()
self.text_ctrl.SetLabel(str(self.themax - self.slider.GetValue()))
def ChangeDone(self, event):
event.Skip()
self.text_ctrl.SetLabel(self.text)
def GetValue(self):
return self.themax - self.slider.GetValue()
def SetValue(self, value):
# Set slider visual position; does not call handler
self.slider.SetValue(self.themax - value)
class _QuiskText1(wx.lib.stattext.GenStaticText):
# Self-drawn text for QuiskText.
def __init__(self, parent, size_text, height, style):
wx.lib.stattext.GenStaticText.__init__(self, parent, -1, '',
pos = wx.DefaultPosition, size = wx.DefaultSize,
style = wx.ST_NO_AUTORESIZE|style,
name = "QuiskText1")
self.size_text = size_text
self.pen = wx.Pen(conf.color_btn, 2)
self.brush = wx.Brush(conf.color_freq)
self.SetSizeHints(-1, height, -1, height)
def _MeasureFont(self, dc, width, height):
# Set decreasing point size until size_text fits in the space available
for points in range(20, 6, -1):
font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL)
dc.SetFont(font)
w, h = dc.GetTextExtent(self.size_text)
if w < width and h < height:
break
self.size_text = ''
self.SetFont(font)
def OnPaint(self, event):
dc = wx.PaintDC(self)
width, height = self.GetClientSize()
if not width or not height:
return
dc.SetPen(self.pen)
dc.SetBrush(self.brush)
dc.DrawRectangle(1, 1, width-1, height-1)
label = self.GetLabel()
if not label:
return
if self.size_text:
self._MeasureFont(dc, width-4, height-4)
else:
dc.SetFont(self.GetFont())
if self.IsEnabled():
dc.SetTextForeground(self.GetForegroundColour())
else:
dc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT))
style = self.GetWindowStyleFlag()
w, h = dc.GetTextExtent(label)
y = (height - h) / 2
if y < 0:
y = 0
if style & wx.ALIGN_RIGHT:
x = width - w - 4
elif style & wx.ALIGN_CENTER:
x = (width - w)/2
else:
x = 4
dc.DrawText(label, x, y)
class QuiskText(wx.BoxSizer):
# A one-line text display left/right/center justified and vertically centered.
# The height of the control is fixed as "height". The width is expanded.
# The font is chosen so size_text fits in the client area.
def __init__(self, parent, size_text, height, style=0):
wx.BoxSizer.__init__(self, wx.HORIZONTAL)
self.TextCtrl = _QuiskText1(parent, size_text, height, style)
self.Add(self.TextCtrl, 1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL)
def SetLabel(self, label):
self.TextCtrl.SetLabel(label)
# Start of our button classes. They are compatible with wxPython GenButton
# buttons. Use the usual methods for access:
# GetLabel(self), SetLabel(self, label): Get and set the label
# Enable(self, flag), Disable(self), IsEnabled(self): Enable / Disable
# GetValue(self), SetValue(self, value): Get / Set check button state True / False
# SetIndex(self, index): For cycle buttons, set the label from its index
class QuiskButtons:
"""Base class for special buttons."""
button_bezel = 1 # size of button bezel in pixels
def InitButtons(self, text):
#self.SetBezelWidth(self.button_bezel)
#self.SetBackgroundColour(conf.color_btn)
self.SetUseFocusIndicator(False)
#self.font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL)
#self.SetFont(self.font)
if text:
w, h = self.GetTextExtent(text)
else:
w, h = self.GetTextExtent("OK")
self.Disable() # create a size for null text, but Disable()
w += self.button_bezel * 2 + self.GetCharWidth()
h = h * 17 / 10
#h += self.button_bezel * 2
self.SetSizeHints(w, h, w * 6, h, 1, 1)
def OnKeyDown(self, event):
pass
def OnKeyUp(self, event):
pass
class QuiskPushbutton(QuiskButtons, wx.lib.buttons.ThemedGenButton):
"""A plain push button widget."""
def __init__(self, parent, command, text, use_right=False):
wx.lib.buttons.ThemedGenButton.__init__(self, parent, -1, text)
self.command = command
self.Bind(wx.EVT_BUTTON, self.OnButton)
self.InitButtons(text)
self.direction = 1
if use_right:
self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
def OnButton(self, event):
if self.command:
self.command(event)
def OnRightDown(self, event):
self.direction = -1
self.OnLeftDown(event)
def OnRightUp(self, event):
self.OnLeftUp(event)
self.direction = 1
class QuiskRepeatbutton(QuiskButtons, wx.lib.buttons.ThemedGenButton):
"""A push button that repeats when held down."""
def __init__(self, parent, command, text, up_command=None, use_right=False):
wx.lib.buttons.GenButton.__init__(self, parent, -1, text)
self.command = command
self.up_command = up_command
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.OnTimer)
self.Bind(wx.EVT_BUTTON, self.OnButton)
self.InitButtons(text)
self.repeat_state = 0 # repeater button inactive
self.direction = 1
if use_right:
self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
def SendCommand(self, command):
if command:
event = wx.PyEvent()
event.SetEventObject(self)
command(event)
def OnLeftDown(self, event):
if self.IsEnabled():
self.shift = event.ShiftDown()
self.control = event.ControlDown()
self.SendCommand(self.command)
self.repeat_state = 1 # first button push
self.timer.Start(milliseconds=300, oneShot=True)
wx.lib.buttons.GenButton.OnLeftDown(self, event)
def OnLeftUp(self, event):
if self.IsEnabled():
self.SendCommand(self.up_command)
self.repeat_state = 0
self.timer.Stop()
wx.lib.buttons.GenButton.OnLeftUp(self, event)
def OnRightDown(self, event):
if self.IsEnabled():
self.shift = event.ShiftDown()
self.control = event.ControlDown()
self.direction = -1
self.OnLeftDown(event)
def OnRightUp(self, event):
if self.IsEnabled():
self.OnLeftUp(event)
self.direction = 1
def OnTimer(self, event):
if self.repeat_state == 1: # after first push, turn on repeats
self.timer.Start(milliseconds=150, oneShot=False)
self.repeat_state = 2
if self.repeat_state: # send commands until button is released
self.SendCommand(self.command)
def OnButton(self, event):
pass # button command not used
class QuiskCheckbutton(QuiskButtons, wx.lib.buttons.ThemedGenToggleButton):
"""A button that pops up and down, and changes color with each push."""
# Check button; get the checked state with self.GetValue()
def __init__(self, parent, command, text, color=None):
wx.lib.buttons.GenToggleButton.__init__(self, parent, -1, text)
self.InitButtons(text)
self.Bind(wx.EVT_BUTTON, self.OnButton)
self.button_down = 0 # used for radio buttons
self.command = command
#if color is None:
# self.color = conf.color_check_btn
#else:
# self.color = color
def SetValue(self, value, do_cmd=False):
wx.lib.buttons.GenToggleButton.SetValue(self, value)
self.button_down = value
#if value:
# self.SetBackgroundColour(self.color)
#else:
# self.SetBackgroundColour(conf.color_btn)
if do_cmd and self.command:
event = wx.PyEvent()
event.SetEventObject(self)
self.command(event)
def OnButton(self, event):
#if self.GetValue():
# self.SetBackgroundColour(self.color)
#else:
# self.SetBackgroundColour(conf.color_btn)
if self.command:
self.command(event)
class QuiskCycleCheckbutton(QuiskCheckbutton):
"""A button that cycles through its labels with each push.
The button is up for labels[0], down for all other labels. Change to the
next label for each push. If you call SetLabel(), the label must be in the list.
The self.index is the index of the current label.
"""
def __init__(self, parent, command, labels, color=None, is_radio=False):
self.labels = list(labels) # Be careful if you change this list
self.index = 0 # index of selected label 0, 1, ...
self.direction = 0 # 1 for up, -1 for down, 0 for no change to index
self.is_radio = is_radio # Is this a radio cycle button?
#if color is None:
# color = conf.color_cycle_btn
QuiskCheckbutton.__init__(self, parent, command, labels[0])#, color)
self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
def SetLabel(self, label, do_cmd=False):
self.index = self.labels.index(label)
QuiskCheckbutton.SetLabel(self, label)
QuiskCheckbutton.SetValue(self, self.index)
if do_cmd and self.command:
event = wx.PyEvent()
event.SetEventObject(self)
self.command(event)
def SetIndex(self, index, do_cmd=False):
self.index = index
QuiskCheckbutton.SetLabel(self, self.labels[index])
QuiskCheckbutton.SetValue(self, index)
if do_cmd and self.command:
event = wx.PyEvent()
event.SetEventObject(self)
self.command(event)
def OnButton(self, event):
if not self.is_radio or self.button_down:
self.direction = 1
self.index += 1
if self.index >= len(self.labels):
self.index = 0
self.SetIndex(self.index)
else:
self.direction = 0
if self.command:
self.command(event)
def OnRightDown(self, event): # Move left in the list of labels
if not self.is_radio or self.GetValue():
self.index -= 1
if self.index < 0:
self.index = len(self.labels) - 1
self.SetIndex(self.index)
self.direction = -1
if self.command:
self.command(event)
class RadioButtonGroup:
"""This class encapsulates a group of radio buttons. This class is not a button!
The "labels" is a list of labels for the toggle buttons. An item
of labels can be a list/tuple, and the corresponding button will
be a cycle button.
"""
def __init__(self, parent, command, labels, default):
self.command = command
self.buttons = []
self.button = None
for text in labels:
if type(text) in (ListType, TupleType):
b = QuiskCycleCheckbutton(parent, self.OnButton, text, is_radio=True)
for t in text:
if t == default and self.button is None:
b.SetLabel(t)
self.button = b
else:
b = QuiskCheckbutton(parent, self.OnButton, text)
if text == default and self.button is None:
b.SetValue(True)
self.button = b
self.buttons.append(b)
def SetLabel(self, label, do_cmd=False):
self.button = None
for b in self.buttons:
if self.button is not None:
b.SetValue(False)
elif isinstance(b, QuiskCycleCheckbutton):
try:
index = b.labels.index(label)
except ValueError:
b.SetValue(False)
continue
else:
b.SetIndex(index)
self.button = b
b.SetValue(True)
elif b.GetLabel() == label:
b.SetValue(True)
self.button = b
else:
b.SetValue(False)
if do_cmd and self.command and self.button:
event = wx.PyEvent()
event.SetEventObject(self.button)
self.command(event)
def GetButtons(self):
return self.buttons
def OnButton(self, event):
win = event.GetEventObject()
for b in self.buttons:
if b is win:
self.button = b
b.SetValue(True)
else:
b.SetValue(False)
if self.command:
self.command(event)
def GetLabel(self):
if not self.button:
return None
return self.button.GetLabel()
def GetSelectedButton(self): # return the selected button
return self.button
class ConfigScreen(wx.ScrolledWindow):
"""Display the configuration and status screen."""
def __init__(self, parent, width, fft_size):
wx.ScrolledWindow.__init__(self, parent,
pos = (0, 0),
size = (width, 100),
style = wx.VSCROLL | wx.NO_BORDER)
self.SetBackgroundColour(conf.color_graph)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_SCROLLWIN, self.OnScroll)
#self.Bind(wx.EVT_SCROLLWIN_THUMBRELEASE, self.OnScrollDone)
self.Bind(wx.EVT_IDLE, self.OnScrollDone)
self.width = width
self.setscroll = True
self.rx_phase = None
self.fft_size = fft_size
self.interupts = 0
self.read_error = -1
self.write_error = -1
self.underrun_error = -1
self.fft_error = -1
self.latencyCapt = -1
self.latencyPlay = -1
self.y_scale = 0
self.y_zero = 0
self.rate_min = -1
self.rate_max = -1
self.chan_min = -1
self.chan_max = -1
self.mic_max_display = 0
self.err_msg = "No response"
self.msg1 = ""
self.dev_capt, self.dev_play = QS.sound_devices()
self.controls = []
self.controls_visible = True
self.tabstops = [0] * 9
ts = self.tabstops
points = 24
while points > 4:
self.font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL)
self.SetFont(self.font)
charx = self.charx = self.GetCharWidth()
chary = self.chary = self.GetCharHeight()
ts[0] = charx
w, h = self.GetTextExtent("Capture errors 99999")
ts[1] = ts[0] + w
ts[2] = ts[1] + charx * 2
w, h = self.GetTextExtent("Capture latency 999999")
ts[3] = ts[2] + w
ts[4] = ts[3] + charx * 2
w, h = self.GetTextExtent("Playback latency 999999")
ts[5] = ts[4] + w
ts[6] = ts[5] + charx * 2
w, h = self.GetTextExtent("Total latency 999999")
ts[7] = ts[6] + w
ts[8] = ts[7] + charx * 2
if ts[8] < width:
break
points -= 2
self.dy = chary # line spacing
self.mem_height = self.dy * 4
self.bitmap = wx.EmptyBitmap(width, self.mem_height)
self.mem_rect = wx.Rect(0, 0, width, self.mem_height)
self.mem_dc = wx.MemoryDC(self.bitmap)
br = wx.Brush(conf.color_graph)
self.mem_dc.SetBackground(br)
self.mem_dc.SetFont(self.font)
self.mem_dc.Clear()
def OnPaint(self, event):
dc = wx.PaintDC(self)
dc.SetFont(self.font)
dc.SetTextForeground('Black')
x0 = self.tabstops[0]
x, y = self.GetViewStart()
self.y = -y
# Make and blit variable data
self.MakeBitmap()
dc.Blit(0, self.y, self.width, self.mem_height, self.mem_dc, 0, 0)
self.y += self.mem_height # height of bitmap
if conf.config_file_exists:
t = "Using configuration file %s" % conf.config_file_path
else:
dc.SetTextForeground('Red')
t = "Configuration file %s was not found" % conf.config_file_path
dc.DrawText(t, x0, self.y)
dc.SetTextForeground('Black')
self.y += self.dy
dc.DrawText(application.config_text, x0, self.y)
self.y += self.dy
if conf.name_of_sound_play:
t = "Play rate %d to %s." % (conf.playback_rate, conf.name_of_sound_play)
else:
t = "No playback device"
dc.DrawText(t, x0, self.y)
self.y += self.dy
if conf.microphone_name:
t = "Microphone sample rate %d from %s." % (conf.mic_sample_rate, conf.microphone_name)
dc.DrawText(t, x0, self.y)
self.y += self.dy
if conf.name_of_mic_play:
t = "Microphone playback rate %d to %s." % (conf.mic_playback_rate, conf.name_of_mic_play)
dc.DrawText(t, x0, self.y)
self.y += self.dy
self.y += self.dy / 2
if not self.rx_phase:
# Make controls
xxx = x0
self.rx_phase = ph = wx.Button(self, -1, "Rx Phase...")
self.Bind(wx.EVT_BUTTON, self.OnBtnPhase, ph)
x1, y1 = ph.GetSizeTuple()
ycenter = self.y + y1 / 2
ph.SetPosition((x0, self.y))
self.controls.append(ph)
xxx += x1 + self.charx * 4
self.control_height = y1
# Choice (combo) box for decimation
lst = Hardware.VarDecimGetChoices()
if lst:
txt = Hardware.VarDecimGetLabel()
t = wx.StaticText(self, -1, txt)
x1, y1 = t.GetSizeTuple()
t.SetPosition((xxx, ycenter - y1 / 2))
self.controls.append(t)
xxx += x1 + self.charx * 2
c = wx.Choice(self, -1, choices=lst)
x1, y1 = c.GetSizeTuple()
c.SetPosition((xxx, ycenter - y1 / 2))
self.controls.append(c)
xxx += x1 + self.charx * 4
self.Bind(wx.EVT_CHOICE, application.OnBtnDecimation, c)
index = Hardware.VarDecimGetIndex()
c.SetSelection(index)
self.y += self.control_height + self.dy
dc.DrawText("Available devices for capture:", x0, self.y)
self.y += self.dy
for name in self.dev_capt:
dc.DrawText(' ' + name, x0, self.y)
self.y += self.dy
dc.DrawText("Available devices for playback:", x0, self.y)
self.y += self.dy
for name in self.dev_play:
dc.DrawText(' ' + name, x0, self.y)
self.y += self.dy
self.y += self.dy
# t = "Rx Phase..."
# w, h = dc.GetTextExtent(t)
# r = wx.Rect(x0, self.y, w + 10, h + 10)
# dc.DrawRoundedRectangleRect(r, 4)
# dc.DrawLabel(t, r, wx.ALIGN_CENTER)
# self.y += h + 10
if self.setscroll: # Set the scroll size once
self.setscroll = False
self.height = self.y
self.SetScrollbars(1, 1, self.width, self.height)
def MakeRow2(self, dc, *args):
for col in range(len(args)):
x = self.tabstops[col]
t = args[col]
if t is not None:
t = str(t)
if col % 2 == 1:
w, h = dc.GetTextExtent(t)
x -= w
dc.DrawText(t, x, self.mem_y)
self.mem_y += self.dy
def MakeBitmap(self):
self.mem_dc.Clear()
self.mem_y = 0
self.MakeRow2(self.mem_dc, "Interrupts", self.interupts,
"Capture latency", self.latencyCapt,
"Playback latency", self.latencyPlay,
"Total latency", self.latencyCapt + self.latencyPlay)
self.MakeRow2(self.mem_dc, "Capture errors", self.read_error,
"Playback errors", self.write_error,
"Underrun errors", self.underrun_error,
"FFT errors", self.fft_error)
if conf.microphone_name:
level = "%3.0f" % self.mic_max_display
else:
level = "None"
self.MakeRow2(self.mem_dc, "Sample rate", application.sample_rate,
"Mic level dB", level,
None, None, "FFT points", self.fft_size)
if self.err_msg: # Error message on line 4
x = self.tabstops[0]
self.mem_dc.SetTextForeground('Red')
self.mem_dc.DrawText(self.err_msg, x, self.mem_y)
self.mem_dc.SetTextForeground('Black')
self.mem_y += self.dy
def OnGraphData(self, data=None):
(self.rate_min, self.rate_max, sample_rate, self.chan_min, self.chan_max,
self.msg1, self.unused, self.err_msg,
self.read_error, self.write_error, self.underrun_error,
self.latencyCapt, self.latencyPlay, self.interupts, self.fft_error, self.mic_max_display,
self.data_poll_usec
) = QS.get_state()
self.mic_max_display = 20.0 * math.log10((self.mic_max_display + 1) / 32767.0)
self.RefreshRect(self.mem_rect)
def ChangeYscale(self, y_scale):
pass
def ChangeYzero(self, y_zero):
pass
def OnIdle(self, event):
pass
def SetTxFreq(self, tx_freq, rx_freq):
pass
def OnBtnPhase(self, event):
application.screenBtnGroup.SetLabel('Graph', do_cmd=True)
if application.w_phase:
application.w_phase.Raise()
else:
application.w_phase = QAdjustPhase(self, self.width, 'rx')
def OnScroll(self, event):
# Scrolling controls within this window works poorly, so we try
# to hide the controls until scrolling is finished.
event.Skip()
if self.controls_visible:
self.controls_visible = False
for c in self.controls:
c.Hide()
def OnScrollDone(self, event):
event.Skip()
self.controls_visible = True
for c in self.controls:
c.Show()
class GraphDisplay(wx.Window):
"""Display the FFT 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 = [(0, 0), (1,1)] # initial fake graph data
self.SetBackgroundColour(conf.color_graph)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown)
self.Bind(wx.EVT_RIGHT_DOWN, parent.OnRightDown)
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.tune_rx = 0 # Current X position of Rx tuning line or zero
self.scale = 20 # pixels per 10 dB
self.peak_hold = 9999 # time constant for holding peak value
self.height = 10
self.y_min = 1000
self.y_max = 0
self.max_height = application.screen_height
self.tuningPenTx = wx.Pen('Red', 1)
self.tuningPenRx = wx.Pen('Green', 1)
self.backgroundPen = wx.Pen(self.GetBackgroundColour(), 1)
self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID)
if sys.platform == 'win32':
self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter)
def OnEnter(self, event):
if not application.w_phase:
self.SetFocus() # Set focus so we get mouse wheel events
def OnPaint(self, event):
#print 'GraphDisplay', self.GetUpdateRegion().GetBox()
dc = wx.PaintDC(self)
#dc.SetPen(wx.BLACK_PEN)
pen = wx.Pen(conf.color_fft, 1, wx.SOLID)
dc.SetPen(pen)
dc.DrawLines(self.line)
x = self.tune_tx
dc.SetPen(self.tuningPenTx)
dc.DrawLine(x, 0, x, self.max_height)
if self.tune_rx:
dc.SetPen(self.tuningPenRx)
dc.DrawLine(self.tune_rx, 20, self.tune_rx, self.max_height)
if not self.parent.in_splitter:
dc.SetPen(self.horizPen)
chary = self.chary
y = self.zeroDB
for i in range(0, -99999, -10):
if y >= chary / 2:
dc.DrawLine(0, y, self.graph_width, y) # y line
y = y + self.scale
if y > self.height:
break
def SetHeight(self, height):
self.height = height
self.SetSize((self.graph_width, height))
def OnGraphData(self, data):
x = 0
for y in data: # y is in dB, -130 to 0
y = self.zeroDB - int(y * self.scale / 10.0 + 0.5)
try:
y0 = self.line[x][1]
except IndexError:
self.line.append([x, y])
else:
if y > y0:
y = min(y, y0 + self.peak_hold)
self.line[x] = [x, y]
x = x + 1
self.Refresh()
def XXOnGraphData(self, data):
line = []
x = 0
y_min = 1000
y_max = 0
for y in data: # y is in dB, -130 to 0
y = self.zeroDB - int(y * self.scale / 10.0 + 0.5)
if y > y_max:
y_max = y
if y < y_min:
y_min = y
line.append((x, y))
x = x + 1
ymax = max(y_max, self.y_max)
ymin = min(y_min, self.y_min)
rect = wx.Rect(0, ymin, 1000, ymax - ymin)
self.y_min = y_min
self.y_max = y_max
self.line = line
self.Refresh() #rect=rect)
def SetTuningLine(self, tune_tx, tune_rx):
dc = wx.ClientDC(self)
dc.SetPen(self.backgroundPen)
dc.DrawLine(self.tune_tx, 0, self.tune_tx, self.max_height)
if self.tune_rx:
dc.DrawLine(self.tune_rx, 0, self.tune_rx, self.max_height)
dc.SetPen(self.tuningPenTx)
dc.DrawLine(tune_tx, 0, tune_tx, self.max_height)
if tune_rx:
dc.SetPen(self.tuningPenRx)
dc.DrawLine(tune_rx, 20, tune_rx, self.max_height)
self.tune_tx = tune_tx
self.tune_rx = tune_rx
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, 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?
if in_splitter:
self.y_scale = conf.waterfall_graph_y_scale
self.y_zero = conf.waterfall_graph_y_zero
else:
self.y_scale = conf.graph_y_scale
self.y_zero = conf.graph_y_zero
self.VFO = 0
self.WheelMod = 50 # Round frequency when using mouse wheel
self.txFreq = 0
self.sample_rate = application.sample_rate
self.data_width = data_width
self.graph_width = graph_width
self.doResize = False
#self.pen_tick = wx.Pen("Black", 1, wx.SOLID)
self.pen_tick = wx.Pen(conf.color_tick, 1, wx.SOLID)
self.font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL)
self.SetFont(self.font)
w = self.GetCharWidth() * 14 / 10
h = self.GetCharHeight()
self.charx = w
self.chary = h
self.tick = max(2, h * 3 / 10)
self.originX = w * 5
self.offsetY = h + self.tick
self.width = self.originX + self.graph_width + self.tick + self.charx * 2
self.height = application.screen_height * 3 / 10
self.x0 = self.originX + self.graph_width / 2 # center of graph
self.tuningX = self.x0
self.originY = 10
self.zeroDB = 10 # y location of zero dB; may be above the top of the graph
self.scale = 10
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_RIGHT_DOWN, self.OnRightDown)
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
self.Bind(wx.EVT_MOTION, self.OnMotion)
self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel)
self.MakeDisplay()
def MakeDisplay(self):
self.display = GraphDisplay(self, self.originX, 0, self.graph_width, 5, self.chary)
self.display.zeroDB = self.zeroDB
def OnPaint(self, event):
dc = wx.PaintDC(self)
if 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
event.Skip()
def ResizeGraph(self):
"""Change the height of the graph.
Changing the width interactively is not allowed because the FFT size is fixed.
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.display.scale = self.scale
self.doResize = False
self.Refresh()
def ChangeYscale(self, y_scale):
self.y_scale = y_scale
self.doResize = True
def ChangeYzero(self, y_zero):
self.y_zero = y_zero
self.doResize = True
def MakeYScale(self):
chary = self.chary
scale = (self.originY - chary) * 10 / (self.y_scale + 20) # Number of pixels per 10 dB
scale = max(1, scale)
q = (self.originY - chary ) / scale / 2
zeroDB = chary + q * scale - self.y_zero * scale / 10
if zeroDB > chary:
zeroDB = chary
self.scale = scale
self.zeroDB = zeroDB
self.display.zeroDB = self.zeroDB
QS.record_graph(self.originX, self.zeroDB, self.scale)
def MakeYTicks(self, dc):
chary = self.chary
x1 = self.originX - self.tick * 1 # left of tick mark
x2 = self.originX - 1 # x location of y axis
x3 = self.originX + self.graph_width # end of graph data
dc.SetPen(self.pen_tick)
dc.DrawLine(x2, 0, x2, self.originY + 1) # y axis
y = self.zeroDB
for i in range(0, -99999, -10):
if y >= chary / 2:
dc.SetPen(self.pen_tick)
dc.DrawLine(x1, y, x2, y) # y tick
t = `i`
w, h = dc.GetTextExtent(t)
dc.DrawText(`i`, x1 - (w+5), y - h / 2) # y text
y = y + self.scale
if y > self.originY:
break
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
# Draw the X axis
dc.SetPen(self.pen_tick)
dc.DrawLine(self.originX, originY, x3, originY)
# Draw the band plan colors below the X axis
x = self.originX
f = float(x - self.x0) * self.sample_rate / self.data_width
c = None
y = originY + 1
for freq, color in conf.BandPlan:
freq -= self.VFO
if f < freq:
xend = int(self.x0 + float(freq) * self.data_width / self.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 = charx * self.sample_rate / self.data_width
if df < 5000:
tfreq = 5000 # tick frequency for labels
elif df < 10000:
tfreq = 10000
elif df < 20000:
tfreq = 20000
elif df < 50000:
tfreq = 50000
stick = 5000
mtick = 10000
ltick = 50000
else:
tfreq = 100000
stick = 5000
mtick = 10000
ltick = 50000
# Draw the X axis ticks and frequency in kHz
dc.SetPen(self.pen_tick)
freq1 = self.VFO - self.sample_rate / 2
freq1 = (freq1 / stick) * stick
freq2 = freq1 + self.sample_rate + stick + 1
y_end = 0
for f in range (freq1, freq2, stick):
x = self.x0 + int(float(f - self.VFO) / self.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 OnGraphData(self, data):
i1 = (self.data_width - self.graph_width) / 2
i2 = i1 + self.graph_width
self.display.OnGraphData(data[i1:i2])
def SetVFO(self, vfo):
self.VFO = vfo
self.doResize = True
def SetTxFreq(self, tx_freq, rx_freq):
self.txFreq = tx_freq
tx_x = self.x0 + int(float(tx_freq) / self.sample_rate * self.data_width)
self.tuningX = tx_x
rx_x = self.x0 + int(float(rx_freq) / self.sample_rate * self.data_width)
if abs(tx_x - rx_x) < 2: # Do not display Rx line for small frequency offset
self.display.SetTuningLine(tx_x - self.originX, 0)
else:
self.display.SetTuningLine(tx_x - self.originX, rx_x - self.originX)
def GetMousePosition(self, event):
"""For mouse clicks in our display, translate to our screen coordinates."""
mouse_x, mouse_y = event.GetPositionTuple()
win = event.GetEventObject()
if win is not self:
x, y = win.GetPositionTuple()
mouse_x += x
mouse_y += y
return mouse_x, mouse_y
def OnRightDown(self, event):
mouse_x, mouse_y = self.GetMousePosition(event)
freq = float(mouse_x - self.x0) * self.sample_rate / self.data_width
freq = int(freq)
if self.VFO > 0:
vfo = self.VFO + freq
vfo = (vfo + 5000) / 10000 * 10000 # round to even number
tune = freq + self.VFO - vfo
self.ChangeHwFrequency(tune, vfo, 'MouseBtn3', event)
def OnLeftDown(self, event):
mouse_x, mouse_y = self.GetMousePosition(event)
self.mouse_x = mouse_x
x = mouse_x - self.originX
if self.display.tune_rx and abs(x - self.display.tune_tx) > abs(x - self.display.tune_rx):
self.mouse_is_rx = True
else:
self.mouse_is_rx = False
if mouse_y < self.originY: # click above X axis
freq = float(mouse_x - self.x0) * self.sample_rate / self.data_width
freq = int(freq)
if self.mouse_is_rx:
application.rxFreq = freq
application.screen.SetTxFreq(self.txFreq, freq)
QS.set_tune(freq + application.ritFreq, self.txFreq)
else:
self.ChangeHwFrequency(freq, self.VFO, 'MouseBtn1', event)
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)
if conf.mouse_tune_method: # Mouse motion changes the VFO frequency
x = (mouse_x - self.mouse_x) # Thanks to VK6JBL
self.mouse_x = mouse_x
freq = x * self.sample_rate / self.data_width
freq = int(freq)
self.ChangeHwFrequency(self.txFreq, self.VFO - freq, 'MouseMotion', event)
else: # Mouse motion changes the tuning frequency
# Frequency changes more rapidly for higher mouse Y position
speed = max(10, self.originY - mouse_y) / float(self.originY)
x = (mouse_x - self.mouse_x)
self.mouse_x = mouse_x
freq = speed * x * self.sample_rate / self.data_width
freq = int(freq)
if self.mouse_is_rx: # Mouse motion changes the receive frequency
application.rxFreq += freq
application.screen.SetTxFreq(self.txFreq, application.rxFreq)
QS.set_tune(application.rxFreq + application.ritFreq, self.txFreq)
else: # Mouse motion changes the transmit frequency
self.ChangeHwFrequency(self.txFreq + freq, self.VFO, 'MouseMotion', event)
def OnWheel(self, event):
wm = self.WheelMod # Round frequency when using mouse wheel
mouse_x, mouse_y = self.GetMousePosition(event)
x = mouse_x - self.originX
if self.display.tune_rx and abs(x - self.display.tune_tx) > abs(x - self.display.tune_rx):
tune = application.rxFreq + wm * event.GetWheelRotation() / event.GetWheelDelta()
if tune >= 0:
tune = tune / wm * wm
else: # tune can be negative when the VFO is zero
tune = - (- tune / wm * wm)
application.rxFreq = tune
application.screen.SetTxFreq(self.txFreq, tune)
QS.set_tune(tune + application.ritFreq, self.txFreq)
else:
tune = self.txFreq + wm * event.GetWheelRotation() / event.GetWheelDelta()
if tune >= 0:
tune = tune / wm * wm
else: # tune can be negative when the VFO is zero
tune = - (- tune / wm * wm)
self.ChangeHwFrequency(tune, self.VFO, 'MouseWheel', event)
def ChangeHwFrequency(self, tune, vfo, source, event):
application.ChangeHwFrequency(tune, vfo, source, event)
def PeakHold(self, name):
if name == 'GraphP1':
self.display.peak_hold = int(self.display.scale * conf.graph_peak_hold_1)
elif name == 'GraphP2':
self.display.peak_hold = int(self.display.scale * conf.graph_peak_hold_2)
else:
self.display.peak_hold = 9999
if self.display.peak_hold < 1:
self.display.peak_hold = 1
class WaterfallDisplay(wx.Window):
"""Create a waterfall display within the waterfall screen."""
def __init__(self, parent, x, y, graph_width, height, margin):
wx.Window.__init__(self, parent,
pos = (x, y),
size = (graph_width, height),
style = wx.NO_BORDER)
self.parent = parent
self.graph_width = graph_width
self.margin = margin
self.height = 10
self.sample_rate = application.sample_rate
self.SetBackgroundColour('Black')
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown)
self.Bind(wx.EVT_RIGHT_DOWN, parent.OnRightDown)
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.tune_rx = 0 # Current X position of Rx tuning line or zero
self.tuningPen = wx.Pen('White', 3)
self.marginPen = wx.Pen(conf.color_graph, 1)
# Size of top faster scroll region is (top_key + 2) * (top_key - 1) / 2
self.top_key = 8
self.top_size = (self.top_key + 2) * (self.top_key - 1) / 2
# Make the palette
pal2 = conf.waterfallPalette
red = []
green = []
blue = []
n = 0
for i in range(256):
if i > pal2[n+1][0]:
n = n + 1
red.append((i - pal2[n][0]) *
(long)(pal2[n+1][1] - pal2[n][1]) /
(long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][1])
green.append((i - pal2[n][0]) *
(long)(pal2[n+1][2] - pal2[n][2]) /
(long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][2])
blue.append((i - pal2[n][0]) *
(long)(pal2[n+1][3] - pal2[n][3]) /
(long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][3])
self.red = red
self.green = green
self.blue = blue
bmp = wx.EmptyBitmap(0, 0)
bmp.x_origin = 0
self.bitmaps = [bmp] * application.screen_height
if sys.platform == 'win32':
self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter)
def OnEnter(self, event):
if not application.w_phase:
self.SetFocus() # Set focus so we get mouse wheel events
def OnPaint(self, event):
dc = wx.BufferedPaintDC(self)
dc.SetBackground(wx.Brush('Black'))
dc.Clear()
y = 0
dc.SetPen(self.marginPen)
x_origin = int(float(self.VFO) / self.sample_rate * self.data_width + 0.5)
for i in range(0, self.margin):
dc.DrawLine(0, y, self.graph_width, y)
y += 1
index = 0
if conf.waterfall_scroll_mode: # Draw the first few lines multiple times
for i in range(self.top_key, 1, -1):
b = self.bitmaps[index]
x = b.x_origin - x_origin
for j in range(0, i):
dc.DrawBitmap(b, x, y)
y += 1
index += 1
while y < self.height:
b = self.bitmaps[index]
x = b.x_origin - x_origin
dc.DrawBitmap(b, x, y)
y += 1
index += 1
dc.SetPen(self.tuningPen)
dc.SetLogicalFunction(wx.XOR)
dc.DrawLine(self.tune_tx, 0, self.tune_tx, self.height)
if self.tune_rx:
dc.DrawLine(self.tune_rx, 0, self.tune_rx, self.height)
def SetHeight(self, height):
self.height = height
self.SetSize((self.graph_width, height))
def OnGraphData(self, data, y_zero, y_scale):
#T('graph start')
row = '' # Make a new row of pixels for a one-line image
for x in data: # x is -130 to 0, or so (dB)
l = int((x + y_zero / 3 + 100) * y_scale / 10)
l = max(l, 0)
l = min(l, 255)
row = row + "%c%c%c" % (chr(self.red[l]), chr(self.green[l]), chr(self.blue[l]))
#T('graph string')
bmp = wx.BitmapFromBuffer(len(row) / 3, 1, row)
bmp.x_origin = int(float(self.VFO) / self.sample_rate * self.data_width + 0.5)
self.bitmaps.insert(0, bmp)
del self.bitmaps[-1]
#self.ScrollWindow(0, 1, None)
#self.Refresh(False, (0, 0, self.graph_width, self.top_size + self.margin))
self.Refresh(False)
#T('graph end')
def SetTuningLine(self, tune_tx, tune_rx):
dc = wx.ClientDC(self)
dc.SetPen(self.tuningPen)
dc.SetLogicalFunction(wx.XOR)
dc.DrawLine(self.tune_tx, 0, self.tune_tx, self.height)
if self.tune_rx:
dc.DrawLine(self.tune_rx, 0, self.tune_rx, self.height)
dc.DrawLine(tune_tx, 0, tune_tx, self.height)
if tune_rx:
dc.DrawLine(tune_rx, 0, tune_rx, self.height)
self.tune_tx = tune_tx
self.tune_rx = tune_rx
class WaterfallScreen(wx.SplitterWindow):
"""Create a splitter window with a graph screen and a waterfall screen"""
def __init__(self, frame, width, data_width, graph_width):
self.y_scale = conf.waterfall_y_scale
self.y_zero = conf.waterfall_y_zero
wx.SplitterWindow.__init__(self, frame)
self.SetSizeHints(width, -1, width)
self.SetMinimumPaneSize(1)
self.SetSize((width, conf.waterfall_graph_size + 100)) # be able to set sash size
self.pane1 = GraphScreen(self, data_width, graph_width, 1)
self.pane2 = WaterfallPane(self, data_width, graph_width)
self.SplitHorizontally(self.pane1, self.pane2, conf.waterfall_graph_size)
def OnIdle(self, event):
self.pane1.OnIdle(event)
self.pane2.OnIdle(event)
def SetTxFreq(self, tx_freq, rx_freq):
self.pane1.SetTxFreq(tx_freq, rx_freq)
self.pane2.SetTxFreq(tx_freq, rx_freq)
def SetVFO(self, vfo):
self.pane1.SetVFO(vfo)
self.pane2.SetVFO(vfo)
def ChangeYscale(self, y_scale): # Test if the shift key is down
if wx.GetKeyState(wx.WXK_SHIFT): # Set graph screen
self.pane1.ChangeYscale(y_scale)
else: # Set waterfall screen
self.y_scale = y_scale
self.pane2.ChangeYscale(y_scale)
def ChangeYzero(self, y_zero): # Test if the shift key is down
if wx.GetKeyState(wx.WXK_SHIFT): # Set graph screen
self.pane1.ChangeYzero(y_zero)
else: # Set waterfall screen
self.y_zero = y_zero
self.pane2.ChangeYzero(y_zero)
def OnGraphData(self, data):
self.pane1.OnGraphData(data)
self.pane2.OnGraphData(data)
class WaterfallPane(GraphScreen):
"""Create a waterfall screen with an X axis and a waterfall display."""
def __init__(self, frame, data_width, graph_width):
GraphScreen.__init__(self, frame, data_width, graph_width)
self.y_scale = conf.waterfall_y_scale
self.y_zero = conf.waterfall_y_zero
self.oldVFO = self.VFO
def MakeDisplay(self):
self.display = WaterfallDisplay(self, self.originX, 0, self.graph_width, 5, self.chary)
self.display.VFO = self.VFO
self.display.data_width = self.data_width
def SetVFO(self, vfo):
GraphScreen.SetVFO(self, vfo)
self.display.VFO = vfo
if self.oldVFO != vfo:
self.oldVFO = vfo
self.Refresh()
def MakeYTicks(self, dc):
pass
def ChangeYscale(self, y_scale):
self.y_scale = y_scale
def ChangeYzero(self, y_zero):
self.y_zero = y_zero
def OnGraphData(self, data):
i1 = (self.data_width - self.graph_width) / 2
i2 = i1 + self.graph_width
self.display.OnGraphData(data[i1:i2], self.y_zero, self.y_scale)
class ScopeScreen(wx.Window):
"""Create an oscilloscope screen (mostly used for debug)."""
def __init__(self, parent, width, data_width, graph_width):
wx.Window.__init__(self, parent, pos = (0, 0),
size=(width, -1), style = wx.NO_BORDER)
self.SetBackgroundColour(conf.color_graph)
self.font = wx.Font(16, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL)
self.SetFont(self.font)
self.Bind(wx.EVT_SIZE, self.OnSize)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID)
self.y_scale = conf.scope_y_scale
self.y_zero = conf.scope_y_zero
self.running = 1
self.doResize = False
self.width = width
self.height = 100
self.originY = self.height / 2
self.data_width = data_width
self.graph_width = graph_width
w = self.charx = self.GetCharWidth()
h = self.chary = self.GetCharHeight()
tick = max(2, h * 3 / 10)
self.originX = w * 3
self.width = self.originX + self.graph_width + tick + self.charx * 2
self.line = [(0,0), (1,1)] # initial fake graph data
self.fpout = None #open("jim96.txt", "w")
def OnIdle(self, event):
if self.doResize:
self.ResizeGraph()
def OnSize(self, event):
self.doResize = True
event.Skip()
def ResizeGraph(self, event=None):
# Change the height of the graph. Changing the width interactively is not allowed.
w, h = self.GetClientSize()
self.height = h
self.originY = h / 2
self.doResize = False
self.Refresh()
def OnPaint(self, event):
dc = wx.PaintDC(self)
dc.SetFont(self.font)
self.MakeYTicks(dc)
self.MakeXTicks(dc)
self.MakeText(dc)
dc.SetPen(wx.BLACK_PEN)
dc.DrawLines(self.line)
def MakeYTicks(self, dc):
chary = self.chary
originX = self.originX
x3 = self.x3 = originX + self.graph_width # end of graph data
dc.SetPen(wx.BLACK_PEN)
dc.DrawLine(originX, 0, originX, self.originY * 3) # y axis
# Find the size of the Y scale markings
themax = 2.5e9 * 10.0 ** - ((160 - self.y_scale) / 50.0) # value at top of screen
themax = int(themax)
l = []
for j in (5, 6, 7, 8):
for i in (1, 2, 5):
l.append(i * 10 ** j)
for yvalue in l:
n = themax / yvalue + 1 # Number of lines
ypixels = self.height / n
if n < 20:
break
dc.SetPen(self.horizPen)
for i in range(1, 1000):
y = self.originY - ypixels * i
if y < chary:
break
# Above axis
dc.DrawLine(originX, y, x3, y) # y line
# Below axis
y = self.originY + ypixels * i
dc.DrawLine(originX, y, x3, y) # y line
self.yscale = float(ypixels) / yvalue
self.yvalue = yvalue
def MakeXTicks(self, dc):
originY = self.originY
x3 = self.x3
# Draw the X axis
dc.SetPen(wx.BLACK_PEN)
dc.DrawLine(self.originX, originY, x3, originY)
# Find the size of the X scale markings in microseconds
for i in (20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000):
xscale = i # X scale in microseconds
if application.sample_rate * xscale * 0.000001 > self.width / 30:
break
# Draw the X lines
dc.SetPen(self.horizPen)
for i in range(1, 999):
x = int(self.originX + application.sample_rate * xscale * 0.000001 * i + 0.5)
if x > x3:
break
dc.DrawLine(x, 0, x, self.height) # x line
self.xscale = xscale
def MakeText(self, dc):
if self.running:
t = " RUN"
else:
t = " STOP"
if self.xscale >= 1000:
t = "%s X: %d millisec/div" % (t, self.xscale)
else:
t = "%s X: %d microsec/div" % (t, self.xscale)
yt = `self.yvalue`
t = "%s Y: %sE%d/div" % (t, yt[0], len(yt) - 1)
dc.DrawText(t, self.originX, self.height - self.chary)
def OnGraphData(self, data):
if not self.running:
if self.fpout:
for cpx in data:
re = int(cpx.real)
im = int(cpx.imag)
ab = int(abs(cpx))
self.fpout.write("%12d %12d %12d\n" % (re, im, ab))
return # Preserve data on screen
line = []
x = self.originX
ymax = self.height
for cpx in data: # cpx is complex raw samples +/- 0 to 2**31-1
y = cpx.real
#y = abs(cpx)
y = self.originY - int(y * self.yscale + 0.5)
if y > ymax:
y = ymax
elif y < 0:
y = 0
line.append((x, y))
x = x + 1
self.line = line
self.Refresh()
def ChangeYscale(self, y_scale):
self.y_scale = y_scale
self.doResize = True
def ChangeYzero(self, y_zero):
self.y_zero = y_zero
def SetTxFreq(self, tx_freq, rx_freq):
pass
class FilterScreen(GraphScreen):
"""Create a graph of the receive filter response."""
def __init__(self, parent, data_width, graph_width):
GraphScreen.__init__(self, parent, data_width, graph_width)
self.y_scale = conf.filter_y_scale
self.y_zero = conf.filter_y_zero
self.VFO = 0
self.txFreq = 0
self.data = []
self.sample_rate = QS.get_filter_rate()
def NewFilter(self):
self.data = QS.get_filter()
def OnGraphData(self, data):
GraphScreen.OnGraphData(self, self.data)
def ChangeHwFrequency(self, tune, vfo, source, event):
GraphScreen.SetTxFreq(self, tune, tune)
application.freqDisplay.Display(tune)
def SetTxFreq(self, tx_freq, rx_freq):
pass
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))
self.y_scale = 0
self.y_zero = 0
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.html')
def OnGraphData(self, data):
pass
def ChangeYscale(self, y_scale):
pass
def ChangeYzero(self, y_zero):
pass
def OnIdle(self, event):
pass
def SetTxFreq(self, tx_freq, rx_freq):
pass
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
title = fp.readline().strip()[1:] + '-fcd1'
fp.close()
wx.Frame.__init__(self, None, -1, title, wx.DefaultPosition,
(width, height), wx.DEFAULT_FRAME_STYLE, 'MainFrame')
#self.SetBackgroundColour(conf.color_bg)
self.Bind(wx.EVT_CLOSE, self.OnBtnClose)
# window icon
icon = wx.Icon('icon.png', wx.BITMAP_TYPE_PNG, 48, 48)
self.SetIcon(icon)
def OnBtnClose(self, event):
application.OnBtnClose(event)
self.Destroy()
## Note: The new amplitude/phase adjustments have ideas provided by Andrew Nilsson, VK6JBL
class QAdjustPhase(wx.Frame):
"""Create a window with amplitude and phase adjustment controls"""
f_ampl = "Amplitude adjustment %.6f"
f_phase = "Phase adjustment degrees %.6f"
def __init__(self, parent, width, rx_tx):
wx.Frame.__init__(self, application.main_frame, -1,
"Adjust Sound Card Amplitude and Phase", pos=(50, 100),
style=wx.CAPTION)
self.rx_tx = rx_tx
panel = wx.Panel(self)
self.MakeControls(panel, width)
self.Show()
def MakeControls(self, panel, width): # Make controls for phase/amplitude adjustment
self.old_amplitude, self.old_phase = application.GetAmplPhase()
self.new_amplitude, self.new_phase = self.old_amplitude, self.old_phase
sl_max = width * 4 / 10 # maximum +/- value for slider
self.ampl_scale = float(conf.rx_max_amplitude_correct) / sl_max
self.phase_scale = float(conf.rx_max_phase_correct) / sl_max
font = wx.Font(12, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL)
chary = self.GetCharHeight()
y = chary * 3 / 10
# Print available data points
if conf.bandAmplPhase.has_key("panadapter"):
self.band = "panadapter"
else:
self.band = application.lastBand
app_vfo = (application.VFO + 500) / 1000
ap = application.bandAmplPhase
if not ap.has_key(self.band):
ap[self.band] = {}
if not ap[self.band].has_key(self.rx_tx):
ap[self.band][self.rx_tx] = []
lst = ap[self.band][self.rx_tx]
freq_in_list = False
if lst:
t = "Band %s: VFO" % self.band
for l in lst:
vfo = (l[0] + 500) / 1000
if vfo == app_vfo:
freq_in_list = True
t = t + (" %d" % vfo)
else:
t = "Band %s: No data." % self.band
txt = wx.StaticText(panel, -1, t, pos=(0, y))
txt.SetFont(font)
y += txt.GetSizeTuple()[1]
self.t_ampl = wx.StaticText(panel, -1, self.f_ampl % self.old_amplitude, pos=(0, y))
self.t_ampl.SetFont(font)
y += self.t_ampl.GetSizeTuple()[1]
self.ampl1 = wx.Slider(panel, -1, 0, -sl_max, sl_max,
pos=(0, y), size=(width, -1))
y += self.ampl1.GetSizeTuple()[1]
self.ampl2 = wx.Slider(panel, -1, 0, -sl_max, sl_max,
pos=(0, y), size=(width, -1))
y += self.ampl2.GetSizeTuple()[1]
self.PosAmpl(self.old_amplitude)
self.t_phase = wx.StaticText(panel, -1, self.f_phase % self.old_phase, pos=(0, y))
self.t_phase.SetFont(font)
y += self.t_phase.GetSizeTuple()[1]
self.phase1 = wx.Slider(panel, -1, 0, -sl_max, sl_max,
pos=(0, y), size=(width, -1))
y += self.phase1.GetSizeTuple()[1]
self.phase2 = wx.Slider(panel, -1, 0, -sl_max, sl_max,
pos=(0, y), size=(width, -1))
y += self.phase2.GetSizeTuple()[1]
sv = QuiskPushbutton(panel, self.OnBtnSave, 'Save %d' % app_vfo)
ds = QuiskPushbutton(panel, self.OnBtnDiscard, 'Destroy %d' % app_vfo)
cn = QuiskPushbutton(panel, self.OnBtnCancel, 'Cancel')
w, h = ds.GetSizeTuple()
sv.SetSize((w, h))
cn.SetSize((w, h))
y += h / 4
x = (width - w * 3) / 4
sv.SetPosition((x, y))
ds.SetPosition((x*2 + w, y))
cn.SetPosition((x*3 + w*2, y))
sv.SetBackgroundColour('light blue')
ds.SetBackgroundColour('light blue')
cn.SetBackgroundColour('light blue')
if not freq_in_list:
ds.Disable()
y += h
y += h / 4
self.ampl1.SetBackgroundColour('aquamarine')
self.ampl2.SetBackgroundColour('orange')
self.phase1.SetBackgroundColour('aquamarine')
self.phase2.SetBackgroundColour('orange')
self.PosPhase(self.old_phase)
self.SetClientSizeWH(width, y)
self.ampl1.Bind(wx.EVT_SCROLL, self.OnChange)
self.ampl2.Bind(wx.EVT_SCROLL, self.OnAmpl2)
self.phase1.Bind(wx.EVT_SCROLL, self.OnChange)
self.phase2.Bind(wx.EVT_SCROLL, self.OnPhase2)
def PosAmpl(self, ampl): # set pos1, pos2 for amplitude
pos2 = round(ampl / self.ampl_scale)
remain = ampl - pos2 * self.ampl_scale
pos1 = round(remain / self.ampl_scale * 50.0)
self.ampl1.SetValue(pos1)
self.ampl2.SetValue(pos2)
def PosPhase(self, phase): # set pos1, pos2 for phase
pos2 = round(phase / self.phase_scale)
remain = phase - pos2 * self.phase_scale
pos1 = round(remain / self.phase_scale * 50.0)
self.phase1.SetValue(pos1)
self.phase2.SetValue(pos2)
def OnChange(self, event):
ampl = self.ampl_scale * self.ampl1.GetValue() / 50.0 + self.ampl_scale * self.ampl2.GetValue()
if abs(ampl) < self.ampl_scale * 3.0 / 50.0:
ampl = 0.0
self.t_ampl.SetLabel(self.f_ampl % ampl)
phase = self.phase_scale * self.phase1.GetValue() / 50.0 + self.phase_scale * self.phase2.GetValue()
if abs(phase) < self.phase_scale * 3.0 / 50.0:
phase = 0.0
self.t_phase.SetLabel(self.f_phase % phase)
QS.set_ampl_phase(ampl, phase)
self.new_amplitude, self.new_phase = ampl, phase
def OnAmpl2(self, event): # re-center the fine slider when the coarse slider is adjusted
ampl = self.ampl_scale * self.ampl1.GetValue() / 50.0 + self.ampl_scale * self.ampl2.GetValue()
self.PosAmpl(ampl)
self.OnChange(event)
def OnPhase2(self, event): # re-center the fine slider when the coarse slider is adjusted
phase = self.phase_scale * self.phase1.GetValue() / 50.0 + self.phase_scale * self.phase2.GetValue()
self.PosPhase(phase)
self.OnChange(event)
def DeleteEqual(self): # Remove entry with the same VFO
ap = application.bandAmplPhase
lst = ap[self.band][self.rx_tx]
vfo = (application.VFO + 500) / 1000
for i in range(len(lst)-1, -1, -1):
if (lst[i][0] + 500) / 1000 == vfo:
del lst[i]
def OnBtnSave(self, event):
data = (application.VFO, application.rxFreq, self.new_amplitude, self.new_phase)
self.DeleteEqual()
ap = application.bandAmplPhase
lst = ap[self.band][self.rx_tx]
lst.append(data)
lst.sort()
application.w_phase = None
self.Destroy()
def OnBtnDiscard(self, event):
self.DeleteEqual()
self.OnBtnCancel()
def OnBtnCancel(self, event=None):
QS.set_ampl_phase(self.old_amplitude, self.old_phase)
application.w_phase = None
self.Destroy()
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()
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 App(wx.App):
"""Class representing the application."""
freq60 = (5330500, 5346500, 5366500, 5371500, 5403500)
StateNames = [ # Names of state attributes to save and restore
'bandState', 'bandAmplPhase', 'lastBand', 'VFO', 'txFreq', 'mode',
'vardecim_set',
]
def __init__(self):
global application
application = self
self.init_path = None
if sys.stdout.isatty():
wx.App.__init__(self, redirect=False)
else:
wx.App.__init__(self, redirect=True)
def QuiskText(self, *args, **kw): # Make our text control available to widget files
return QuiskText(*args, **kw)
def QuiskPushbutton(self, *args, **kw): # Make our buttons available to widget files
return QuiskPushbutton(*args, **kw)
def QuiskRepeatbutton(self, *args, **kw):
return QuiskRepeatbutton(*args, **kw)
def QuiskCheckbutton(self, *args, **kw):
return QuiskCheckbutton(*args, **kw)
def QuiskCycleCheckbutton(self, *args, **kw):
return QuiskCycleCheckbutton(*args, **kw)
def RadioButtonGroup(self, *args, **kw):
return RadioButtonGroup(*args, **kw)
def OnInit(self):
"""Perform most initialization of the app here (called by wxPython on startup)."""
wx.lib.colourdb.updateColourDB() # Add additional color names
global conf # conf is the module for all configuration data
import quisk_conf_defaults as conf
setattr(conf, 'config_file_path', ConfigPath)
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
execfile(ConfigPath, d) # execute the user's config file
for k, v in d.items(): # add user's config items to conf
if k[0] != '_': # omit items starting with '_'
setattr(conf, k, v)
else:
setattr(conf, 'config_file_exists', False)
if conf.invertSpectrum:
QS.invert_spectrum(1)
self.bandState = {}
self.bandState.update(conf.bandState)
self.bandAmplPhase = conf.bandAmplPhase
# Open hardware file
global Hardware
if hasattr(conf, "Hardware"): # Hardware defined in config file
Hardware = conf.Hardware(self, conf)
else:
Hardware = conf.quisk_hardware.Hardware(self, conf)
# Initialization - may be over-written by persistent state
self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow
self.smeter_db_count = 0 # average the S-meter
self.smeter_db_sum = 0
self.smeter_db = 0
self.smeter_sunits = -87.0
self.timer = time.time() # A seconds clock
self.heart_time0 = self.timer # timer to call HeartBeat at intervals
self.smeter_db_time0 = self.timer
self.smeter_sunits_time0 = self.timer
self.band_up_down = 0 # Are band Up/Down buttons in use?
self.lastBand = 'Audio'
self.VFO = 0
self.ritFreq = 0
self.txFreq = 0 # Transmit frequency as +/- sample_rate/2
self.rxFreq = 0 # Receive frequency as +/- sample_rate/2
self.oldRxFreq = 0 # Last value of self.rxFreq
self.screen = None
self.audio_volume = 0.0 # Set output volume, 0.0 to 1.0
self.sidetone_volume = 0.0 # Set sidetone volume, 0.0 to 1.0
self.sound_error = 0
self.sound_thread = None
self.mode = conf.default_mode
self.bottom_widgets = None
self.color_list = None
self.color_index = 0
self.vardecim_set = None
self.w_phase = None
self.split_rxtx = False # Are we in split Rx/Tx mode?
dc = wx.ScreenDC() # get the screen size
(self.screen_width, self.screen_height) = dc.GetSizeTuple()
del dc
self.Bind(wx.EVT_IDLE, self.OnIdle)
self.Bind(wx.EVT_QUERY_END_SESSION, self.OnEndSession)
# Restore persistent program state
if conf.persistent_state:
self.init_path = os.path.join(os.path.dirname(ConfigPath), '.quisk_init.pkl')
try:
fp = open(self.init_path, "rb")
d = pickle.load(fp)
fp.close()
for k, v in d.items():
if k in self.StateNames:
if k == 'bandState':
self.bandState.update(v)
else:
setattr(self, k, v)
except:
pass #traceback.print_exc()
for k, (vfo, tune, mode) in self.bandState.items(): # Historical: fix bad frequencies
try:
f1, f2 = conf.BandEdge[k]
if not f1 <= vfo + tune <= f2:
self.bandState[k] = conf.bandState[k]
except KeyError:
pass
if self.bandAmplPhase and type(self.bandAmplPhase.values()[0]) is not DictType:
print """Old sound card amplitude and phase corrections must be re-entered (sorry).
The new code supports multiple corrections per band."""
self.bandAmplPhase = {}
if Hardware.VarDecimGetChoices(): # Hardware can change the decimation.
self.sample_rate = Hardware.VarDecimSet() # Get the sample rate.
self.vardecim_set = self.sample_rate
else: # Use the sample rate from the config file.
self.sample_rate = conf.sample_rate
if not hasattr(conf, 'playback_rate'):
if conf.use_sdriq or conf.use_rx_udp:
conf.playback_rate = 48000
else:
conf.playback_rate = conf.sample_rate
# Find the data width from a list of prefered sizes; it is the width of returned graph data.
# The graph_width is the width of data_width that is displayed.
width = self.screen_width * conf.graph_width
percent = conf.display_fraction # display central fraction of total width
percent = int(percent * 100.0 + 0.4)
width = width * 100 / percent
for x in fftPreferedSizes:
if x > width:
self.data_width = x
break
else:
self.data_width = fftPreferedSizes[-1]
self.graph_width = self.data_width * percent / 100
if self.graph_width % 2 == 1: # Both data_width and graph_width are even numbers
self.graph_width += 1
# The FFT size times the average_count controls the graph refresh rate
factor = float(self.sample_rate) / conf.graph_refresh / self.data_width
ifactor = int(factor + 0.5)
if conf.fft_size_multiplier >= ifactor: # Use large FFT and average count 1
fft_mult = ifactor
average_count = 1
elif conf.fft_size_multiplier > 0: # Specified fft_size_multiplier
fft_mult = conf.fft_size_multiplier
average_count = int(factor / fft_mult + 0.5)
if average_count < 1:
average_count = 1
else: # Calculate the split between fft size and average
if self.sample_rate <= 240000:
maxfft = 8000 # Maximum fft size
else:
maxfft = 15000
fft1 = maxfft / self.data_width
if fft1 >= ifactor:
fft_mult = ifactor
average_count = 1
else:
av1 = int(factor / fft1 + 0.5)
if av1 < 1:
av1 = 1
err1 = factor / (fft1 * av1)
av2 = av1 + 1
fft2 = int(factor / av2 + 0.5)
err2 = factor / (fft2 * av2)
if 0.9 < err1 < 1.1 or abs(1.0 - err1) <= abs(1.0 - err2):
fft_mult = fft1
average_count = av1
else:
fft_mult = fft2
average_count = av2
self.fft_size = self.data_width * fft_mult
# if we have custom window size in conf use that
if hasattr(conf, 'window_width'):
self.width = conf.window_width
else:
self.width = self.screen_width * 8 / 10
if hasattr(conf, 'window_height'):
self.height = conf.window_height
else:
self.height = self.screen_height * 5 / 10
self.main_frame = frame = QMainFrame(self.width, self.height)
self.SetTopWindow(frame)
# Record the basic application parameters
if sys.platform == 'win32':
h = self.main_frame.GetHandle()
else:
h = 0
QS.record_app(self, conf, self.data_width, self.fft_size,
average_count, self.sample_rate, h)
#print 'FFT size %d, FFT mult %d, average_count %d' % (
# self.fft_size, self.fft_size / self.data_width, average_count)
#print 'Refresh %.2f Hz' % (float(self.sample_rate) / self.fft_size / average_count)
QS.record_graph(0, 0, 1.0)
# Make all the screens and hide all but one
self.graph = GraphScreen(frame, self.data_width, self.graph_width)
self.screen = self.graph
width = self.graph.width
button_width = width # try to estimate the final button width
self.config_screen = ConfigScreen(frame, width, self.fft_size)
self.config_screen.Hide()
self.waterfall = WaterfallScreen(frame, width, self.data_width, self.graph_width)
self.waterfall.Hide()
self.scope = ScopeScreen(frame, width, self.data_width, self.graph_width)
self.scope.Hide()
self.filter_screen = FilterScreen(frame, self.data_width, self.graph_width)
self.filter_screen.Hide()
self.help_screen = HelpScreen(frame, width, self.screen_height / 10)
self.help_screen.Hide()
frame.SetSizeHints(width, 100)
frame.SetClientSizeWH(width, self.screen_height * 5 / 10)
# Make a vertical box to hold all the screens and the bottom box
vertBox = self.vertBox = wx.BoxSizer(wx.VERTICAL)
frame.SetSizer(vertBox)
# Add the screens
vertBox.Add(self.config_screen, 1)
vertBox.Add(self.graph, 1)
vertBox.Add(self.waterfall, 1)
vertBox.Add(self.scope, 1)
vertBox.Add(self.filter_screen, 1)
vertBox.Add(self.help_screen, 1)
# Add the spacer
vertBox.Add(Spacer(frame), 0, wx.EXPAND)
# Add the bottom box
hBoxA = wx.BoxSizer(wx.HORIZONTAL)
vertBox.Add(hBoxA, 0, wx.EXPAND)
# End of vertical box. Add items to the horizontal box.
# Add two sliders on the left
margin = 3
self.sliderVol = SliderBoxV(frame, 'Vol', 300, 1000, self.ChangeVolume)
button_width -= self.sliderVol.width + margin * 2
self.ChangeVolume() # set initial volume level
hBoxA.Add(self.sliderVol, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin)
if Hardware.use_sidetone:
self.sliderSto = SliderBoxV(frame, 'STo', 300, 1000, self.ChangeSidetone)
button_width -= self.sliderSto.width + margin * 2
self.ChangeSidetone()
hBoxA.Add(self.sliderSto, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin)
# Add the sizer for the middle
gap = 2
gbs = wx.GridBagSizer(gap, gap)
self.gbs = gbs
button_width -= gap * 15
hBoxA.Add(gbs, 1, wx.EXPAND, 0)
gbs.SetEmptyCellSize((5, 5))
button_width -= 5
for i in range(0, 6) + range(7, 13):
gbs.AddGrowableCol(i)
# Add two sliders on the right
self.sliderYs = SliderBoxV(frame, 'Ys', 0, 160, self.ChangeYscale, True)
button_width -= self.sliderYs.width + margin * 2
hBoxA.Add(self.sliderYs, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin)
self.sliderYz = SliderBoxV(frame, 'Yz', 0, 160, self.ChangeYzero, True)
button_width -= self.sliderYz.width + margin * 2
hBoxA.Add(self.sliderYz, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin)
zbw, button_height = self.MakeButtons(frame, gbs)
button_width /= 12 # This is our estimate of the final button size
self.MakeTopRow(frame, gbs, button_width, button_height)
if conf.quisk_widgets:
self.bottom_widgets = conf.quisk_widgets.BottomWidgets(self, Hardware, conf, frame, gbs, vertBox)
if QS.open_key(conf.key_method):
print 'open_key failed for name "%s"' % conf.key_method
if hasattr(conf, 'mixer_settings'):
for dev, numid, value in conf.mixer_settings:
err_msg = QS.mixer_set(dev, numid, value)
if err_msg:
print "Mixer", err_msg
# Create transmit audio filters
if conf.microphone_name:
filtI, filtQ = self.MakeFilterCoef(conf.mic_sample_rate, 540, 2700, 1650)
QS.set_tx_filters(filtI, filtQ, ())
# Open the hardware. This must be called before open_sound().
self.config_text = Hardware.open()
if not self.config_text:
self.config_text = "Missing config_text"
QS.capt_channels (conf.channel_i, conf.channel_q)
QS.play_channels (conf.channel_i, conf.channel_q)
QS.micplay_channels (conf.mic_play_chan_I, conf.mic_play_chan_Q)
# 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, conf.name_of_sound_play, self.sample_rate,
conf.data_poll_usec, conf.latency_millisecs,
conf.microphone_name, conf.tx_ip, conf.tx_audio_port,
conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q,
conf.mic_out_volume, conf.name_of_mic_play, conf.mic_playback_rate)
tune, vfo = Hardware.ReturnFrequency() # Request initial frequency to set band
if tune is not None:
for band, (f1, f2) in conf.BandEdge.items():
if f1 <= tune <= f2: # Change to the correct band based on frequency
self.lastBand = band
break
self.bandBtnGroup.SetLabel(self.lastBand, do_cmd=True)
self.ChangeHwFrequency(None, None) # Request initial VFO and tuning
# Note: The filter rate is not valid until after the call to open_sound().
# Create FM audio filter
frate = QS.get_filter_rate() # filter rate
filtI, filtQ = self.MakeFmFilterCoef(frate, 600, 340, 2800)
QS.set_fm_filters(filtI)
# Record filter rate for the filter screen
self.filter_screen.sample_rate = frate
#if info[8]: # error message
# self.sound_error = 1
# self.config_screen.err_msg = info[8]
# print info[8]
if self.sound_error:
self.screenBtnGroup.SetLabel('Config', do_cmd=True)
frame.Show()
else:
self.screenBtnGroup.SetLabel(conf.default_screen, do_cmd=True)
frame.Show()
self.Yield()
self.sound_thread = SoundThread()
self.sound_thread.start()
return True
def OnIdle(self, event):
if self.screen:
self.screen.OnIdle(event)
def OnEndSession(self, event):
event.Skip()
self.OnBtnClose()
def OnBtnClose(self, event):
if self.sound_thread:
self.sound_thread.stop()
for i in range(0, 20):
if threading.activeCount() == 1:
break
time.sleep(0.1)
def OnExit(self):
QS.close_rx_udp()
Hardware.close()
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, "wb")
pickle.dump(d, fp)
fp.close()
except:
pass #traceback.print_exc()
def MakeTopRow(self, frame, gbs, button_width, button_height):
szr = wx.BoxSizer(wx.HORIZONTAL)
# Down button
b_down = QuiskRepeatbutton(frame, self.OnBtnDownBand, "Dn",
self.OnBtnUpDnBandDone, use_right=True)
szr.Add(b_down, 1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL)
# Up button
b_up = QuiskRepeatbutton(frame, self.OnBtnUpBand, "Up",
self.OnBtnUpDnBandDone, use_right=True)
szr.Add(b_up, 1, flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL)
gbs.Add(szr, (0, 4), flag=wx.EXPAND)
bw, bh = b_down.GetMinSize() # make top row buttons the same size
bw = (bw + button_width) / 2
#b_down.SetSizeHints(bw, button_height, bw * 5, button_height)
#b_up.SetSizeHints(bw, button_height, bw * 5, button_height)
# RIT slider
self.ritScale = wx.Slider(frame, -1, self.ritFreq, -2000, 2000, size=(-1, -1), style=wx.SL_LABELS)
self.ritScale.Bind(wx.EVT_SCROLL, self.OnRitScale)
gbs.Add(self.ritScale, (0, 8), (1, 3), flag=wx.EXPAND)
sw, sh = self.ritScale.GetSize()
# Frequency display
h = max(button_height, sh) # larger of button and slider height
self.freqDisplay = FrequencyDisplay(frame, gbs, button_width * 25 / 10, h)
self.freqDisplay.Display(self.txFreq + self.VFO)
# Frequency entry
e = wx.TextCtrl(frame, -1, '', style=wx.TE_PROCESS_ENTER)
font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL)
e.SetFont(font)
w, h = e.GetSizeTuple()
border = (self.freqDisplay.height_and_border - h) / 2
e.SetMinSize((1, 1))
e.SetBackgroundColour(conf.color_entry)
gbs.Add(e, (0, 3), flag = wx.EXPAND | wx.TOP | wx.BOTTOM, border=border)
frame.Bind(wx.EVT_TEXT_ENTER, self.FreqEntry, source=e)
# S-meter
self.smeter = QuiskText(frame, 'ZZS 9 -100.00 dBZZ', bh, wx.ALIGN_CENTER)
gbs.Add(self.smeter, (0, 11), (1, 2), flag=wx.EXPAND)
def MakeButtons(self, frame, gbs):
# There are six columns, a small gap column, and then six more columns
# RIT button
self.ritButton = QuiskCheckbutton(frame, self.OnBtnRit, "RIT")
gbs.Add(self.ritButton, (0, 7), flag=wx.ALIGN_CENTER)
all_buttons = [self.ritButton]
# Split button
if not conf.mouse_tune_method: # Mouse motion changes the VFO frequency
self.splitButton = QuiskCheckbutton(frame, self.OnBtnSplit, "Split")
gbs.Add(self.splitButton, (0, 5), flag=wx.ALIGN_CENTER)
all_buttons.append(self.splitButton)
### Left bank of buttons
flag = wx.EXPAND
self.bandBtnGroup = RadioButtonGroup(frame, self.OnBtnBand, conf.bandLabels, None)
btns = self.bandBtnGroup.buttons
all_buttons += btns
i = 0
n1 = len(conf.bandLabels) / 2
n2 = len(conf.bandLabels) - n1
for col in range(0, n1):
gbs.Add(btns[i], (1, col), flag=flag)
i += 1
for col in range(0, n2):
gbs.Add(btns[i], (2, col), flag=flag)
i += 1
# Mute, AGC
buttons = []
b = QuiskCheckbutton(frame, self.OnBtnMute, text='Mute')
buttons.append(b)
b = QuiskCycleCheckbutton(frame, self.OnBtnAGC, ('AGC', 'AGC 1', 'AGC 2'))
buttons.append(b)
b.SetLabel('AGC 1', True)
b = QuiskCheckbutton(frame, self.OnBtnNB, text='')
buttons.append(b)
try:
labels = Hardware.rf_gain_labels
except:
labels = ()
if labels:
self.BtnRfGain = QuiskCycleCheckbutton(frame, Hardware.OnButtonRfGain, labels)
buttons.append(self.BtnRfGain)
else:
b = QuiskCheckbutton(frame, None, text='')
buttons.append(b)
self.BtnRfGain = None
#b = QuiskRepeatbutton(frame, self.OnBtnColor, '', use_right=True)
if conf.add_fdx_button:
b = QuiskCheckbutton(frame, self.OnBtnFDX, 'FDX', color=conf.color_test)
else:
b = QuiskCheckbutton(frame, None, text='')
buttons.append(b)
b = QuiskCheckbutton(frame, self.OnBtnTest1, 'Test 1', color=conf.color_test)
buttons.append(b)
all_buttons += buttons
for col in range(0, 6):
gbs.Add(buttons[col], (3, col), flag=flag)
### Right bank of buttons
labels = [('CWL', 'CWU'), ('LSB', 'USB'), 'AM', 'FM', conf.add_extern_demod, '']
if conf.add_imd_button:
labels[-1] = ('IMD', 'IMD -3dB', 'IMD -6dB')
self.modeButns = RadioButtonGroup(frame, self.OnBtnMode, labels, None)
btns = self.modeButns.GetButtons()
all_buttons += btns
btns[-1].color = conf.color_test
for col in range(0, 6):
gbs.Add(btns[col], (1, col + 7), flag=flag)
labels = ('0',) * 6
self.filterButns = RadioButtonGroup(frame, self.OnBtnFilter, labels, None)
btns = self.filterButns.GetButtons()
all_buttons += btns
for col in range(0, 6):
gbs.Add(btns[col], (2, col + 7), flag=flag)
labels = (('Graph', 'GraphP1', 'GraphP2'), 'WFall', ('Scope', 'Scope'), 'Config', 'RX Filter', 'Help')
self.screenBtnGroup = RadioButtonGroup(frame, self.OnBtnScreen, labels, conf.default_screen)
btns = self.screenBtnGroup.GetButtons()
all_buttons += btns
for col in range(0, 6):
gbs.Add(btns[col], (3, col + 7), flag=flag)
bw = bh = 0
for b in all_buttons: # find the largest button size
w, h = b.GetMinSize()
bw = max(bw, w)
bh = max(bh, h)
for b in all_buttons: # set all buttons to the same size
b.SetSizeHints(bw, bh, bw * 5, bh)
return bw, bh # return the button size
def NewSmeter(self):
#avg_seconds = 5.0 # seconds for S-meter average
avg_seconds = 1.0
self.smeter_db_count += 1 # count for average
x = QS.get_smeter()
self.smeter_db_sum += x # sum for average
if self.timer - self.smeter_db_time0 > avg_seconds: # average time reached
self.smeter_db = self.smeter_db_sum / self.smeter_db_count
self.smeter_db_count = self.smeter_db_sum = 0
self.smeter_db_time0 = self.timer
if self.smeter_sunits < x: # S-meter moves to peak value
self.smeter_sunits = x
else: # S-meter decays at this time constant
self.smeter_sunits -= (self.smeter_sunits - x) * (self.timer - self.smeter_sunits_time0)
self.smeter_sunits_time0 = self.timer
s = self.smeter_sunits / 6.0 # change to S units; 6db per S unit
s += Hardware.correct_smeter # S-meter correction for the gain, band, etc.
if s < 0:
s = 0
if s >= 9.5:
s = (s - 9.0) * 6
t = "S9 + %.0f %.2f dB" % (s, self.smeter_db)
else:
t = "S %.0f %.2f dB" % (s, self.smeter_db)
self.smeter.SetLabel(t)
def MakeFilterButtons(self, *args):
# Change the filter selections depending on the mode: CW, SSB, etc.
i = 0
for b in self.filterButns.GetButtons():
b.SetLabel(str(args[i]))
b.Refresh()
i += 1
def MakeFilterCoef(self, rate, N, bw, center):
"""Make an I/Q filter with rectangular passband."""
K = bw * N / rate
filtI = []
filtQ = []
pi = math.pi
sin = math.sin
cos = math.cos
tune = 2. * pi * center / rate
for k in range(-N/2, N/2 + 1):
# Make a lowpass filter
if k == 0:
z = float(K) / N
else:
z = 1.0 / N * sin(pi * k * K / N) / sin(pi * k / N)
# Apply a windowing function
if 1: # Blackman window
w = 0.42 + 0.5 * cos(2. * pi * k / N) + 0.08 * cos(4. * pi * k / N)
elif 0: # Hamming
w = 0.54 + 0.46 * cos(2. * pi * k / N)
elif 0: # Hanning
w = 0.5 + 0.5 * cos(2. * pi * k / N)
else:
w = 1
z *= w
# Make a bandpass filter by tuning the low pass filter to new center frequency.
# Make two quadrature filters.
if tune:
z *= 2.0 * cmath.exp(-1j * tune * k)
filtI.append(z.real)
filtQ.append(z.imag)
else:
filtI.append(z)
filtQ.append(z)
return filtI, filtQ
def MakeFmFilterCoef(self, rate, N, f1, f2):
"""Make an audio filter with FM de-emphasis; remove CTCSS tones."""
bw = f2 - f1
center = (f1 + f2) / 2
N2 = N / 2 # Half the number of points
K2 = bw * N / rate / 2 # Half the bandwidth in points
filtI = []
filtQ = []
passb = [0] * (N + 1) # desired passband response
idft = [0] * (N + 1) # inverse DFT of desired passband
pi = math.pi
sin = math.sin
cos = math.cos
tune = 2. * pi * center / rate
# indexing can be from - N2 thru + N2 inclusive; total points is 2 * N2 + 1
# indexing can be from 0 thru 2 * N2 inclusive; total points is 2 * N2 + 1
for j in range(-K2, K2 + 1): # Filter shape is -6 bB per octave
jj = j + N2
freq = center - bw / 2.0 * float(j) / K2
passb[jj] = float(center) / freq * 0.3
for k in range(-N2 + 1, N2 + 1): # Take inverse DFT of passband response
kk = k + N2
x = 0 + 0J
for m in range(-N2, N2 + 1):
mm = m + N2
if passb[mm]:
x += passb[mm] * cmath.exp(1J * 2.0 * pi * m * k / N)
x /= N
idft[kk] = x
idft[0] = idft[-1] # this value is missing
for k in range(-N2, N2 + 1):
kk = k + N2
z = idft[kk]
# Apply a windowing function
if 1: # Blackman window
w = 0.42 + 0.5 * cos(2. * pi * k / N) + 0.08 * cos(4. * pi * k / N)
elif 0: # Hamming
w = 0.54 + 0.46 * cos(2. * pi * k / N)
elif 0: # Hanning
w = 0.5 + 0.5 * cos(2. * pi * k / N)
else:
w = 1
z *= w
# Make a bandpass filter by tuning the low pass filter to new center frequency.
# Make two quadrature filters.
if tune:
z *= 2.0 * cmath.exp(-1j * tune * k)
filtI.append(z.real)
filtQ.append(z.imag)
else:
filtI.append(z.real)
filtQ.append(z.real)
return filtI, filtQ
def OnBtnFilter(self, event, bw=None):
if event is None: # called by application
self.filterButns.SetLabel(str(bw))
else: # called by button
btn = event.GetEventObject()
bw = int(btn.GetLabel())
mode = self.mode
if mode in ("CWL", "CWU"):
N = 1000
center = max(conf.cwTone, bw/2)
elif mode in ('LSB', 'USB'):
N = 540
center = 300 + bw / 2
else: # AM and FM
N = 140
center = 0
frate = QS.get_filter_rate()
filtI, filtQ = self.MakeFilterCoef(frate, N, bw, center)
QS.set_filters(filtI, filtQ, bw)
if self.screen is self.filter_screen:
self.screen.NewFilter()
def OnBtnScreen(self, event, name=None):
if event is not None:
win = event.GetEventObject()
name = win.GetLabel()
self.screen.Hide()
if name == 'Config':
self.screen = self.config_screen
elif name[0:5] == 'Graph':
self.screen = self.graph
self.screen.SetTxFreq(self.txFreq, self.rxFreq)
self.freqDisplay.Display(self.VFO + self.txFreq)
self.screen.PeakHold(name)
elif name == 'WFall':
self.screen = self.waterfall
self.screen.SetTxFreq(self.txFreq, self.rxFreq)
self.freqDisplay.Display(self.VFO + self.txFreq)
sash = self.screen.GetSashPosition()
elif name == 'Scope':
if win.direction: # Another push on the same button
self.scope.running = 1 - self.scope.running # Toggle run state
else: # Initial push of button
self.scope.running = 1
self.screen = self.scope
elif name == 'RX Filter':
self.screen = self.filter_screen
self.freqDisplay.Display(self.screen.txFreq)
self.screen.NewFilter()
elif name == 'Help':
self.screen = self.help_screen
self.screen.Show()
self.vertBox.Layout() # This destroys the initialized sash position!
self.sliderYs.SetValue(self.screen.y_scale)
self.sliderYz.SetValue(self.screen.y_zero)
if name == 'WFall':
self.screen.SetSashPosition(sash)
def ChangeYscale(self, event):
self.screen.ChangeYscale(self.sliderYs.GetValue())
def ChangeYzero(self, event):
self.screen.ChangeYzero(self.sliderYz.GetValue())
def OnBtnMute(self, event):
btn = event.GetEventObject()
if btn.GetValue():
QS.set_volume(0)
else:
QS.set_volume(self.audio_volume)
def OnBtnDecimation(self, event):
i = event.GetSelection()
rate = Hardware.VarDecimSet(i)
self.vardecim_set = rate
if rate != self.sample_rate:
self.sample_rate = rate
self.graph.sample_rate = rate
self.waterfall.pane1.sample_rate = rate
self.waterfall.pane2.sample_rate = rate
self.waterfall.pane2.display.sample_rate = rate
average_count = float(rate) / conf.graph_refresh / self.fft_size
average_count = int(average_count + 0.5)
average_count = max (1, average_count)
QS.change_rate(rate, average_count)
tune = self.txFreq
vfo = self.VFO
self.txFreq = self.VFO = -1 # demand change
self.ChangeHwFrequency(tune, vfo, 'NewDecim')
def ChangeVolume(self, event=None):
# Caution: event can be None
value = self.sliderVol.GetValue()
# Simulate log taper pot
x = (10.0 ** (float(value) * 0.003000434077) - 1) / 1000.0
self.audio_volume = x # audio_volume is 0 to 1.000
QS.set_volume(x)
def ChangeSidetone(self, event=None):
# Caution: event can be None
value = self.sliderSto.GetValue()
# Simulate log taper pot
x = (10.0 ** (float(value) * 0.003) - 1) / 1000.0
self.sidetone_volume = x
QS.set_sidetone(x, self.ritFreq, conf.keyupDelay)
def OnRitScale(self, event=None): # Called when the RIT slider is moved
# Caution: event can be None
if self.ritButton.GetValue():
value = self.ritScale.GetValue()
value = int(value)
self.ritFreq = value
QS.set_tune(self.rxFreq + self.ritFreq, self.txFreq)
QS.set_sidetone(self.sidetone_volume, self.ritFreq, conf.keyupDelay)
def OnBtnSplit(self, event): # Called when the Split check button is pressed
self.split_rxtx = self.splitButton.GetValue()
if self.split_rxtx:
self.rxFreq = self.oldRxFreq
d = self.sample_rate * 49 / 100 # Move rxFreq on-screen
if self.rxFreq < -d:
self.rxFreq = -d
elif self.rxFreq > d:
self.rxFreq = d
else:
self.oldRxFreq = self.rxFreq
self.rxFreq = self.txFreq
self.screen.SetTxFreq(self.txFreq, self.rxFreq)
QS.set_tune(self.rxFreq + self.ritFreq, self.txFreq)
def OnBtnRit(self, event=None): # Called when the RIT check button is pressed
# Caution: event can be None
if self.ritButton.GetValue():
self.ritFreq = self.ritScale.GetValue()
else:
self.ritFreq = 0
QS.set_tune(self.rxFreq + self.ritFreq, self.txFreq)
QS.set_sidetone(self.sidetone_volume, self.ritFreq, conf.keyupDelay)
def SetRit(self, freq):
if freq:
self.ritButton.SetValue(1)
else:
self.ritButton.SetValue(0)
self.ritScale.SetValue(freq)
self.OnBtnRit()
def OnBtnFDX(self, event):
btn = event.GetEventObject()
if btn.GetValue():
QS.set_fdx(1)
else:
QS.set_fdx(0)
def OnBtnTest1(self, event):
btn = event.GetEventObject()
if btn.GetValue():
QS.add_tone(10000)
else:
QS.add_tone(0)
def OnBtnTest2(self, event):
return
def OnBtnColor(self, event):
if not self.color_list:
clist = wx.lib.colourdb.getColourInfoList()
self.color_list = [(0, clist[0][0])]
self.color_index = 0
for i in range(1, len(clist)):
if self.color_list[-1][1].replace(' ', '') != clist[i][0].replace(' ', ''):
#if 'BLUE' in clist[i][0]:
self.color_list.append((i, clist[i][0]))
else:
btn = event.GetEventObject()
if btn.shift:
del self.color_list[self.color_index]
else:
self.color_index += btn.direction
if self.color_index >= len(self.color_list):
self.color_index = 0
elif self.color_index < 0:
self.color_index = len(self.color_list) -1
color = self.color_list[self.color_index][1]
print self.color_index, color
self.main_frame.SetBackgroundColour(color)
self.main_frame.Refresh()
self.screen.Refresh()
def OnBtnAGC(self, event):
btn = event.GetEventObject()
# Set AGC: agcInUse, agcAttack, agcRelease
if btn.index == 1:
QS.set_agc(1, 1.0, 0.01)
elif btn.index == 2:
QS.set_agc(2, 1.0, 0.1)
else:
QS.set_agc(0, 0, 0)
def OnBtnNB(self, event):
pass
def FreqEntry(self, event):
freq = event.GetString()
if not freq:
return
try:
if '.' in freq:
freq = int(float(freq) * 1E6 + 0.1)
else:
freq = int(freq)
except ValueError:
win = event.GetEventObject()
win.Clear()
win.AppendText("Error")
else:
for band, (f1, f2) in conf.BandEdge.items():
if f1 <= freq <= f2: # Change to the correct band based on frequency
self.bandBtnGroup.SetLabel(band, do_cmd=True)
break
tune = freq % 10000
vfo = freq - tune
self.ChangeHwFrequency(tune, vfo, 'FreqEntry')
def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None):
"""Change the VFO and tuning frequencies, and notify the hardware.
tune: the new tuning frequency in +- sample_rate/2;
vfo: the new vfo frequency in Hertz; this is the RF frequency at zero Hz audio
source: a string indicating the source or widget requesting the change;
band: if source is "BtnBand", the band requested;
event: for a widget, the event (used to access control/shift key state).
Try to update the hardware by calling Hardware.ChangeFrequency().
The hardware will reply with the updated frequencies which may be different
from those requested; use and display the returned tune and vfo.
If tune or vfo is None, query the hardware for the current frequency.
"""
change = 0
if tune is None or vfo is None:
tune, vfo = Hardware.ReturnFrequency()
if tune is None or vfo is None: # hardware did not change the frequency
return change
else:
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event)
tune -= vfo
if tune != self.txFreq:
change = 1
self.txFreq = tune
if not self.split_rxtx:
self.rxFreq = self.txFreq
self.screen.SetTxFreq(self.txFreq, self.rxFreq)
QS.set_tune(self.rxFreq + self.ritFreq, self.txFreq)
if vfo != self.VFO:
change = 1
self.VFO = vfo
self.graph.SetVFO(vfo)
self.waterfall.SetVFO(vfo)
if self.w_phase: # Phase adjustment screen can not change its VFO
self.w_phase.Destroy()
self.w_phase = None
ampl, phase = self.GetAmplPhase()
QS.set_ampl_phase(ampl, phase)
if change:
self.freqDisplay.Display(self.txFreq + self.VFO)
return change
def OnBtnMode(self, event, mode=None):
if event is None: # called by application
self.modeButns.SetLabel(mode)
else: # called by button
mode = self.modeButns.GetLabel()
Hardware.ChangeMode(mode)
self.mode = mode
if mode in ('CWL', 'CWU'):
if mode == 'CWL':
QS.set_rx_mode(0)
self.SetRit(conf.cwTone)
else: # CWU
QS.set_rx_mode(1)
self.SetRit(-conf.cwTone)
self.MakeFilterButtons(200, 300, 400, 500, 1000, 3000)
self.OnBtnFilter(None, 1000)
elif mode in ('LSB', 'USB'):
if mode == 'LSB':
QS.set_rx_mode(2) # LSB
else:
QS.set_rx_mode(3) # USB
self.SetRit(0)
self.MakeFilterButtons(1800, 2000, 2200, 2500, 2800, 3300)
self.OnBtnFilter(None, 2800)
elif mode == 'AM':
QS.set_rx_mode(4)
self.SetRit(0)
self.MakeFilterButtons(4000, 5000, 6000, 7000, 8000, 9000)
self.OnBtnFilter(None, 6000)
elif mode == 'FM':
QS.set_rx_mode(5)
self.SetRit(0)
self.MakeFilterButtons(10000, 12000, 15000, 25000, 35000, 45000)
self.OnBtnFilter(None, 12000)
elif mode[0:3] == 'IMD':
QS.set_rx_mode(10 + self.modeButns.GetSelectedButton().index) # 10, 11, 12
self.SetRit(0)
self.MakeFilterButtons(1800, 2000, 2200, 2500, 2800, 3300)
self.OnBtnFilter(None, 2800)
elif mode == conf.add_extern_demod: # External demodulation
QS.set_rx_mode(6)
self.SetRit(0)
self.MakeFilterButtons(10000, 12000, 15000, 25000, 35000, 45000)
self.OnBtnFilter(None, 12000)
def OnBtnBand(self, event):
band = self.lastBand # former band in use
try:
f1, f2 = conf.BandEdge[band]
if f1 <= self.VFO + self.txFreq <= f2:
self.bandState[band] = (self.VFO, self.txFreq, self.mode)
except KeyError:
pass
btn = event.GetEventObject()
band = btn.GetLabel() # new band
self.lastBand = band
try:
vfo, tune, mode = self.bandState[band]
except KeyError:
vfo, tune, mode = (0, 0, 'LSB')
if band == '60':
freq = vfo + tune
if btn.direction:
vfo = self.VFO
if 5100000 < vfo < 5600000:
if btn.direction > 0: # Move up
for f in self.freq60:
if f > vfo + self.txFreq:
freq = f
break
else:
freq = self.freq60[0]
else: # move down
l = list(self.freq60)
l.reverse()
for f in l:
if f < vfo + self.txFreq:
freq = f
break
else:
freq = self.freq60[-1]
half = self.sample_rate / 2 * self.graph_width / self.data_width
while freq - vfo <= -half + 1000:
vfo -= 10000
while freq - vfo >= +half - 5000:
vfo += 10000
tune = freq - vfo
elif band == 'Time':
vfo, tune, mode = conf.bandTime[btn.index]
self.OnBtnMode(None, mode)
self.txFreq = self.VFO = -1 # demand change
self.ChangeHwFrequency(tune, vfo, 'BtnBand', band=band)
Hardware.ChangeBand(band)
def OnBtnUpDnBandDelta(self, event, is_band_down):
btn = event.GetEventObject()
if btn.direction > 0: # left button was used, move a bit
d = max(10000, int(self.sample_rate / 9))
else: # right button was used, move to edge
d = max(10000, int(self.sample_rate * 49 / 100))
d = (d / 10000) * 10000
if is_band_down:
d = -d
self.VFO += d
self.txFreq -= d
self.rxFreq -= d
# Set the display but do not change the hardware
self.graph.SetVFO(self.VFO)
self.waterfall.SetVFO(self.VFO)
self.screen.SetTxFreq(self.txFreq, self.rxFreq)
self.freqDisplay.Display(self.txFreq + self.VFO)
def OnBtnDownBand(self, event):
self.band_up_down = 1
self.OnBtnUpDnBandDelta(event, True)
def OnBtnUpBand(self, event):
self.band_up_down = 1
self.OnBtnUpDnBandDelta(event, False)
def OnBtnUpDnBandDone(self, event):
self.band_up_down = 0
tune = self.txFreq
vfo = self.VFO
self.txFreq = self.VFO = 0 # Force an update
self.ChangeHwFrequency(tune, vfo, 'BtnUpDown')
def GetAmplPhase(self):
if conf.bandAmplPhase.has_key("panadapter"):
band = "panadapter"
else:
band = self.lastBand
try:
lst = self.bandAmplPhase[band]["rx"]
except KeyError:
return (0.0, 0.0)
length = len(lst)
if length == 0:
return (0.0, 0.0)
elif length == 1:
return lst[0][2], lst[0][3]
elif self.VFO < lst[0][0]: # before first data point
i1 = 0
i2 = 1
elif lst[length - 1][0] < self.VFO: # after last data point
i1 = length - 2
i2 = length - 1
else:
# Binary search for the bracket VFO
i1 = 0
i2 = length
index = (i1 + i2) / 2
for i in range(length):
diff = lst[index][0] - self.VFO
if diff < 0:
i1 = index
elif diff > 0:
i2 = index
else: # equal VFO's
return lst[index][2], lst[index][3]
if i2 - i1 <= 1:
break
index = (i1 + i2) / 2
d1 = self.VFO - lst[i1][0] # linear interpolation
d2 = lst[i2][0] - self.VFO
dx = d1 + d2
ampl = (d1 * lst[i2][2] + d2 * lst[i1][2]) / dx
phas = (d1 * lst[i2][3] + d2 * lst[i1][3]) / dx
return ampl, phas
def PostStartup(self): # called once after sound attempts to start
self.config_screen.OnGraphData(None) # update config in case sound is not running
def OnReadSound(self): # called at frequent intervals
self.timer = time.time()
if self.screen == self.scope:
data = QS.get_graph(0) # get raw data
if data:
self.scope.OnGraphData(data) # Send message to draw new data
return 1 # we got new scope data
else:
data = QS.get_graph(1) # get FFT data
if data:
#T('')
self.NewSmeter() # update the S-meter
if self.screen == self.graph:
self.waterfall.OnGraphData(data) # save waterfall data
self.graph.OnGraphData(data) # Send message to draw new data
elif self.screen == self.config_screen:
pass
else:
self.screen.OnGraphData(data) # Send message to draw new data
#T('graph data')
#application.Yield()
#T('Yield')
return 1 # We got new graph/scope data
if QS.get_overrange():
self.clip_time0 = self.timer
self.freqDisplay.Clip(1)
if self.clip_time0:
if self.timer - self.clip_time0 > 1.0:
self.clip_time0 = 0
self.freqDisplay.Clip(0)
if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks
self.heart_time0 = self.timer
if self.screen == self.config_screen:
self.screen.OnGraphData() # Send message to draw new data
Hardware.HeartBeat()
if not self.band_up_down:
# Poll the hardware for changed frequency. This is used for hardware
# that can change its frequency independently of Quisk; eg. K3.
if self.ChangeHwFrequency(None, None): # Returns true for a change
try:
f1, f2 = conf.BandEdge[self.lastBand]
if f1 <= self.VFO + self.txFreq <= f2:
self.bandState[self.lastBand] = (self.VFO, self.txFreq, self.mode)
return
except KeyError:
pass
# Frequency is not within the current band. Change to the correct band based on frequency.
for band, (f1, f2) in conf.BandEdge.items():
if f1 <= self.VFO + self.txFreq <= f2:
self.lastBand = band
self.bandBtnGroup.SetLabel(band, do_cmd=True)
break
def main():
"""If quisk is installed as a package, you can run it with quisk.main()."""
App()
application.MainLoop()
if __name__ == '__main__':
main()
# Please do not change the configuration file quisk_conf_defaults.py.
# Instead copy one of the other quisk_conf_*.py files to your own
# .quisk_conf.py and make changes there. For a normal sound card
# configuration, copy quisk_conf_model.py to your .quisk_conf.py.
#
# Quisk imports quisk_conf_defaults to set its configuration.
# If you have a configuration file, it then overwrites the defaults
# with your parameters. Your configuration file must be named
# ~/.quisk_conf.py, where "~" means your home directory. Or
# you may specify a different name with the -c or --config command
# line option. Try --help. Check the config screen to make sure that
# the correct configuration file is in use.
#
# The Quisk receiver can use a high quality sound card for capture and playback,
# or it can use the SDR-IQ by RfSpace for capture and a lower quality
# sound card for playback. Quisk can also be used as a panadapter.
# Quisk can control some rigs. See quisk_hardware_*.py. If you have a rig
# to control, copy one of the quisk_hardware_*.py files to your own file named
# quisk_hardware.py, and edit that file. If there is no quisk_hardware.py, then
# quisk_hardware_model.py is used instead.
import sys
# Import the default Hardware module. You can import a different module in
# your .quisk_conf.py.
import quisk_hardware_model as quisk_hardware
# Module for additional widgets (advanced usage).
quisk_widgets = None
# Select the default screen when Quisk starts:
default_screen = 'Graph'
#default_screen = 'WFall'
#default_screen = 'Config'
# The width of the graph data as a fraction of the total screen size. This
# will be adjusted by Quisk to accommodate preferred FFT sizes. It can
# not be changed once Quisk starts. It can not be made too small because
# of the space needed for all the buttons. If you have two displays, Quisk
# will try to fill both, so set graph_width to 0.4 or smaller.
graph_width = 0.8
# This controls the speed of the graph peak hold. Lower numbers give a longer time constant.
graph_peak_hold_1 = 0.25
graph_peak_hold_2 = 0.10
# Select the default mode when Quisk starts (overruled by persistent_state):
# default_mode = 'FM'
default_mode = 'USB'
# Select the way the waterfall screen scrolls:
# waterfall_scroll_mode = 0 # scroll at a constant rate.
waterfall_scroll_mode = 1 # scroll faster at the top so that a new signal appears sooner.
# Select the initial size in pixels (minimum 1) of the graph at the top of the waterfall.
waterfall_graph_size = 80
# These are the initial values for the Y-scale and Y-zero sliders for each screen.
# The sliders go from zero to 160.
graph_y_scale = 100
graph_y_zero = 0
waterfall_y_scale = 80
waterfall_y_zero = 40
waterfall_graph_y_scale = 100
waterfall_graph_y_zero = 60
scope_y_scale = 80
scope_y_zero = 0 # Currently doesn't do anything
filter_y_scale = 90
filter_y_zero = 0
# Quisk can save its current state in a file on exit, and restore it when you restart.
# State includes band, frequency and mode, but not every item of state (not screen).
# The file is .quisk_init.pkl in the same directory as your config file. If this file
# becomes corrupted, just delete it and it will be reconstructed.
#persistent_state = False
persistent_state = True
# This converts from dB to S-units for the S-meter (it is in S-units).
correct_smeter = 15.5
# This is the fraction of spectrum to display from zero to one. It is needed if
# the passband edges are not valid. Use 0.85 for the SDR-IQ.
display_fraction = 1.00
# Define colors used by all widgets in wxPython colour format:
color_bg = 'light steel blue' # Lower screen background
color_graph = 'lemonchiffon1' # Graph background
color_gl = 'grey' # Lines on the graph
color_btn = 'steelblue2' # button color
color_check_btn = 'yellow2' # color of a check button when it is checked
color_cycle_btn = 'goldenrod3' # color of a cycle button when it is checked
color_test = 'hot pink' # color of a button used for test (turn off for tx)
color_freq = 'lightcyan1' # background color of frequency and s-meter
color_entry = color_freq # frequency entry box
# Added by AC
color_tick = 'black'
color_fft = (0x4c,0x4c,0xff)
# These are the palettes for the waterfall. The one used is named waterfallPallette,
# so to use a different one, overwrite this name in your .quisk_conf.py.
waterfallPalette = (
( 0, 0, 0, 0),
( 36, 85, 0, 255),
( 73, 153, 0, 255),
(109, 255, 0, 128),
(146, 255, 119, 0),
(182, 85, 255, 100),
(219, 255, 255, 0),
(255, 255, 255, 255)
)
digipanWaterfallPalette = (
( 0, 0, 0, 0),
( 32, 0, 0, 62),
( 64, 0, 0, 126),
( 96, 145, 142, 96),
(128, 181, 184, 48),
(160, 223, 226, 105),
(192, 254, 254, 4),
(255, 255, 58, 0)
)
# Quisk can access your sound card through PortAudio or through ALSA drivers.
# In PortAudio, soundcards have an index number 0, 1, 2, ... and a name.
# The name can be something like "HDA NVidia: AD198x Analog (hw:0,0)" or
# "surround41". In Quisk, all PortAudio device names start with "portaudio".
# A device name like "portaudio#6" directly specifies the index. A name like
# "portaudio:text" means to search for "text" in all available devices. And
# there is a default device "portaudiodefault". So these portaudio names are useful:
#name_of_sound_capt = "portaudio:(hw:0,0)" # First sound card
#name_of_sound_capt = "portaudio:(hw:1,0)" # Second sound card, etc.
#name_of_sound_capt = "portaudio#1" # Directly specified index
#name_of_sound_capt = "portaudiodefault" # May give poor performance on capture
# In ALSA, soundcards have these names. The "hw" devices are the raw
# hardware devices, and should be used for soundcard capture.
#name_of_sound_capt = "hw:0" # First sound card
#name_of_sound_capt = "hw:1" # Second sound card, etc.
#name_of_sound_capt = "plughw"
#name_of_sound_capt = "plughw:1"
#name_of_sound_capt = "default"
# Normally you would capture and play on the same soundcard to avoid problems with the
# two clocks running at slightly different rates. But you can define name_of_sound_play
# to play back on a different device. Define this as the empty string "" to turn off
# play (for a panadapter).
#
# For the SDR-IQ the soundcard is not used for capture; it only plays back audio.
# Playback is always 48 kHz stereo.
# Configuration for soundcard capture and playback
use_sdriq = 0 # Get ADC samples from SDR-IQ is not used
use_rx_udp = 0 # Get ADC samples from UDP is not used
sample_rate = 48000 # ADC hardware sample rate in Hertz
if sys.platform == "win32":
name_of_sound_capt = "Primary"
else:
name_of_sound_capt = "hw:0" # Name of soundcard capture hardware device.
name_of_sound_play = name_of_sound_capt # Use the same device for play back
#name_of_sound_play = "" # Panadapter: Do not play
channel_i = 0 # Soundcard index of in-phase channel: 0, 1, 2, ...
channel_q = 1 # Soundcard index of quadrature channel: 0, 1, 2, ...
# Thanks to Franco Spinelli for this fix:
# The H101 hardware using the PCM2904 chip has a one-sample delay between
# channels, which must be fixed in software. If you have this problem,
# change channel_delay to either channel_i or channel_q. Use -1 for no delay.
channel_delay = -1
# If you use a soundcard with Ethernet control of the VFO, set these parameters:
rx_ip = "" # Receiver IP address for VFO control
# If you use an SDR-IQ for capture, set these parameters:
# import quisk_hardware_sdriq as quisk_hardware # Use different hardware file
# use_sdriq = 1 # Capture device is the SDR-IQ
# sdriq_name = "/dev/ft2450" # Name of the SDR-IQ device to open
# sdriq_clock = 66666667.0 # actual sample rate (66666667 nominal)
# sdriq_decimation = 500 # Must be 360, 500, 600, or 1250
# sample_rate = int(float(sdriq_clock) / sdriq_decimation + 0.5) # Don't change this
# name_of_sound_capt = "" # We do not capture from the soundcard
# name_of_sound_play = "hw:0" # Play back on this soundcard
# playback_rate = 48000 # Radio sound play rate, default 48000
# channel_i = 0 # Soundcard index of left channel
# channel_q = 1 # Soundcard index of right channel
# display_fraction = 0.85 # The edges of the full bandwidth are not valid
# If you receive ADC samples from a UDP port, set these parameters:
# import quisk_hardware_n2adr as quisk_hardware # Use different hardware file
# use_rx_udp = 1 # Get ADC samples from UDP
# rx_udp_ip = "192.168.1.91" # Sample source IP address
# rx_udp_port = 0xBC77 # Sample source UDP port
# rx_udp_clock = 122880000 # ADC sample rate in Hertz
# rx_udp_decimation = 8 * 8 * 8 # Decimation from clock to UDP sample rate
# The allowable decimations are 8 times 8 times (2 or 4 or 8) times (1 or 5).
# So you could enter 8 * 8 * one of (2, 4, 5, 8, 10, 20, 40).
# These decimations result in a sample rate of 48 to 960 kHz.
# sample_rate = int(float(rx_udp_clock) /rx_udp_decimation + 0.5) # Don't change this
# name_of_sound_capt = "" # We do not capture from the soundcard
# name_of_sound_play = "hw:0" # Play back on this soundcard
# playback_rate = 48000 # Radio sound play rate, default 48000
# This is the received radio sound playback rate. The default will
# be 48 kHz for the SDR-IQ and UDP port samples, and sample_rate for sound
# card capture. Set it yourself for other rates or hardware.
# playback_rate = 48000
# If you use quisk_hardware_fixed.py, this is the fixed VFO frequency in Hertz
fixed_vfo_freq = 7056000
# Some hardware must be polled to get the key up/down state. This is the time
# between polls in milliseconds. Use zero to turn off the poll.
key_poll_msec = 0
# This determines what happens when you tune by dragging the mouse. The correct
# choice depends on how your hardware performs tuning. You may want to use a
# custom hardware file with a custom ChangeFrequency() method too.
mouse_tune_method = 0 # The Quisk tune frequency changes and the VFO frequency is unchanged.
#mouse_tune_method = 1 # The Quisk tune frequency is unchanged and the VFO changes.
# This is the CW tone frequency in Hertz
cwTone = 600
# If you use the microphone feature, the mic_channel_I and Q are the two capture
# microphone channels. Quisk uses a monophonic mic, so audio is taken from the I
# channel, and the Q channel is (currently) ignored. It is OK to set the same
# channel number for both, and this is necessary for a USB mono mic. If you
# change the sample rate, you will need to change the C code to use different filters.
# Mic samples can be sent to an Ethernet device (use tx_ip and name_of_mic_play = "")
# or to a sound card (use name_of_mic_play="hw:1" or other device).
# If mic samples are sent to a sound card for Tx, the samples are tuned to the audio
# transmit frequency, and are set to zero unless the key is down.
# If there is no mic (microphone_name = ""), it is still possible to transmit CW,
# and you should set mic_playback_rate to the I/Q receive capture rate.
# Microphone capture:
microphone_name = "" # Name of microphone capture device (or "hw:1")
mic_sample_rate = 48000 # Microphone capture sample rate in Hertz, must be 48000
mic_channel_I = 0 # Soundcard index of mic capture audio channel
mic_channel_Q = 0 # Soundcard index of ignored capture channel
# Microphone samples sent to soundcard:
name_of_mic_play = "" # Name of play device if mic I/Q is sent to a sound card
mic_playback_rate = 48000 # Playback rate must be a multiple 1, 2, ... of mic_sample_rate
mic_play_chan_I = 0 # Soundcard index of mic I play channel
mic_play_chan_Q = 1 # Soundcard index of mic Q play channel
mic_out_volume = 1.0 # Microphone output volume (after all processing) as a fraction 0.0 to 1.0
# Microphone samples sent to UDP:
tx_ip = "" # Transmit IP address for mic sent to UDP (or "192.168.2.195")
tx_audio_port = 0 # UDP port for mic samples (or 0x553B)
# Microphone audio processing:
# The original audio processing used mic_clip = 4.0; mic_preemphasis = -1.0
# For no mic audio processing, use mic_clip = 1.0; mic_preemphasis = 0.0
mic_clip = 3.0 # Mic amplitude clipping; larger numbers give more clipping
mic_preemphasis = 0.6 # Mic pre-emphasis 0.0 (none) to 1.0; or -1.0 for a Butterworth filter
# If your mixing scheme inverts the RF spectrum, set this option to un-invert it
invertSpectrum = 0
# Use "amixer -c 1 contents" to get a list of mixer controls and their numid's for
# card 1 (or "-c 0" for card 0). Then make a list of (device_name, numid, value)
# for each control you need to set. The sample settings are for my USB microphone.
#mixer_settings = [
# ("hw:1", 2, 0.80), # numid of microphone volume control, volume 0.0 to 1.0;
# ("hw:1", 1, 1.0) # numid of capture on/off control, turn on with 1.0;
# ]
# If you want Quisk to add a button to generate a 2-tone IMD test signal,
# set this to 1. This feature requires the microphone to work.
add_imd_button = 0
# If you want Quisk to add a full duplex button (transmit and receive at the
# same time), set this to 1.
add_fdx_button = 0
# If you want to write your own I/Q filter and demodulation module, set
# this to the name of the button to add, and change extdemod.c.
# add_extern_demod = "WFM"
add_extern_demod = ""
# This is the data used to draw colored lines on the frequency X axis to
# indicate CW and Phone sub-bands. You can make it anything you want.
# These are the colors used for sub-bands:
CW = '#FF4444' # General class CW
eCW = '#FF8888' # Extra class CW
Phone = '#4444FF' # General class phone
ePhone = '#8888FF' # Extra class phone
# ARRL band plan special frequencies
Data = '#FF9900'
DxData = '#CC6600'
RTTY = '#FF9900'
SSTV = '#FFFF00'
AM = '#00FF00'
Packet = '#00FFFF'
Beacons = '#66FF66'
Satellite = '#22AA88'
Repeater = '#AA00FF'
Simplex = '#00FF44'
Other = '#888888'
# Colors start at the indicated frequency and continue until the
# next frequency. The special color "None" turns off color.
BandPlan = [
# 160 meters
[ 1800000, Data],
[ 1809000, Other],
[ 1811000, CW],
[ 1843000, Phone],
[ 1908000, Other],
[ 1912000, Phone],
[ 1995000, Other],
[ 2000000, None],
# 80 meters
[ 3500000, eCW],
[ 3525000, CW],
[ 3570000, Data],
[ 3589000, DxData],
[ 3591000, Data],
[ 3600000, ePhone],
[ 3790000, Other],
[ 3800000, Phone],
[ 3844000, SSTV],
[ 3846000, Phone],
[ 3880000, AM],
[ 3890000, Phone],
[ 4000000, None],
# 60 meters
[ 5330600, Phone],
[ 5333400, None],
[ 5346600, Phone],
[ 5349400, None],
[ 5366600, Phone],
[ 5369400, None],
[ 5371600, Phone],
[ 5374400, None],
[ 5403600, Phone],
[ 5406400, None],
# 40 meters
[ 7000000, eCW],
[ 7025000, CW],
[ 7039000, DxData],
[ 7041000, CW],
[ 7080000, Data],
[ 7125000, ePhone],
[ 7170000, SSTV],
[ 7172000, ePhone],
[ 7175000, Phone],
[ 7285000, AM],
[ 7295000, Phone],
[ 7300000, None],
# 30 meters
[10100000, CW],
[10130000, RTTY],
[10140000, Packet],
[10150000, None],
# 20 meters
[14000000, eCW],
[14025000, CW],
[14070000, RTTY],
[14095000, Packet],
[14099500, Other],
[14100500, Packet],
[14112000, CW],
[14150000, ePhone],
[14225000, Phone],
[14229000, SSTV],
[14231000, Phone],
[14281000, AM],
[14291000, Phone],
[14350000, None],
# 17 meters
[18068000, CW],
[18100000, RTTY],
[18105000, Packet],
[18110000, Phone],
[18168000, None],
# 15 meters
[21000000, eCW],
[21025000, CW],
[21070000, RTTY],
[21110000, CW],
[21200000, ePhone],
[21275000, Phone],
[21339000, SSTV],
[21341000, Phone],
[21450000, None],
# 12 meters
[24890000, CW],
[24920000, RTTY],
[24925000, Packet],
[24930000, Phone],
[24990000, None],
# 10 meters
[28000000, CW],
[28070000, RTTY],
[28150000, CW],
[28200000, Beacons],
[28300000, Phone],
[28679000, SSTV],
[28681000, Phone],
[29000000, AM],
[29200000, Phone],
[29300000, Satellite],
[29520000, Repeater],
[29590000, Simplex],
[29610000, Repeater],
[29700000, None],
# 6 meters
[50000000, Beacons],
[50100000, Phone],
[54000000, None],
]
# For each band, this dictionary gives the lower and upper band edges. Frequencies
# outside these limits will not be remembered as the last frequency in the band.
BandEdge = {
'160':( 1800000, 2000000), '80' :( 3500000, 4000000),
'60' :( 5300000, 5430000), '40' :( 7000000, 7300000),
'30' :(10100000, 10150000), '20' :(14000000, 14350000),
'17' :(18068000, 18168000), '15' :(21000000, 21450000),
'12' :(24890000, 24990000), '10' :(28000000, 29700000),
'6' :(50000000, 54000000),
}
# For each band, this dictionary gives the initial center frequency, tuning
# frequency as an offset from the center frequency, and the mode. This is
# no longer too useful because the persistent_state feature saves and then
# overwrites these values anyway.
bandState = {'Audio':(0, 0, 'LSB'),
'160':( 1890000, -10000, 'LSB'), '80' :( 3660000, -10000, 'LSB'),
'60' :( 5370000, 1500, 'USB'), '40' :( 7180000, -5000, 'LSB'), '30':(10120000, -10000, 'CWL'),
'20' :(14200000, -10000, 'USB'), '17' :(18120000, 10000, 'USB'), '15':(21250000, -10000, 'USB'),
'12' :(24940000, 10000, 'USB'), '10' :(28400000, -10000, 'USB'),
'Time':( 5000000, 0, 'AM'), '6' :(50040000, 10000, 'USB'),
}
# For the Time band, this is the center frequency, tuning frequency and mode:
bandTime = [
( 2500000-10000, 10000, 'AM'),
( 3330000-10000, 10000, 'AM'),
( 5000000-10000, 10000, 'AM'),
( 7335000-10000, 10000, 'AM'),
(10000000-10000, 10000, 'AM'),
(14670000-10000, 10000, 'AM'),
(15000000-10000, 10000, 'AM'),
(20000000-10000, 10000, 'AM'),
]
# This is the list of band buttons that Quisk displays, and it should have
# a length of 12. Empty buttons can have a null string "" label.
# Note that the 60 meter band and the Time band have buttons that support
# multiple presses.
bandLabels = ['Audio', '160', '80', ('60',) * 5, '40', '30', '20', '17',
'15', '12', '10', ('Time',) * len(bandTime)]
# If you get your I/Q samples from a sound card, you will need to correct the
# amplitude and phase for inaccuracies in the analog hardware. The data is
# entered using the controls from the "Rx Phase" button on the config screen.
# The corrections are saved by the persistent_state feature.
#
# The available range of the amplitude and phase controls for receive:
rx_max_amplitude_correct = 0.2 # Correction relative to 1.000000 (ideally 0.0000)
rx_max_phase_correct = 10.0 # Correction in degrees of phase (ideally 0.0000)
#
# The bandAmplPhase dictionary gives the amplitude and phase corrections for
# sound card data. The format is a dictionary with key "band", giving a dictionary
# with key "rx" or "tx", giving a list of tuples (VFO, tune, amplitude, phase).
#
# If you use Quisk as a panadapter, the corrections will not depend on the band.
# In that case create a band "panadapter" in your config file, and all corrections
# will be read/written to that band.
bandAmplPhase = {} # Empty dictionary to start
#bandAmplPhase = {'panadapter':{}} # Create "panadapter" band for all corrections
# The program polls the soundcard or SDR-IQ for data every data_poll_usec microseconds.
# A lower time reduces latency; a higher time is less taxing on the hardware.
if sys.platform == "win32":
data_poll_usec = 20000 # poll time in microseconds
else:
data_poll_usec = 5000 # poll time in microseconds
# The fft_size is the width of the data on the screen (about 800 to
# 1200 pixels) times the fft_size_multiplier. Multiple FFTs are averaged
# together to achieve your graph refresh rate. If fft_size_multiplier is
# too small you will get many fft errors. You can specify fft_size_multiplier,
# or enter a large number (use 9999) to maximize it, or enter zero to let
# quisk calculate it for you. Look for fft_size_multiplier in quisk.py.
# If your hardware can change the decimation, there are further compilcations.
# The FFT size is fixed, and only the average count can change to adjust the
# refresh rate.
fft_size_multiplier = 0
# The graph_refresh is the frequency at which the graph is updated,
# and should be about 5 to 10 Hertz. Higher rates require more processor power.
graph_refresh = 7 # update the graph at this rate in Hertz
# latency_millisecs determines how many samples are in the soundcard play buffer.
# A larger number makes it less likely that you will run out of samples to play,
# but increases latency. It is OK to suffer a certain number of play buffer
# underruns in order to get lower latency.
latency_millisecs = 150 # latency time in milliseconds
# Select the method to test the state of the key; see is_key_down.c
key_method = "" # No keying, or internal method
# key_method = "/dev/parport0" # Use the named parallel port
# key_method = "/dev/ttyS0" # Use the named serial port
# key_method = "192.168.1.44" # Use UDP from this address
# If you are using keying, key-down throws away the current capture buffer
# and starts a sidetone with a rise time of 5 milliseconds. For
# key-up, the sidetone is ended with a fall time of 5 milliseconds, then
# a silent period starts, then normal audio starts with a rise time of
# 5 milliseconds. The length of the silent period is given by keyupDelay,
# but will be at least the time necessary to collect enough samples to
# refill the filters. A larger keyupDelay may be needed to accomodate
# antenna switching or other requirement of your hardware.
keyupDelay = 23 # extra milliseconds silence on key up
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment