Files
quisk/quisk.py
T

6988 lines
263 KiB
Python

#!/usr/bin/env python3
# All QUISK software is Copyright (C) 2006-2025 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().
"""
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
# Change to the directory of quisk.py. This is necessary to import Quisk packages,
# to load other extension modules that link against _quisk.so, to find shared libraries *.dll and *.so,
# and to find ./__init__.py and ./help.html.
import sys, os
#print ('getcwd', os.getcwd())
#print ('__file__', __file__)
os.chdir(os.path.normpath(os.path.dirname(__file__))) # change directory to the location of this script
if sys.path[0] != '': # Make sure the current working directory is on path
sys.path.insert(0, '')
#print ('getcwd', os.getcwd())
import wx, wx.html, wx.lib.stattext, wx.lib.colourdb, wx.grid
import math, cmath, time, traceback, string, select, subprocess
import threading, pickle, webbrowser, json
try:
from xmlrpc.client import ServerProxy
except ImportError:
from xmlrpclib import ServerProxy
import _quisk as QS
from quisk_widgets import *
from filters import Filters
import dxcluster
import configure
import quisk_conf_defaults as conf
import quisk_wdsp
for name in configure.name2format: # Add defaults. These may be overwritten by a user config file.
setattr(conf, name, configure.name2format[name][1])
DEBUGSHELL = False
if DEBUGSHELL:
from wx.py.crust import CrustFrame
from wx.py.shell import ShellFrame
# Fldigi XML-RPC control opens a local socket. If socket.setdefaulttimeout() is not
# called, the timeout on Linux is zero (1 msec) and on Windows is 2 seconds. So we
# call it to insure consistent behavior.
import socket
socket.setdefaulttimeout(0.005)
HAMLIB_DEBUG = 0
application = None
if sys.version_info.major > 2:
Q3StringTypes = str
else:
Q3StringTypes = (str, unicode)
class StdOutput(wx.Frame):
def __init__(self, app):
self.app = app
self.ctrl = None
self.old_stdout = sys.stdout
self.old_stderr = sys.stderr
self.old_excepthook = sys.excepthook
self.path = os.path.join(app.QuiskFilesDir, "quisk_logfile.txt")
try:
size = os.path.getsize(self.path)
except:
size = 0
if size > 10000:
try:
fp = open(self.path, 'r')
lines = fp.readlines()
fp.close()
fp = open(self.path, 'w')
for i in range(len(lines) // 2, len(lines)):
fp.write(lines[i])
fp.close()
except:
pass
try:
self.fp = open(self.path, "a", buffering=1, encoding='utf-8', errors='replace', newline=None)
except:
self.fp = None
sys.stdout = self
sys.stderr = self
sys.excepthook = self.ExceptHook
if self.fp:
self.fp.write("\n\n*** Quisk started on %s at %s\n" % (sys.platform, time.asctime()))
def Create(self, parent):
w = self.app.screen_width * 4 // 10
h = self.app.screen_height * 4 // 10
title = "Quisk Log File %s" % self.path
wx.Frame.__init__(self, parent, -1, title, size = (w, h), style=wx.DEFAULT_FRAME_STYLE)
self.ctrl = wx.TextCtrl(self, style=wx.TE_MULTILINE|wx.TE_READONLY|wx.HSCROLL)
self.Bind(wx.EVT_SIZE, self.OnSize)
self.Bind(wx.EVT_CLOSE, self.OnBtnClose)
self.ctrl.write("*** Quisk started on %s at %s\n" % (sys.platform, time.asctime()))
def OnSize(self, event):
event.Skip()
w, h = self.GetClientSize()
self.ctrl.SetSize(w, h)
def OnBtnClose(self, event):
self.Show(False)
def Logfile(self, text):
if self.fp:
self.fp.write(text)
self.fp.write("\n")
def write(self, text):
if self.fp:
self.fp.write(text)
if self.old_stdout:
self.old_stdout.write(text)
if self.ctrl:
self.ctrl.write(text)
def flush(self):
pass
def ExceptHook(self, typ, value, traceb):
self.Show(True)
self.Raise()
self.old_excepthook(typ, value, traceb)
# Command line parsing: be able to specify the config file.
from optparse import OptionParser
parser = OptionParser()
parser.add_option('-c', '--config', dest='config_file_path',
help='Specify the configuration file path')
parser.add_option('', '--config2', dest='config_file_path2', default='',
help='Specify a second configuration file to read after the first')
parser.add_option('-r', '--radio', dest="radio", default='',
help='Specify the radio to use when starting')
parser.add_option('-a', '--ask', action="store_true", dest='AskMe', default=False,
help='Ask which radio to use when starting')
parser.add_option('', '--local', dest='local_option', default='',
help='Specify a custom option that you have programmed yourself')
argv_options = parser.parse_args()[0]
ConfigPath = argv_options.config_file_path # Get config file path
ConfigPath2 = argv_options.config_file_path2
LocalOption = argv_options.local_option
if sys.platform == 'win32':
path = os.getenv('HOMEDRIVE', '') + os.getenv('HOMEPATH', '')
for thedir in ("Documents", "My Documents", "Eigene Dateien", "Documenti", "Mine Dokumenter"):
config_dir = os.path.join(path, thedir)
if os.path.isdir(config_dir):
break
else:
config_dir = os.path.join(path, "My Documents")
try:
try:
import winreg as Qwinreg
except ImportError:
import _winreg as Qwinreg
key = Qwinreg.OpenKey(Qwinreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders")
val = Qwinreg.QueryValueEx(key, "Personal")
val = Qwinreg.ExpandEnvironmentStrings(val[0])
Qwinreg.CloseKey(key)
if os.path.isdir(val):
DefaultConfigDir = val
else:
DefaultConfigDir = config_dir
except:
traceback.print_exc()
DefaultConfigDir = config_dir
if not ConfigPath:
ConfigPath = os.path.join(DefaultConfigDir, "quisk_conf.py")
if not os.path.isfile(ConfigPath):
path = os.path.join(config_dir, "quisk_conf.py")
if os.path.isfile(path):
ConfigPath = path
del config_dir
else:
DefaultConfigDir = os.path.expanduser('~')
if not ConfigPath:
ConfigPath = os.path.join(DefaultConfigDir, ".quisk_conf.py")
# These FFT sizes have multiple small factors, and are preferred for efficiency. FFT size must be an even number.
fftPreferedSizes = []
for f2 in range(1, 13):
for y in (1, 3, 5, 7, 9, 11, 13, 15):
for z in (1, 3, 5, 7, 9, 11, 13, 15):
x = 2**f2 * y * z
if 300 <= x <= 5000 and x not in fftPreferedSizes:
fftPreferedSizes.append(x)
fftPreferedSizes.sort()
def round(x): # round float to nearest integer
if x >= 0:
return int(x + 0.5)
else:
return - int(-x + 0.5)
def str2freq (freq):
if '.' in freq:
freq = int(float(freq) * 1E6 + 0.1)
else:
freq = int(freq)
return freq
def get_filter_tx(mode): # Return the bandwidth, center of the Tx filters
if mode in ('LSB', 'USB'):
bw = 2700
center = 1650
elif mode in ('CWL', 'CWU'):
bw = 10
center = 0
elif mode in ('AM', 'DGT-IQ'):
bw = 6000
center = 0
elif mode in ('FM', 'DGT-FM'):
bw = 10000
center = 0
elif mode in ('FDV-L', 'FDV-U'):
bw = 2700
center = 1500
else:
bw = 2700
center = 1650
if mode in ('CWL', 'LSB', 'DGT-L', 'FDV-L'):
center = - center
return bw, center
Mode2Index = {'CWL':0, 'CWU':1, 'LSB':2, 'USB':3, 'AM':4, 'FM':5, 'EXT':6, 'DGT-U':7, 'DGT-L':8, 'DGT-IQ':9,
'IMD':10, 'FDV-U':11, 'FDV-L':12, 'DGT-FM':13}
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 msg in self.timers:
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, end=' ')
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), end=' ')
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 HamlibHandlerSerial:
"Create a serial port for Hamlib control that emulates the FlexRadio PowerSDR 2.x command set."
# This implements some Kenwood TS-2000 commands, but it is far from complete.
# See http://k5fr.com/binary/CatCommandReferenceGuide.pdf
# Test on linux by setting the Quisk serial port to /tmp/QuiskTTY0 and using putty with font Ubuntu Mono.
# Commands are "ZZAR;" and "ZZAR+030;" - no newline.
# Additional logic contributed by Dr. Karsten Schmidt
Mo2CoKen = {'CWL':7, 'CWU':3, 'LSB':1, 'USB':2, 'AM':5, 'FM':4, 'DGT-U':9, 'FDV-U':9, 'DGT-L':6, 'FDV-L':6, 'DGT-FM':4, 'DGT-IQ':9}
Co2MoKen = {1:'LSB', 2:'USB', 3:'CWU', 4:'FM', 5:'AM', 6:'DGT-L', 7:'CWL', 9:'DGT-U'}
Mo2CoFlex = {'CWL':3, 'CWU':4, 'LSB':0, 'USB':1, 'AM':6, 'FM':5, 'DGT-U':7, 'FDV-U':7, 'DGT-L':9, 'FDV-L':9, 'DGT-FM':5, 'DGT-IQ':7}
Co2MoFlex = {0:'LSB', 1:'USB', 3:'CWL', 4:'CWU', 5:'FM', 6:'AM', 7:'DGT-U', 9:'DGT-L'}
ConvertZZAC_P1_2_Step = { 0: 1,
1: 10,
2: 50,
3: 100,
4: 250,
5: 500,
6: 1000,
7: 5000,
8: 9000,
9: 10000,
10: 100000,
11: 250000,
12: 500000,
13: 1000000,
14: 10000000}
ConvertZZAC_Step_2_P1 = { 1: 0,
10: 1,
50: 2,
100: 3,
250: 4,
500: 5,
1000: 6,
5000: 7,
9000: 8,
10000: 9,
100000: 10,
250000: 11,
500000: 12,
1000000: 13,
10000000: 14}
def __init__(self, app, public_name):
self.app = app
self.port = None
self.received = ''
self.radio_id = '019'
self.public_name = public_name # the public name for the serial port
self.tune_step = 1000
if sys.platform == 'win32' or public_name.startswith("/dev/"):
self.port_is_mod_serial = True # self.port is from the serial module
try:
import serial
except:
print ("Please install the pyserial module.")
else:
try:
self.port = serial.Serial(public_name, timeout=0, write_timeout=0)
if HAMLIB_DEBUG: print ("Open CAT serial port", public_name)
except:
print ("The serial port %s could not be opened." % public_name)
else:
import tty
self.port_is_mod_serial = False # self.port is a file descriptor
if os.path.lexists(public_name):
try:
os.remove(public_name)
except:
print ("Can not remove the file", public_name)
try:
self.port, slave = os.openpty() # we are the master device fd, slave is a pseudo tty
tty.setraw(self.port)
tty.setraw(slave)
except:
print ("Can not create the serial port")
self.port = None
else:
try:
os.symlink(os.ttyname(slave), public_name) # create a link from the specified name to the slave device
except:
print ("Can not create a link named", public_name)
self.port = None
else:
if HAMLIB_DEBUG: print ("Create", public_name, "from", os.ttyname(slave))
def open(self):
return
def close(self):
if self.port_is_mod_serial:
if self.port:
self.port.close()
self.port = None
else:
if self.public_name:
try:
os.remove(self.public_name)
except:
pass
def Read(self):
if self.port is None:
return
if self.port_is_mod_serial:
text = self.port.read(99)
if not isinstance(text, Q3StringTypes):
text = text.decode('utf-8')
self.received += text
else:
while True:
r, w, x = select.select((self.port,), (), (), 0)
if not r:
break
text = os.read(self.port, 1)
if not isinstance(text, Q3StringTypes):
text = text.decode('utf-8')
self.received += text
def Process(self):
"""This is the main processing loop, and is called frequently. It reads and satisfies requests."""
self.Read()
if ';' in self.received: # A complete command ending with semicolon is available
cmd, self.received = self.received.split(';', 1) # Split off the command, save any further characters
else:
return
cmd = cmd.strip() # Here is our command and data
if cmd[0:2] in ('ZZ', 'zz', 'Zz', 'zZ'):
data = cmd[4:]
cmd = cmd[0:4].upper()
func = cmd
else:
data = cmd[2:]
cmd = cmd[0:2].upper()
if cmd in ('FA', 'FB', 'IF', 'PS'): # Use the ZZxx command method
func = 'ZZ' + cmd
else: # Use the two-letter method
func = cmd
if data:
if HAMLIB_DEBUG: print ("Process command :", cmd, data)
try:
func = getattr(self, func)
except:
print ("Unimplemented serial port function", func, 'cmd', cmd, 'data', data)
self.Write('?;')
return
func(cmd, data, len(data))
def Error(self, cmd, data):
self.Write('?;')
print ("*** Error for cmd %s data %s" % (cmd, data))
def Write(self, data):
if HAMLIB_DEBUG: print ("Serial port write:", data)
if self.port is None:
return
if isinstance(data, Q3StringTypes):
data = data.encode('utf-8', errors='ignore')
if self.port_is_mod_serial:
self.port.write(data)
else:
r, w, x = select.select((), (self.port,), (), 0)
if w:
os.write(self.port, data)
def set_frequency(self, freq): # This changes the Rx frequency
self.app.ChangeRxTxFrequency(freq, None)
if HAMLIB_DEBUG: print("New Freq rx,tx", self.app.txFreq + self.app.VFO, self.app.rxFreq + self.app.VFO)
def ZZAC(self, cmd, data, length): # Set or read tune steps
if length == 0:
xxx = self.ConvertZZAC_Step_2_P1.get(self.tune_step)
self.Write("%s%02d;" % (cmd, xxx))
elif length == 2:
p1 = int(data, base=10)
try:
self.tune_step = self.ConvertZZAC_P1_2_Step.get(p1)
except:
self.Error(cmd, data)
else:
self.Error(cmd, data)
def ZZAD(self, cmd, data, length): # VFO A down by a selected step
if length == 0:
oldfreq = self.app.rxFreq + self.app.VFO
freq = oldfreq - self.tune_step
self.set_frequency(freq)
else:
self.Error(cmd, data)
def AG(self, cmd, data, length): # audio gain
if length == 1:
self.Write("%s%s120;" % (cmd, data[0]))
def ZZAG(self, cmd, data, length): # audio gain
if length == 0:
n = (self.app.volumeAudio + 5) / 10
self.Write("%s%03d;" % (cmd, n))
elif length == 3:
vol = int(data, base=10) * 10
self.app.sliderVol.SetValue(vol)
self.app.ChangeVolume()
else:
self.Error(cmd, data)
def ZZAR(self, cmd, data, length): # AGC level
if length == 0:
n = self.app.levelAGC * 140 // 1000 - 20 # Convert AGC 0 to 1000 -> -20 to 120
if n < 0:
self.Write("%s-%03d;" % (cmd, - n))
else:
self.Write("%s+%03d;" % (cmd, n))
elif length == 4:
val = int(data, base=10)
val = (val + 20) * 1000 // 140 # Convert AGC -20 to 120 -> 0 to 1000
self.app.SliderAGC.SetSlider(value_on=val)
self.app.BtnAGC.SetValue(True, True)
else:
self.Error(cmd, data)
def ZZAU(self, cmd, data, length): # VFO A up by a selected step
if length == 0:
oldfreq = self.app.rxFreq + self.app.VFO
freq = oldfreq + self.tune_step
self.set_frequency(freq)
else:
self.Error(cmd, data)
def ZZBS(self, cmd, data, length): # band switch
if length == 0:
band = self.app.lastBand
try:
band = int(band, base=10)
self.Write("%s%03d;" % (cmd, band))
except:
self.Write("%s888;" % cmd)
elif length == 3:
band = data
if band[0] == '0':
band = band[1:]
if band[0] == '0':
band = band[1:]
try:
self.app.bandBtnGroup.SetLabel(band, do_cmd=True, direction=0)
except:
self.Error(cmd, data)
else:
self.Error(cmd, data)
def ZZAI(self, cmd, data, length): # broadcast changes
if length == 0:
self.Write("%s0;" % cmd)
elif length == 1 and data[0] == '0':
pass
else:
self.Error(cmd, data)
def ZZFA(self, cmd, data, length): # frequency of VFO A, the receive frequency
if length == 0:
self.Write("%s%011d;" % (cmd, self.app.rxFreq + self.app.VFO))
elif length == 11:
freq = int(data, base=10)
self.set_frequency(freq)
else:
self.Error(cmd, data)
def ZZFB(self, cmd, data, length): # frequency of VFO B
if length == 0:
self.Write("%s%011d;" % (cmd, self.app.txFreq + self.app.VFO))
elif length == 11:
freq = int(data, base=10)
tune = freq - self.app.VFO
d = self.app.sample_rate * 45 // 100
if -d <= tune <= d: # Frequency is on-screen
vfo = self.app.VFO
else: # Change the VFO
vfo = (freq // 5000) * 5000 - 5000
tune = freq - vfo
self.app.BandFromFreq(freq)
self.app.ChangeHwFrequency(tune, vfo, 'FreqEntry')
else:
self.Error(cmd, data)
def FR(self, cmd, data, length): # receive VFO is always VFO A
if length == 0:
self.Write("%s0;" % cmd)
elif length == 1 and data[0] == '0':
pass
else:
self.Error(cmd, data)
def FT(self, cmd, data, length): # transmit VFO
if self.app.split_rxtx:
vfo = '1'
else:
vfo = '0'
if length == 0:
self.Write("%s%s;" % (cmd, vfo))
elif length == 1 and data[0] == vfo:
pass
else:
self.Error(cmd, data)
def ID(self, cmd, data, length): # return radio ID
if length == 0:
self.Write('%s%s;' % (cmd, self.radio_id))
else:
self.Error(cmd, data)
def ZZID(self, cmd, data, length): # set radio id to Flex
if length == 0:
self.radio_id = '900'
else:
self.Error(cmd, data)
def ZZIF(self, cmd, data, length): # return information for ZZIF and IF
ritFreq = self.app.ritScale.GetValue()
if self.app.ritButton.GetValue():
rit = 1
else:
rit = 0
mode = self.app.mode
info = cmd
info += "%011d" % (self.app.rxFreq + self.app.VFO) # frequency, ZZFA
if len(cmd) == 4: # Flex ZZIF
info += '0000'
if ritFreq < 0: # RIT freq
info += "-%05d" % -ritFreq
else:
info += "+%05d" % ritFreq
else: # Kenwood IF
info += '00000'
if ritFreq < 0: # RIT freq
info += "-%04d" % -ritFreq
else:
info += "+%04d" % ritFreq
info += "%d" % rit # RIT status
info += '0000'
if QS.is_key_down(): # MOX, key down
info += '1'
else:
info += '0'
if len(cmd) == 4: # Flex ZZIF
code = self.Mo2CoFlex.get(mode, 1)
info += "%02d" % code # operating mode
else: # Kenwood IF
code = self.Mo2CoKen.get(mode, 1)
info += "%d" % code # operating mode
info += '00'
if self.app.split_rxtx: # VFO split status
info += '1'
else:
info += '0'
info += '0000'
info += ';'
self.Write(info)
def MD(self, cmd, data, length): # the mode; USB, CW, etc.
if length == 0:
mode = self.app.mode
code = self.Mo2CoKen.get(mode, 2)
self.Write("%s%d;" % (cmd, code))
elif length == 1:
code = int(data, base=10)
mode = self.Co2MoKen.get(code, 'USB')
self.app.modeButns.SetLabel(mode, True) # Set mode
else:
self.Error(cmd, data)
def ZZMD(self, cmd, data, length): # the mode; USB, CW, etc.
if length == 0:
mode = self.app.mode
code = self.Mo2CoFlex.get(mode, 1)
self.Write("%s%02d;" % (cmd, code))
elif length == 2:
code = int(data, base=10)
mode = self.Co2MoFlex.get(code, 'USB')
self.app.modeButns.SetLabel(mode, True) # Set mode
else:
self.Error(cmd, data)
def ZZMU(self, cmd, data, length): # MultiRx on/off
if length == 0:
self.Write("%s0;" % cmd)
def OI(self, cmd, data, length): # return information
self.ZZIF(cmd, data, length)
def ZZPS(self, cmd, data, length): # power status
if length == 0:
self.Write("%s1;" % cmd)
def ZZRS(self, cmd, data, length): # the RX2 status
if length == 0:
self.Write("%s0;" % cmd)
elif length == 1 and data[0] == '0':
pass
else:
self.Error(cmd, data)
def RX(self, cmd, data, length): # turn off MOX
if length == 0:
self.app.pttButton.SetValue(0, True)
else:
self.Error(cmd, data)
def ZZSM(self, cmd, data, length): # return the S-meter value in dB * 2; 0 to 260
if length == 0: # 0 to 260 is -140 to -10 dB; S9 == -73 dB == 134; dB = ZZSM / 2 - 140
i = round((self.app.hamlib_strength + 67) * 2)
if i < 0:
i = 0
elif i > 260:
i = 260
self.Write('%s%03d;' % (cmd, i))
else:
self.Error(cmd, data)
def ZZSP(self, cmd, data, length): # the split status
if length == 0:
if self.app.split_rxtx:
self.Write("%s1;" % cmd)
else:
self.Write("%s0;" % cmd)
else:
self.Error(cmd, data)
def ZZSW(self, cmd, data, length): # transmit VFO is A or B
if length == 0:
if self.app.split_rxtx:
self.Write("%s1;" % cmd)
else:
self.Write("%s0;" % cmd)
def TX(self, cmd, data, length): # turn on MOX
if length == 0:
self.app.pttButton.SetValue(1, True)
else:
self.Error(cmd, data)
def ZZTX(self, cmd, data, length): # the MOX status
if length == 0:
if QS.is_key_down():
self.Write("%s1;" % cmd)
else:
self.Write("%s0;" % cmd)
elif length == 1:
if data[0] == '0':
self.app.pttButton.SetValue(0, True)
else:
self.app.pttButton.SetValue(1, True)
else:
self.Error(cmd, data)
def ZZVE(self, cmd, data, length): # is VOX enabled
if length == 0:
if self.app.useVOX:
self.Write("%s1;" % cmd)
else:
self.Write("%s0;" % cmd)
else:
self.Error(cmd, data)
def XT(self, cmd, data, length): # the XIT
if length == 0:
self.Write("%s0;" % cmd)
elif length == 1 and data[0] == '0':
pass
else:
self.Error(cmd, data)
class HamlibHandlerRig2: # Test with telnet localhost 4532
"""This class is created for each connection to the server. It services requests from each client."""
SingleLetters = { # convert single-letter commands to long commands
'_':'info',
'f':'freq',
'i':'split_freq',
'l':'level',
'm':'mode',
'p':'parm',
's':'split_vfo',
't':'ptt',
'u':'func',
'v':'vfo',
}
def __init__(self, app, sock, address):
self.app = app # Reference back to the "hardware"
self.sock = sock
sock.settimeout(0.0)
self.address = address
self.params = '' # params is the string following the command
self.received = ''
self.input = ''
self.vfo = "Main"
self.split_mode = 0
self.split_vfo = 'VFO'
self.lock_mode = 0
h = self.Handlers = {}
h[''] = self.ErrProtocol
h['dump_state'] = self.DumpState
h['chk_vfo'] = self.ChkVfo # Thanks to Franco Spinelli, IW2DHW
h['get_freq'] = self.GetFreq
h['set_freq'] = self.SetFreq
h['get_info'] = self.GetInfo
h['get_mode'] = self.GetMode
h['set_mode'] = self.SetMode
h['get_vfo'] = self.GetVfo
h['set_vfo'] = self.SetVfo
h['get_func'] = self.GetFunc
h['set_func'] = self.SetFunc
h['get_level'] = self.GetLevel
h['set_level'] = self.SetLevel
h['get_parm'] = self.GetParm
h['set_parm'] = self.SetParm
h['get_ptt'] = self.GetPtt
h['set_ptt'] = self.SetPtt
h['get_split_freq'] = self.GetSplitFreq
h['set_split_freq'] = self.SetSplitFreq
h['get_split_vfo'] = self.GetSplitVfo
h['set_split_vfo'] = self.SetSplitVfo
h['get_lock_mode'] = self.GetLockMode
h['set_lock_mode'] = self.SetLockMode
self.MakeDumpState()
def MakeDumpState(self):
dump_state = []
# rigctld protocol version
dump_state.append( b"0" )
# rig model
dump_state.append( b"2" )
# ITU region
dump_state.append( b"1" ) # Europe, Africa and Northern Asia
#dump_state.append( b"2" ) # North America, South America and Greenland
#dump_state.append( b"3" ) # South Pacific and Southern Asia
# RX frequency ranges
# start, end, modes, low_power, high_power, vfo, ant
#dump_state.append( b"150000.000000 1500000000.000000 0x1ff -1 -1 0x10000003 0x3" )
# AM | CW | USB | LSB | NFM | WFM | DSB
dump_state.append( b"100000.000000 6000000000.000000 0x8006f -1 -1 0x4000000 0x3" ) # HackRF (100kHz - 6GHz), VFO:Main
# End of RX frequency ranges
dump_state.append( b"0 0 0 0 0 0 0" )
# TX frequency ranges
# start, end, modes, low_power, high_power, vfo, ant
dump_state.append( b"100000.000000 6000000000.000000 0x8006f -1 -1 0x4000000 0x3" ) # HackRF (100kHz - 6GHz), VFO:Main
# End of TX frequency ranges
dump_state.append( b"0 0 0 0 0 0 0" )
# Tuning steps
# mode, tuning_step
dump_state.append( b"0x1ff 1" )
dump_state.append( b"0x1ff 0" )
# End of tuning steps
dump_state.append( b"0 0" )
# Filter sizes
# mode, width
dump_state.append( b"0x1e 2400" )
dump_state.append( b"0x2 500" )
dump_state.append( b"0x1 8000" )
dump_state.append( b"0x1 2400" )
dump_state.append( b"0x20 15000" )
dump_state.append( b"0x20 8000" )
dump_state.append( b"0x40 230000" )
# End of filter sizes
dump_state.append( b"0 0" )
# ------ ??? ------
# max_rit
dump_state.append( b"9990" )
# max_xit
dump_state.append( b"9990" )
# max_ifshift
dump_state.append( b"10000" )
# announces
dump_state.append( b"0" )
# MAXDBLSTSIZ preamp
dump_state.append( b"10" )
# MAXDBLSTSIZ attenuator
dump_state.append( b"10 20 30" )
# has_get_func
dump_state.append( b"0x3effffff" )
# has_set_func
dump_state.append( b"0x3effffff" )
# has_get_level
dump_state.append( b"0xfffffffff7ffffff" )
# has_set_level
dump_state.append( b"0xffffffff83ffffff" )
# has_get_parm
dump_state.append( b"0xffffffffffffffff" )
# has_set_parm
dump_state.append( b"0xffffffffffffffbf" )
# END
self.dump_state = b'\n'.join(dump_state)
self.dump_state += b'\n'
def Send(self, text):
"""Send text back to the client."""
if isinstance(text, Q3StringTypes):
text = text.encode('utf-8', errors='ignore')
if HAMLIB_DEBUG:
print ("Rig2 send", text)
try:
self.sock.sendall(text)
except socket.error:
self.sock.close()
self.sock = None
def Reply(self, *args): # args is name, value, name, value, ..., int
"""Create a string reply of name, value pairs, and an ending integer code."""
if self.extended: # Use extended format; echo the command and parameters.
t = "%s:" % self.command
if self.params:
t += " %s" % self.params
t += self.extended
for i in range(0, len(args) - 1, 2):
t = "%s%s: %s%c" % (t, args[i], args[i+1], self.extended)
t += "RPRT %d\n" % args[-1]
elif len(args) > 1: # Use simple format
t = ''
for i in range(1, len(args) - 1, 2):
t = "%s%s\n" % (t, args[i])
else: # No names; just the required integer code
t = "RPRT %d\n" % args[0]
self.Send(t)
def Reply2(self, text, code):
"""Create a reply string in a different format."""
if self.extended: # Use extended format
t = "%s: %s%s%s\nRPRT %d\n" % (self.command, self.params, self.extended, text, code)
self.Send(t)
else:
if self.command[0:4] == "set_":
self.Send("%s\nRPRT %d\n" % (text, code))
else:
self.Send("%s\n" % text)
def ErrParam(self): # Invalid parameter
self.input = ''
self.Reply(-1)
def UnImplemented(self): # Command not implemented
self.input = ''
self.Reply(-4)
def ErrProtocol(self): # Protocol error
self.input = ''
self.Reply(-8)
def GetParamNumber(self):
if not self.input:
return None
number = ''
for i in range(len(self.input)):
ch = self.input[i]
if ch in "+-.0123456789":
number = number + ch
else:
self.input = self.input[i:].strip()
break
else:
self.input = ''
if self.params:
self.params = self.params + ' ' + number
else:
self.params = number
try:
if '.' in number:
return float(number)
else:
return int(number)
except:
return None
def GetParamName(self):
if not self.input:
return ''
if self.input[0] == '?':
self.params = '?'
self.input = self.input[1:].strip()
return '?'
name = ''
for i in range(len(self.input)):
ch = self.input[i]
if ch in string.ascii_letters or ch in "_-":
name = name + ch
else:
self.input = self.input[i:].strip()
break
else:
self.input = ''
if self.params:
self.params = self.params + ' ' + name
else:
self.params = name
return name
def Process(self):
"""This is the main processing loop, and is called frequently. It reads and satisfies requests."""
if not self.sock:
return 0
try: # Read any data from the socket
text = self.sock.recv(1024)
except socket.timeout: # This does not work
pass
except socket.error: # Nothing to read
pass
else: # We got some characters
if not isinstance(text, Q3StringTypes):
text = text.decode('utf-8')
self.received += text
if HAMLIB_DEBUG and text:
print ("Raw rig2 received:", text)
if '\n' in self.received: # A complete command ending with newline is available
self.input, self.received = self.received.split('\n', 1) # Split off the command, save any further characters
else:
return 1
self.input = self.input.strip() # Here is our command line
if HAMLIB_DEBUG:
print ("Rig2 received", self.input)
while self.input:
# Parse the commands and call the appropriate handlers
self.params = ''
if self.input[0] == '+': # rigctld Extended Response Protocol
self.extended = '\n'
self.input = self.input[1:].strip()
elif self.input[0] in ';|,': # rigctld Extended Response Protocol
self.extended = self.input[0]
self.input = self.input[1:].strip()
else:
self.extended = None
letter = self.input[0:1]
if letter == '\\': # long form command starting with backslash
args = self.input[1:].split(None, 1)
self.command = args[0]
if len(args) == 1:
self.input = ''
else:
self.input = args[1].strip()
else: # single-letter commands
self.input = self.input[1:].strip()
if letter in 'Qq': # Quit command
self.sock.close()
self.input = ''
return 0
try:
command = self.SingleLetters[letter.lower()]
except KeyError:
self.UnImplemented()
return 1
else:
if letter in string.ascii_uppercase:
self.command = 'set_' + command
else:
self.command = 'get_' + command
self.Handlers.get(self.command, self.UnImplemented)()
return 1
# These are the handlers for each request
def DumpState(self):
self.Send(self.dump_state)
def ChkVfo(self):
if self.extended:
self.Send('ChkVFO: 0\n')
else:
self.Send('0\n')
def GetFreq(self): # The Rx frequency
# This is always the main Rx frequency without reference to added receivers.
self.Reply('Frequency', self.app.rxFreq + self.app.VFO, 0)
if HAMLIB_DEBUG:
print ("GetFreq app", self.app.rxFreq, self.app.txFreq, self.app.VFO)
def SetFreq(self): # The Rx frequency
# This is always the main Rx frequency without reference to added receivers.
freq = self.GetParamNumber()
try:
freq = float(freq)
self.Reply(0)
except:
self.ErrParam()
else:
freq = int(freq + 0.5)
# Like a real radio, keep the mode. Hamlib caches the mode, so the client would not notice a mode change.
self.app.ChangeRxTxFrequency(freq, None, keep_mode=True)
def GetSplitFreq(self): # The Tx Frequency
self.Reply('TX Frequency', self.app.txFreq + self.app.VFO, 0)
def SetSplitFreq(self): # The Tx Frequency
freq = self.GetParamNumber()
try:
freq = float(freq)
self.Reply(0)
except:
self.ErrParam()
else:
freq = int(freq + 0.5)
# This always controls the Tx frequency
self.app.ChangeRxTxFrequency(None, freq, keep_mode=True)
def GetSplitVfo(self):
self.Reply('Split', self.split_mode, 'TX VFO', self.split_vfo, 0)
def SetSplitVfo(self):
split = self.GetParamNumber()
vfo = self.GetParamName()
try:
split = int(split)
self.Reply(0)
except:
# traceback.print_exc()
self.ErrParam()
else:
self.split_mode = split
if split:
self.split_vfo = vfo
else:
self.split_vfo = 'VFO'
# If there are no multi receivers, this sets split Rx/Tx mode.
if not self.app.multi_rx_screen.receiver_list:
self.app.splitButton.SetValue(split, True)
def GetInfo(self):
self.Reply("Info", self.app.main_frame.title, 0)
def GetMode(self):
mode = self.app.mode
if mode == 'CWU':
mode = 'CW'
elif mode == 'CWL': # Is this what CWR means?
mode = 'CWR'
elif mode in ('DGT-U', 'FDV-U'):
mode = 'PKTUSB'
elif mode in ('DGT-L', 'FDV-L'):
mode = 'PKTLSB'
elif mode == 'DGT-FM':
mode = 'PKTFM'
elif mode[0:4] == 'DGT-IQ':
mode = 'PKTUSB'
self.Reply('Mode', mode, 'Passband', self.app.filter_bandwidth, 0)
def SetMode(self):
mode = self.GetParamName()
bw = self.GetParamNumber()
if mode == '?': # send back supported parameters
self.Reply2('USB LSB AM FM CW CWR PKTUSB PKTLSB PKTFM', 0)
return
try:
bw = int(bw)
except:
self.ErrParam()
return
if mode in ('USB', 'LSB', 'AM', 'FM'):
self.Reply(0)
elif mode[0:4] == 'DGT-':
self.Reply(0)
elif mode == 'PKTUSB':
mode = 'DGT-U'
self.Reply(0)
elif mode == 'PKTLSB':
mode = 'DGT-L'
self.Reply(0)
elif mode == 'PKTFM':
mode = 'DGT-FM'
self.Reply(0)
elif mode == 'CW':
mode = 'CWU'
self.Reply(0)
elif mode == 'CWR':
mode = 'CWL'
self.Reply(0)
else:
self.ErrParam()
return
self.app.modeButns.SetLabel(mode, True) # Set mode
if bw <= 0: # use default bandwidth
return
# Choose button closest to requested bandwidth
buttons = self.app.filterButns.GetButtons()
Lab = buttons[0].GetLabel()
diff = abs(int(Lab) - bw)
for i in range(1, len(buttons) - 1):
label = buttons[i].GetLabel()
df = abs(int(label) - bw)
if df < diff:
Lab = label
diff = df
self.app.filterButns.SetLabel(Lab, True)
# The code for the lock_mode and keep_mode was supplied by Karl Heinz - K5KHK. Thanks Karl!
def GetLockMode(self):
# Hamlib netrigctl always reads two lines, so an error reply "RPRT -4" makes it wait for its 10 second timeout.
# Reply like rigctld with the lock value and then "RPRT 0".
if self.extended:
self.Reply('Locked', self.lock_mode, 0)
else:
self.Send("%d\nRPRT 0\n" % self.lock_mode)
def SetLockMode(self):
lock = self.GetParamNumber()
try:
self.lock_mode = int(lock)
except:
self.ErrParam()
else:
self.Reply(0)
def GetVfo(self):
self.Reply('VFO', self.vfo, 0)
def SetVfo(self):
name = self.GetParamName()
if name == '?': # send back supported parameters
self.Reply2('Main', 0)
else:
self.vfo = name
self.Reply(0)
def GetPtt(self):
if QS.is_key_down():
self.Reply('PTT', 1, 0)
else:
self.Reply('PTT', 0, 0)
def SetPtt(self):
ptt = self.GetParamNumber()
try:
ptt = int(ptt)
self.Reply(0)
except:
self.ErrParam()
else:
self.app.pttButton.SetValue(ptt, True)
def GetFunc(self):
name = self.GetParamName()
if name == '?': # send back supported functions
self.Reply2('TUNER', 0)
elif name == 'TUNER':
if self.app.spotButton.GetValue():
self.Reply2(1, 0)
else:
self.Reply2(0, 0)
else:
self.ErrParam()
def SetFunc(self):
name = self.GetParamName()
value = self.GetParamNumber()
if name == '?': # send back supported functions
self.Reply2('TUNER', 0)
elif name == 'TUNER':
try:
value = int(value)
except:
self.ErrParam()
else:
self.Reply(0)
if value:
self.app.spotButton.SetValue(1, True)
else:
self.app.spotButton.SetValue(0, True)
else:
self.ErrParam()
def GetLevel(self):
name = self.GetParamName()
if name == '?': # send back supported parameters
self.Reply2('AGC AF STRENGTH', 0)
elif name == 'AGC':
if self.app.BtnAGC.GetValue():
self.Reply2(4, 0)
else:
self.Reply2(0, 0)
elif name == 'AF':
x = self.app.audio_volume # audio_volume is 0 to 1.000
self.Reply2("%.5f" % x, 0)
elif name == 'STRENGTH':
i = round(self.app.hamlib_strength)
self.Reply2(i, 0)
else:
self.ErrParam()
def SetLevel(self):
name = self.GetParamName()
value = self.GetParamNumber()
if name == '?': # send back supported parameters
self.Reply2('AGC AF', 0)
elif name == 'AGC':
try:
value = int(value)
except:
self.ErrParam()
else:
self.Reply(0)
if value:
self.app.BtnAGC.SetValue(1, True)
else:
self.app.BtnAGC.SetValue(0, True)
elif name == 'AF':
try:
value = float(value)
except:
self.ErrParam()
else:
self.Reply(0)
v = 1000.0 * math.log10(49.0 * value + 1) / math.log10(50)
self.app.sliderVol.SetValue(int(v + 0.5))
self.app.ChangeVolume()
else:
self.ErrParam()
def GetParm(self):
name = self.GetParamName()
if name == '?': # send back supported parameters
self.Reply2('', 0)
else:
self.ErrParam()
def SetParm(self):
name = self.GetParamName()
if name == '?': # send back supported parameters
self.Reply2('', 0)
else:
self.ErrParam()
class CatControl:
def GetFreqVfo(self, vfoB=False):
if vfoB: # The Tx Frequency
return self.app.txFreq + self.app.VFO
else: # The Rx Frequency
rx = self.app.multi_rx_screen.receiver_list
if rx:
rx = rx[0]
return rx.txFreq + rx.VFO
else:
return self.app.rxFreq + self.app.VFO
def SetFreqVfo(self, freq, vfoB=False):
if vfoB: # The Tx Frequency
self.app.ChangeRxTxFrequency(None, freq)
else: # The Rx Frequency
# For multiple receivers this controls the first added receiver frequency; else it controls split Rx/Tx frequency.
rx = self.app.multi_rx_screen.receiver_list
if rx:
rx[0].ChangeRxTxFrequency(freq)
else:
self.app.ChangeRxTxFrequency(freq, None)
def GetFilterBW(self):
return self.app.filter_bandwidth
def SetFilterBW(self, bw):
# Choose button closest to requested bandwidth
buttons = self.app.filterButns.GetButtons()
Lab = buttons[0].GetLabel()
diff = abs(int(Lab) - bw)
for i in range(1, len(buttons) - 1):
label = buttons[i].GetLabel()
df = abs(int(label) - bw)
if df < diff:
Lab = label
diff = df
self.app.filterButns.SetLabel(Lab, True)
class ElecraftK4Handler(CatControl): # Test with telnet localhost 9200
Mo2CoElecraft = {'LSB':1, 'USB':2, 'CWU':3, 'FM':4, 'AM':5, 'DGT-U':6, 'CWL':7, 'DGT-L':9, 'DGT-FM':4, 'DGT-IQ':6}
Co2MoElecraft = {1:'LSB', 2:'USB', 3:'CWU', 4:'FM', 5:'AM', 6:'DGT-U', 7:'CWL', 9:'DGT-L'}
def __init__(self, app, conf, sock, address):
self.app = app
self.conf = conf
self.sock = sock
sock.settimeout(0.0)
self.address = address
self.params = '' # params is the string following the command
self.received = ''
self.cmds = []
self.K31Elecraft = False # K31 mode
h = self.Handlers = {} # Branch on the first two letters of the command
h['AI'] = self.AiAutoInfo
h['CW'] = self.CwCWPitch
h['DT'] = self.DtDataMode
h['FA'] = self.FaFbVfoFreq
h['FB'] = self.FaFbVfoFreq
h['FT'] = self.FtTxVfo
h['FW'] = self.FwFilterBW
h['ID'] = self.IdIdent
h['IF'] = self.IfInfo
h['IS'] = self.IsCenterPitch
h['KS'] = self.KsKeyerSpeed
h['K3'] = self.K3Elecraft
h['LN'] = self.LnLinkVfo
h['MD'] = self.MdMode
h['OM'] = self.OmOptionModules
h['RV'] = self.RvFirmware
h['RX'] = self.RxReceive
h['SB'] = self.SbSubReceiver
h['SM'] = self.SmSmeter
h['TX'] = self.TxTransmit
def Send(self, text):
"""Send text back to the client."""
if HAMLIB_DEBUG:
for cmd in self.cmds[0:-1]:
print ("CatTcp received: %-10s" % cmd)
print ("CatTcp received: %-10s ; sent %s" % (self.cmds[-1], text))
del self.cmds[:]
text = text.encode('utf-8', errors='ignore')
try:
self.sock.sendall(text)
except socket.error:
self.sock.close()
self.sock = None
def Process(self):
"""This is the main processing loop, and is called frequently. It reads and satisfies requests."""
if not self.sock:
return 0
try: # Read any data from the socket
text = self.sock.recv(1024)
except socket.timeout: # This does not work
pass
except socket.error: # Nothing to read
pass
else: # We got some characters
text = text.decode('utf-8', errors='ignore')
self.received += text
if HAMLIB_DEBUG and text:
pass #print ("Raw CatTcp received:", text)
if ';' in self.received: # A complete command ending with semicolon is available
cmd, self.received = self.received.split(';', 1) # Split off the command, save any further characters
else:
return 1
cmd = cmd.strip() # Here is our command line
if len(cmd) < 2:
return 1
if HAMLIB_DEBUG:
self.cmds.append(cmd)
base = cmd[0:2].upper()
try:
handler = self.Handlers[base]
except KeyError:
self.Send(cmd[0:2] + "?;")
#print ("Unknown command", cmd)
return 1
if len(cmd) == 2:
args = ''
elif cmd[2] == '$': # base is the first two letters upper case and any following "$"
base += '$'
args = cmd[3:]
else:
args = cmd[2:]
handler(base, args) # args is the remainder
return 1
def AiAutoInfo(self, base, args):
if not args:
self.Send("AI0;")
elif args != '0':
self.Send("AI0;")
def CwCWPitch(self, base, args):
pitch = self.conf.cwTone
pitch = (pitch + 5) // 10
if pitch < 25:
pitch = 25
elif pitch > 95:
pitch = 95
self.Send("CW%d;" % pitch)
def DtDataMode(self, base, args):
self.Send("%s0;" % base)
def IdIdent(self, base, args):
if args:
self.Send("ID?;")
else:
self.Send("ID017;")
#self.Send("IDQuisk;")
def IfInfo(self, base, args):
rxfreq = self.GetFreqVfo()
info = "%011d " % rxfreq # Rx frequency, five blanks
ritFreq = self.app.ritScale.GetValue()
if ritFreq < 0: # +/- RIT freq
info += "-%04d" % -ritFreq
else:
info += "+%04d" % ritFreq
if self.app.ritButton.GetValue(): # RIT status, XIT status, blank "00"
info += "10 00"
else:
info += "00 00"
if QS.is_key_down(): # MOX, key down
info += '1'
else:
info += '0'
mode = self.Mo2CoElecraft.get(self.app.mode, 2)
info += "%d" % mode # Mode 1-9
info += '00' # Rx on VFOA, no scan
if self.app.split_rxtx: # VFO split status
info += '1'
else:
info += '0'
info += '001 ;'
self.Send(info)
def FaFbVfoFreq(self, base, args):
if base[1] in ('B', 'b'):
vfoB = True
else:
vfoB = False
if not args: # Get command
freq = "%06d" % self.GetFreqVfo(vfoB)
self.Send(base + freq + ';')
else: # Put command
try:
freq = int(args)
except:
self.Send(base + "?;")
else:
l = len(args)
if l <= 2:
freq *= 1000000
elif l <= 5:
freq *= 1000
self.SetFreqVfo(freq, vfoB)
def FtTxVfo(self, base, args):
if not args:
if self.app.split_rxtx:
self.Send("FT1;")
else:
self.Send("FT0;")
else:
# Set split Rx/Tx mode.
is_split = self.app.splitButton.GetValue()
split = int(args)
if split and not is_split:
self.app.splitButton.SetValue(split, True)
elif is_split and not split:
self.app.splitButton.SetValue(split, True)
def FwFilterBW(self, base, args):
if not args:
code = self.GetFilterBW()
self.Send("%s%04d;" % (base, (code + 5) // 10))
else:
self.SetFilterBW(int(args) * 10)
def IsCenterPitch(self, base, args):
if not args:
code = self.GetFilterBW() // 2
if self.K31Elecraft:
self.Send("%s %04d;" % (base, code))
else:
code = (code + 5) // 10
self.Send("%s%04d;" % (base, code))
else:
if self.K31Elecraft:
self.SetFilterBW(int(args) * 2)
else:
self.SetFilterBW(int(args) * 2 * 10)
def K3Elecraft(self, base, args):
if args == '1':
self.K31Elecraft = True
else:
self.K31Elecraft = False
def KsKeyerSpeed(self, base, args):
if len(base) == 2:
self.Send("KS013;")
def LnLinkVfo(self, base, args):
if args != '0':
self.Send("LN0;")
def MdMode(self, base, args):
if args: # set the mode
code = int(args)
mode = self.Co2MoElecraft[code]
self.app.modeButns.SetLabel(mode, True)
else: # return the mode
code = self.Mo2CoElecraft.get(self.app.mode, 2)
self.Send("%s%d;" % (base, code))
def OmOptionModules(self, base, args):
self.Send("OM ------------;")
def RvFirmware(self, base, args):
self.Send("%s99.99;" % base)
def RxReceive(self, base, args):
self.app.pttButton.SetValue(0, True)
def SbSubReceiver(self, base, args):
self.Send("SB0;")
def SmSmeter(self, base, args):
if base[0:3].upper() == "SMH":
self.Send(base + "?;")
elif args:
self.Send(base + "?;")
elif self.K31Elecraft:
self.Send("SM0000;")
else:
self.Send("SM00;")
def TxTransmit(self, base, args):
self.app.pttButton.SetValue(1, True)
class SoundThread(threading.Thread):
"""Create a second (non-GUI) thread to read, process and play sound."""
def __init__(self, samples_from_python):
self.samples_from_python = samples_from_python
self.do_init = 1
threading.Thread.__init__(self, name="QuiskSound")
self.doQuit = threading.Event()
self.doQuit.clear()
#self.time0 = time.time()
if hasattr(Hardware, "PollCwKey"):
self.poll_cw = True
else:
self.poll_cw = False
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.is_set():
#tm = time.time()
#print ("Quisk thread %12.3f" % ((tm - self.time0) * 1E3))
#self.time0 = tm
if self.samples_from_python:
samples = Hardware.GetRxSamples()
if samples:
QS.set_params(rx_samples=samples)
if self.poll_cw:
Hardware.PollCwKey()
if conf.midi_cwkey_device:
byts = QS.control_midi(get_event=1)
if byts:
wx.CallAfter(application.OnReadMIDI, byts)
application.wdsp.control()
QS.read_sound()
wx.CallAfter(application.OnReadSound)
#if sys.platform == 'win32':
# time.sleep(0.000)
QS.control_midi(close_port=1)
QS.close_sound()
def stop(self):
"""Set a flag to indicate that the sound thread should end."""
self.doQuit.set()
class ConfigScreen(wx.Panel):
"""Display a notebook with status and configuration data"""
def __init__(self, parent, width, fft_size):
self.y_scale = 0
self.y_zero = 0
self.zoom_control = 0
self.finish_pages = True
self.width = width
wx.Panel.__init__(self, parent)
self.notebook = notebook = wx.Notebook(self)
font = wx.Font(conf.config_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
notebook.SetFont(font)
sizer = wx.BoxSizer()
sizer.Add(notebook, 1, wx.EXPAND)
self.SetSizer(sizer)
# create the page windows
self.status = ConfigStatus(notebook, width, fft_size)
self.SetBackgroundColour(self.status.bg_color)
self.SetForegroundColour(self.status.tfg_color)
notebook.bg_color = self.status.bg_color
notebook.tfg_color = self.status.tfg_color
notebook.AddPage(self.status, "Status")
self.config = configure.ConfigConfig(notebook, width)
notebook.AddPage(self.config, "Config")
self.favorites = ConfigFavorites(notebook, width)
notebook.AddPage(self.favorites, "Favorites")
tx_audio = configure.ConfigTxAudio(notebook)
notebook.AddPage(tx_audio, "Tx Audio")
tx_audio.status = self.status
def FinishPages(self):
if self.finish_pages:
self.finish_pages = False
application.local_conf.AddPages(self.notebook, self.width)
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 OnGraphData(self, data=None):
self.status.OnGraphData(data)
def InitBitmap(self): # Initial construction of bitmap
self.status.InitBitmap()
class ConfigStatus(wx.ScrolledCanvas):
"""Display the status screen."""
def __init__(self, parent, width, fft_size):
wx.ScrolledCanvas.__init__(self, parent)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.bg_color = self.GetBackgroundColour()
try:
va = wx.StaticText.GetClassDefaultAttributes()
self.tfg_color = wx.Colour(va.colFg) # use for text foreground
except:
self.tfg_color = self.GetForegroundColour()
self.width = width
self.fft_size = fft_size
self.scroll_height = None
self.interrupts = 0
self.read_error = -1
self.write_error = -1
self.underrun_error = -1
self.hl2_txbuf_errors = 0
self.fft_error = -1
self.latencyCapt = -1
self.latencyPlay = -1
self.y_scale = 0
self.y_zero = 0
self.zoom_control = 0
self.rate_min = -1
self.rate_max = -1
self.chan_min = -1
self.chan_max = -1
self.mic_max_display = -90.0
self.err_msg = "No response"
self.msg1 = ""
self.font = wx.Font(conf.status_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
if wxVersion in ('2', '3'):
self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
else:
self.SetBackgroundStyle(wx.BG_STYLE_PAINT)
self.SetFont(self.font)
charx = self.charx = self.GetCharWidth()
chary = self.chary = self.GetCharHeight()
self.dy = chary # line spacing
self.rjustify1 = (0, 1, 0)
self.tabstops1 = [0] * 3
self.tabstops1[0] = x = charx
self.tabstops1[1] = x = x + self.GetTextExtent("FFT number of errors 1234567890")[0]
self.tabstops1[2] = x = x + self.GetTextExtent("XXXX")[0]
self.rjustify2 = (0, 0, 1, 1, 1, 1)
self.tabstops2 = []
def MakeTabstops(self):
luse = lname = 0
for use, name, rate, latency, errors, level, dev_errmsg in QS.sound_errors():
name = self.TrimName(name)
w, h = self.GetTextExtent(use)
luse = max(luse, w)
w, h = self.GetTextExtent(name)
lname = max(lname, w)
if luse == 0:
return
charx = self.charx
self.tabstops2 = [0] * 6
self.tabstops2[0] = x = charx
self.tabstops2[1] = x = x + luse + charx * 6
self.tabstops2[2] = x = x + lname + self.GetTextExtent("Sample rateXXXXXX")[0]
self.tabstops2[3] = x = x + charx * 12
self.tabstops2[4] = x = x + charx * 12
self.tabstops2[5] = x = x + charx * 12
def TrimName(self, name):
if len(name) > 50:
name = name[0:30] + '|||' + name[-17:]
return name
def OnPaint(self, event):
# Make and blit variable data
self.MakeBitmap()
dc = wx.AutoBufferedPaintDC(self)
x, y = self.GetViewStart()
dc.Blit(0, 0, self.mem_width, self.mem_height, self.mem_dc, x, y)
def MakeRow2(self, *args):
for col in range(len(args)):
t = args[col]
if t is None:
continue
if not isinstance(t, Q3StringTypes):
t = str(t)
x = self.tabstops[col]
if self.rjustify[col]:
w, h = self.mem_dc.GetTextExtent(t)
x -= w
if ("Error" in t or "Stream error" in t) and t != "Errors":
self.mem_dc.SetTextForeground('Red')
self.mem_dc.DrawText(t, x, self.mem_y)
self.mem_dc.SetTextForeground(self.tfg_color)
else:
self.mem_dc.DrawText(t, x, self.mem_y)
self.mem_y += self.dy
def InitBitmap(self): # Initial construction of bitmap
self.mem_height = application.screen_height
self.mem_width = application.screen_width
self.bitmap = EmptyBitmap(self.mem_width, self.mem_height)
self.mem_dc = wx.MemoryDC()
self.mem_rect = wx.Rect(0, 0, self.mem_width, self.mem_height)
self.mem_dc.SelectObject(self.bitmap)
br = wx.Brush(self.bg_color)
self.mem_dc.SetBackground(br)
self.mem_dc.SetFont(self.font)
self.mem_dc.SetTextForeground(self.tfg_color)
self.mem_dc.Clear()
def MakeBitmap(self):
self.mem_dc.Clear()
self.mem_y = self.charx
self.tabstops = self.tabstops1
self.rjustify = self.rjustify1
if conf.config_file_exists:
cfile = "Configuration file: %s" % conf.config_file_path
else:
cfile = "Configuration file: None"
qfile = "Quisk user files are in %s" % application.QuiskFilesDir
level = "%3.0f" % self.mic_max_display
if self.err_msg:
err_msg = self.err_msg
else:
err_msg = None
self.MakeRow2("Sample interrupts", self.interrupts, cfile)
self.MakeRow2("Microphone or DGT level dB", level, qfile)
self.MakeRow2("FFT number of points", self.fft_size, err_msg)
if application.dxCluster:
msg = application.dxCluster.dxStatus()
self.MakeRow2("FFT number of errors", self.fft_error, msg)
else:
self.MakeRow2("FFT number of errors", self.fft_error)
if conf.use_rx_udp == 10: # Hermes UDP protocol
self.MakeRow2("Hermes-Lite2 Tx buffer errors", self.hl2_txbuf_errors)
self.mem_y += self.dy
if not self.tabstops2:
return
self.tabstops = self.tabstops2
self.rjustify = self.rjustify2
self.font.SetUnderlined(True)
self.mem_dc.SetFont(self.font)
self.MakeRow2("Device", "Name", "Sample rate", "Latency", "Errors", "Level dB")
self.font.SetUnderlined(False)
self.mem_dc.SetFont(self.font)
self.mem_y += self.dy * 3 // 10
if conf.use_sdriq:
self.MakeRow2("Capture radio samples", "SDR-IQ", application.sample_rate, self.latencyCapt, self.read_error)
elif conf.use_rx_udp:
self.MakeRow2("Capture radio samples", "UDP", application.sample_rate, self.latencyCapt, self.read_error)
elif conf.use_soapy:
self.MakeRow2("Capture radio samples", "SoapySDR", application.sample_rate, self.latencyCapt, self.read_error)
for use, name, rate, latency, errors, level, dev_errmsg in QS.sound_errors():
level = math.sqrt(level) / 2**31
if level < 1.1E-5:
level = " - "
else:
level = 20 * math.log10(level)
level = "%.2f" % level
self.MakeRow2(use, self.TrimName(name), rate, latency, errors, level)
if dev_errmsg:
x = self.tabstops[0] + self.charx * 4
self.mem_dc.SetTextForeground('Red')
self.mem_dc.DrawText(dev_errmsg, x, self.mem_y)
self.mem_dc.SetTextForeground(self.tfg_color)
self.mem_y += self.dy
if self.scroll_height is None:
self.scroll_height = self.mem_y + self.dy
self.SetScrollbars(1, 1, 100, self.scroll_height)
def OnGraphData(self, data=None):
if not self.tabstops2: # Must wait for sound to start
self.MakeTabstops()
(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.interrupts, 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)
if conf.use_rx_udp == 10: # Hermes UDP protocol
self.hl2_txbuf_errors = QS.get_params("hl2_txbuf_errors")
self.RefreshRect(self.mem_rect)
class ConfigFavorites(wx.grid.Grid):
def __init__(self, parent, width):
wx.grid.Grid.__init__(self, parent)
self.changed = False
self.RepeaterDict = {}
font = wx.Font(conf.favorites_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_BOLD, False, conf.quisk_typeface)
self.SetFont(font)
self.SetBackgroundColour(parent.bg_color)
self.SetLabelFont(font)
font = wx.Font(conf.favorites_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
self.SetDefaultCellFont(font)
self.SetDefaultRowSize(self.GetCharHeight()+3)
self.Bind(wx.grid.EVT_GRID_LABEL_RIGHT_CLICK, self.OnRightClickLabel)
self.Bind(wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self.OnLeftClickLabel)
if wxVersion in ('2', '3'):
self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.OnChange) # wxPython 3
else:
self.Bind(wx.grid.EVT_GRID_CELL_CHANGED, self.OnChange) # wxPython 4
self.Bind(wx.grid.EVT_GRID_LABEL_LEFT_DCLICK, self.OnLeftDClick)
self.CreateGrid(0, 6)
self.EnableDragRowSize(False)
w = self.GetTextExtent(' 999 ')[0]
self.SetRowLabelSize(w)
self.SetColLabelValue(0, 'Name')
self.SetColLabelValue(1, 'Freq MHz')
self.SetColLabelValue(2, 'Mode') # This column has a choice editor
self.SetColLabelValue(3, 'Description')
self.SetColLabelValue(4, 'Offset kHz')
self.SetColLabelValue(5, 'Tone Hz')
w = self.GetTextExtent("xFrequencyx")[0]
self.SetColSize(0, w * 3 // 2)
self.SetColSize(1, w)
self.SetColSize(4, w)
self.SetColSize(5, w)
self.SetColSize(2, w)
ww = width - w * 7 - self.GetRowLabelSize() - 20
if ww < w:
ww = w
self.SetColSize(3, ww)
if conf.favorites_file_path:
self.init_path = conf.favorites_file_path
else:
self.init_path = os.path.join(os.path.dirname(ConfigPath), 'quisk_favorites.txt')
conf.favorites_file_in_use = self.init_path
self.ReadIn()
if self.GetNumberRows() < 1:
self.AppendRows()
# Make a popup menu
self.popupmenu = wx.Menu()
item = self.popupmenu.Append(-1, 'Tune to')
self.Bind(wx.EVT_MENU, self.OnPopupTuneto, item)
self.popupmenu.AppendSeparator()
item = self.popupmenu.Append(-1, 'Append')
self.Bind(wx.EVT_MENU, self.OnPopupAppend, item)
item = self.popupmenu.Append(-1, 'Insert')
self.Bind(wx.EVT_MENU, self.OnPopupInsert, item)
item = self.popupmenu.Append(-1, 'Delete')
self.Bind(wx.EVT_MENU, self.OnPopupDelete, item)
self.popupmenu.AppendSeparator()
item = self.popupmenu.Append(-1, 'Move Up')
self.Bind(wx.EVT_MENU, self.OnPopupMoveUp, item)
item = self.popupmenu.Append(-1, 'Move Down')
self.Bind(wx.EVT_MENU, self.OnPopupMoveDown, item)
# Make a timer
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.OnTimer)
def SetModeEditor(self, mode_names):
self.mode_names = mode_names
for row in range(self.GetNumberRows()):
self.SetCellEditor(row, 2, wx.grid.GridCellChoiceEditor(mode_names, True))
def FormatFloat(self, freq):
freq = "%.6f" % freq
for i in range(3):
if freq[-1] == '0':
freq = freq[:-1]
else:
break
return freq
def ReadIn(self):
try:
fp = open(self.init_path, 'r')
lines = fp.readlines()
fp.close()
except:
lines = ("my net|7210000|LSB|My net 2030 UTC every Thursday",
"10m FM 1|29.620|FM|Fm local 10 meter repeater")
for row in range(len(lines)):
self.AppendRows()
fields = lines[row].split('|')
for col in range(len(fields)):
if col == 1: # Correct old entries made in Hertz
freq = fields[1]
try:
freq = float(freq)
except:
pass
else:
if freq > 30000.0: # Must be in Hertz
freq *= 1E-6
fields[1] = self.FormatFloat(freq)
if col <= 5:
self.SetCellValue(row, col, fields[col].strip())
self.MakeRepeaterDict()
def WriteOut(self):
ncols = self.GetNumberCols()
if ncols != 6:
print ("Bad logic in favorites WriteOut()")
return
self.changed = False
try:
fp = open(self.init_path, 'w')
except:
return
for row in range(self.GetNumberRows()):
out = []
for col in range(0, ncols):
cell = self.GetCellValue(row, col)
cell = cell.replace('|', ';')
out.append(cell)
t = "%20s | %10s | %10s | %30s | %10s | %10s\n" % tuple(out)
fp.write(t)
fp.close()
def AddNewFavorite(self):
self.InsertRows(0)
self.SetCellValue(0, 0, 'New station');
freq = (application.rxFreq + application.VFO) * 1E-6 # convert to megahertz
freq = self.FormatFloat(freq)
self.SetCellValue(0, 1, freq)
self.SetCellValue(0, 2, application.mode);
self.SetCellEditor(0, 2, wx.grid.GridCellChoiceEditor(self.mode_names, True))
self.OnChange()
def OnRightClickLabel(self, event):
event.Skip()
self.menurow = event.GetRow()
if self.menurow >= 0:
pos = event.GetPosition()
self.PopupMenu(self.popupmenu, pos)
def OnLeftClickLabel(self, event):
self.OnRightClickLabel(event)
def OnLeftDClick(self, event): # Thanks to Christof, DJ4CM
self.menurow = event.GetRow()
if self.menurow >= 0:
self.OnPopupTuneto(event)
def OnPopupAppend(self, event):
self.InsertRows(self.menurow + 1)
self.SetCellEditor(self.menurow + 1, 2, wx.grid.GridCellChoiceEditor(self.mode_names, True))
self.OnChange()
def OnPopupInsert(self, event):
self.InsertRows(self.menurow)
self.SetCellEditor(self.menurow, 2, wx.grid.GridCellChoiceEditor(self.mode_names, True))
self.OnChange()
def OnPopupDelete(self, event):
self.DeleteRows(self.menurow)
if self.GetNumberRows() < 1:
self.AppendRows()
self.SetCellEditor(0, 2, wx.grid.GridCellChoiceEditor(self.mode_names, True))
self.OnChange()
def OnPopupMoveUp(self, event):
row = self.menurow
if row < 1:
return
for i in range(self.GetNumberCols()):
c = self.GetCellValue(row - 1, i)
self.SetCellValue(row - 1, i, self.GetCellValue(row, i))
self.SetCellValue(row, i, c)
def OnPopupMoveDown(self, event):
row = self.menurow
if row == self.GetNumberRows() - 1:
return
for i in range(self.GetNumberCols()):
c = self.GetCellValue(row + 1, i)
self.SetCellValue(row + 1, i, self.GetCellValue(row, i))
self.SetCellValue(row, i, c)
def OnPopupTuneto(self, event):
freq = self.GetCellValue(self.menurow, 1)
if not freq:
return
try:
freq = str2freq (freq)
except ValueError:
print('Bad frequency')
return
if self.changed:
if self.timer.IsRunning():
self.timer.Stop()
self.WriteOut()
application.ChangeRxTxFrequency(None, freq)
mode = self.GetCellValue(self.menurow, 2)
mode = mode.upper()
application.modeButns.SetLabel(mode, True)
application.screenBtnGroup.SetLabel(conf.default_screen, do_cmd=True)
def MakeRepeaterDict(self):
self.RepeaterDict = {}
for row in range(self.GetNumberRows()):
offset = self.GetCellValue(row, 4)
offset = offset.strip()
if not offset:
continue
freq = self.GetCellValue(row, 1)
tone = self.GetCellValue(row, 5)
tone = tone.strip()
if not tone:
tone = '0'
try:
offset = float(offset)
freq = float(freq)
tone = float(tone)
except:
traceback.print_exc()
else:
freq = int(freq * 1E6 + 0.5) # frequency in Hertz
freq = (freq + 500) // 1000 # frequency in units of 1 kHz
self.RepeaterDict[freq * 1000] = (offset, tone)
def OnChange(self, event=None):
self.MakeRepeaterDict()
self.changed = True
if self.timer.IsRunning():
self.timer.Stop()
self.timer.Start(5000, oneShot=True)
def OnTimer(self, event):
if self.changed:
self.WriteOut()
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.display_text = ""
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.backgroundPen = wx.Pen(self.GetBackgroundColour(), 1)
self.tuningPenTx = wx.Pen(conf.color_txline, 1)
self.tuningPenRx = wx.Pen(conf.color_rxline, 1)
self.backgroundBrush = wx.Brush(self.GetBackgroundColour())
self.filterBrush = wx.Brush(conf.color_bandwidth, wx.SOLID)
self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID)
self.font = wx.Font(conf.graph_msg_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
self.SetFont(self.font)
if wxVersion in ('2', '3'):
self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
else:
self.SetBackgroundStyle(wx.BG_STYLE_PAINT)
def OnPaint(self, event):
#print 'GraphDisplay', self.GetUpdateRegion().GetBox()
dc = wx.AutoBufferedPaintDC(self)
dc.Clear()
# Draw the tuning line and filter display to the screen.
# If self.tune_rx is zero, draw the Rx filter at the Tx tuning line. There is no separate Rx display.
# Otherwise draw both an Rx and Tx tuning display.
self.DrawFilter(dc)
dc.SetPen(wx.Pen(conf.color_graphline, 1))
dc.DrawLines(self.line)
dc.SetPen(self.horizPen)
for y in self.parent.y_ticks:
dc.DrawLine(0, y, self.graph_width, y) # y line
if self.display_text:
dc.SetFont(self.font)
dc.SetTextBackground(conf.color_graph_msg_bg)
dc.SetTextForeground(conf.color_graph_msg_fg)
dc.SetBackgroundMode(wx.SOLID)
dc.DrawText(self.display_text, 0, 0)
if application.tx_inhibit:
dc.SetFont(self.font)
dc.SetTextBackground(conf.color_graph_msg_bg)
dc.SetTextForeground("red")
dc.SetBackgroundMode(wx.SOLID)
dc.DrawText(" *** Tx Inhibit ***", 0, self.chary * 15 // 10)
def DrawFilter(self, dc):
dc.SetPen(wx.TRANSPARENT_PEN)
dc.SetLogicalFunction(wx.COPY)
scale = 1.0 / self.parent.zoom / self.parent.sample_rate * self.graph_width
dc.SetBrush(self.filterBrush)
if self.tune_rx:
x, w, rit = self.parent.GetFilterDisplayXWR(rx_filters=False)
dc.DrawRectangle(self.tune_tx + x, 0, w, self.height)
x, w, rit = self.parent.GetFilterDisplayXWR(rx_filters=True)
dc.DrawRectangle(self.tune_rx + rit + x, 0, w, self.height)
dc.SetPen(self.tuningPenRx)
dc.DrawLine(self.tune_rx, 0, self.tune_rx, self.height)
else:
x, w, rit = self.parent.GetFilterDisplayXWR(rx_filters=True)
dc.DrawRectangle(self.tune_tx + rit + x, 0, w, self.height)
dc.SetPen(self.tuningPenTx)
dc.DrawLine(self.tune_tx, 0, self.tune_tx, self.height)
return rit
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, -200 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 SetTuningLine(self, tune_tx, tune_rx):
dc = wx.ClientDC(self)
rit = self.parent.GetFilterDisplayRit()
# Erase the old display
dc.SetPen(self.backgroundPen)
if self.tune_rx:
dc.DrawLine(self.tune_rx, 0, self.tune_rx, self.height)
dc.DrawLine(self.tune_tx, 0, self.tune_tx, self.height)
# Draw a new display
if self.tune_rx:
dc.SetPen(self.tuningPenRx)
dc.DrawLine(tune_rx, 0, tune_rx, self.height)
dc.SetPen(self.tuningPenTx)
dc.DrawLine(tune_tx, 0, tune_tx, self.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?
self.split_unavailable = False # Are we a multi receive graph or waterfall 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.zoom_control = 0
self.y_ticks = []
self.VFO = 0
self.filter_mode = 'AM'
self.filter_bandwidth = 0
self.filter_center = 0
self.ritFreq = 0 # receive incremental tuning frequency offset
self.mouse_x = 0
self.WheelMod = conf.mouse_wheelmod # Round frequency when using mouse wheel
self.txFreq = 0
self.sample_rate = application.sample_rate
self.zoom = 1.0
self.zoom_deltaf = 0
self.data_width = data_width
self.graph_width = graph_width
self.doResize = False
self.pen_tick = wx.Pen(conf.color_graphticks, 1)
self.pen_center = wx.Pen(conf.color_graphticks, 3)
self.font = wx.Font(conf.graph_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
self.SetFont(self.font)
w = self.GetCharWidth() * 14 // 10
h = self.GetCharHeight()
self.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.mouse_is_rx = False
self.SetSize((self.width, self.height))
self.SetSizeHints(self.width, 1, self.width)
self.SetBackgroundColour(conf.color_graph)
self.backgroundBrush = wx.Brush(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)
# handle bandplan info
self.bandplanWindow = wx.PopupWindow (parent)
self.bandplanInfo = wx.TextCtrl(self.bandplanWindow, style=wx.TE_READONLY)
self.bandplanInfo.SetFont(wx.Font(conf.status_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface))
self.bandplanWindow.Hide();
self.MakeDisplay()
def MakeDisplay(self):
self.display = GraphDisplay(self, self.originX, 0, self.graph_width, 5, self.chary)
self.display.zeroDB = self.zeroDB
def SetDisplayMsg(self, text=''):
self.display.display_text = text
self.display.Refresh()
def ScrollMsg(self, chars): # Add characters to a scrolling message
self.display.display_text = self.display.display_text + chars
self.display.display_text = self.display.display_text[-50:]
self.display.Refresh()
def OnPaint(self, event):
dc = wx.PaintDC(self)
dc.SetBackground(self.backgroundBrush)
dc.Clear()
dc.SetFont(self.font)
dc.SetTextForeground(conf.color_graphlabels)
if self.in_splitter:
self.MakeYTicks(dc)
else:
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
if self.originY < 0:
self.originY = 0
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 ChangeZoom(self, zoom, deltaf, zoom_control):
self.zoom = zoom
self.zoom_deltaf = deltaf
self.zoom_control = zoom_control
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 * 3 # 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
del self.y_ticks[:]
y_old = y
for i in range(0, -99999, -10):
if y >= chary // 2:
dc.SetPen(self.pen_tick)
dc.DrawLine(x1, y, x2, y) # y tick
self.y_ticks.append(y)
t = repr(i)
w, h = dc.GetTextExtent(t)
# draw text on Y axis
if y - y_old > h:
if y + h // 2 <= self.originY:
dc.DrawText(repr(i), x1 - w, y - h // 2)
elif h < self.scale:
dc.DrawText(repr(i), x1 - w, self.originY - h)
y_old = y
y = y + self.scale
if y >= self.originY - 3:
break
def MakeXTicks(self, dc):
sample_rate = int(self.sample_rate * self.zoom)
VFO = self.VFO + self.zoom_deltaf
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) * sample_rate / self.data_width
c = None
y = originY + 1
for freq, color in application.BandPlan:
freq -= VFO
if f < freq:
xend = int(self.x0 + float(freq) * self.data_width / sample_rate + 0.5)
if c is not None:
dc.SetPen(wx.TRANSPARENT_PEN)
dc.SetBrush(wx.Brush(c))
dc.DrawRectangle(x, y, min(x3, xend) - x, tick0) # x axis
if xend >= x3:
break
x = xend
f = freq
c = color
# check the width of the frequency label versus frequency span
df = charx * sample_rate // self.data_width
if VFO >= 10E9: # Leave room for big labels
df *= 1.33
elif VFO >= 1E9:
df *= 1.17
# tfreq: tick frequency for labels in Hertz
# stick: small tick in Hertz
# mtick: medium tick
# ltick: large tick
s2 = 1000
tfreq = None
while tfreq is None:
if df < s2:
tfreq = s2
stick = s2 // 10
mtick = s2 // 2
ltick = tfreq
elif df < s2 * 2:
tfreq = s2 * 2
stick = s2 // 10
mtick = s2 // 2
ltick = s2
elif df < s2 * 5:
tfreq = s2 * 5
stick = s2 // 2
mtick = s2
ltick = tfreq
s2 *= 10
# Draw the X axis ticks and frequency in kHz
dc.SetPen(self.pen_tick)
freq1 = VFO - sample_rate // 2
freq1 = (freq1 // stick) * stick
freq2 = freq1 + sample_rate + stick + 1
y_end = 0
for f in range (freq1, freq2, stick):
x = self.x0 + int(float(f - VFO) / sample_rate * self.data_width)
if self.originX <= x <= x3:
if f % ltick == 0: # large tick
dc.DrawLine(x, originY, x, originY + tick2)
elif f % mtick == 0: # medium tick
dc.DrawLine(x, originY, x, originY + tick1)
else: # small tick
dc.DrawLine(x, originY, x, originY + tick0)
if f % tfreq == 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.SetPen(self.pen_center)
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.raw_graph_data = data[i1:i2]
self.display.OnGraphData(data[i1:i2])
def SetVFO(self, vfo):
self.VFO = vfo
self.doResize = True
def SetTxFreq(self, tx_freq, rx_freq):
sample_rate = int(self.sample_rate * self.zoom)
self.txFreq = tx_freq
tx_x = self.x0 + int(float(tx_freq - self.zoom_deltaf) / sample_rate * self.data_width)
self.tuningX = tx_x
rx_x = self.x0 + int(float(rx_freq - self.zoom_deltaf) / 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 GetFilterDisplayXWR(self, rx_filters):
mode = self.filter_mode
rit = self.ritFreq
if rx_filters: # return Rx filter
bandwidth = self.filter_bandwidth
center = self.filter_center
else: # return Tx filter
bandwidth, center = get_filter_tx(mode)
x = center - bandwidth // 2
scale = 1.0 / self.zoom / self.sample_rate * self.data_width
x = int(x * scale + 0.5)
bandwidth = int(bandwidth * scale + 0.5)
if bandwidth < 2:
bandwidth = 1
rit = int(rit * scale + 0.5)
return x, bandwidth, rit # Starting x, bandwidth and RIT frequency
def GetFilterDisplayRit(self):
rit = self.ritFreq
scale = 1.0 / self.zoom / self.sample_rate * self.data_width
rit = int(rit * scale + 0.5)
return rit
def GetMousePosition(self, event):
"""For mouse clicks in our display, translate to our screen coordinates."""
mouse_x, mouse_y = event.GetPosition()
win = event.GetEventObject()
if win and win is not self:
x, y = win.GetPosition().Get()
mouse_x += x
mouse_y += y
return mouse_x, mouse_y
def FreqRound(self, tune, vfo):
if conf.freq_spacing and not conf.freq_round_ssb:
freq = tune + vfo
n = int(freq) - conf.freq_base
if n >= 0:
n = (n + conf.freq_spacing // 2) // conf.freq_spacing
else:
n = - ( - n + conf.freq_spacing // 2) // conf.freq_spacing
freq = conf.freq_base + n * conf.freq_spacing
return freq - vfo
else:
return tune
def OnRightDown(self, event):
sample_rate = int(self.sample_rate * self.zoom)
VFO = self.VFO + self.zoom_deltaf
mouse_x, mouse_y = self.GetMousePosition(event)
freq = float(mouse_x - self.x0) * sample_rate / self.data_width
freq = int(freq)
if VFO > 0:
vfo = VFO + freq - self.zoom_deltaf
if sample_rate > 40000:
vfo = (vfo + 5000) // 10000 * 10000 # round to even number
elif sample_rate > 5000:
vfo = (vfo + 500) // 1000 * 1000
else:
vfo = (vfo + 50) // 100 * 100
tune = freq + VFO - vfo
tune = self.FreqRound(tune, vfo)
self.ChangeHwFrequency(tune, vfo, 'MouseBtn3', event=event)
def OnLeftDown(self, event):
sample_rate = int(self.sample_rate * self.zoom)
mouse_x, mouse_y = self.GetMousePosition(event)
if mouse_x <= self.originX: # click left of Y axis
return
if mouse_x >= self.originX + self.graph_width: # click past FFT data
return
shift = wx.GetKeyState(wx.WXK_SHIFT)
if shift:
mouse_x -= self.filter_center * self.data_width / sample_rate
self.mouse_x = mouse_x
x = mouse_x - self.originX
if self.split_unavailable:
self.mouse_is_rx = False
elif application.split_rxtx and application.split_locktx:
self.mouse_is_rx = True
elif application.split_rxtx and application.split_lockrx:
self.mouse_is_rx = False
elif 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) * sample_rate / self.data_width + self.zoom_deltaf
freq = int(freq)
if shift:
pass
elif conf.freq_spacing:
if not self.mouse_is_rx:
freq = self.FreqRound(freq, self.VFO)
elif application.mode in ('LSB', 'USB', 'AM', 'FM', 'FDV-U', 'FDV-L'):
rnd = conf.freq_round_ssb
if rnd:
freq = (freq + rnd // 2) // rnd * rnd
elif application.mode in ('CWU', 'CWL'): # Move to a nearby peak
CW_width = self.data_width * application.filter_bandwidth // sample_rate // 2 # width tolerance
CW_mouse = mouse_x - self.originX
CW_max = -9999 # look for a peak significantly greater than the average
CW_avg = 0.0
CW_x1 = CW_mouse - CW_width
CW_x2 = CW_mouse + CW_width
try:
for x in range(CW_x1, CW_x2):
y = self.raw_graph_data[x]
CW_avg += y
if y > CW_max:
CW_max = y
CW_x = x
CW_avg /= (CW_x2 - CW_x1)
yp = self.raw_graph_data[CW_x + 1]
ym = self.raw_graph_data[CW_x - 1]
CW_correct = 0.5 * (ym - yp) / (ym - 2 * CW_max + yp)
except: # missing or short raw_graph_data, zero CW_width
pass
else:
if CW_max - CW_avg > 5 and CW_max > yp and CW_max > ym:
CW_x += CW_correct
freq = float(CW_x + self.originX - self.x0) * sample_rate / self.data_width + self.zoom_deltaf
freq = int(freq)
if self.mouse_is_rx:
self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseBtn1', event=event, rx_freq=freq)
else:
self.ChangeHwFrequency(freq, self.VFO, 'MouseBtn1', event=event)
if not self.HasCapture():
self.CaptureMouse()
def OnLeftUp(self, event):
if self.HasCapture():
self.ReleaseMouse()
freq = self.FreqRound(self.txFreq, self.VFO)
if freq != self.txFreq:
self.ChangeHwFrequency(freq, self.VFO, 'MouseMotion', event=event)
def OnMotion(self, event):
sample_rate = int(self.sample_rate * self.zoom)
if event.Dragging() and event.LeftIsDown():
mouse_x, mouse_y = self.GetMousePosition(event)
if wx.GetKeyState(wx.WXK_SHIFT):
mouse_x -= self.filter_center * self.data_width / sample_rate
if False: #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 = float(x) * sample_rate / self.data_width
freq = int(freq)
self.ChangeHwFrequency(self.txFreq, self.VFO - freq, 'MouseMotion', event=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 + 1)
x = (mouse_x - self.mouse_x)
self.mouse_x = mouse_x
freq = speed * x * sample_rate / self.data_width
freq = int(freq)
if self.mouse_is_rx: # Mouse motion changes the receive frequency
freq2 = application.rxFreq + freq
self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseMotion', event=event, rx_freq=freq2)
else: # Mouse motion changes the transmit frequency
self.ChangeHwFrequency(self.txFreq + freq, self.VFO, 'MouseMotion', event=event)
else:
mouse_x, mouse_y = self.GetMousePosition(event)
if not self.in_splitter and self.originY <= mouse_y <= self.originY + self.tick and (
self.originX <= mouse_x <= self.originX + self.graph_width):
#sample_rate = int(self.sample_rate * self.zoom) # Measure the frequency
#freq = float(mouse_x - self.x0) * sample_rate / self.data_width + self.zoom_deltaf + self.VFO
#freq = int(freq)
#color = None
#for start_freq, start_color in application.BandPlan:
# if start_freq >= freq:
# break
# color = start_color
dc = wx.ClientDC(self) # Read the pixel at the mouse position
pixel = dc.GetPixel(mouse_x, mouse_y)
pixel = "#%02X%02X%02X" % (pixel[0], pixel[1], pixel[2])
txt = application.BandPlanC2T.get(pixel, '')
if pixel == "#000000":
pass
elif txt:
self.bandplanInfo.Clear()
self.bandplanInfo.write(txt)
width, height = self.bandplanInfo.GetTextExtent(txt)
width += self.bandplanInfo.GetCharWidth() * 2
height += self.bandplanInfo.GetCharHeight() // 2
self.bandplanInfo.SetSize(wx.Size(width, height))
self.bandplanWindow.SetClientSize((width, height))
rect = self.GetScreenRect()
if mouse_x > self.originX + self.graph_width * 3 // 4:
self.bandplanWindow.Move(mouse_x + rect.GetX() - width, mouse_y + rect.GetY() - height * 12 // 10)
else:
self.bandplanWindow.Move(mouse_x + rect.GetX(), mouse_y + rect.GetY() - height * 12 // 10)
if not self.bandplanWindow.IsShown():
self.bandplanWindow.Show()
else:
if self.bandplanWindow.IsShown():
self.bandplanWindow.Hide()
elif self.bandplanWindow.IsShown():
self.bandplanWindow.Hide()
def OnWheel(self, event):
if conf.freq_spacing:
wm = conf.freq_spacing
else:
wm = self.WheelMod # Round frequency when using mouse wheel
mouse_x, mouse_y = self.GetMousePosition(event)
x = mouse_x - self.originX
if self.split_unavailable:
self.mouse_is_rx = False
elif application.split_rxtx and application.split_locktx:
self.mouse_is_rx = True
elif application.split_rxtx and application.split_lockrx:
self.mouse_is_rx = False
elif 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 self.mouse_is_rx:
freq = application.rxFreq + self.VFO + wm * event.GetWheelRotation() // event.GetWheelDelta()
if conf.freq_spacing:
freq = self.FreqRound(freq, 0)
elif freq >= 0:
freq = freq // wm * wm
else: # freq can be negative when the VFO is zero
freq = - (- freq // wm * wm)
tune = freq - self.VFO
self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseWheel', event=event, rx_freq=tune)
else:
freq = self.txFreq + self.VFO + wm * event.GetWheelRotation() // event.GetWheelDelta()
if conf.freq_spacing:
freq = self.FreqRound(freq, 0)
elif freq >= 0:
freq = freq // wm * wm
else: # freq can be negative when the VFO is zero
freq = - (- freq // wm * wm)
tune = freq - self.VFO
self.ChangeHwFrequency(tune, self.VFO, 'MouseWheel', event=event)
def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None, rx_freq=None):
application.ChangeHwFrequency(tune, vfo, source, band, event, rx_freq)
def PeakHold(self, name):
if name in ('GraphP1', 'WFallP1'):
self.display.peak_hold = int(self.display.scale * conf.graph_peak_hold_1)
elif name in ('GraphP2', 'WFallP2'):
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 StationScreen(wx.Window): # This code was contributed by Christof, DJ4CM. Many Thanks!! Edited May 2022.
"""Create a window below the graph X axis to display interesting frequencies."""
def __init__(self, parent, width, lines):
self.lineMargin = 2
self.lines = lines
self.mouse_x = 0
self.stationList = []
graph = self.graph = application.graph
height = lines * (graph.GetCharHeight() + self.lineMargin) # The height may be zero
wx.Window.__init__(self, parent, size=(graph.width, height), style = wx.NO_BORDER)
self.font = wx.Font(conf.graph_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
self.SetFont(self.font)
self.SetBackgroundColour(conf.color_graph)
self.width = application.screen_width
self.Bind(wx.EVT_PAINT, self.OnPaint)
if lines:
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
self.Bind(wx.EVT_MOTION, self.OnMotion)
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
# handle station info
self.stationWindow = wx.PopupWindow (parent)
self.stationInfo = wx.TextCtrl(self.stationWindow, style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_NO_VSCROLL)
self.stationInfo.SetFont(wx.Font(conf.status_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface))
self.stationWindow.Hide();
self.firstStationInRange = None
self.lastStationX = 0
self.nrStationInRange = 0
self.tunedStation = 0
def OnPaint(self, event):
dc = wx.PaintDC(self)
if not self.lines:
return
dc.SetFont(self.font)
graph = self.graph
dc.SetTextForeground(conf.color_graphlabels)
dc.SetPen(graph.pen_tick)
originX = graph.originX
originY = graph.originY
endX = originX + graph.graph_width # end of fft data
sample_rate = int(graph.sample_rate * graph.zoom)
VFO = graph.VFO + graph.zoom_deltaf
hl = self.GetCharHeight()
y = 0
for i in range (self.lines):
dc.DrawLine(originX, y, endX, y)
y += hl + self.lineMargin
# create a sorted list of favorites in the frequency range
freq1 = VFO - sample_rate // 2
freq2 = VFO + sample_rate // 2
self.stationList = []
fav = application.config_screen.favorites
for row in range (fav.GetNumberRows()):
fav_f = fav.GetCellValue(row, 1)
if fav_f:
try:
fav_f = str2freq(fav_f)
if freq1 < fav_f < freq2:
self.stationList.append((fav_f, conf.Xsym_stat_fav, fav.GetCellValue(row, 0),
fav.GetCellValue(row, 2), fav.GetCellValue(row, 3)))
except ValueError:
pass
# add memory stations
for mem_f, mem_band, mem_vfo, mem_txfreq, mem_mode in application.memoryState:
if freq1 < mem_f < freq2:
self.stationList.append((mem_f, conf.Xsym_stat_mem, '', mem_mode, ''))
#add dx spots
if application.dxCluster:
for entry in application.dxCluster.dxSpots:
if freq1 < entry.getFreq() < freq2:
for i in range (0, entry.getLen()):
descr = entry.getSpotter(i) + '\t' + entry.getTime(i) + '\t' + entry.getLocation(i) + '\n' + entry.getComment(i)
if i < entry.getLen()-1:
descr += '\n'
self.stationList.append((entry.freq, conf.Xsym_stat_dx, entry.dx, '', descr))
# draw stations on graph
self.stationList.sort()
lastX = []
line = 0
for i in range (0, self.lines):
lastX.append(graph.width)
for statFreq, symbol, statName, statMode, statDscr in reversed (self.stationList):
ws = dc.GetTextExtent(symbol)[0]
statX = graph.x0 + int(float(statFreq - VFO) / sample_rate * graph.data_width)
w, h = dc.GetTextExtent(statName)
# shorten name until it fits into remaining space
maxLen = 25
tName = statName
while (w > lastX[line] - statX - ws - 4) and maxLen > 0:
maxLen -= 1
tName = statName[:maxLen] + '..'
w, h = dc.GetTextExtent(tName)
dc.DrawLine(statX, line * (hl+self.lineMargin), statX, line * (hl+self.lineMargin) + 4)
dc.DrawText(symbol + ' ' + tName, statX - ws//2, line * (hl+self.lineMargin) + self.lineMargin//2+1)
lastX[line] = statX
line = (line+1)%self.lines
def OnLeftDown(self, event):
if self.firstStationInRange != None:
# tune to station
if self.tunedStation >= self.nrStationInRange:
self.tunedStation = 0
freq, symbol, name, mode, dscr = self.stationList[self.firstStationInRange+self.tunedStation]
self.tunedStation += 1
if mode != '': # information about mode available
mode = mode.upper()
application.modeButns.SetLabel(mode, True)
application.ChangeRxTxFrequency(None, freq)
def OnMotion(self, event):
mouse_x, mouse_y = event.GetPosition()
x = (mouse_x - self.mouse_x)
application.isTuning = False
# show detailed station info
if abs(self.lastStationX - mouse_x) > 30:
self.firstStationInRange = None
found = False
graph = self.graph
sample_rate = int(graph.sample_rate * graph.zoom)
VFO = graph.VFO + graph.zoom_deltaf
if abs(x) > 5: # ignore small mouse moves
for index in range (0, len(self.stationList)):
statFreq, symbol, statName, statMode, statDscr = self.stationList[index]
statX = graph.x0 + int(float(statFreq - VFO) / sample_rate * graph.data_width)
if abs(mouse_x-statX) < 10:
self.lastStationX = mouse_x
if found == False:
self.firstStationInRange = index
self.firstStationX = statX
self.nrStationInRange = 0
self.stationInfo.Clear()
found = True
self.nrStationInRange += 1
t = " %s %s" % (symbol, statName)
width, height = self.stationInfo.GetTextExtent(t)
txt = t
t = " %s Hz %s" % (FreqFormatter(statFreq), statMode)
w, h = self.stationInfo.GetTextExtent(t)
width = max(width, w)
height += h
txt += '\n'
txt += t
if len(statDscr) > 0:
t = " %s" % statDscr
w, h = self.stationInfo.GetTextExtent(t)
width = max(width, w)
height += h
txt += '\n'
txt += t
self.stationInfo.write(txt)
width += self.stationInfo.GetCharWidth() * 3
height += self.stationInfo.GetCharHeight() // 2
self.stationInfo.SetSize(wx.Size(width, height))
self.stationWindow.SetClientSize((width, height))
self.mouse_x = mouse_x
if self.firstStationInRange != None:
w, h = self.stationInfo.GetSize()
# convert coordinates to screen
rect = self.GetScreenRect()
self.stationWindow.Move(self.firstStationX, rect.GetY() - h)
if not self.stationWindow.IsShown():
self.stationWindow.Show()
else:
self.stationWindow.Hide()
def OnLeaveWindow(self, event):
self.stationWindow.Hide()
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.zoom = 1.0
self.zoom_deltaf = 0
self.rf_gain = 0 # Keep waterfall colors constant for variable RF gain
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.marginPen = wx.Pen(conf.color_graph, 1)
self.tuningPen = wx.Pen('White', 3)
self.tuningPenTx = wx.Pen(conf.color_txline, 3)
self.tuningPenRx = wx.Pen(conf.color_rxline, 3)
self.filterBrush = wx.Brush(conf.color_bandwidth, wx.SOLID)
#self.backgroundBrush = wx.Brush(conf.color_graph)
# 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
if conf.waterfall_palette == 'A':
pal2 = conf.waterfallPalette
else:
pal2 = getattr(conf, "waterfallPalette" + conf.waterfall_palette)
red = bytearray(256)
green = bytearray(256)
blue = bytearray(256)
n = 0
for i in range(256):
if i > pal2[n+1][0]:
n = n + 1
red[i] = (i - pal2[n][0]) * (pal2[n+1][1] - pal2[n][1]) // (pal2[n+1][0] - pal2[n][0]) + pal2[n][1]
green[i] = (i - pal2[n][0]) * (pal2[n+1][2] - pal2[n][2]) // (pal2[n+1][0] - pal2[n][0]) + pal2[n][2]
blue[i] = (i - pal2[n][0]) * (pal2[n+1][3] - pal2[n][3]) // (pal2[n+1][0] - pal2[n][0]) + pal2[n][3]
self.rgb_data = QS.watfall_RgbData(red, green, blue, self.graph_width, application.screen_height)
self.pixels = bytearray(self.graph_width * application.screen_height * 3)
def OnPaint(self, event):
dc = wx.PaintDC(self)
dc.SetTextForeground(conf.color_graphlabels)
dc.SetBackground(wx.Brush('Black'))
rit = self.DrawFilter(dc)
dc.SetLogicalFunction(wx.COPY)
sample_rate = int(self.sample_rate * self.zoom)
x_origin = int(float(self.VFO) / sample_rate * self.data_width + 0.5)
width = self.graph_width
height = self.height - self.margin
if height <= 0:
height = 1
QS.watfall_GetPixels(self.rgb_data, self.pixels, x_origin, width, height)
if wxVersion in ('2', '3'):
bmap = wx.BitmapFromBuffer(width, height, self.pixels)
else:
bmap = wx.Bitmap.FromBuffer(width, height, self.pixels)
dc.DrawBitmap(bmap, 0, self.margin)
dc.SetPen(self.tuningPen)
dc.SetLogicalFunction(wx.XOR)
dc.DrawLine(self.tune_tx, self.margin, self.tune_tx, self.height)
if self.tune_rx:
dc.DrawLine(self.tune_rx, self.margin, self.tune_rx, self.height)
def SetHeight(self, height):
self.height = height
self.SetSize((self.graph_width, height))
def DrawFilter(self, dc):
# Erase area at the top of the waterfall
dc.SetPen(wx.TRANSPARENT_PEN)
dc.SetLogicalFunction(wx.COPY)
dc.SetBrush(self.parent.backgroundBrush)
dc.DrawRectangle(0, 0, self.graph_width, self.margin)
# Draw the filter and top tuning lines
scale = 1.0 / self.zoom / self.sample_rate * self.data_width
dc.SetBrush(self.filterBrush)
if self.tune_rx:
x, w, rit = self.parent.GetFilterDisplayXWR(rx_filters=False)
dc.DrawRectangle(self.tune_tx + x, 0, w, self.margin)
x, w, rit = self.parent.GetFilterDisplayXWR(rx_filters=True)
dc.DrawRectangle(self.tune_rx + rit + x, 0, w, self.margin)
dc.SetPen(self.tuningPenRx)
dc.DrawLine(self.tune_rx, 0, self.tune_rx, self.margin)
else:
x, w, rit = self.parent.GetFilterDisplayXWR(rx_filters=True)
dc.DrawRectangle(self.tune_tx + rit + x, 0, w, self.margin)
dc.SetPen(self.tuningPenTx)
dc.DrawLine(self.tune_tx, 0, self.tune_tx, self.margin)
return rit
def OnGraphData(self, data, y_zero, y_scale):
# y_scale and y_zero range from zero to 160.
# y_zero controls the center position of the colors. Set to a bit over the noise level.
# y_scale controls how much the colors change when the sample deviates from y_zero.
gain = self.rf_gain
sample_rate = int(self.sample_rate * self.zoom)
x_origin = int(float(self.VFO) / sample_rate * self.data_width + 0.5)
QS.watfall_OnGraphData(self.rgb_data, data, y_zero, y_scale, gain, x_origin)
self.Refresh(False)
def SetTuningLine(self, tune_tx, tune_rx):
dc = wx.ClientDC(self)
rit = self.DrawFilter(dc)
dc.SetPen(self.tuningPen)
dc.SetLogicalFunction(wx.XOR)
dc.DrawLine(self.tune_tx, self.margin, self.tune_tx, self.height)
if self.tune_rx:
dc.DrawLine(self.tune_rx, self.margin, self.tune_rx, self.height)
dc.DrawLine(tune_rx, self.margin, tune_rx, self.height)
dc.DrawLine(tune_tx, self.margin, tune_tx, self.height)
self.tune_tx = tune_tx
self.tune_rx = tune_rx
def ChangeZoom(self, zoom, deltaf, zoom_control):
self.zoom = zoom
self.zoom_deltaf = deltaf
self.zoom_control = zoom_control
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
self.zoom_control = 0
wx.SplitterWindow.__init__(self, frame)
self.SetSizeHints(width, -1)
self.SetSashGravity(0.50)
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 SetDisplayMsg(self, text=''):
self.pane1.SetDisplayMsg(text)
def ScrollMsg(self, char): # Add a character to a scrolling message
self.pane1.ScrollMsg(char)
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 SetPane1(self, ysz):
y_scale, y_zero = ysz
self.pane1.ChangeYscale(y_scale)
self.pane1.ChangeYzero(y_zero)
def SetPane2(self, ysz):
y_scale, y_zero = ysz
self.y_scale = y_scale
self.pane2.ChangeYscale(y_scale)
self.y_zero = y_zero
self.pane2.ChangeYzero(y_zero)
def OnGraphData(self, data):
self.pane1.OnGraphData(data)
self.pane2.OnGraphData(data)
def ChangeRfGain(self, gain): # Set the correction for RF gain
self.pane2.display.rf_gain = gain
def ChangeZoom(self, zoom, deltaf, zoom_control):
self.zoom_control = zoom_control
self.pane1.ChangeZoom(zoom, deltaf, zoom_control)
self.pane2.ChangeZoom(zoom, deltaf, zoom_control)
self.pane2.display.ChangeZoom(zoom, deltaf, zoom_control)
def PeakHold(self, name):
self.pane1.PeakHold(name)
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.zoom_control = 0
self.oldVFO = self.VFO
self.filter_mode = 'AM'
self.filter_bandwidth = 0
self.filter_center = 0
self.ritFreq = 0 # receive incremental tuning frequency offset
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.raw_graph_data = data[i1:i2]
self.display.OnGraphData(data[i1:i2], self.y_zero, self.y_scale)
class MultiRxGraph(GraphScreen):
# The screen showing each added receiver
the_modes = ('CWL', 'CWU', 'LSB', 'USB', 'AM', 'FM', 'DGT-U', 'DGT-L', 'DGT-FM', 'DGT-IQ')
def __init__(self, parent, data_width, graph_width, index):
multi_rx = application.multi_rx_screen
width = multi_rx.rx_data_width
GraphScreen.__init__(self, parent, width, width)
self.graph_display = self.display
self.waterfall_display = WaterfallDisplay(self, self.originX, 0, self.graph_width, 5, self.chary)
self.waterfall_display.Hide()
self.waterfall_display.VFO = self.VFO
self.waterfall_display.data_width = self.data_width
self.waterfall_y_scale = conf.waterfall_y_scale
self.waterfall_y_zero = conf.waterfall_y_zero
self.split_unavailable = True
width = self.originX + self.graph_width
self.tabX = width + (multi_rx.graph.width - width - multi_rx.rx_btn_width) // 2
self.popupX = self.tabX - multi_rx.rx_btn_width * 2
self.multirx_index = index
self.is_playing = False
self.mode_index = 0
self.band = '40'
# Create controls
posY = 0
half_width = multi_rx.rx_btn_width // 2
half_size = half_width, multi_rx.rx_btn_height
self.rx_btn = QuiskPushbutton(self, self.OnPopButton, "Rx %d .." % (index + 1))
self.rx_btn.SetSize(half_size)
self.rx_btn.SetPosition((self.tabX, posY))
self.play_btn = QuiskCheckbutton(self, self.OnPlayButton, "Play")
self.play_btn.SetSize(half_size)
self.play_btn.SetPosition((self.tabX + half_width, posY))
posY += multi_rx.rx_btn_height
btn1 = QuiskPushbutton(self, self.OnBtnDownBand, conf.Xbtn_text_range_dn, use_right=True)
btn2 = QuiskPushbutton(self, self.OnBtnUpBand, conf.Xbtn_text_range_up, use_right=True)
btn1.SetSize(half_size)
btn2.SetSize(half_size)
btn1.SetPosition((self.tabX, posY))
btn2.SetPosition((self.tabX + half_width, posY))
posY += multi_rx.rx_btn_height
self.sliderYs = SliderBoxV(self, 'Ys', self.y_scale, 160, self.OnChangeYscale, True)
self.sliderYz = SliderBoxV(self, 'Yz', self.y_zero, 160, self.OnChangeYzero, True)
x = self.tabX + (half_width * 2 - self.sliderYs.width - self.sliderYz.width) // 2
self.sliderYs.SetDimension(x, posY, self.sliderYs.width, 100)
x += self.sliderYs.width
self.sliderYz.SetDimension(x, posY, self.sliderYz.width, 100)
# Create menu
self.multi_rx_menu = wx.Menu()
item = self.multi_rx_menu.Append(-1, 'Show graph')
self.Bind(wx.EVT_MENU, self.OnShowGraph, item)
item = self.multi_rx_menu.Append(-1, 'Show waterfall')
self.Bind(wx.EVT_MENU, self.OnShowWaterfall, item)
self.multi_rx_menu.AppendSeparator()
menu = wx.Menu()
self.multi_rx_menu.AppendSubMenu(menu, "Band")
for band in conf.bandLabels:
if not isinstance(band, Q3StringTypes):
band = band[0]
if band == 'Time':
continue
item = menu.Append(-1, band)
self.Bind(wx.EVT_MENU, self.OnChangeBand, item)
self.mode_menu = wx.Menu()
self.multi_rx_menu.AppendSubMenu(self.mode_menu, "Mode")
for mode in self.the_modes:
item = self.mode_menu.AppendRadioItem(-1, mode)
self.Bind(wx.EVT_MENU, self.OnChangeMode, item)
self.filter_menu = wx.Menu()
self.multi_rx_menu.AppendSubMenu(self.filter_menu, "Filter")
for i in range(6):
item = self.filter_menu.AppendRadioItem(-1, '0')
self.Bind(wx.EVT_MENU, self.OnChangeFilter, item)
self.multi_rx_menu.AppendSeparator()
item = self.multi_rx_menu.Append(-1, 'Delete receiver')
self.Bind(wx.EVT_MENU, self.OnDeleteReceiver, item)
self.ChangeBand(application.lastBand)
if multi_rx.rx_zero == multi_rx.waterfall:
self.OnShowWaterfall()
def ResizeGraph(self):
GraphScreen.ResizeGraph(self)
w, h = self.GetClientSize()
x, y = self.sliderYs.GetPosition()
height = max(h - y, self.sliderYs.text_height * 2)
self.sliderYs.SetDimension(x, y, self.sliderYs.width, height)
x, y = self.sliderYz.GetPosition()
self.sliderYz.SetDimension(x, y, self.sliderYz.width, height)
def MakeYTicks(self, dc):
if self.display == self.graph_display:
GraphScreen.MakeYTicks(self, dc)
def OnPopButton(self, event):
pos = (self.popupX, 10)
self.PopupMenu(self.multi_rx_menu, pos)
def OnDeleteReceiver(self, event):
if self.is_playing:
QS.set_multirx_play_channel(-1)
application.multi_rx_screen.DeleteReceiver(self)
def OnShowGraph(self, event):
self.waterfall_display.Hide()
self.display = self.graph_display
self.SetTxFreq(self.txFreq, self.txFreq)
self.sliderYs.SetValue(self.y_scale)
self.sliderYz.SetValue(self.y_zero)
self.display.Show()
self.doResize = True
def OnShowWaterfall(self, event=None):
self.graph_display.Hide()
self.display = self.waterfall_display
self.SetTxFreq(self.txFreq, self.txFreq)
self.sliderYs.SetValue(self.waterfall_y_scale)
self.sliderYz.SetValue(self.waterfall_y_zero)
self.display.Show()
self.doResize = True
def OnGraphData(self, data):
self.raw_graph_data = data
if self.display == self.graph_display:
self.display.OnGraphData(data)
else:
self.display.OnGraphData(data, self.waterfall_y_zero, self.waterfall_y_scale)
def OnPlayButton(self, event):
application.multi_rx_screen.StopPlaying(self)
self.is_playing = event.GetEventObject().GetValue()
if self.is_playing:
QS.set_filters(self.filter_I, self.filter_Q, self.filter_bandwidth, 0, 1)
QS.set_multirx_play_channel(self.multirx_index)
else:
QS.set_multirx_play_channel(-1)
def SetVFO(self, vfo):
GraphScreen.SetVFO(self, vfo)
self.waterfall_display.VFO = self.VFO
self.waterfall_display.Refresh()
def OnChangeBand(self, event):
idd = event.GetId()
band = event.GetEventObject().GetLabel(idd)
self.ChangeBand(band)
def OnChangeMode(self, event=None):
if event is None:
try:
idx = self.the_modes.index(self.mode)
except ValueError:
self.mode = 'USB'
idx = self.the_modes.index(self.mode)
self.mode_menu.FindItemByPosition(idx).Check(True)
else:
idd = event.GetId()
self.mode = event.GetEventObject().GetLabel(idd)
bws = application.Mode2Filters(self.mode)
self.mode_index = Mode2Index.get(self.mode, 3)
QS.set_multirx_mode(self.multirx_index, self.mode_index)
for i in range(6):
item = self.filter_menu.FindItemByPosition(i)
item.SetItemLabel(str(bws[i]))
if i == 2:
item.Check(True)
self.filter_bandwidth = bws[2]
self.OnChangeFilter()
def OnChangeFilter(self, event=None):
if event is not None:
idd = event.GetId()
self.filter_bandwidth = int(event.GetEventObject().GetLabel(idd))
center = application.GetFilterCenter(self.mode, self.filter_bandwidth)
frate = QS.get_filter_rate(Mode2Index.get(self.mode, 3), self.filter_bandwidth)
self.filter_I, self.filter_Q = application.MakeFilterCoef(frate, None, self.filter_bandwidth, center)
if self.is_playing:
QS.set_filters(self.filter_I, self.filter_Q, self.filter_bandwidth, 0, 1) # filter for receiver that is playing sound
if self.multirx_index == 0:
QS.set_filters(self.filter_I, self.filter_Q, self.filter_bandwidth, 0, 2) # filter for digital mode output to sound device
self.filter_mode = self.mode
self.filter_center = center
def ChangeBand(self, band):
self.band = band
try:
vfo, tune, self.mode = application.bandState[band]
#print (vfo, tune, self.mode)
except:
try:
f1, f2 = conf.BandEdge[band]
except KeyError:
f1, f2 = 10000000, 12000000
vfo = (f1 + f2) // 2
vfo = vfo // 10000
vfo *= 10000
if vfo < 9000000:
self.mode = 'LSB'
else:
self.mode = 'USB'
tune = 0
self.OnChangeMode()
self.ChangeHwFrequency(tune, vfo, 'ChangeBand')
if hasattr(application.Hardware, "ChangeBandFilters"):
application.Hardware.ChangeBandFilters()
def OnBtnDownBand(self, event):
self.OnBtnUpBand(event, True)
def OnBtnUpBand(self, event, is_band_down=False):
sample_rate = application.sample_rate
btn = event.GetEventObject()
oldvfo = self.VFO
if btn.direction > 0: # left button was used, move a bit
d = int(sample_rate // 9)
else: # right button was used, move to edge
d = int(sample_rate * 45 // 100)
if is_band_down:
d = -d
vfo = self.VFO + d
if sample_rate > 40000:
vfo = (vfo + 5000) // 10000 * 10000 # round to even number
delta = 10000
elif sample_rate > 5000:
vfo = (vfo + 500) // 1000 * 1000
delta = 1000
else:
vfo = (vfo + 50) // 100 * 100
delta = 100
if oldvfo == vfo:
if is_band_down:
d = -delta
else:
d = delta
else:
d = vfo - oldvfo
self.ChangeHwFrequency(self.txFreq - d, self.VFO + d, 'BandUpDown', event=event)
def OnChangeYscale(self, event):
y_scale = self.sliderYs.GetValue()
if self.display == self.graph_display:
self.ChangeYscale(y_scale)
else:
self.waterfall_y_scale = y_scale
def OnChangeYzero(self, event):
y_zero = self.sliderYz.GetValue()
if self.display == self.graph_display:
self.ChangeYzero(y_zero)
else:
self.waterfall_y_zero = y_zero
def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None, rx_freq=None):
#print ("Quisk", self.multirx_index, self.VFO, vfo, self.band)
self.SetTxFreq(tune, tune)
if self.VFO != vfo:
self.SetVFO(vfo)
Hardware.MultiRxFrequency(self.multirx_index, vfo, self.band)
QS.set_multirx_freq(self.multirx_index, tune)
def ChangeRxTxFrequency(self, freq):
tune = freq - self.VFO
d = self.sample_rate * 45 // 100
if -d <= tune <= d: # Frequency is on-screen
vfo = self.VFO
else: # Change the VFO
vfo = (freq // 5000) * 5000 - 5000
tune = freq - vfo
self.ChangeHwFrequency(tune, vfo, 'FreqEntry')
class MultiReceiverScreen(wx.SplitterWindow):
# The top level screen showing a graph, waterfall and any additional receivers.
# The first receiver is zero; additional receivers are in self.receiver_list[]
def __init__(self, frame, data_width, graph_width):
application.multi_rx_screen = self # prevent phase error
self.data_width = data_width
self.graph_width = graph_width
wx.SplitterWindow.__init__(self, frame)
self.SetSashGravity(0.50)
self.receiver_list = []
self.graph = GraphScreen(self, data_width, graph_width)
self.width = self.graph.width
self.waterfall = WaterfallScreen(self, self.width, data_width, graph_width)
self.rx_zero = self.graph
self.Initialize(self.rx_zero)
self.waterfall.Hide()
self.SetSizeHints(self.width, -1)
# Calculate control width
rx_btn = QuiskPushbutton(self, None, "Rx 8....", style=wx.BU_EXACTFIT)
self.rx_btn_width, self.rx_btn_height = rx_btn.GetSize().Get()
self.rx_btn_width *= 2
rx_btn.Destroy()
del rx_btn
self.SetMinimumPaneSize(self.rx_btn_height)
self.rx_btn_border = 5
width = data_width - self.rx_btn_width - self.rx_btn_border * 2
self.rx_data_width = fftPreferedSizes[0]
for x in fftPreferedSizes:
if x >= width:
break
else:
self.rx_data_width = x
def __getattr__(self, name):
return getattr(self.rx_zero, name)
def ChangeRxZero(self, show_graph):
if self.IsSplit():
old = self.GetWindow2()
else:
old = self.GetWindow1()
if show_graph:
new = self.graph
else:
new = self.waterfall
if old != new:
self.ReplaceWindow(old, new)
new.Show()
old.Hide()
self.rx_zero = new
def StopPlaying(self, excpt): # change to not playing on all panes except excpt
for pane in self.receiver_list:
if pane != excpt:
pane.play_btn.SetValue(False)
pane.is_playing = False
def OnAddReceiver(self, event=None, hide_it=False):
index = len(self.receiver_list)
if index >= 7:
return
if index == 0:
pane1 = MultiRxGraph(self, self.data_width, self.graph_width, index)
self.receiver_list.append(pane1)
if hide_it:
pane1.Show(False)
pane1.graph_display.Show(False)
else:
pane2 = self.rx_zero
splitter = pane2.GetParent()
splitter.SplitHorizontally(pane1, self.rx_zero)
elif index == 1 and not self.receiver_list[0].IsShown():
pane1 = self.receiver_list[0]
pane2 = self.rx_zero
splitter = pane2.GetParent()
pane1.Show(True)
pane1.graph_display.Show(True)
splitter.SplitHorizontally(pane1, self.rx_zero)
else:
pane2 = self.receiver_list[-1]
parent = pane2.GetParent()
splitter = wx.SplitterWindow(parent)
splitter.SetSizeHints(self.width, -1, self.width)
splitter.SetMinimumPaneSize(self.rx_btn_height)
splitter.SetSashGravity(0.50)
pane1 = MultiRxGraph(splitter, self.data_width, self.graph_width, index)
self.receiver_list.append(pane1)
pane2.Reparent(splitter)
parent.ReplaceWindow(pane2, splitter)
splitter.SplitHorizontally(pane1, pane2)
self.SizeEqually()
index += 1 # len(self.receiver_list)
Hardware.MultiRxCount(index)
pane1.ChangeBand(application.lastBand)
def DeleteReceiver(self, pane):
Hardware.MultiRxCount(len(self.receiver_list) - 1)
if len(self.receiver_list) == 1:
self.Unsplit(pane)
self.receiver_list.remove(pane)
del pane
elif pane in self.receiver_list[-2:]:
self.receiver_list.remove(pane)
splitter2 = pane.GetParent()
splitter1 = splitter2.GetParent()
del pane
pane = self.receiver_list[-1]
pane.Reparent(splitter1)
splitter1.ReplaceWindow(splitter2, pane)
splitter2.Destroy()
else:
self.receiver_list.remove(pane)
splitter2 = pane.GetParent()
splitter1 = splitter2.GetParent()
splitter3 = splitter2.GetWindow1()
del pane
splitter3.Reparent(splitter1)
splitter1.ReplaceWindow(splitter2, splitter3)
splitter2.Destroy()
index = 0
for pane in self.receiver_list:
pane.multirx_index = index
pane.rx_btn.SetLabel("Rx %d" % (index + 1))
QS.set_multirx_mode(index, pane.mode_index)
QS.set_multirx_freq(index, pane.txFreq)
index += 1
if hasattr(application.Hardware, "ChangeBandFilters"):
application.Hardware.ChangeBandFilters()
def SizeEqually(self):
w, h = self.GetClientSize()
num = len(self.receiver_list)
self.SetSashPosition(h * num // (num + 1))
for rx in self.receiver_list[:-1]:
splitter = rx.GetParent()
w, h = splitter.GetClientSize()
num -= 1
splitter.SetSashPosition(h * num // (num + 1))
def OnIdle(self, event):
self.rx_zero.OnIdle(event)
for pane in self.receiver_list:
pane.OnIdle(event)
def OnGraphData(self, data, index=None):
if index is None: # data is for the principal receiver
self.waterfall.OnGraphData(data) # Save data for switch to waterfall
if self.rx_zero == self.graph:
self.graph.OnGraphData(data)
elif index < len(self.receiver_list):
self.receiver_list[index].OnGraphData(data)
def ChangeSampleRate(self, 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
for pane in self.receiver_list:
pane.sample_rate = rate
tune = pane.txFreq
vfo = pane.VFO
pane.txFreq = pane.VFO = -1 # demand change
pane.ChangeHwFrequency(tune, vfo, 'NewDecim')
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(conf.config_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
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.zoom_control = 0
self.yscale = 1
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)
dc.SetTextForeground(conf.color_graphlabels)
self.MakeYTicks(dc)
self.MakeXTicks(dc)
self.MakeText(dc)
dc.SetPen(wx.Pen(conf.color_graphline, 1))
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.Pen(conf.color_graphticks,1))
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.Pen(conf.color_graphticks,1))
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 // 1000)
else:
t = "%s X: %d microsec/div" % (t, self.xscale)
t = "%s Y: %.0E/div" % (t, self.yvalue)
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))
ph = math.atan2(im, re) * 360. / (2.0 * math.pi)
self.fpout.write("%12d %12d %12d %12.1d\n" % (re, im, ab, ph))
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 BandscopeScreen(WaterfallScreen):
def __init__(self, frame, width, data_width, graph_width, clock):
self.zoom = 1.0
self.zoom_deltaf = 0
self.zoom_control = 0
WaterfallScreen.__init__(self, frame, width, data_width, graph_width)
self.sample_rate = self.pane1.sample_rate = self.pane2.sample_rate = int(clock) // 2
self.VFO = clock // 4
self.SetVFO(self.VFO)
def SetTxFreq(self, tx_freq, rx_freq):
freq = tx_freq + application.VFO - self.VFO
WaterfallScreen.SetTxFreq(self, freq, freq)
def SetFrequency(self, freq): # freq is 7000000, not the offset from VFO
freq = freq - self.VFO
WaterfallScreen.SetTxFreq(self, freq, freq)
def ChangeZoom(self, zoom_control): # zoom_control is the slider value 0 to 1000
self.zoom_control = zoom_control
if zoom_control < 50:
zoom = 1.0
zoom_deltaf = 0
else:
zoom = 1.0 - zoom_control / 1000.0 * 0.95
freq = application.rxFreq + application.VFO
srate = int(self.sample_rate * zoom) # reduced (zoomed) sample rate
if freq - srate // 2 < 0:
zoom_deltaf = srate // 2 - self.VFO
elif freq + srate // 2 > self.sample_rate:
zoom_deltaf = self.VFO - srate // 2
else:
zoom_deltaf = freq - self.VFO
self.zoom = zoom
self.zoom_deltaf = zoom_deltaf
self.pane1.ChangeZoom(zoom, zoom_deltaf, zoom_control)
self.pane2.ChangeZoom(zoom, zoom_deltaf, zoom_control)
self.pane2.display.ChangeZoom(zoom, zoom_deltaf, zoom_control)
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.zoom_control = 0
self.VFO = 0
self.txFreq = 0
self.data = []
self.sample_rate = QS.get_filter_rate(-1, -1)
def NewFilter(self):
self.sample_rate = QS.get_filter_rate(-1, -1)
self.data = QS.get_filter()
mx = -1000
for x in self.data:
if mx < x:
mx = x
mx -= 3.0
f1 = None
for i in range(len(self.data)):
x = self.data[i]
if x > mx:
if f1 is None:
f1 = i
f2 = i
bw3 = float(f2 - f1) / len(self.data) * self.sample_rate
mx -= 3.0
f1 = None
for i in range(len(self.data)):
x = self.data[i]
if x > mx:
if f1 is None:
f1 = i
f2 = i
bw6 = float(f2 - f1) / len(self.data) * self.sample_rate
self.display.display_text = "Filter 3 dB bandwidth %.0f, 6 dB %.0f" % (bw3, bw6)
#self.data = QS.get_tx_filter()
self.doResize = True
def OnGraphData(self, data):
GraphScreen.OnGraphData(self, self.data)
def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None, rx_freq=None):
GraphScreen.SetTxFreq(self, tune, tune)
application.freqDisplay.Display(tune)
def SetTxFreq(self, tx_freq, rx_freq):
pass
class AudioFFTScreen(GraphScreen):
"""Create an FFT graph of the transmit audio."""
def __init__(self, parent, data_width, graph_width, sample_rate):
GraphScreen.__init__(self, parent, data_width, graph_width)
self.y_scale = conf.filter_y_scale
self.y_zero = conf.filter_y_zero
self.zoom_control = 0
self.VFO = 0
self.txFreq = 0
self.sample_rate = sample_rate
def OnGraphData(self, data):
GraphScreen.OnGraphData(self, data)
def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None, rx_freq=None):
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
self.zoom_control = 0
if "gtk2" in wx.PlatformInfo:
self.SetStandardFonts()
self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22])
if not self.LoadFile('help.html'):
self.LoadFile('/usr/share/doc/quisk/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
self.title = fp.readline().strip()[1:]
fp.close()
x = conf.window_posX
y = conf.window_posY
wx.Frame.__init__(self, None, -1, self.title, (x, y),
(width, height), wx.DEFAULT_FRAME_STYLE, 'MainFrame')
self.SetBackgroundColour(conf.color_bg)
self.SetForegroundColour(conf.color_bg_txt)
self.Bind(wx.EVT_CLOSE, self.OnBtnClose)
if DEBUGSHELL:
#debugshell = CrustFrame()
debugshell = ShellFrame(parent=self)
debugshell.Show()
debugshell.shell.write("hw=quisk.application.Hardware")
def OnBtnClose(self, event):
application.OnBtnClose(event)
self.Destroy()
def SetConfigText(self, text):
if len(text) > 100:
text = text[0:80] + '|||' + text[-17:]
self.SetTitle("Radio %s %s %s" % (application.local_conf.RadioName, self.title, text))
class Spacer(wx.Window):
"""Create a bar between the graph screen and the controls"""
def __init__(self, parent):
wx.Window.__init__(self, parent, pos = (0, 0),
size=(-1, 6), style = wx.NO_BORDER)
self.Bind(wx.EVT_PAINT, self.OnPaint)
r, g, b = parent.GetBackgroundColour().Get(False)
dark = (r * 7 // 10, g * 7 // 10, b * 7 // 10)
light = (r + (255 - r) * 5 // 10, g + (255 - g) * 5 // 10, b + (255 - b) * 5 // 10)
self.dark_pen = wx.Pen(dark, 1, wx.SOLID)
self.light_pen = wx.Pen(light, 1, wx.SOLID)
self.width = application.screen_width
def OnPaint(self, event):
dc = wx.PaintDC(self)
w = self.width
dc.SetPen(self.dark_pen)
dc.DrawLine(0, 0, w, 0)
dc.DrawLine(0, 1, w, 1)
dc.DrawLine(0, 2, w, 2)
dc.SetPen(self.light_pen)
dc.DrawLine(0, 3, w, 3)
dc.DrawLine(0, 4, w, 4)
dc.DrawLine(0, 5, w, 5)
class App(wx.App):
"""Class representing the application."""
StateNames = [ # Names of state attributes to save and restore
'bandState', 'bandAmplPhase', 'lastBand', 'VFO', 'txFreq', 'mode',
'vardecim_set', 'filterAdjBw1', 'levelAGC', 'levelOffAGC', 'volumeAudio', 'levelSpot',
'levelSquelch', 'levelSquelchSSB', 'levelVOX', 'timeVOX', 'sidetone_volume',
'txAudioClipUsb', 'txAudioClipAm','txAudioClipFm', 'txAudioClipFdv',
'txAudioPreemphUsb', 'txAudioPreemphAm', 'txAudioPreemphFm', 'txAudioPreemphFdv',
'wfallScaleZ', 'wfallGrScaleZ', 'graphScaleZ', 'split_rxtx_play', 'modeFilter',
'file_name_rec_audio', 'file_name_rec_samples', 'file_name_rec_mic', 'file_name_rec_tmp',
'file_name_play_audio', 'file_name_play_samples', 'file_name_play_cq',
'file_play_source', 'hermes_LNA_dB', 'hermes_atten_dB']
def __init__(self):
global application
QS.AppStatus(1)
application = self
self.bottom_widgets = None
self.dxCluster = None
self.main_frame = None
self.remote_control_head = False
self.remote_control_slave = False
self.keys_down = []
wx.App.__init__(self)
def QuiskText(self, *args, **kw): # Make our text control available to widget files
return QuiskText(*args, **kw)
def QuiskText1(self, *args, **kw): # Make our text control available to widget files
return QuiskText1(*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 SliderBoxHH(self, *args, **kw):
return SliderBoxHH(*args, **kw)
def FilterEvent(self, event):
try: # I am not sure if older wxPython can do this
typ = event.GetEventType()
if typ == wx.EVT_KEY_DOWN.typeId:
key = event.GetKeyCode() # This is always upper case for letters
if key not in self.keys_down:
self.keys_down.append(key)
#print (self.keys_down)
elif typ == wx.EVT_KEY_UP.typeId:
key = event.GetKeyCode()
self.keys_down.remove(key)
#print (self.keys_down)
except:
self.keys_down = []
return -1
def QuiskGetKeyState(self, key): # Replace normal wx.GetKeyState() because it only works with Shift, Control, Alt
if 97 <= key <= 122: # key is an integer; convert to upper case
key -= 32
return key in self.keys_down
def OnInit(self):
"""Perform most initialization of the app here (called by wxPython on startup)."""
wx.lib.colourdb.updateColourDB() # Add additional color names
import quisk_widgets # quisk_widgets needs the application object
quisk_widgets.application = self
del quisk_widgets
global conf # conf is the module for all configuration data
setattr(conf, 'config_file_path', ConfigPath)
setattr(conf, 'DefaultConfigDir', DefaultConfigDir)
if os.path.isfile(ConfigPath): # See if the user has a config file
setattr(conf, 'config_file_exists', True)
d = {}
d.update(conf.__dict__) # make items from conf available
exec(compile(open(ConfigPath).read(), ConfigPath, 'exec'), d) # execute the user's config file
if os.path.isfile(ConfigPath2): # See if the user has a second config file
exec(compile(open(ConfigPath2).read(), ConfigPath2, 'exec'), d) # execute the user's second config file
for k in d: # add user's config items to conf
v = d[k]
if k[0] != '_': # omit items starting with '_'
setattr(conf, k, v)
else:
setattr(conf, 'config_file_exists', False)
self.QuiskFilesDir = os.path.dirname(conf.settings_file_path) # directory for Quisk files
if not os.path.isdir(self.QuiskFilesDir):
self.QuiskFilesDir = DefaultConfigDir
self.std_out_err = StdOutput(self)
self.std_out_err.Logfile("WxPython " + wx.version())
QS.set_params(quisk_is_vna=0) # We are not the VNA program
# Read in configuration from the selected radio
self.BandPlan = []
if self.main_frame:
self.local_conf = configure.Configuration(self, 'Same')
else:
self.local_conf = configure.Configuration(self, argv_options.AskMe, argv_options.radio)
self.local_conf.UpdateConf()
# Choose whether to use Unicode or text symbols
for k in ('sym_stat_mem', 'sym_stat_fav', 'sym_stat_dx',
'btn_text_range_dn', 'btn_text_range_up', 'btn_text_play', 'btn_text_rec', 'btn_text_file_rec',
'btn_text_file_play', 'btn_text_fav_add',
'btn_text_fav_recall', 'btn_text_mem_add', 'btn_text_mem_next', 'btn_text_mem_del'):
if conf.use_unicode_symbols:
setattr(conf, 'X' + k, getattr(conf, 'U' + k))
else:
setattr(conf, 'X' + k, getattr(conf, 'T' + k))
MakeWidgetGlobals()
if conf.invertSpectrum:
QS.invert_spectrum(1)
if conf.use_sdriq:
sample_rate = int(66666667.0 / conf.sdriq_decimation + 0.5)
if conf.use_sdriq or conf.use_rx_udp:
name_of_sound_capt = ''
name_of_mic_play = ''
self.wfallScaleZ = {} # scale and zero for the waterfall pane2
self.wfallGrScaleZ = {} # scale and zero for the waterfall graph pane1
self.graphScaleZ = {} # scale and zero for the graph
self.bandState = {} # for key band, the current (self.VFO, self.txFreq, self.mode)
self.bandState.update(conf.bandState)
self.memoryState = [] # a list of (freq, band, self.VFO, self.txFreq, self.mode)
self.bandAmplPhase = {}
self.samples_from_python = False
self.NewIdList = [] # Hack: list of Ids in use for accelerator table
self.midi_message = []
self.idName2Button = {}
self.midi_handler = None
self.tx_inhibit = 0
self.old_tx_inhibit = 0
if conf.use_rx_udp == 10: # Hermes UDP protocol
self.bandscope_clock = conf.rx_udp_clock
else:
self.bandscope_clock = 0
self.modeFilter = { # the filter button index in use for each mode
'CW' : 3,
'SSB' : 3,
'AM' : 3,
'FM' : 3,
'DGT' : 1,
'FDV' : 2,
'IMD' : 3,
conf.add_extern_demod : 3,
}
if sys.platform == 'win32' and (conf.hamlib_com1_name or conf.hamlib_com2_name):
try: # make sure the pyserial module exists
import serial
except:
dlg = wx.MessageDialog(None, "The Python pyserial module is required but not installed. Do you want me to install it?",
"Install Python pyserial", style = wx.YES|wx.NO)
if dlg.ShowModal() == wx.ID_YES:
subprocess.call([sys.executable, "-m", "pip", "install", "pyserial"])
try:
import serial
except:
dlg = wx.MessageDialog(None, "Installation of Python pyserial failed. Please install it by hand.",
"Installation failed", style=wx.OK)
dlg.ShowModal()
# Open hardware file
global Hardware
if self.local_conf.GetHardware():
pass
else:
if hasattr(conf, "Hardware"): # Hardware defined in config file
self.Hardware = conf.Hardware(self, conf)
hname = ConfigPath
else:
self.Hardware = conf.quisk_hardware.Hardware(self, conf)
hname = conf.quisk_hardware.__file__
if hname[-3:] == 'pyc':
hname = hname[0:-1]
setattr(conf, 'hardware_file_name', hname)
if conf.quisk_widgets:
hname = conf.quisk_widgets.__file__
if hname[-3:] == 'pyc':
hname = hname[0:-1]
setattr(conf, 'widgets_file_name', hname)
else:
setattr(conf, 'widgets_file_name', '')
Hardware = self.Hardware
self.use_fast_heart_beat = hasattr(Hardware, "FastHeartBeat")
self.poll_gui_control = hasattr(Hardware, "PollGuiControl")
# Initialization - may be over-written by persistent state
self.local_conf.Initialize()
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_avg_seconds = 1.0 # seconds for S-meter average
self.smeter_sunits = -87.0
self.smeter_usage = "smeter" # specify use of s-meter display
self.hamlib_strength = 0.0;
self.timer = time.time() # A seconds clock
self.heart_time0 = self.timer # timer to call HeartBeat at intervals
self.save_time0 = self.timer
self.smeter_db_time0 = self.timer
self.smeter_sunits_time0 = self.timer
self.slowheart_time0 = self.timer
self.multi_rx_index = 0
self.multi_rx_timer = self.timer
self.band_up_down = 0 # Are band Up/Down buttons in use?
self.lastBand = 'Audio'
self.filterAdjBw1 = 1000
self.levelAGC = 500 # AGC level ON control, 0 to 1000
self.levelOffAGC = 100 # AGC level OFF control, 0 to 1000
self.levelSquelch = 500 # FM squelch level, 0 to 1000
self.levelSquelchSSB = 200 # SSB squelch level, 0 to 1000
self.levelVOX = -20 # audio level that triggers VOX
self.timeVOX = 500 # hang time for VOX
self.useVOX = False # Is the VOX button down?
self.txAudioClipUsb = 5 # Tx audio clip level in dB
self.txAudioClipAm = 0
self.txAudioClipFm = 0
self.txAudioClipFdv = 0
self.txAudioPreemphUsb = 70 # Tx audio preemphasis 0 to 100
self.txAudioPreemphAm = 0
self.txAudioPreemphFm = 0
self.txAudioPreemphFdv = 0
self.levelSpot = 500 # Spot level control, 0 to 1000
self.volumeAudio = 300 # audio volume
self.VFO = 0 # frequency of the VFO
self.ritFreq = 0 # receive incremental tuning frequency offset
self.txFreq = 0 # Transmit frequency as +/- sample_rate/2
self.rxFreq = 0 # Receive frequency as +/- sample_rate/2
self.tx_level = 100 # initially 100%; Caution: there is also a conf.tx_level dictionary
self.digital_tx_level = conf.digital_tx_level
self.fft_size = 1
self.accel_list = []
self.old_RxTx = 0
self.wsjtx_process = [None] * 10 # Maximum of 10 receivers
self.config_midi_window = None
self.hot_key_ptt_is_down = False
self.hot_key_ptt_was_down = False
self.hot_key_ptt_pressed = False
self.hot_key_ptt_active = False
self.serial_ptt_active = False
self.vox_ptt_active = False
self.tci_ptt_active = False
self.hermes_LNA_dB = 20
self.hermes_atten_dB = 0
if hasattr(Hardware, "OnChangeRxTx"):
self.want_RxTx = True
else:
self.want_RxTx = False
if conf.do_repeater_offset and hasattr(Hardware, "RepeaterOffset"):
QS.tx_hold_state(1)
# Quisk control by Hamlib through a serial port
if conf.hamlib_com1_name:
self.hamlib_com1_handler = HamlibHandlerSerial(self, conf.hamlib_com1_name)
else:
self.hamlib_com1_handler = None
if conf.hamlib_com2_name:
self.hamlib_com2_handler = HamlibHandlerSerial(self, conf.hamlib_com2_name)
else:
self.hamlib_com2_handler = None
# Quisk control by Hamlib through rig 2
self.hamlib_clients = [] # list of TCP connections to handle
if conf.hamlib_port:
try:
self.hamlib_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.hamlib_socket.bind((conf.hamlib_ip, conf.hamlib_port))
self.hamlib_socket.settimeout(0.0)
self.hamlib_socket.listen(5) # listen for TCP connections from multiple clients
except:
self.hamlib_socket = None
# traceback.print_exc()
else:
self.hamlib_socket = None
# Quisk control by K4 commands through a TCP port
self.k4_tcp_clients = [] # list of TCP connections to handle
if conf.k4_tcp_port:
try:
self.k4_tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.k4_tcp_socket.bind((conf.k4_tcp_ip, conf.k4_tcp_port))
self.k4_tcp_socket.settimeout(0.0)
self.k4_tcp_socket.listen(5) # listen for TCP connections from multiple clients
except:
self.k4_tcp_socket = None
# traceback.print_exc()
else:
self.k4_tcp_socket = None
# Quisk control by fldigi
self.fldigi_new_freq = None
self.fldigi_freq = None
if conf.digital_xmlrpc_url:
self.fldigi_server = ServerProxy(conf.digital_xmlrpc_url)
else:
self.fldigi_server = None
self.fldigi_rxtx = 'rx'
self.fldigi_timer = 0
self.screen = None
# Display the audio FFT instead of the RX filter or bandscope when self.rate_audio_fft > 0.
# The sample rate is self.rate_audio_fft. Add an instance of quisk_calc_audio_graph() to C code to provide data.
self.rate_audio_fft = 0
self.audio_fft_screen = None
self.audio_volume = 0.0 # Set output volume, 0.0 to 1.0
self.sidetone_volume = 0 # sidetone control value 0 to 1000
self.sidetone_0to1 = 0 # log taper sidetone volume 0.0 to 1.0
self.sound_thread = None
self.mode = conf.default_mode
self.color_list = None
self.color_index = 0
self.vardecim_set = 48000
self.w_phase = None
self.zoom = 1.0
self.filter_bandwidth = 1000 # filter bandwidth
self.zoom_deltaf = 0
self.zooming = False
self.split_rxtx = False # Are we in split Rx/Tx mode?
self.split_locktx = False # Split mode Tx frequency is fixed.
self.split_lockrx = False # Split mode Rx frequency is fixed.
self.split_rxtx_play = 2 # Play 1=both, high on Right; 2=both, high on Left; 3=only Rx; 4=only Tx
self.split_offset = 0 # current frequency difference when using Split
self.savedState = {}
self.tx_indicator = False
self.tmp_playing = False
self.file_play_state = 0 # 0 == not playing a file, 1 == playing a file, 2 == waiting for the repeat time
self.file_play_repeat = 0 # Repeat time in seconds, or zero for no repeat
self.file_play_timer = 0
self.file_play_source = 0 # 10 == play audio file, 11 == play I/Q sample file, 12 == play CQ message
self.file_name_rec_audio = ''
self.file_name_rec_samples = ''
self.file_name_rec_mic = ''
self.file_name_rec_tmp = ''
self.file_name_play_audio = ''
self.file_name_play_samples = ''
self.file_name_play_cq = ''
self.midiControls = {} # Control object and associated function for Midi control name
self.tci_vfo = 0
self.tci_dds = 0
self.tci_modulation = ''
self.tci_split_enable = 0
# get the screen size - thanks to Lucian Langa
x, y, self.screen_width, self.screen_height = wx.Display().GetGeometry() # Using display index 0
self.Bind(wx.EVT_IDLE, self.OnIdle)
self.Bind(wx.EVT_QUERY_END_SESSION, self.OnEndSession)
# Restore persistent program state
state = None
path = os.path.join(self.QuiskFilesDir, 'quisk_init.json')
if os.path.isfile(path):
try:
fp = open(path, "r")
state = json.load(fp)
fp.close()
except:
traceback.print_exc()
else: # Load from obsolete location
path = os.path.join(os.path.dirname(ConfigPath), '.quisk_init.pkl')
if os.path.isfile(path):
try:
fp = open(path, "rb") # Pickle requires a bytes object
state = pickle.load(fp)
fp.close()
except:
pass #traceback.print_exc()
if state:
try:
for k in state:
v = state[k]
if k in self.StateNames:
self.savedState[k] = v
attr = getattr(self, k)
if isinstance(attr, dict):
attr.update(v)
else:
setattr(self, k, v)
except:
pass #traceback.print_exc()
if "Version" not in self.bandAmplPhase:
self.FixAmplPhase() # Convert to new format Oct 2020
self.FixAmplPhase2() # New logic June 2024
if Hardware.VarDecimGetChoices(): # Hardware can change the decimation.
self.sample_rate = Hardware.VarDecimSet() # Get the sample rate.
self.vardecim_set = self.sample_rate
try:
var_rate1, var_rate2 = Hardware.VarDecimRange()
except:
var_rate1, var_rate2 = (48000, 960000)
else: # Use the sample rate from the config file.
var_rate1 = None
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
# Create the main frame
if conf.window_width > 0: # fixed width of the main frame
self.width = conf.window_width
else:
self.width = self.screen_width * 8 // 10
if conf.window_height > 0: # fixed height of the main frame
self.height = conf.window_height
else:
self.height = self.screen_height * 5 // 10
if self.main_frame:
frame = self.main_frame
szr = frame.GetSizer()
szr.Clear(True)
frame.SetSizer(None, True)
frame.SetSize(wx.Size(self.width, self.height))
else:
self.main_frame = frame = QMainFrame(self.width, self.height)
self.SetTopWindow(frame)
self.std_out_err.Create(frame)
if conf.pulse_audio_verbose_output == 0:
self.std_out_err.Show(False)
else:
self.std_out_err.Show(True)
wClient, hClient = self.main_frame.GetClientSize()
wFrame, hFrame = self.main_frame.GetSize()
main_border_w = wFrame - wClient
#print ('Main frame: size', wFrame, hFrame, 'client', wClient, hClient, 'border', main_border_w)
# Find the data width from a list of preferred sizes; it is the width of returned graph data.
# The graph_width is the width of data_width that is displayed.
# The total width of the graph is graph_width + self.graph_borders
if conf.window_width > 0:
graph = GraphScreen(frame, self.width//2, self.width//2, None) # make a GraphScreen to calculate borders
self.graph_width = wClient - (graph.width - graph.graph_width) # less graph borders equals actual graph_width
graph.Destroy()
del graph
if self.graph_width % 2 == 1: # Both data_width and graph_width are even numbers
self.graph_width -= 1
width = int(self.graph_width / conf.display_fraction) # estimated data width
for x in fftPreferedSizes:
if x >= width:
self.data_width = x
break
else:
self.data_width = fftPreferedSizes[-1]
else: # use conf.graph_width to determine the width
width = self.screen_width * conf.graph_width # estimated graph width
percent = conf.display_fraction # display central fraction of total width
percent = int(percent * 100.0 + 0.4)
width = width * 100 // percent # estimated data width
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
#print('graph_width', self.graph_width, 'data_width', self.data_width)
# 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) # fft size multiplier * average count
if conf.fft_size_multiplier >= 999: # 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
elif var_rate1 is None: # Calculate an equal split between fft size and average
fft_mult = 1
for mult in (32, 27, 24, 18, 16, 12, 9, 8, 6, 4, 3, 2, 1): # product of small factors
average_count = int(factor / mult + 0.5)
if average_count >= mult:
fft_mult = mult
break
average_count = int(factor / fft_mult + 0.5)
if average_count < 1:
average_count = 1
else: # Calculate a compromise for variable rates
fft_mult = int(float(var_rate1) / conf.graph_refresh / self.data_width + 0.5) # large fft_mult at low rate
if fft_mult > 8:
fft_mult = 8
elif fft_mult == 5:
fft_mult = 4
elif fft_mult == 7:
fft_mult = 6
average_count = int(factor / fft_mult + 0.5)
if average_count < 1:
average_count = 1
self.fft_size = self.data_width * fft_mult
# Record the basic application parameters
self.multi_rx_screen = MultiReceiverScreen(frame, self.data_width, self.graph_width)
if sys.platform == 'win32':
h = self.main_frame.GetHandle()
else:
h = 0
wisdom_path = os.path.join(self.QuiskFilesDir, 'quisk_wisdom.cache')
if conf.midi_cwkey_device:
QS.control_midi(midi_cwkey_note = conf.midi_cwkey_note)
midi = QS.control_midi(get_in_devices = 1)
if sys.platform == 'win32':
for i in range(len(midi)):
midi[i] = (midi[i], i)
found = None
for friendly, device in midi:
if conf.midi_cwkey_device == friendly:
found = device
break
if found is None:
for friendly, device in midi:
if conf.midi_cwkey_device.startswith(friendly) or friendly.startswith(conf.midi_cwkey_device):
found = device
break
if found is None:
print("Can not find MIDI device", conf.midi_cwkey_device)
elif sys.platform == 'win32':
QS.control_midi(client=found)
else:
QS.control_midi(device=found)
QS.record_app(self, conf, self.data_width, self.graph_width, self.fft_size,
self.multi_rx_screen.rx_data_width, self.sample_rate, h, wisdom_path)
# Start wdsp
self.wdsp = quisk_wdsp.Cwdsp(self)
self.wdsp_channel = 1
self.wdsp_SNB = 0
self.wdsp_NR2 = 0
self.wdsp.open(self.wdsp_channel)
#print ('data_width %d, FFT size %d, FFT mult %d, average_count %d, rate %d, Refresh %.2f Hz' % (
# self.data_width, self.fft_size, self.fft_size / self.data_width, average_count, self.sample_rate,
# float(self.sample_rate) / self.fft_size / average_count))
QS.record_graph(0, 0, 1.0)
QS.set_tx_audio(vox_level=20, vox_time=self.timeVOX) # Turn off VOX, set VOX time
# Make all the screens and hide all but one. MultiReceiver creates the graph and waterfall screens.
self.screen = self.multi_rx_screen
self.graph = self.multi_rx_screen.graph
self.waterfall = self.multi_rx_screen.waterfall
width = self.multi_rx_screen.graph.width
self.config_screen = ConfigScreen(frame, width, self.fft_size)
self.config_screen.Hide()
self.scope = ScopeScreen(frame, width, self.data_width, self.graph_width)
self.scope.Hide()
self.bandscope_screen = BandscopeScreen(frame, width, self.graph_width, self.graph_width, self.bandscope_clock)
self.bandscope_screen.Hide()
self.filter_screen = FilterScreen(frame, self.data_width, self.graph_width)
self.filter_screen.Hide()
if self.rate_audio_fft:
self.audio_fft_screen = AudioFFTScreen(frame, self.data_width, self.graph_width, self.rate_audio_fft)
self.audio_fft_screen.Hide()
self.help_screen = HelpScreen(frame, width, self.screen_height // 10)
self.help_screen.Hide()
self.station_screen = StationScreen(frame, width, conf.station_display_lines)
self.station_screen.Hide()
# 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, wx.EXPAND)
vertBox.Add(self.multi_rx_screen, 1, wx.EXPAND)
vertBox.Add(self.scope, 1, wx.EXPAND)
vertBox.Add(self.bandscope_screen, 1, wx.EXPAND)
vertBox.Add(self.filter_screen, 1, wx.EXPAND)
if self.rate_audio_fft:
vertBox.Add(self.audio_fft_screen, 1, wx.EXPAND)
vertBox.Add(self.help_screen, 1, wx.EXPAND)
vertBox.Add(self.station_screen, 0, wx.EXPAND)
# Add the spacer
vertBox.Add(Spacer(frame), 0, wx.EXPAND)
# Add the sizer for the controls
gap = 2
gbs = wx.GridBagSizer(gap, gap)
self.gbs = gbs
vertBox.Add(gbs, flag=wx.EXPAND)
gbs.SetEmptyCellSize((5, 5))
# Add the bottom spacer
vertBox.AddSpacer(5) # Thanks to Christof, DJ4CM
# End of vertical box.
self.MakeButtons(frame, gbs)
if conf.window_width > 0:
minw = maxw = conf.window_width
else:
minw = self.graph.width + main_border_w
maxw = -1
if conf.window_height > 0:
minh = maxh = self.height = conf.window_height
else:
minh = 100
maxh = -1
self.main_frame.SetSizeHints(minw, minh, maxw, maxh)
self.main_frame.SetClientSize(wx.Size(width, self.height))
if hasattr(Hardware, 'pre_open'): # pre_open() is called before open()
Hardware.pre_open()
try:
self.is_HermesLite2 = Hardware.is_HermesLite2 # This is set in pre_open()
except:
self.is_HermesLite2 = False
if self.local_conf.GetWidgets(self, Hardware, conf, frame, gbs, vertBox):
pass
elif conf.quisk_widgets:
self.bottom_widgets = conf.quisk_widgets.BottomWidgets(self, Hardware, conf, frame, gbs, vertBox)
if self.bottom_widgets: # Extend the sliders to the bottom of the screen
try:
i = self.bottom_widgets.num_rows_added # No way to get total rows until ver 2.9 !!
except:
i = 1
rows = self.widget_row + i
for i in self.slider_columns:
item = gbs.FindItemAtPosition((0, i))
item.SetSpan((rows, 1))
self.OpenHardware()
msg = QS.open_key(port=conf.quisk_serial_port, cts=conf.quisk_serial_cts, dsr=conf.quisk_serial_dsr)
if msg:
print(msg)
self.OpenSound()
tune, vfo = Hardware.ReturnFrequency() # Request initial frequency
if tune is None or vfo is None: # Set last-used frequency
self.bandBtnGroup.SetLabel(self.lastBand, do_cmd=True, direction=0)
else: # Set requested frequency
self.BandFromFreq(tune)
self.ChangeDisplayFrequency(tune - vfo, vfo)
# Record filter rate for the filter screen
self.filter_screen.sample_rate = QS.get_filter_rate(-1, -1)
self.config_screen.InitBitmap()
self.screenBtnGroup.SetLabel(conf.default_screen, do_cmd=True)
frame.Show()
self.Yield()
self.sound_thread = SoundThread(self.samples_from_python)
self.sound_thread.start()
if self.samples_from_python:
Hardware.StartSamples()
if conf.dxClHost:
# create DX Cluster and register listener for change notification
self.dxCluster = dxcluster.DxCluster()
self.dxCluster.start()
# Create shortcut keys for buttons
if conf.button_layout == 'Large screen':
for button in self.modeButns.GetButtons(): # mode buttons
self.idName2Button[button.idName] = button
if button.char_shortcut:
rid = self.QuiskNewId()
self.main_frame.Bind(wx.EVT_MENU, self.modeButns.Shortcut, id=rid)
self.accel_list.append(wx.AcceleratorEntry(wx.ACCEL_ALT, ord(button.char_shortcut), rid))
for button in self.bandBtnGroup.GetButtons(): # band buttons
self.idName2Button[button.idName] = button
if button.char_shortcut:
rid = self.QuiskNewId()
self.main_frame.Bind(wx.EVT_MENU, self.bandBtnGroup.Shortcut, id=rid)
self.accel_list.append(wx.AcceleratorEntry(wx.ACCEL_ALT, ord(button.char_shortcut), rid))
else: # Small screen
for button in self.modeButns.GetButtons(): # mode buttons
self.idName2Button[button.idName] = self.modeButns
for button in self.bandBtnGroup.GetButtons(): # band buttons
self.idName2Button[button.idName] = self.bandBtnGroup
self.main_frame.SetAcceleratorTable(wx.AcceleratorTable(self.accel_list))
self.modeButns.SetLabel(self.mode, True)
# self.OnTestTimer(None)
if hasattr(Hardware, 'post_open'): # post_open() is called after open() and after sound is started
Hardware.post_open()
self.StartWsjtx()
self.midiControls["Tune"] = (None, None)
self.midiControls["Vol"] = (self.sliderVol, self.ChangeVolume)
self.midiControls["STo"] = (self.sliderSto, self.ChangeSidetone)
self.midiControls["Rit"] = (self.ritScale, self.OnRitScale)
self.midiControls["Ys"] = (self.sliderYs, self.ChangeYscale)
self.midiControls["Yz"] = (self.sliderYz, self.ChangeYzero)
self.midiControls["Zo"] = (self.sliderZo, self.OnChangeZoom)
bw = self.bottom_widgets
if hasattr(bw, "sliderLNA"):
self.midiControls["RfLna"] = (bw.sliderLNA, bw.OnLNA)
self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyHook)
# Start the TCI server
if conf.tci_ip and conf.tci_port:
value = getattr(conf, "pulse_audio_verbose_output")
QS.tci_set_params(verbose=value)
QS.tci_set_params(start=1);
self.tci_dds = self.VFO
QS.tci_set_params(tci_dds=self.tci_dds)
self.tci_vfo = self.rxFreq + self.VFO
QS.tci_set_params(tci_vfo=self.tci_vfo)
self.tci_modulation = self.mode
QS.tci_set_params(tci_modulation=self.tci_modulation)
QS.tci_set_params(tci_trx=0)
QS.tci_set_params(tci_split_enable=0)
return True
def OnStartWsjtx(self, ctrl):
method = ctrl.GetValue()
if method[-3:] == "now":
self.StartWsjtx(method, ctrl)
else:
self.local_conf.globals["start_wsjtx"] = method
self.local_conf.settings_changed = True
def StartWsjtx(self, method=None, ctrl=None):
path = self.local_conf.globals.get('path_to_wsjtx', '')
if not path:
if sys.platform == 'win32':
path = "C:\\WSJT\\wsjtx\\bin\\wsjtx.exe"
else:
path = "/usr/bin/wsjtx"
cfg = self.local_conf.globals.get('config_wsjtx', '')
rig = self.local_conf.globals.get('rig_name_wsjtx', 'quisk')
if cfg:
prog = [path, "--rig-name", rig, "--config", cfg]
else:
prog = [path, "--rig-name", rig]
if method is None:
method = self.local_conf.globals.get("start_wsjtx", "Never")
if method == "Never":
return
if not os.path.isfile(path):
self.wsjtx_wait = wx.BusyInfo("Path to WSJT-X was not found.")
wx.CallLater(5000, self.StartWsjtxDone, None)
if ctrl:
ctrl.SetText("Never")
return
if method == "Main Rx0 on startup":
self.wsjtx_process[0] = subprocess.Popen(prog, shell=False)
elif method == "Main Rx0 now":
if self.wsjtx_process[0] is None or self.wsjtx_process[0].poll() is not None:
self.wsjtx_wait = wx.BusyInfo("Starting WSJT-X...")
self.wsjtx_process[0] = subprocess.Popen(prog, shell=False)
else:
self.wsjtx_wait = wx.BusyInfo("WSJT-X was already started")
wx.CallLater(5000, self.StartWsjtxDone, ctrl)
def StartWsjtxDone(self, ctrl):
del self.wsjtx_wait
if ctrl:
method = self.local_conf.globals.get("start_wsjtx", "Never")
ctrl.SetText(method)
#def OnTestTimer(self, event): # temporary code to switch bands and look for a bug
# if event is None:
# self.test_time0 = 0
# self.test_band = '40'
# self.test_timer = wx.Timer(self)
# self.Bind(wx.EVT_TIMER, self.OnTestTimer)
# self.test_timer.Start(1000, oneShot=True)
# return
# self.bandBtnGroup.SetLabel(self.test_band, do_cmd=True)
# if self.test_band == '40':
# self.test_timer.Start(250, oneShot=True)
# self.test_band = '30'
# else:
# self.test_timer.Start(250, oneShot=True)
# self.test_band = '40'
def OpenHardware(self):
if conf.use_rx_udp and conf.use_rx_udp != 10:
self.add_version = True # Add firmware version to config text
else:
self.add_version = False
if conf.use_rx_udp == 10: # Hermes UDP protocol
if conf.tx_ip == '':
conf.tx_ip = Hardware.hermes_ip
elif conf.tx_ip == 'disable':
conf.tx_ip = ''
if conf.tx_audio_port == 0:
conf.tx_audio_port = conf.rx_udp_port
elif conf.use_rx_udp:
conf.rx_udp_decimation = 8 * 8 * 8
if conf.tx_ip == '':
conf.tx_ip = conf.rx_udp_ip
elif conf.tx_ip == 'disable':
conf.tx_ip = ''
if conf.tx_audio_port == 0:
conf.tx_audio_port = conf.rx_udp_port + 2
# Open the hardware. This must be called before open_sound().
self.config_text = Hardware.open()
if self.config_text:
self.main_frame.SetConfigText(self.config_text)
else:
self.config_text = "Missing config_text"
def MakeSoundDeviceList(self):
# Create the list of capture and play devices (play, prefix, description, device, alsa_device, alsa_description).
# Play is 1 for play, 0 for capture. Prefix is "alsa:", "pulse:", "wasapi:"
self.sound_devices = []
if sys.platform == 'win32':
dev_capt, dev_play = QS.wasapi_sound_devices()
for description, device, raw in dev_capt:
if raw:
self.sound_devices.append((0, 'wasapi:', description, device, '', ''))
for description, device, raw in dev_play:
if raw:
self.sound_devices.append((1, 'wasapi:', description, device, '', ''))
elif sys.platform == 'darwin':
# Add PortAudio names
dev_capt, dev_play = QS.portaudio_sound_devices()
for name in dev_capt:
self.sound_devices.append((0, 'portaudio:', name, name, '', ''))
for name in dev_play:
self.sound_devices.append((1, 'portaudio:', name, name, '', ''))
else:
# Add Alsa names
dev_capt, dev_play = QS.alsa_sound_devices()
for name in dev_capt:
i = name.rfind(" (hw:")
if i > 5:
device = name[i + 2 : -1]
description = name[0:i]
else: # should not happen
device = name
description = name
self.sound_devices.append((0, 'alsa:', description, device, device, description))
for name in dev_play:
i = name.rfind(" (hw:")
if i > 5:
device = name[i + 2 : -1]
description = name[0:i]
else: # should not happen
device = name
description = name
self.sound_devices.append((1, 'alsa:', description, device, device, description))
if QS.get_params("QUISK_HAVE_PULSEAUDIO"):
# Add PulseAudio names
self.sound_devices.append((0, 'pulse:', 'default', 'default', '', ''))
self.sound_devices.append((1, 'pulse:', 'default', 'default', '', ''))
dev_capt, dev_play = QS.pulseaudio_sound_devices() # device, description, alsa description (alsa device) or ""
for device, description, alsa in dev_capt:
i = alsa.rfind(" (hw:")
if i > 5:
alsa_device = alsa[i + 2 : -1]
alsa_description = alsa[0:i]
else:
alsa_device = ''
alsa_description = ''
self.sound_devices.append((0, 'pulse:', description, device, alsa_device, alsa_description))
for device, description, alsa in dev_play:
i = alsa.rfind(" (hw:")
if i > 5:
alsa_device = alsa[i + 2 : -1]
alsa_description = alsa[0:i]
else:
alsa_device = ''
alsa_description = ''
self.sound_devices.append((1, 'pulse:', description, device, alsa_device, alsa_description))
if conf.pulse_audio_verbose_output > 2:
print ("Sound devices:")
for play, prefix, description, device, alsa_device, alsa_description in self.sound_devices:
print (prefix, play, description, ';', device, ';', alsa_device, ';', alsa_description, '.')
# Create the list of sound card names
self.dev_capt = ['']
self.dev_play = ['']
pulse_capt = []
pulse_play = []
for play, prefix, description, device, alsa_device, alsa_description in self.sound_devices:
if sys.platform == 'win32':
t = description
elif alsa_device:
t = "%s%s (%s)" % (prefix, description, alsa_device)
else:
t = "%s%s" % (prefix, description)
if prefix == "pulse:":
if "QuiskDigital" in t: # Add these names selectively in configure.py
pass
elif play:
pulse_play.append(t)
else:
pulse_capt.append(t)
else:
if play:
self.dev_play.append(t)
else:
self.dev_capt.append(t)
if QS.get_params("QUISK_HAVE_PULSEAUDIO"):
self.dev_capt += pulse_capt
self.dev_play += pulse_play
def SetSoundDevices(self):
# These are parallel to lists in sound.c: (conf name, is_play, index in sound.c)
sound_cards = (
("name_of_sound_play", 1, 0),
("name_of_mic_play", 1, 1),
("digital_output_name", 1, 2),
("sample_playback_name", 1, 3),
("digital_rx1_name", 1, 4),
("digital_rx2_name", 1, 5),
("digital_rx3_name", 1, 6),
("digital_rx4_name", 1, 7),
("digital_rx5_name", 1, 8),
("digital_rx6_name", 1, 9),
("digital_rx7_name", 1, 10),
("digital_rx8_name", 1, 11),
("digital_rx9_name", 1, 12),
("name_of_sound_capt", 0, 0),
("microphone_name", 0, 1),
("digital_input_name", 0, 2)
)
# These must equal the enum in quisk.h
DEV_DRIVER_NONE = 0
DEV_DRIVER_PORTAUDIO = 1
DEV_DRIVER_ALSA = 2
DEV_DRIVER_PULSEAUDIO = 3
DEV_DRIVER_DIRECTX = 4
DEV_DRIVER_WASAPI = 5
DEV_DRIVER_WASAPI2 = 6
for name, is_play, index in sound_cards:
value = getattr(conf, name)
if not value:
QS.set_sound_name(index, is_play, DEV_DRIVER_NONE, '', '')
continue
if sys.platform == 'win32':
found_desc, found_dev, a_dev, a_desc = self.GetSoundDevice(is_play, value, "wasapi:")
if not found_desc:
found_desc = value
found_dev = value
if conf.use_fast_sound:
if name in ("name_of_sound_play", "name_of_mic_play"):
windows_driver = DEV_DRIVER_WASAPI2
else:
windows_driver = DEV_DRIVER_WASAPI
else:
windows_driver = DEV_DRIVER_DIRECTX
QS.set_sound_name(index, is_play, windows_driver, found_desc, found_dev)
if conf.pulse_audio_verbose_output > 2:
print (name, "Windows driver %d" % windows_driver, found_desc, ';', found_dev)
continue
if value[0:9] == 'portaudio':
QS.set_sound_name(index, is_play, DEV_DRIVER_PORTAUDIO, value, '')
continue
# strip off ending " (hw:1,0)"
i = value.rfind(" (hw:")
if i > 5:
value = value[0:i]
if value[0:6] == 'pulse:':
# Pulse opens devices based only on the device. We must strip off the "pulse:".
if value[6:].strip() == "default":
found_desc = value
found_dev = "default"
else:
found_desc, found_dev, a_dev, a_desc = self.GetSoundDevice(is_play, value[6:], "pulse:")
if not found_desc:
found_desc = value
found_dev = value[6:].strip()
QS.set_sound_name(index, is_play, DEV_DRIVER_PULSEAUDIO, found_desc, found_dev)
if conf.pulse_audio_verbose_output > 2:
print (name, "Pulse", found_desc, ';', found_dev)
continue
if value[0:5] == 'alsa:':
# Alsa searches for the description if it starts with "alsa:"; else it opens the device name.
found_desc, found_dev, a_dev, a_desc = self.GetSoundDevice(is_play, value[5:], "alsa:")
if not found_desc:
found_desc = value
found_dev = value[5:]
QS.set_sound_name(index, is_play, DEV_DRIVER_ALSA, found_desc, found_dev)
if conf.pulse_audio_verbose_output > 2:
print (name, "Alsa driver %d" % DEV_DRIVER_ALSA, found_desc, ';', found_dev)
continue
# No initial "alsa:", "pulse:", etc.
found_desc, found_dev, a_dev, a_desc = self.GetSoundDevice(is_play, value, "pulse:") # Look for a pulse name
if not found_desc:
a_dev = value
a_desc = value
QS.set_sound_name(index, is_play, DEV_DRIVER_ALSA, a_desc, a_dev)
if conf.pulse_audio_verbose_output > 2:
print (name, "Alsa driver %d" % DEV_DRIVER_ALSA, a_desc, ';', a_dev)
def GetSoundDevice(self, in_play, in_value, in_prefix):
if in_prefix == "pulse:" and in_value[0:10] == " Use name ": # Convert null-sink names "pulse: Use name xxxx"
dev = in_value[10:]
if dev[-8:] == ".monitor":
dev = dev[0:-8]
else:
dev = dev + ".monitor"
found = [in_value[1:], dev, '', '']
return found
found = None
partial = None
for data in self.sound_devices:
play, prefix, description, device, alsa_device, alsa_description = data
if play != in_play:
continue
if prefix != in_prefix:
continue
if in_value == description or in_value == device:
found = data
break
if in_value in description:
partial = data
if found:
return found[2:] # description, device, alsa_device, alsa_description
elif partial:
return partial[2:]
else:
return None, None, None, None
def OpenSound(self):
self.MakeSoundDeviceList()
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)
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.
self.SetSoundDevices()
QS.open_sound(0, conf.data_poll_usec, conf.latency_millisecs,
conf.tx_ip, conf.tx_audio_port,
conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q,
0.7, conf.mic_playback_rate)
def OnIdle(self, event):
if self.screen:
self.screen.OnIdle(event)
def OnEndSession(self, event):
event.Skip()
self.OnBtnClose(event)
def OnBtnClose(self, event=None):
msg = QS.GetQuiskPrintf()
if msg:
print(msg, end='')
QS.close_key()
if QS.tci_get_params("tci_clients_count") > 0:
QS.tci_set_params(close=1)
QS.set_file_name(record_button=0) # Turn off file recording
time.sleep(0.1)
if self.sound_thread:
if self.samples_from_python:
Hardware.StopSamples()
self.sound_thread.stop()
for i in range(0, 20):
if not self.sound_thread.is_alive():
break
time.sleep(0.1)
self.sound_thread = None
QS.close_rx_udp()
Hardware.close()
self.SaveState()
self.local_conf.SaveState()
if self.hamlib_socket:
self.hamlib_socket.close()
self.hamlib_socket = None
if self.k4_tcp_socket:
self.k4_tcp_socket.close()
self.k4_tcp_socket = None
if self.dxCluster:
self.dxCluster.stop()
if self.hamlib_com1_handler:
self.hamlib_com1_handler.close()
if self.hamlib_com2_handler:
self.hamlib_com2_handler.close()
msg = QS.GetQuiskPrintf()
if msg:
print(msg, end='')
def OnBtnOnOff(self, event):
if event.GetEventObject().GetValue(): # Start samples
try:
wx.BeginBusyCursor()
time.sleep(2.0) # delay needed to release sound devices
self.OnInit()
finally:
wx.EndBusyCursor()
else: # Stop samples
try:
wx.BeginBusyCursor()
self.OnBtnClose()
time.sleep(2.0) # delay needed to release sound devices
finally:
wx.EndBusyCursor()
def FixAmplPhase(self):
# Convert from [[VFO, freq, ampl, phase], ...] to [[VFO, [[freq, ampl, phase], ...]], ...]
self.bandAmplPhase["Version"] = {'rx':[]} # marker for new format
for band in self.bandAmplPhase:
if not isinstance(self.bandAmplPhase[band], dict):
self.bandAmplPhase[band] = {}
try:
for rx_tx in self.bandAmplPhase[band]:
if rx_tx in ('rx', 'tx'):
data = self.bandAmplPhase[band][rx_tx]
if data and not isinstance(data[0][1], (list, tuple)):
lst = []
for vfo, freq, am, ph in data:
lst.append([vfo, [[freq, am, ph]]])
self.bandAmplPhase[band][rx_tx] = lst
except:
traceback.print_exc()
self.bandAmplPhase[band] = {}
def FixAmplPhase2(self): # New definition of phase
if self.bandAmplPhase["Version"]["rx"] == [1]:
return
degrad = 360.0 / 2 / math.pi
for band in self.bandAmplPhase:
if band == "Version":
continue
for rx_tx in self.bandAmplPhase[band]:
if rx_tx in ('rx', 'tx'):
data = self.bandAmplPhase[band][rx_tx]
for item in data:
vfo = item[0]
lst_framph = item[1]
for framph in lst_framph:
freq, am, ph = framph
p = math.atan(math.tan(ph / degrad) / (1.0 + am))
#print ("%5s %3s %8d %8d %8.4f %8.3f %8.3f" % (band, rx_tx, vfo, freq, am, ph, p * degrad))
framph[2] = p * degrad
self.bandAmplPhase["Version"]["rx"] = [1]
def ImmediateChange(self, name):
if name == "pulse_audio_verbose_output":
value = getattr(conf, name)
QS.tci_set_params(verbose=value)
if value == 0:
self.std_out_err.Show(False)
else:
self.std_out_err.Show(True)
elif name == "quisk_serial_port":
QS.open_key(port=conf.quisk_serial_port)
elif name == "quisk_serial_cts":
QS.open_key(cts=conf.quisk_serial_cts)
elif name == "quisk_serial_dsr":
QS.open_key(dsr=conf.quisk_serial_dsr)
if hasattr(Hardware, "ImmediateChange"):
Hardware.ImmediateChange(name)
QS.ImmediateChange(name)
def CheckState(self): # check whether state has changed
changed = False
for n in self.StateNames:
try:
if getattr(self, n) != self.savedState[n]:
changed = True
break
except:
changed = True
break
return changed
def SaveState(self):
state = {}
for n in self.StateNames:
state[n] = v = getattr(self, n)
self.savedState[n] = v
path = os.path.join(self.QuiskFilesDir, 'quisk_init.json')
try:
fp = open(path, "w")
json.dump(state, fp, indent=2)
fp.close()
except:
pass #traceback.print_exc()
def Mode2Filters(self, mode): # return the list of filter bandwidths for each mode
if mode in ('CWL', 'CWU'):
return conf.FilterBwCW
if mode in ('LSB', 'USB'):
return conf.FilterBwSSB
if mode == 'AM':
return conf.FilterBwAM
if mode in ('FM', 'DGT-FM', 'DGT-IQ'):
return conf.FilterBwFM
if mode in ('DGT-U', 'DGT-L'):
return conf.FilterBwDGT
if mode[0:4] == 'FDV-':
return conf.FilterBwFDV
if mode == 'IMD':
return conf.FilterBwIMD
if mode == 'EXT':
return conf.FilterBwEXT
return conf.FilterBwSSB
def OnSmeterRightDown(self, event):
try:
pos = event.GetPosition() # works for right-click
self.smeter.TextCtrl.PopupMenu(self.smeter_menu, pos)
except:
btn = event.GetEventObject() # works for button
btn.PopupMenu(self.smeter_menu, (0,0))
def OnSmeterMeterA(self, event):
self.smeter_avg_seconds = 1.0
self.smeter_usage = "smeter"
QS.measure_frequency(0)
def OnSmeterMeterB(self, event):
self.smeter_avg_seconds = 5.0
self.smeter_usage = "smeter"
QS.measure_frequency(0)
def OnSmeterFrequencyA(self, event):
self.smeter_usage = "freq"
QS.measure_frequency(2)
def OnSmeterFrequencyB(self, event):
self.smeter_usage = "freq"
QS.measure_frequency(10)
def OnSmeterAudioA(self, event):
self.smeter_usage = "audio"
QS.measure_audio(1)
def OnSmeterAudioB(self, event):
self.smeter_usage = "audio"
QS.measure_audio(5)
def QuiskNewId(self):
try:
ref = wx.NewIdRef()
self.NewIdList.append(ref)
rid = ref.GetValue()
except AttributeError:
rid = wx.NewId()
return rid
def MakeAccel(self, button):
rid = self.QuiskNewId()
self.main_frame.Bind(wx.EVT_MENU, button.Shortcut, id=rid)
self.accel_list.append(wx.AcceleratorEntry(wx.ACCEL_ALT, ord(button.char_shortcut), rid))
def MakeButtons(self, frame, gbs):
from quisk_widgets import button_text_width
margin = button_text_width
# Make one or two sliders on the left
self.sliderVol = SliderBoxV(frame, 'Vol', self.volumeAudio, 1000, self.ChangeVolume)
self.ChangeVolume() # set initial volume level
if conf.use_sidetone:
self.sliderSto = SliderBoxV(frame, 'STo', self.sidetone_volume, 1000, self.ChangeSidetone)
self.ChangeSidetone()
else:
self.sliderSto = None
# Make four sliders on the right
self.ritScale = SliderBoxV(frame, 'Rit', self.ritFreq, 2000, self.OnRitScale, False, themin=-2000)
self.sliderYs = SliderBoxV(frame, 'Ys', 0, 160, self.ChangeYscale, True)
self.sliderYz = SliderBoxV(frame, 'Yz', 0, 160, self.ChangeYzero, True)
self.sliderZo = SliderBoxV(frame, 'Zo', 0, 1000, self.OnChangeZoom)
self.sliderZo.SetValue(0)
flag = wx.EXPAND
# Add band buttons
if conf.button_layout == 'Large screen':
self.widget_row = 4 # Next available row for widgets
shortcuts = []
for label in conf.bandLabels:
if isinstance(label, (list, tuple)):
label = label[0]
shortcuts.append(conf.bandShortcuts.get(label, ''))
self.bandBtnGroup = RadioButtonGroup(frame, self.OnBtnBand, conf.bandLabels, None, shortcuts)
else:
self.widget_row = 6 # Next available row for widgets
self.bandBtnGroup = RadioBtnPopup(frame, self.OnBtnBand, conf.bandLabels, None, 'bandBtnGroup')
self.bandBtnGroup.idName = 'bandBtnGroup'
# Add sliders on the left
gbs.Add(self.sliderVol, (0, 0), (self.widget_row, 1), flag=wx.EXPAND|wx.LEFT, border=margin)
if conf.use_sidetone:
button_start_col = 2
self.slider_columns = [0, 1]
gbs.Add(self.sliderSto, (0, 1), (self.widget_row, 1), flag=flag)
else:
self.slider_columns = [0]
button_start_col = 1
# Receive button row: Mute, NR2, AGC
left_row2 = []
b = b_mute = QuiskCheckbutton(frame, self.OnBtnMute, text='Mute')
b.char_shortcut = 'u'
self.MakeAccel(b)
left_row2.append(b)
b = QuiskCycleCheckbutton(frame, self.OnBtnNR2, ['NR2', 'NR2', 'NR2t'])
if not self.wdsp.version:
b.Enable(False)
left_row2.append(b)
self.BtnAGC = agc = QuiskCheckbutton(frame, self.OnBtnAGC, 'AGC')
agc.char_shortcut = 'G'
self.MakeAccel(agc)
b = self.SliderAGC = WrapSlider(agc, self.OnBtnAGC, display=True)
self.midiControls["AGCSlider"] = (b, None)
b.SetDual(True)
b.SetSlider(value_off=self.levelOffAGC, value_on=self.levelAGC)
agc.SetValue(True, True)
left_row2.append(b)
b = self.BtnSquelch = QuiskCheckbutton(frame, self.OnBtnSquelch, text='Sqlch')
b.char_shortcut = 'q'
self.MakeAccel(b)
self.sliderSquelch = WrapSlider(b, self.OnBtnSquelch, display=True)
self.midiControls["SqlchSlider"] = (self.sliderSquelch, None)
left_row2.append(self.sliderSquelch)
# Noise Blanker
self.NB_menu = QuiskMenu("NB_menu")
for t in ("NB 1", "NB 2", "NB 3"):
self.NB_menu.AppendRadioItem(t, self.OnMenuNB, t == "NB 1")
item = self.NB_menu.AppendRadioItem("SNB", self.OnMenuNB)
if not self.wdsp.version:
item.Enable(False)
self.btnNB = QuiskCheckbutton(frame, self.OnBtnNB, text='NB 1')
self.btnNB.char_shortcut = 'B'
self.MakeAccel(self.btnNB)
b = WrapMenu(self.btnNB, self.NB_menu)
left_row2.append(b)
# Notch
b = QuiskCheckbutton(frame, self.OnBtnAutoNotch, text='Notch')
b.char_shortcut = 'h'
self.MakeAccel(b)
left_row2.append(b)
try:
gain_labels = Hardware.rf_gain_labels
except:
gain_labels = ()
try:
ant_labels = Hardware.antenna_labels
except:
ant_labels = ()
self.BtnRfGain = None
add_2 = 0 # Add two more buttons
if gain_labels:
b = self.BtnRfGain = QuiskCycleCheckbutton(frame, Hardware.OnButtonRfGain, gain_labels)
left_row2.append(b)
add_2 += 1
if ant_labels:
b = QuiskCycleCheckbutton(frame, Hardware.OnButtonAntenna, ant_labels)
left_row2.append(b)
add_2 += 1
if add_2 == 0:
b = QuiskCheckbutton(frame, None, text='RfGain')
b.Enable(False)
left_row2.append(b)
add_2 += 1
if add_2 == 1:
if 0: # Display a color chooser
#b_test1 = QuiskPushbutton(frame, self.OnBtnColorDialog, 'Color')
b_test1 = QuiskRepeatbutton(frame, self.OnBtnColor, 'Color', use_right=True)
else:
b_test1 = self.test1Button = QuiskCheckbutton(frame, self.OnBtnTest1, 'Test 1', color=conf.color_test)
left_row2.append(b_test1)
else:
b_test1 = None
# Transmit button row: Spot
left_row3=[]
bt = self.spotButton = QuiskCheckbutton(frame, self.OnBtnSpot, 'Spot', color=conf.color_test)
b = WrapSlider(bt, self.OnBtnSpot, slider_value=self.levelSpot, display=True)
self.midiControls["SpotSlider"] = (b, None)
if hasattr(Hardware, 'OnSpot'):
bt.char_shortcut = 'o'
self.MakeAccel(bt)
else:
b.Enable(False)
left_row3.append(b)
# Split button
self.split_menu = QuiskMenu("split_menu")
pl = self.split_rxtx_play
self.split_menu.AppendRadioItem('Play both, High Freq on R', self.OnMenuSplitPlay1, pl == 1)
self.split_menu.AppendRadioItem('Play both, High Freq on L', self.OnMenuSplitPlay2, pl == 2)
self.split_menu.AppendRadioItem('Play only Rx', self.OnMenuSplitPlay3, pl == 3)
self.split_menu.AppendRadioItem('Play only Tx', self.OnMenuSplitPlay4, pl == 4)
self.split_menu.AppendSeparator()
self.split_menu.AppendRadioItem('Unlocked', self.OnMenuSplitLock, True)
self.split_menu.AppendRadioItem('Lock Tx, Split Rx', self.OnMenuSplitLock, False)
self.split_menu.AppendRadioItem('Lock Rx, Split Tx', self.OnMenuSplitLock, False)
self.split_menu.AppendSeparator()
self.split_menu.Append('Reverse Rx and Tx', self.OnMenuSplitRev)
b = QuiskCheckbutton(frame, self.OnBtnSplit, "Split")
b.char_shortcut = 'l'
self.MakeAccel(b)
self.splitButton = WrapMenu(b, self.split_menu)
if False: #conf.mouse_tune_method: # Mouse motion changes the VFO frequency
self.splitButton.Enable(False)
left_row3.append(self.splitButton)
b = QuiskCheckbutton(frame, self.OnBtnFDX, 'FDX', color=conf.color_test)
if conf.add_fdx_button:
b.char_shortcut = 'X'
self.MakeAccel(b)
else:
b.Enable(False)
left_row3.append(b)
bt = QuiskCheckbutton(frame, self.OnButtonPTT, 'PTT', color='red')
self.pttButton = bt
b = WrapIndicator(bt, "Tx", "white", 'red')
self.pttButton.Tx = b
left_row3.append(b)
b = QuiskCheckbutton(frame, self.OnButtonVOX, 'VOX')
b.char_shortcut = 'V'
self.MakeAccel(b)
left_row3.append(b)
# add another receiver
self.multi_rx_menu = wx.Menu()
item = self.multi_rx_menu.AppendRadioItem(-1, 'Play only')
self.Bind(wx.EVT_MENU, self.OnMultirxPlayBoth, item)
item = self.multi_rx_menu.AppendRadioItem(-1, 'Play on left')
self.Bind(wx.EVT_MENU, self.OnMultirxPlayLeft, item)
item = self.multi_rx_menu.AppendRadioItem(-1, 'Play on right')
self.Bind(wx.EVT_MENU, self.OnMultirxPlayRight, item)
btn_addrx = QuiskPushbutton(frame, self.multi_rx_screen.OnAddReceiver, "Add Rx")
btn_addrx = WrapMenu(btn_addrx, self.multi_rx_menu)
if not hasattr(Hardware, 'MultiRxCount'):
btn_addrx.Enable(False)
# Record and Playback buttons
b = self.btnTmpRecord = QuiskCheckbutton(frame, self.OnBtnTmpRecord, text=conf.Xbtn_text_rec)
#left_row3.append(b)
b = self.btnTmpPlay = QuiskCheckbutton(frame, self.OnBtnTmpPlay, text=conf.Xbtn_text_play, use_right=True)
b.Enable(0)
#left_row3.append(b)
self.btn_file_record = QuiskCheckbutton(frame, self.OnBtnFileRecord, conf.Xbtn_text_file_rec)
left_row3.append(self.btn_file_record)
self.btnFilePlay = QuiskCheckbutton(frame, self.OnBtnFilePlay, conf.Xbtn_text_file_play)
left_row3.append(self.btnFilePlay)
self.config_screen.config.InitRecPlay()
self.config_screen.config.EnableRecPlay()
### Right bank of buttons
mode_names = ['CWL', 'CWU', 'LSB', 'USB', 'AM', 'FM', 'DGT-U', 'DGT-L', 'DGT-FM', 'DGT-IQ', 'FDV-U', 'FDV-L', 'IMD']
labels = [('CWL', 'CWU'), ('LSB', 'USB'), 'AM', 'FM', ('DGT-U', 'DGT-L', 'DGT-FM', 'DGT-IQ'), ('FDV-U', 'FDV-L')]
shortcuts = ['C', 'S', 'A', 'M', 'D', 'F']
count = 6 # There is room for seven buttons
if conf.add_imd_button:
n_imd = count
count += 1
labels.append('IMD')
shortcuts.append('I')
if count < 7 and conf.add_extern_demod:
count += 1
labels.append(conf.add_extern_demod)
mode_names.append(conf.add_extern_demod)
shortcuts.append('')
while count < 7:
count += 1
labels.append('')
shortcuts.append('')
mode_names.sort()
self.config_screen.favorites.SetModeEditor(mode_names)
if conf.button_layout == 'Large screen':
self.modeButns = RadioButtonGroup(frame, self.OnBtnMode, labels, None, shortcuts)
self.modeButns.buttons[0].idName = "CW U/L"
self.modeButns.buttons[1].idName = "SSB U/L"
self.modeButns.buttons[4].idName = "DGT"
self.modeButns.buttons[5].idName = "FDV"
else:
labels = ['CWL', 'CWU', 'LSB', 'USB', 'AM', 'FM', 'DGT-U', 'DGT-L', 'DGT-FM', 'DGT-IQ', 'FDV-U', 'IMD']
self.modeButns = RadioBtnPopup(frame, self.OnBtnMode, labels, None, 'modeButns')
self.modeButns.idName = "modeButns"
if conf.add_imd_button:
val = 1000
QS.set_imd_level(val)
if conf.button_layout == 'Large screen':
b = QuiskCheckbutton(frame, None, 'IMD', color=conf.color_test)
b.char_shortcut = 'I'
b = WrapSlider(b, self.OnImdSlider, slider_value=val, display=True)
self.midiControls["IMDSlider"] = (b, None)
self.modeButns.ReplaceButton(n_imd, b)
else:
self.modeButns.AddSlider('IMD', self.OnImdSlider, slider_value=val, display=True)
labels = ('2000', '2000', '2000', '2000', '2000', '2000')
self.filterButns = RadioButtonGroup(frame, self.OnBtnFilter, labels, None)
self.filterButns.idName = "filterButns"
b = QuiskCheckbutton(frame, None, str(self.filterAdjBw1))
b = WrapSlider(b, self.OnBtnAdjFilter, slider_value=self.filterAdjBw1, wintype='filter')
self.midiControls["Filter 6Slider"] = (b, None)
self.filterButns.ReplaceButton(5, b)
right_row2 = self.filterButns.GetButtons()
for i in range(0, 6):
b = right_row2[i]
if isinstance(b, WrapSlider):
b.button.idName = "Filter %d" % (i + 1)
#b.idName = "Filter"
else:
b.idName = "Filter %d" % (i + 1)
if self.rate_audio_fft:
t = "Audio FFT"
elif self.bandscope_clock: # Hermes UDP protocol
t = "Bscope"
else:
t = "RX Filter"
if conf.button_layout == 'Large screen':
labels = (('Graph', 'GraphP1', 'GraphP2'), ('WFall', 'WFallP1', 'WFallP2'), ('Scope', 'Scope'), 'Config', t, 'Help')
self.screenBtnGroup = RadioButtonGroup(frame, self.OnBtnScreen, labels, conf.default_screen)
right_row3 = self.screenBtnGroup.GetButtons()
else:
labels = ('Graph', 'GraphP1', 'GraphP2', 'WFall', 'WFallP1', 'WFallP2', 'Scope', 'Config', t)
self.screenBtnGroup = RadioBtnPopup(frame, self.OnBtnScreen, labels, conf.default_screen, 'screenBtnGroup')
for button in self.screenBtnGroup.GetButtons():
self.idName2Button[button.idName] = self.screenBtnGroup
self.screenBtnGroup.idName = "screenBtnGroup"
# Top row -----------------
# Band down button
szr = wx.BoxSizer(wx.HORIZONTAL) # add control to box sizer for centering
b_bandupdown = szr
b = QuiskRepeatbutton(frame, self.OnBtnDownBand, conf.Xbtn_text_range_dn,
self.OnBtnUpDnBandDone, use_right=True)
b.idName = "Band " + b.idName
self.idName2Button[b.idName] = b
szr.Add(b, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=1)
# Band up button
b = QuiskRepeatbutton(frame, self.OnBtnUpBand, conf.Xbtn_text_range_up,
self.OnBtnUpDnBandDone, use_right=True)
b.idName = "Band " + b.idName
self.idName2Button[b.idName] = b
szr.Add(b, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=1)
# Memory buttons
szr = wx.BoxSizer(wx.HORIZONTAL) # add control to box sizer for centering
b_membtn = szr
b = QuiskPushbutton(frame, self.OnBtnMemSave, conf.Xbtn_text_mem_add)
szr.Add(b, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=1)
b = self.memNextButton = QuiskPushbutton(frame, self.OnBtnMemNext, conf.Xbtn_text_mem_next)
b.Enable(False)
self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightClickMemory, b)
szr.Add(b, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT|wx.RIGHT, border=1)
b = self.memDeleteButton = QuiskPushbutton(frame, self.OnBtnMemDelete, conf.Xbtn_text_mem_del)
b.Enable(False)
szr.Add(b, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=1)
# Favorites buttons
szr = wx.BoxSizer(wx.HORIZONTAL) # add control to box sizer for centering
b_fav = szr
b = self.StationNewButton = QuiskPushbutton(frame, self.OnBtnFavoritesNew, conf.Xbtn_text_fav_add)
szr.Add(b, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=1)
b = self.StationNewButton = QuiskPushbutton(frame, self.OnBtnFavoritesShow, conf.Xbtn_text_fav_recall)
szr.Add(b, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=1)
# Add another receiver
szr = wx.BoxSizer(wx.HORIZONTAL) # add control to box sizer for centering
b_addrx = szr
szr.Add(btn_addrx, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=1)
# Temporary play and record
szr = wx.BoxSizer(wx.HORIZONTAL) # add control to box sizer for centering
b_tmprec = szr
szr.Add(self.btnTmpRecord, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=1)
szr.Add(self.btnTmpPlay, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=1)
# RIT button
szr = wx.BoxSizer(wx.HORIZONTAL) # add control to box sizer for centering
b_rit = szr
self.ritButton = QuiskCheckbutton(frame, self.OnBtnRit, "RIT")
self.idName2Button[self.ritButton.idName] = self.ritButton
szr.Add(self.ritButton, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=1)
self.ritButton.SetLabel("RIT %d" % self.ritFreq)
self.ritButton.Refresh()
# Frequency display
bw, bh = b_mute.GetMinSize()
b_freqdisp = self.freqDisplay = FrequencyDisplay(frame, 99999, bh * 15 // 10)
self.freqDisplay.Display(self.txFreq + self.VFO)
# On/Off button
if conf.button_layout == 'Large screen':
b_onoff = QuiskCheckbutton(frame, self.OnBtnOnOff, "On", color=conf.color_onoff)
b_onoff.text_color = conf.color_onoff_text
b_onoff.SetValue(True, do_cmd=False)
h = b_freqdisp.height
b_onoff.SetSizeHints(h, h, h, h)
# Frequency entry
if conf.button_layout == 'Large screen':
e = wx.TextCtrl(frame, -1, '', size=(10, bh), style=wx.TE_PROCESS_ENTER)
font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
e.SetFont(font)
e.SetBackgroundColour(conf.color_entry)
e.SetForegroundColour(conf.color_entry_txt)
szr = wx.BoxSizer(wx.HORIZONTAL) # add control to box sizer for centering
b_freqenter = szr
szr.Add(e, 1, flag=wx.ALIGN_CENTER_VERTICAL)
frame.Bind(wx.EVT_TEXT_ENTER, self.FreqEntry, source=e)
# S-meter
self.smeter = QuiskText(frame, ' S9+23 -166.00 dB ', bh, 0, True)
from quisk_widgets import _bitmap_menupop
b = QuiskBitmapButton(frame, self.OnSmeterRightDown, _bitmap_menupop)
szr = wx.BoxSizer(wx.HORIZONTAL)
b_smeter = szr
szr.Add(self.smeter, 1, flag=wx.ALIGN_CENTER_VERTICAL)
szr.Add(b, 0, flag=wx.ALIGN_CENTER_VERTICAL)
self.smeter.TextCtrl.Bind(wx.EVT_RIGHT_DOWN, self.OnSmeterRightDown)
self.smeter.TextCtrl.SetBackgroundColour(conf.color_freq)
self.smeter.TextCtrl.SetForegroundColour(conf.color_freq_txt)
# Make a popup menu for the s-meter
self.smeter_menu = QuiskMenu("smeter_menu")
self.smeter_menu.AppendRadioItem('S-meter 1', self.OnSmeterMeterA)
self.smeter_menu.AppendRadioItem('S-meter 5', self.OnSmeterMeterB)
self.smeter_menu.AppendRadioItem('Frequency 2', self.OnSmeterFrequencyA)
self.smeter_menu.AppendRadioItem('Frequency 10', self.OnSmeterFrequencyB)
self.smeter_menu.AppendRadioItem('Audio 1', self.OnSmeterAudioA)
self.smeter_menu.AppendRadioItem('Audio 5', self.OnSmeterAudioB)
# Make a popup menu for the memory buttons
self.memory_menu = wx.Menu()
# Place the buttons on the screen
if conf.button_layout == 'Large screen':
# There are fourteen columns, a small gap column, and then twelve more columns
band_buttons = self.bandBtnGroup.buttons
if len(band_buttons) <= 7:
bmax = 7
span = 2
else:
bmax = 14
span = 1
col = 0
for b in band_buttons[0:bmax]:
self.idName2Button[b.idName] = b
gbs.Add(b, (1, button_start_col + col), (1, span), flag=flag)
col += span
while col < 14:
b = QuiskCheckbutton(frame, None, text='')
gbs.Add(b, (1, button_start_col + col), (1, span), flag=flag)
col += span
col = button_start_col
for b in left_row2:
self.idName2Button[b.idName] = b
if b.idName in ("Mute", "NR2"):
gbs.Add(b, (2, col), (1, 1), flag=flag)
col += 1
else:
gbs.Add(b, (2, col), (1, 2), flag=flag)
col += 2
col = button_start_col
for b in left_row3:
self.idName2Button[b.idName] = b
gbs.Add(b, (3, col), (1, 2), flag=flag)
col += 2
col = 15
for b in self.modeButns.GetButtons():
self.idName2Button[b.idName] = b
if col in (19, 20): # single column
gbs.Add(b, (1, button_start_col + col), flag=flag)
col += 1
else: # double column
gbs.Add(b, (1, button_start_col + col), (1, 2), flag=flag)
col += 2
col = button_start_col + 15
for i in range(0, 6):
b = right_row2[i]
gbs.Add(b, (2, col), (1, 2), flag=flag)
self.idName2Button[b.idName] = b
b = right_row3[i]
gbs.Add(b, (3, col), (1, 2), flag=flag)
self.idName2Button[b.idName] = b
col += 2
gbs.Add(b_onoff, (0, button_start_col), (1, 1),
flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=self.freqDisplay.border)
self.idName2Button[b_onoff.idName] = b_onoff
gbs.Add(b_freqdisp, (0, button_start_col + 1), (1, 5),
flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=self.freqDisplay.border)
gbs.Add(b_freqenter, (0, button_start_col + 6), (1, 2), flag = wx.EXPAND|wx.LEFT|wx.RIGHT, border=5)
gbs.Add(b_bandupdown, (0, button_start_col + 8), (1, 2), flag=wx.EXPAND)
gbs.Add(b_membtn, (0, button_start_col + 11), (1, 3), flag = wx.EXPAND)
gbs.Add(b_fav, (0, button_start_col + 15), (1, 2), flag=wx.EXPAND)
gbs.Add(b_tmprec, (0, button_start_col + 17), (1, 2), flag=wx.EXPAND)
gbs.Add(b_addrx, (0, button_start_col + 19), (1, 2), flag=wx.EXPAND)
gbs.Add(b_smeter, (0, button_start_col + 21), (1, 4), flag=wx.EXPAND)
gbs.Add(b_rit, (0, button_start_col + 25), (1, 2), flag=wx.EXPAND)
col = button_start_col + 28
self.slider_columns += [col, col + 1, col + 2, col + 3]
gbs.Add(self.ritScale, (0, col ), (self.widget_row, 1), flag=wx.EXPAND|wx.LEFT, border=margin)
gbs.Add(self.sliderYs, (0, col + 1), (self.widget_row, 1), flag=flag)
gbs.Add(self.sliderYz, (0, col + 2), (self.widget_row, 1), flag=flag)
gbs.Add(self.sliderZo, (0, col + 3), (self.widget_row, 1), flag=flag)
for i in range(button_start_col, button_start_col + 14):
gbs.AddGrowableCol(i,1)
for i in range(button_start_col + 15, button_start_col + 27):
gbs.AddGrowableCol(i,1)
else: # Small screen
gbs.Add(b_freqdisp, (0, button_start_col), (1, 6),
flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=self.freqDisplay.border)
gbs.Add(b_bandupdown, (0, button_start_col + 6), (1, 2), flag=wx.EXPAND)
gbs.Add(b_smeter, (0, button_start_col + 8), (1, 4), flag=wx.EXPAND)
gbs.Add(self.bandBtnGroup.GetPopControl(), (1, button_start_col), (1, 2), flag=flag)
gbs.Add(self.modeButns.GetPopControl(), (3, button_start_col), (1, 2), flag=flag)
gbs.Add(self.screenBtnGroup.GetPopControl(), (4, button_start_col), (1, 2), flag=flag)
b = QuiskCheckbutton(frame, self.OnBtnHelp, 'Help')
gbs.Add(b, (5, button_start_col), (1, 2), flag=flag)
gbs.Add(b_membtn, (1, button_start_col + 2), (1, 3), flag = wx.EXPAND)
gbs.Add(b_fav, (1, button_start_col + 5), (1, 2), flag = wx.EXPAND)
gbs.Add(b_tmprec, (1, button_start_col + 7), (1, 2), flag=wx.EXPAND)
b = QuiskPushbutton(frame, None, '')
gbs.Add(b, (1, button_start_col + 9), (1, 1), flag=wx.EXPAND)
gbs.Add(b_rit, (1, button_start_col + 10), (1, 2), flag=wx.EXPAND)
row = 2
col = button_start_col
for b in self.filterButns.GetButtons():
self.idName2Button[b.idName] = b
gbs.Add(b, (row, col), (1, 2), flag=flag)
col += 2
buttons = left_row2 + left_row3
if b_test1:
buttons.remove(b_test1)
buttons += [b_test1, b_addrx]
else:
buttons += [b_addrx]
row = 3
col = 2
for b in buttons:
if hasattr(b, "idName"):
self.idName2Button[b.idName] = b
if hasattr(b, "idName") and b.idName in ("Mute", "NR2"):
gbs.Add(b, (row, button_start_col + col), (1, 1), flag=flag)
col += 1
else:
gbs.Add(b, (row, button_start_col + col), (1, 2), flag=flag)
col += 2
if col >= 12:
row += 1
col = 2
col = button_start_col + 12
self.slider_columns += [col, col + 1, col + 2, col + 3]
gbs.Add(self.ritScale, (0, col), (self.widget_row, 1), flag=wx.EXPAND|wx.LEFT, border=margin)
gbs.Add(self.sliderYs, (0, col + 1), (self.widget_row, 1), flag=flag)
gbs.Add(self.sliderYz, (0, col + 2), (self.widget_row, 1), flag=flag)
gbs.Add(self.sliderZo, (0, col + 3), (self.widget_row, 1), flag=flag)
for i in range(button_start_col, button_start_col + 12):
gbs.AddGrowableCol(i,1)
self.button_start_col = button_start_col
def MeasureAudioVoltage(self):
v = QS.measure_audio(-1)
t = "%11.3f" % v
t = t[0:1] + ' ' + t[1:4] + ' ' + t[4:] + ' uV'
self.smeter.SetLabel(t)
def MeasureFrequency(self):
vfo = Hardware.ReturnVfoFloat()
if vfo is None:
vfo = self.VFO
vfo += Hardware.transverter_offset
t = '%13.2f' % (QS.measure_frequency(-1) + vfo)
t = t[0:4] + ' ' + t[4:7] + ' ' + t[7:] + ' Hz'
self.smeter.SetLabel(t)
def NewSmeter(self):
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 > self.smeter_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.
self.hamlib_strength = (s - 9.0) * 6.0 # convert to dB; 0 dB is S9
if s < 0:
s = 0
if s >= 9.5:
s = (s - 9.0) * 6
t = " S9+%2.0f %7.2f dB" % (s, self.smeter_db)
else:
t = " S%.0f %7.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.
# Do not change the adjustable filter buttons.
buttons = self.filterButns.GetButtons()
for i in range(0, len(buttons) - 1):
label = str(args[i])
buttons[i].SetLabel(label)
buttons[i].Refresh()
if label:
buttons[i].Enable(1)
else:
buttons[i].Enable(0)
def MakeFilterCoef(self, rate, N, bw, center):
"""Make an I/Q filter with rectangular passband."""
center = abs(center)
lowpass = bw * 24000 // rate // 2
if lowpass in Filters:
filtD = Filters[lowpass]
#print ("Custom filter key %d rate %d bandwidth %d size %d" % (lowpass, rate, bw, len(filtD)))
else:
#print ("Window filter key %d rate %d bandwidth %d" % (lowpass, rate, bw))
if N is None:
shape = 1.5 # Shape factor at 88 dB
trans = (bw / 2.0 / rate) * (shape - 1.0) # 88 dB atten
N = int(4.0 / trans)
if N > 1000:
N = 1000
N = (N // 2) * 2 + 1
K = bw * N // rate
filtD = []
pi = math.pi
sin = math.sin
cos = math.cos
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
filtD.append(z)
if center:
# Make a bandpass filter by tuning the low pass filter to new center frequency.
# Make two quadrature filters.
filtI = []
filtQ = []
tune = -1j * 2.0 * math.pi * center / rate
NN = len(filtD)
D = (NN - 1.0) / 2.0
for i in range(NN):
z = 2.0 * cmath.exp(tune * (i - D)) * filtD[i]
filtI.append(z.real)
filtQ.append(z.imag)
return filtI, filtQ
return filtD, filtD
def SetFilterByMode(self, mode):
index = self.modeFilter[mode]
try:
Lab = self.filterButns.buttons[index].GetLabel()
except:
Lab = self.filterButns.buttons[0].GetLabel()
self.filterButns.SetLabel(Lab, True)
def GetFilterCenter(self, mode, bandwidth):
if mode in ('CWU', 'CWL'):
center = max(conf.cwTone, bandwidth // 2)
elif mode in ('LSB', 'USB'):
center = 300 + bandwidth // 2
elif mode in ('AM',):
center = 0
elif mode in ('FM',):
center = 0
elif mode in ('DGT-U', 'DGT-L'):
center = max(1500, bandwidth // 2)
elif mode in ('DGT-IQ', 'DGT-FM'):
center = 0
elif mode in ('FDV-U', 'FDV-L'):
if bandwidth <= 3000:
center = 1500
else:
center = bandwidth // 2
elif mode in ('IMD',):
center = 300 + bandwidth // 2
else:
center = 300 + bandwidth // 2
if mode in ('CWL', 'LSB', 'DGT-L', 'FDV-L'):
center = - center
return center
def OnBtnAdjFilter(self, event):
btn = event.GetEventObject()
bw = int(btn.GetLabel())
self.filterAdjBw1 = bw
if self.filterButns.GetIndex() == 5:
self.OnBtnFilter(event)
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())
index = self.filterButns.GetIndex()
mode = self.mode
frate = QS.get_filter_rate(Mode2Index.get(mode, 3), bw)
bw = min(bw, frate // 2)
self.filter_bandwidth = bw
center = self.GetFilterCenter(mode, bw)
# save and restore filter when changing modes
if mode in ('CWU', 'CWL'):
self.modeFilter['CW'] = index
elif mode in ('LSB', 'USB'):
self.modeFilter['SSB'] = index
elif mode in ('AM',):
self.modeFilter['AM'] = index
elif mode in ('FM',):
self.modeFilter['FM'] = index
elif mode in ('DGT-U', 'DGT-L'):
self.modeFilter['DGT'] = index
elif mode in ('DGT-IQ', 'DGT-FM'):
self.modeFilter['DGT'] = index
elif mode in ('FDV-U', 'FDV-L'):
self.modeFilter['FDV'] = index
elif mode in ('IMD',):
self.modeFilter['IMD'] = index
filtI, filtQ = self.MakeFilterCoef(frate, None, bw, center)
lower_edge = center - bw // 2
QS.set_filters(filtI, filtQ, bw, lower_edge, 0)
self.multi_rx_screen.graph.filter_mode = mode
self.multi_rx_screen.graph.filter_bandwidth = bw
self.multi_rx_screen.graph.filter_center = center
self.multi_rx_screen.waterfall.pane1.filter_mode = mode
self.multi_rx_screen.waterfall.pane1.filter_bandwidth = bw
self.multi_rx_screen.waterfall.pane1.filter_center = center
self.multi_rx_screen.waterfall.pane2.filter_mode = mode
self.multi_rx_screen.waterfall.pane2.filter_bandwidth = bw
self.multi_rx_screen.waterfall.pane2.filter_center = center
if self.screen is self.filter_screen:
self.screen.NewFilter()
def OnBtnHelp(self, event):
if event.GetEventObject().GetValue():
self.OnBtnScreen(None, 'Help')
else:
self.OnBtnScreen(None, self.screenBtnGroup.GetLabel())
def OnBtnScreen(self, event, name=None):
if event is not None:
win = event.GetEventObject()
name = win.GetLabel()
self.screen.Hide()
self.station_screen.Hide()
if name == 'Config':
self.config_screen.FinishPages()
self.screen = self.config_screen
elif name[0:5] == 'Graph':
self.screen = self.multi_rx_screen
self.screen.ChangeRxZero(True)
self.screen.SetTxFreq(self.txFreq, self.rxFreq)
self.freqDisplay.Display(self.VFO + self.txFreq)
self.screen.PeakHold(name)
self.station_screen.Show()
elif name[0:5] == 'WFall':
self.screen = self.multi_rx_screen
self.screen.ChangeRxZero(False)
self.screen.SetTxFreq(self.txFreq, self.rxFreq)
self.freqDisplay.Display(self.VFO + self.txFreq)
self.screen.PeakHold(name)
sash = self.screen.GetSashPosition()
self.station_screen.Show()
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 == 'Bscope':
self.screen = self.bandscope_screen
self.screen.SetTxFreq(self.txFreq, self.rxFreq)
elif name == 'Audio FFT':
self.screen = self.audio_fft_screen
self.freqDisplay.Display(self.screen.txFreq)
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)
self.sliderZo.SetValue(self.screen.zoom_control)
if name[0:5] == 'WFall':
self.screen.SetSashPosition(sash)
def OnBtnFileRecord(self, event):
record = event.GetEventObject().GetValue()
self.config_screen.config.OnFileRecordButton(record)
def ChangeYscale(self, event=None):
self.screen.ChangeYscale(self.sliderYs.GetValue())
if self.screen == self.multi_rx_screen:
if self.multi_rx_screen.rx_zero == self.waterfall:
self.wfallScaleZ[self.lastBand] = (self.waterfall.y_scale, self.waterfall.y_zero)
self.wfallGrScaleZ[self.lastBand] = (self.waterfall.pane1.y_scale, self.waterfall.pane1.y_zero)
elif self.multi_rx_screen.rx_zero == self.graph:
self.graphScaleZ[self.lastBand] = (self.graph.y_scale, self.graph.y_zero)
def ChangeYzero(self, event=None):
self.screen.ChangeYzero(self.sliderYz.GetValue())
if self.screen == self.multi_rx_screen:
if self.multi_rx_screen.rx_zero == self.waterfall:
self.wfallScaleZ[self.lastBand] = (self.waterfall.y_scale, self.waterfall.y_zero)
self.wfallGrScaleZ[self.lastBand] = (self.waterfall.pane1.y_scale, self.waterfall.pane1.y_zero)
elif self.multi_rx_screen.rx_zero == self.graph:
self.graphScaleZ[self.lastBand] = (self.graph.y_scale, self.graph.y_zero)
def OnChangeZoom(self, event=None):
zoom_control = self.sliderZo.GetValue()
if self.screen == self.bandscope_screen:
self.bandscope_screen.ChangeZoom(zoom_control)
self.bandscope_screen.SetTxFreq(self.txFreq, self.rxFreq)
return
# The display runs from f1 to f2. The original sample rate is "rate".
# The new effective sample rate is rate * zoom.
# f1 = deltaf + rate * (1 - zoom) / 2
if zoom_control < 50:
self.zoom = 1.0 # change back to not-zoomed mode
self.zoom_deltaf = 0
self.zooming = False
else:
a = 1000.0 * self.sample_rate / (self.sample_rate - 2500.0)
self.zoom = 1.0 - zoom_control / a
if not self.zooming: # set deltaf when zoom mode starts
center = self.multi_rx_screen.graph.filter_center
freq = self.rxFreq + center
self.zoom_deltaf = freq
self.zooming = True
zoom = self.zoom
deltaf = self.zoom_deltaf
self.graph.ChangeZoom(zoom, deltaf, zoom_control)
self.waterfall.ChangeZoom(zoom, deltaf, zoom_control)
self.screen.SetTxFreq(self.txFreq, self.rxFreq)
self.station_screen.Refresh()
def OnLevelVOX(self, event):
self.levelVOX = event.GetEventObject().GetValue()
if self.useVOX:
QS.set_tx_audio(vox_level=self.levelVOX)
def OnTimeVOX(self, event):
self.timeVOX = event.GetEventObject().GetValue()
QS.set_tx_audio(vox_time=self.timeVOX)
def OnButtonVOX(self, event):
self.useVOX = event.GetEventObject().GetValue()
if self.useVOX:
QS.set_tx_audio(vox_level=self.levelVOX)
else:
QS.set_tx_audio(vox_level=20)
if self.pttButton.GetValue():
self.pttButton.SetValue(0, True)
def OnButtonPTT(self, event):
if self.file_play_source == 12 and self.btnFilePlay.GetValue(): # playing CQ file
self.btnFilePlay.SetValue(False, False)
self.file_play_state = 0 # Not playing a file
QS.set_record_state(3)
btn = event.GetEventObject()
if btn.GetValue():
QS.set_PTT(1)
else:
QS.set_PTT(0)
Hardware.OnButtonPTT(event)
def SetPTT(self, value):
self.pttButton.SetValue(value, False)
if value:
QS.set_PTT(1)
else:
QS.set_PTT(0)
event = wx.PyEvent()
event.SetEventObject(self.pttButton)
Hardware.OnButtonPTT(event)
def OnTxAudioClip(self, event):
v = event.GetEventObject().GetValue()
if self.mode in ('USB', 'LSB'):
self.txAudioClipUsb = v
elif self.mode == 'AM':
self.txAudioClipAm = v
elif self.mode == 'FM':
self.txAudioClipFm = v
elif self.mode in ('FDV-U', 'FDV-L'):
self.txAudioClipFdv = v
else:
return
QS.set_tx_audio(mic_clip=v)
def OnTxAudioPreemph(self, event):
v = event.GetEventObject().GetValue()
if self.mode in ('USB', 'LSB'):
self.txAudioPreemphUsb = v
elif self.mode == 'AM':
self.txAudioPreemphAm = v
elif self.mode == 'FM':
self.txAudioPreemphFm = v
elif self.mode in ('FDV-U', 'FDV-L'):
self.txAudioPreemphFdv = v
else:
return
QS.set_tx_audio(mic_preemphasis = v * 0.01)
def SetTxAudio(self):
if self.mode[0:3] in ('CWL', 'CWU', 'FDV', 'DGT'):
self.CtrlTxAudioClip.slider.Enable(False)
self.CtrlTxAudioPreemph.slider.Enable(False)
else:
self.CtrlTxAudioClip.slider.Enable(True)
self.CtrlTxAudioPreemph.slider.Enable(True)
if self.mode in ('USB', 'LSB'):
clp = self.txAudioClipUsb
pre = self.txAudioPreemphUsb
elif self.mode == 'AM':
clp = self.txAudioClipAm
pre = self.txAudioPreemphAm
elif self.mode == 'FM':
clp = self.txAudioClipFm
pre = self.txAudioPreemphFm
else:
clp = 0
pre = 0
QS.set_tx_audio(mic_clip=clp, mic_preemphasis=pre * 0.01)
self.CtrlTxAudioClip.SetValue(clp)
self.CtrlTxAudioPreemph.SetValue(pre)
def OnBtnMute(self, event):
btn = event.GetEventObject()
if btn.GetValue():
QS.set_volume(0)
else:
QS.set_volume(self.audio_volume)
def OnMultirxPlayBoth(self, event):
QS.set_multirx_play_method(0)
def OnMultirxPlayLeft(self, event):
QS.set_multirx_play_method(1)
def OnMultirxPlayRight(self, event):
QS.set_multirx_play_method(2)
def OnBtnDecimation(self, event=None, rate=None):
if event:
i = event.GetSelection()
rate = Hardware.VarDecimSet(i)
self.vardecim_set = rate
if rate != self.sample_rate:
self.sample_rate = rate
self.multi_rx_screen.ChangeSampleRate(rate)
QS.change_rate(rate, 1)
#print ('FFT size %d, FFT mult %d, average_count %d, rate %d, Refresh %.2f Hz' % (
# self.fft_size, self.fft_size / self.data_width, average_count, rate,
# float(rate) / self.fft_size / 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()
self.volumeAudio = value
# Simulate log taper pot
B = 50.0 # This controls the gain at mid-volume
x = (B ** (value/1000.0) - 1.0) / (B - 1.0) # x is 0.0 to 1.0
#print ("Vol %3d %10.6f" % (value, x))
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()
self.sidetone_volume = value
# Simulate log taper pot
B = 50.0 # This controls the gain at mid-volume
x = (B ** (value/1000.0) - 1.0) / (B - 1.0) # x is 0.0 to 1.0
self.sidetone_0to1 = x
QS.set_sidetone(value, x, self.ritFreq, conf.keyupDelay)
if hasattr(Hardware, 'ChangeSidetone'):
Hardware.ChangeSidetone(x)
def OnRitScale(self, event=None): # Called when the RIT slider is moved
# Caution: event can be None
value = self.ritScale.GetValue()
self.ritButton.SetLabel("RIT %d" % value)
self.ritButton.Refresh()
if self.ritButton.GetValue():
value = int(value)
self.ritFreq = value
self.graph.ritFreq = value
self.waterfall.pane1.ritFreq = value
self.waterfall.pane2.ritFreq = value
QS.set_tune(self.rxFreq + self.ritFreq, self.txFreq)
QS.set_sidetone(self.sidetone_volume, self.sidetone_0to1, 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:
if self.split_offset == 0:
if self.mode in ("CWL", "CWU"):
self.split_offset = 1000
else:
self.split_offset = 3000
QS.set_split_rxtx(self.split_rxtx_play)
self.txFreq = self.rxFreq + self.split_offset
self.ChangeHwFrequency(self.txFreq, self.VFO, 'OnSplit', event=event, rx_freq=self.rxFreq)
else:
QS.set_split_rxtx(0)
self.split_offset = self.txFreq - self.rxFreq
self.txFreq = self.rxFreq
self.ChangeHwFrequency(self.txFreq, self.VFO, 'OnSplit', event=event, rx_freq=self.rxFreq)
self.screen.SetTxFreq(self.txFreq, self.rxFreq)
def OnMenuSplitPlay1(self, event):
self.split_rxtx_play = 1
if self.split_rxtx:
QS.set_split_rxtx(1)
def OnMenuSplitPlay2(self, event):
self.split_rxtx_play = 2
if self.split_rxtx:
QS.set_split_rxtx(2)
def OnMenuSplitPlay3(self, event):
self.split_rxtx_play = 3
if self.split_rxtx:
QS.set_split_rxtx(3)
def OnMenuSplitPlay4(self, event):
self.split_rxtx_play = 4
if self.split_rxtx:
QS.set_split_rxtx(4)
def OnMenuSplitLock(self, event):
menu = self.split_menu
if menu.IsItemChecked('Lock Tx, Split Rx'):
self.split_locktx = True
self.split_lockrx = False
self.splitButton.SetLabel("SplitRx")
elif menu.IsItemChecked('Lock Rx, Split Tx'):
self.split_locktx = False
self.split_lockrx = True
self.splitButton.SetLabel("SplitTx")
else:
self.split_locktx = False
self.split_lockrx = False
self.splitButton.SetLabel("Split")
self.splitButton.Refresh()
def OnMenuSplitRev(self, event): # Called when the Split Reverse button is pressed
if self.split_rxtx:
self.ChangeHwFrequency(self.rxFreq, self.VFO, 'FreqEntry', rx_freq=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
self.graph.ritFreq = self.ritFreq
self.waterfall.pane1.ritFreq = self.ritFreq
self.waterfall.pane2.ritFreq = self.ritFreq
QS.set_tune(self.rxFreq + self.ritFreq, self.txFreq)
QS.set_sidetone(self.sidetone_volume, self.sidetone_0to1, self.ritFreq, conf.keyupDelay)
def SetRit(self, freq):
if freq:
self.ritButton.SetValue(1)
else:
self.ritButton.SetValue(0)
self.ritScale.SetValue(freq)
self.ritButton.SetLabel("RIT %d" % freq)
self.ritButton.Refresh()
self.OnBtnRit()
def OnBtnFDX(self, event):
btn = event.GetEventObject()
if btn.GetValue():
QS.set_fdx(1)
if hasattr(Hardware, 'OnBtnFDX'):
Hardware.OnBtnFDX(1)
else:
QS.set_fdx(0)
if hasattr(Hardware, 'OnBtnFDX'):
Hardware.OnBtnFDX(0)
def OnImdSlider(self, event):
value = event.GetEventObject().slider_value
QS.set_imd_level(value)
def OnBtnSpot(self, event):
btn = event.GetEventObject()
self.levelSpot = btn.slider_value
if btn.GetValue():
value = btn.slider_value
else:
value = -1
QS.set_spot_level(value)
Hardware.OnSpot(value)
if conf.spot_button_keys_tx:
Hardware.OnButtonPTT(event)
if btn.GetValue():
QS.set_key_down(1)
else:
QS.set_key_down(0)
def OnBtnTmpRecord(self, event):
btn = event.GetEventObject()
if btn.GetValue():
self.btnTmpPlay.Enable(0)
QS.set_record_state(0)
else:
self.btnTmpPlay.Enable(1)
QS.set_record_state(1)
def OnBtnTmpPlay(self, event):
btn = event.GetEventObject()
if btn.direction == 1: # left click
if btn.GetValue():
if QS.is_key_down() and conf.mic_sample_rate != conf.playback_rate:
self.btnTmpPlay.SetValue(False, False)
else:
self.btnTmpRecord.Enable(0)
QS.set_record_state(2)
self.tmp_playing = True
else:
self.btnTmpRecord.Enable(1)
QS.set_record_state(3)
self.tmp_playing = False
else: # right click
btn.SetValue(False)
dr, fn = os.path.split(self.file_name_rec_tmp)
dlg = wx.FileDialog(self.main_frame, 'Choose WAV file', dr, fn, style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT, wildcard="Wave files (*.wav)|*.wav")
if dlg.ShowModal() == wx.ID_OK:
path = dlg.GetPath()
if path[-4:].lower() != '.wav':
path = path + '.wav'
self.file_name_rec_tmp = path
QS.tmp_record_save(path)
dlg.Destroy()
def OnBtnFilePlay(self, event):
btn = event.GetEventObject()
enable = btn.GetValue()
self.config_screen.config.OnFilePlayButton(enable)
if enable:
self.file_play_state = 1 # Start playing a file
if self.file_play_source == 10: # Play speaker audio file
QS.set_record_state(5)
elif self.file_play_source == 11: # Play sample file
QS.set_record_state(6)
elif self.file_play_source == 12: # Play CQ file
QS.set_record_state(5)
self.SetPTT(True)
else:
self.file_play_state = 0 # Not playing a file
QS.set_record_state(3)
if self.file_play_source == 12: # Play CQ file
self.SetPTT(False)
def TurnOffFilePlay(self):
self.btnFilePlay.SetValue(False, False)
self.file_play_state = 0 # Not playing a file
QS.set_record_state(3)
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 OnBtnColorDialog(self, event):
btn = event.GetEventObject()
dlg = wx.ColourDialog(self.main_frame)
dlg.GetColourData().SetChooseFull(True)
if dlg.ShowModal() == wx.ID_OK:
data = dlg.GetColourData()
print (data.GetColour().Get(False))
btn.text_color = data.GetColour().Get(False)
btn.Refresh()
dlg.Destroy()
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]))
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()
#btn.SetBackgroundColour(color)
btn.text_color = color
btn.Refresh()
def OnBtnAGC(self, event):
btn = self.BtnAGC
self.levelOffAGC = btn.slider_value_off
self.levelAGC = btn.slider_value_on
value = btn.GetValue()
if value:
level = self.levelAGC
else:
level = self.levelOffAGC
# Simulate log taper pot. Volume is 0 to 1.
x = (10.0 ** (float(level) * 0.003000434077) - 0.99999) / 1000.0
QS.set_agc(x * conf.agc_max_gain)
def OnBtnSquelch(self, event=None):
btn = self.BtnSquelch
value = btn.GetValue()
if self.mode == 'FM':
self.levelSquelch = btn.slider_value
if value:
QS.set_squelch(self.levelSquelch / 12.0 - 120.0)
else:
QS.set_squelch(-999.0)
else:
self.levelSquelchSSB = btn.slider_value
if value:
QS.set_ssb_squelch(1, self.levelSquelchSSB)
else:
QS.set_ssb_squelch(0, self.levelSquelchSSB)
def OnBtnAutoNotch(self, event):
if event.GetEventObject().GetValue():
QS.set_auto_notch(1)
else:
QS.set_auto_notch(0)
def OnBtnNR2(self, event):
btn = event.GetEventObject()
if btn.index == 1:
self.wdsp.put(self.wdsp.SetRXAEMNRgainMethod, self.wdsp_channel, 2)
self.wdsp.put(self.wdsp.SetRXAEMNRRun, self.wdsp_channel, 1)
self.wdsp_NR2 = 1
elif btn.index == 2:
self.wdsp.put(self.wdsp.SetRXAEMNRgainMethod, self.wdsp_channel, 3)
self.wdsp.put(self.wdsp.SetRXAEMNRRun, self.wdsp_channel, 1)
self.wdsp_NR2 = 1
else:
self.wdsp.put(self.wdsp.SetRXAEMNRRun, self.wdsp_channel, 0)
self.wdsp_NR2 = 0
QS.wdsp_set_parameter(self.wdsp_channel, in_use=self.wdsp_NR2 + self.wdsp_SNB)
def OnMenuNB(self, event):
# menu is ("NB 1", "NB 2", "NB 3", "SNB")
for item in self.NB_menu.GetMenuItems():
if item.IsChecked():
text = item.GetItemLabel()
self.btnNB.SetLabel(text)
self.btnNB.Refresh()
break
self.OnBtnNB(None)
def OnBtnNB(self, event):
text = self.btnNB.GetLabel()
if self.btnNB.GetValue(): # Noise blanker On
if text == "SNB":
QS.set_noise_blanker(0)
self.wdsp.put(self.wdsp.SetRXASNBARun, self.wdsp_channel, 1)
self.wdsp_SNB = 1
else:
self.wdsp.put(self.wdsp.SetRXASNBARun, self.wdsp_channel, 0)
self.wdsp_SNB = 0
index = int(text[-1])
QS.set_noise_blanker(index)
else: # Noise blanker Off
QS.set_noise_blanker(0)
self.wdsp.put(self.wdsp.SetRXASNBARun, self.wdsp_channel, 0)
self.wdsp_SNB = 0
QS.wdsp_set_parameter(self.wdsp_channel, in_use=self.wdsp_NR2 + self.wdsp_SNB)
def FreqEntry(self, event):
freq = event.GetString()
win = event.GetEventObject()
win.Clear()
if not freq:
return
try:
freq = str2freq (freq)
except ValueError:
pass
else:
tune = freq % 10000
vfo = freq - tune
self.BandFromFreq(freq)
self.ChangeHwFrequency(tune, vfo, 'FreqEntry')
def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None, rx_freq=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).
rx_freq: the new changed value of App.rxFreq, the receive frequency in +- sample_rate/2
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.
"""
#print ("ChangeHW", source, tune, vfo, vfo + tune)
if not self.split_rxtx:
self.rxFreq = tune # rxFreq must be correct before the call to Hardware.ChangeFrequency()
elif rx_freq is not None:
self.rxFreq = rx_freq
rx_offset = self.rxFreq - tune
if self.screen == self.bandscope_screen:
freq = vfo + tune
tune = freq % 10000
vfo = freq - tune
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event)
self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None)
elif conf.fixed_tune_offset:
# Tune with the VFO and keep a constant audio offset from the center. Tx tune is always conf.fixed_tune_offset.
tune_freq = vfo + tune
self.rxFreq = conf.fixed_tune_offset + rx_offset
if source == "BtnUpDown":
tune_freq = vfo + conf.fixed_tune_offset
else:
vfo = tune_freq - conf.fixed_tune_offset
tune_freq, vfo = Hardware.ChangeFrequency(tune_freq, vfo, source, band, event)
self.ChangeDisplayFrequency(tune_freq - vfo, vfo, True)
else:
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event)
self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None)
def ChangeDisplayFrequency(self, tune, vfo, new_rxfreq=False):
'Change the frequency displayed by Quisk'
change = 0
if tune != self.txFreq or new_rxfreq:
change = 1
self.txFreq = tune
if not self.split_rxtx:
self.rxFreq = self.txFreq
if self.screen == self.bandscope_screen:
self.screen.SetFrequency(tune + vfo)
else:
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)
self.station_screen.Refresh()
if change:
if (conf.name_of_sound_capt or conf.name_of_mic_play) and QS.get_params("softrock_correct_active") == 0:
ampl, phase = self.GetAmplPhase('rx')
QS.set_ampl_phase(ampl, phase, 0)
ampl, phase = self.GetAmplPhase('tx')
QS.set_ampl_phase(ampl, phase, 1)
self.freqDisplay.Display(self.txFreq + self.VFO)
self.fldigi_new_freq = self.txFreq + self.VFO
return change
def ChangeRxTxFrequency(self, rx_freq=None, tx_freq=None, keep_mode=False):
if not self.split_rxtx and not tx_freq:
tx_freq = rx_freq
if tx_freq:
tune = tx_freq - self.VFO
d = self.sample_rate * 45 // 100
if -d <= tune <= d: # Frequency is on-screen
vfo = self.VFO
else: # Change the VFO
vfo = (tx_freq // 5000) * 5000 - 5000
tune = tx_freq - vfo
self.BandFromFreq(tx_freq, keep_mode)
self.ChangeHwFrequency(tune, vfo, 'FreqEntry')
if rx_freq and self.split_rxtx: # Frequency must be on-screen
tune = rx_freq - self.VFO
self.ChangeHwFrequency(self.txFreq, self.VFO, 'FreqEntry', rx_freq=tune)
def OnBtnMode(self, event):
mode = self.modeButns.GetLabel()
delta = 0 # Change frequency so switch between CW and SSB keeps tone constant
if self.mode == 'USB':
if mode in ('CWL', 'CWU'):
delta = 1
elif self.mode == 'LSB':
if mode in ('CWL', 'CWU'):
delta = -1
elif self.mode in ('CWL', 'CWU'):
if mode == 'USB':
delta = -1
elif mode == 'LSB':
delta = 1
if delta:
delta *= conf.cwTone
self.ChangeRxTxFrequency(self.rxFreq + delta + self.VFO, self.txFreq + delta + self.VFO)
Hardware.ChangeMode(mode)
self.mode = mode
self.MakeFilterButtons(self.Mode2Filters(mode))
QS.set_rx_mode(Mode2Index.get(mode, 3))
if mode == 'CWL':
self.SetRit(conf.cwTone)
elif mode == 'CWU':
self.SetRit(-conf.cwTone)
else:
self.SetRit(0)
if mode in ('CWL', 'CWU'):
self.SetFilterByMode('CW')
elif mode in ('LSB', 'USB'):
self.SetFilterByMode('SSB')
elif mode == 'AM':
self.SetFilterByMode('AM')
elif mode == 'FM':
self.SetFilterByMode('FM')
elif mode[0:4] == 'DGT-':
self.SetFilterByMode('DGT')
elif mode[0:4] == 'FDV-':
self.SetFilterByMode('FDV')
elif mode == 'IMD':
self.SetFilterByMode('IMD')
elif mode == conf.add_extern_demod:
self.SetFilterByMode(conf.add_extern_demod)
self.sliderSquelch.DeleteSliderWindow()
if mode == 'FM':
self.sliderSquelch.SetSlider(self.levelSquelch)
else:
self.sliderSquelch.SetSlider(self.levelSquelchSSB)
self.OnBtnSquelch()
if mode not in ('FDV-L', 'FDV-U'):
self.graph.SetDisplayMsg()
self.waterfall.SetDisplayMsg()
self.SetTxAudio()
def MakeMemPopMenu(self):
self.memory_menu.Destroy()
self.memory_menu = wx.Menu()
for data in self.memoryState:
txt = FreqFormatter(data[0])
item = self.memory_menu.Append(-1, txt)
self.Bind(wx.EVT_MENU, self.OnPopupMemNext, item)
def OnPopupMemNext(self, event):
frq = self.memory_menu.GetLabel(event.GetId())
frq = frq.replace(' ','')
frq = int(frq)
for freq, band, vfo, txfreq, mode in self.memoryState:
if freq == frq:
break
else:
return
if band == self.lastBand: # leave band unchanged
self.modeButns.SetLabel(mode, True)
self.ChangeHwFrequency(txfreq, vfo, 'FreqEntry')
else: # change to new band
self.bandState[band] = (vfo, txfreq, mode)
self.bandBtnGroup.SetLabel(band, do_cmd=True)
def OnBtnMemSave(self, event):
frq = self.VFO + self.txFreq
for i in range(len(self.memoryState)):
data = self.memoryState[i]
if data[0] == frq:
self.memoryState[i] = (self.VFO + self.txFreq, self.lastBand, self.VFO, self.txFreq, self.mode)
return
self.memoryState.append((self.VFO + self.txFreq, self.lastBand, self.VFO, self.txFreq, self.mode))
self.memoryState.sort()
self.memNextButton.Enable(True)
self.memDeleteButton.Enable(True)
self.MakeMemPopMenu()
self.station_screen.Refresh()
def OnBtnMemNext(self, event):
frq = self.VFO + self.txFreq
for freq, band, vfo, txfreq, mode in self.memoryState:
if freq > frq:
break
else:
freq, band, vfo, txfreq, mode = self.memoryState[0]
if band == self.lastBand: # leave band unchanged
self.modeButns.SetLabel(mode, True)
self.ChangeHwFrequency(txfreq, vfo, 'FreqEntry')
else: # change to new band
self.bandState[band] = (vfo, txfreq, mode)
self.bandBtnGroup.SetLabel(band, do_cmd=True)
def OnBtnMemDelete(self, event):
frq = self.VFO + self.txFreq
for i in range(len(self.memoryState)):
data = self.memoryState[i]
if data[0] == frq:
del self.memoryState[i]
break
self.memNextButton.Enable(bool(self.memoryState))
self.memDeleteButton.Enable(bool(self.memoryState))
self.MakeMemPopMenu()
self.station_screen.Refresh()
def OnRightClickMemory(self, event):
event.Skip()
pos = event.GetPosition()
self.memNextButton.PopupMenu(self.memory_menu, pos)
def OnBtnFavoritesShow(self, event):
self.screenBtnGroup.SetLabel("Config", do_cmd=False)
self.screen.Hide()
self.config_screen.FinishPages()
self.screen = self.config_screen
self.config_screen.notebook.SetSelection(2)
self.screen.Show()
self.vertBox.Layout() # This destroys the initialized sash position!
def OnBtnFavoritesNew(self, event):
self.config_screen.favorites.AddNewFavorite()
self.OnBtnFavoritesShow(event)
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 = (1000000, 0, 'LSB')
if band == '60':
if self.mode in ('CWL', 'CWU'):
freq60 = []
for f in conf.freq60:
freq60.append(f + 1500)
else:
freq60 = conf.freq60
freq = vfo + tune
if btn.direction:
vfo = self.VFO
if 5100000 < vfo < 5600000:
if btn.direction > 0: # Move up
for f in freq60:
if f > vfo + self.txFreq:
freq = f
break
else:
freq = freq60[0]
else: # move down
l = list(freq60)
l.reverse()
for f in l:
if f < vfo + self.txFreq:
freq = f
break
else:
freq = 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.modeButns.SetLabel(mode, True)
self.txFreq = self.VFO = -1 # demand change
self.ChangeBand(band)
self.ChangeHwFrequency(tune, vfo, 'BtnBand', band=band)
if band in ('Time', 'Audio') or conf.tx_level.get(band, 127) == 0:
self.pttButton.Enable(False)
else:
self.pttButton.Enable(True)
def BandFromFreq(self, frequency, keep_mode=False): # Change to a new band based on the frequency
# If keep_mode is True, do not change to the mode saved for the new band.
if self.screen == self.bandscope_screen:
return
try:
f1, f2 = conf.BandEdge[self.lastBand]
if f1 <= frequency <= f2:
return # We are within the current band
except KeyError:
f1 = f2 = -1
# Frequency is not within the current band. Save the current band data.
if f1 <= self.VFO + self.txFreq <= f2:
self.bandState[self.lastBand] = (self.VFO, self.txFreq, self.mode)
# Change to the correct band based on frequency.
for band in conf.BandEdge:
f1, f2 = conf.BandEdge[band]
if f1 <= frequency <= f2:
self.lastBand = band
self.bandBtnGroup.SetLabel(band, do_cmd=False)
try:
vfo, tune, mode = self.bandState[band]
except KeyError:
vfo, tune, mode = (0, 0, 'LSB')
if not keep_mode:
self.modeButns.SetLabel(mode, True)
self.ChangeBand(band)
break
def ChangeBand(self, band):
if self.remote_control_head:
self.Hardware.RemoteCtlSend(f'JsonAppFunc;{json.dumps(("ChangeBand", band))}\n')
Hardware.ChangeBand(band)
s, z = self.graphScaleZ.get(band, (conf.graph_y_scale, conf.graph_y_zero))
self.waterfall.SetPane1(self.wfallGrScaleZ.get(band, (s, z)))
self.waterfall.SetPane2(self.wfallScaleZ.get(band, (conf.waterfall_y_scale, conf.waterfall_y_zero)))
self.graph.ChangeYscale(s)
self.graph.ChangeYzero(z)
if self.screen == self.multi_rx_screen and self.multi_rx_screen.rx_zero in (self.waterfall, self.graph):
self.sliderYs.SetValue(self.screen.y_scale)
self.sliderYz.SetValue(self.screen.y_zero)
def OnBtnUpDnBandDelta(self, event, is_band_down):
sample_rate = int(self.sample_rate * self.zoom)
oldvfo = self.VFO
btn = event.GetEventObject()
if btn.direction > 0: # left button was used, move a bit
d = int(sample_rate // 9)
else: # right button was used, move to edge
d = int(sample_rate * 45 // 100)
if is_band_down:
d = -d
vfo = self.VFO + d
if sample_rate > 40000:
vfo = (vfo + 5000) // 10000 * 10000 # round to even number
delta = 10000
elif sample_rate > 5000:
vfo = (vfo + 500) // 1000 * 1000
delta = 1000
else:
vfo = (vfo + 50) // 100 * 100
delta = 100
if oldvfo == vfo:
if is_band_down:
d = -delta
else:
d = delta
else:
d = vfo - oldvfo
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.station_screen.Refresh()
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 = -1 # Force an update
self.ChangeHwFrequency(tune, vfo, 'BtnUpDown')
def SearchFreqAmPh(self, freq, data, index):
# If index is -1, search data for the VFO frequency.
# Otherwise, search in the frequencies for the VFO at index.
# The list must be sorted. Search for a bracket lst[i] <= freq <= lst[i + 1]
# Return 0, 1 for freq <= lst[0]; return len(lst) - 2, len(lst) - 1 for freq >= lst[-1]
# Also return the length.
if index < 0: # VFO frequency is at data[i][0]
items = data
else: # The Rx/Tx frequency for the VFO at index is data[index][1][i][0]
items = data[index][1]
length = len(items)
if length == 0:
return 0, 0, 0
if length == 1:
return 1, 0, 0
f1 = items[0][0]
if freq <= f1: # before first frequency
return length, 0, 1
for i in range(1, length):
f1 = items[i - 1][0]
f2 = items[i][0]
if f1 <= freq <= f2:
return length, i - 1, i
return length, length - 2, length - 1 # after last frequency
def GetAmplPhase(self, rx_tx):
band = self.lastBand
data = self.bandAmplPhase.get(band, {})
data = data.get(rx_tx, [])
if not data: # No corrections for this band
return 0.0, 0.0
if rx_tx == 'rx':
Freq0 = self.rxFreq
else:
Freq0 = self.txFreq
nVFO, iVfo1, iVfo2 = self.SearchFreqAmPh(self.VFO, data, -1)
#print ('iVfo1', iVfo1, 'iVfo2', iVfo2)
nFreqV1, iV1F1, iV1F2 = self.SearchFreqAmPh(Freq0, data, iVfo1)
#print (' V1', iV1F1, iV1F2, data[iVfo1][1][iV1F1])
if iVfo1 == iVfo2:
nFreqV2 = nFreqV1
iV2F1 = iV1F1
iV2F2 = iV1F2
else:
nFreqV2, iV2F1, iV2F2 = self.SearchFreqAmPh(Freq0, data, iVfo2)
#print (' V2', iV2F1, iV2F2, data[iVfo2][1][iV2F1])
#print ("nVFO", nVFO, "nFreq", nFreqV1, nFreqV2)
try: # guard against divide by zero
if nVFO == 1:
if nFreqV1 == 1: # duplicate old logic
freq, ampl, phase = data[iVfo1][1][0]
#print ("new %10.6f %10.6f" % (ampl, phase))
else: # linear interpolation/extrapolation of Freq0
freq1, ampl1, phase1 = data[iVfo1][1][iV1F1]
freq2, ampl2, phase2 = data[iVfo1][1][iV1F2]
t = (Freq0 - freq1) / (freq2 - freq1) # linear interpolation
ampl = (1.0 - t) * ampl1 + t * ampl2
phase = (1.0 - t) * phase1 + t * phase2
#print ("new %10.6f %10.6f" % (ampl, phase))
elif nFreqV1 == 1 and nFreqV2 == 1: # duplicate old logic
fVfo1 = data[iVfo1][0] # linear interpolation/extrapolation of self.VFO
fVfo2 = data[iVfo2][0]
freq1, ampl1, phase1 = data[iVfo1][1][0]
freq2, ampl2, phase2 = data[iVfo2][1][0]
t = (self.VFO - fVfo1) / (fVfo2 - fVfo1)
ampl = (1.0 - t) * ampl1 + t * ampl2
phase = (1.0 - t) * phase1 + t * phase2
#print ("new %10.6f %10.6f" % (ampl, phase))
else:
fVfo1 = data[iVfo1][0]
fVfo2 = data[iVfo2][0]
freq11, ampl11, phase11 = data[iVfo1][1][iV1F1]
freq12, ampl12, phase12 = data[iVfo1][1][iV1F2]
freq21, ampl21, phase21 = data[iVfo2][1][iV2F1]
freq22, ampl22, phase22 = data[iVfo2][1][iV2F2]
#print ("Freq0", Freq0)
#print ("Try Vfo 1 %d freq %d %d" % (fVfo1, freq11, freq12))
#print ("Try Vfo 2 %d freq %d %d" % (fVfo2, freq21, freq22))
if freq11 == freq21 and freq12 == freq22: # rectangular box: bilinear interpolation
#print ("Vfo %d %d %d freq %d %d %d" % (fVfo1, self.VFO, fVfo2, freq11, Freq0, freq12))
t = (Freq0 - freq11) / (freq12 - freq11)
u = (self.VFO - fVfo1) / (fVfo2 - fVfo1)
#print ("%10.4f %10.4f" % (t, u))
#print (ampl11, ampl12, ampl22, ampl21)
#print (phase11, phase12, phase22, phase21)
ampl = (1.0 - t) * (1.0 - u) * ampl11 + t * (1.0 - u) * ampl12 + t * u * ampl22 + (1.0 - t) * u * ampl21
phase = (1.0 - t) * (1.0 - u) * phase11 + t * (1.0 - u) * phase12 + t * u * phase22 + (1.0 - t) * u * phase21
#print ('Box', fVfo1, fVfo2, freq11, freq12)
#print ("new B %10.6f %10.6f" % (ampl, phase))
else: # linear interpolation/extrapolation of Freq0 at nearest VFO
if abs(self.VFO - fVfo1) <= abs(self.VFO - fVfo2):
nFreq = nFreqV1 # Use VFO 1
freq1, ampl1, phase1 = data[iVfo1][1][iV1F1]
#print ('Vfo1', fVfo1, freq1)
if nFreq > 1:
freq2, ampl2, phase2 = data[iVfo1][1][iV1F2]
else: # Use VFO 2
nFreq = nFreqV2
freq1, ampl1, phase1 = data[iVfo2][1][iV2F1]
#print ('Vfo2', fVfo2, freq1)
if nFreq > 1:
freq2, ampl2, phase2 = data[iVfo2][1][iV2F2]
if nFreq == 1:
ampl, phase = ampl1, phase1
#print ("new %10.6f %10.6f" % (ampl, phase))
else: # linear interpolation/extrapolation of Freq0
#print ('Lin', freq1, freq2)
t = (Freq0 - freq1) / (freq2 - freq1) # linear interpolation
ampl = (1.0 - t) * ampl1 + t * ampl2
phase = (1.0 - t) * phase1 + t * phase2
#print ("new %10.6f %10.6f" % (ampl, phase))
except:
traceback.print_exc()
return 0.0, 0.0
#print ("new %10.6f %10.6f" % (ampl, phase))
return ampl, phase
def PostStartup(self): # called once after sound attempts to start
self.config_screen.OnGraphData(None) # update config in case sound is not running
#txt = self.sound_thread.config_text # change config_text if StartSamples() returns a string
#if txt:
# self.config_text = txt
# self.main_frame.SetConfigText(txt)
def FldigiPoll(self): # Keep Quisk and Fldigi frequencies equal; control Fldigi PTT from Quisk
if self.fldigi_server is None:
return
if self.fldigi_new_freq: # Our frequency changed; send to fldigi
try:
self.fldigi_server.main.set_frequency(float(self.fldigi_new_freq))
except:
# traceback.print_exc()
pass
self.fldigi_new_freq = None
self.fldigi_timer = time.time()
return
try:
freq = self.fldigi_server.main.get_frequency()
except:
# traceback.print_exc()
return
else:
freq = int(freq + 0.5)
try:
rxtx = self.fldigi_server.main.get_trx_status() # returns rx, tx, tune
except:
return
if time.time() - self.fldigi_timer < 0.3: # If timer is small, change originated in Quisk
self.fldigi_rxtx = rxtx
self.fldigi_freq = freq
return
if self.fldigi_freq != freq:
self.fldigi_freq = freq
#print "Change freq", freq
self.ChangeRxTxFrequency(None, freq)
self.fldigi_new_freq = None
if self.fldigi_rxtx != rxtx:
self.fldigi_rxtx = rxtx
#print 'Fldigi changed to', rxtx
if rxtx == 'rx':
self.pttButton.SetValue(0, True)
else:
self.pttButton.SetValue(1, True)
self.fldigi_timer = time.time()
else:
if QS.is_key_down():
if rxtx == 'rx':
self.fldigi_server.main.tx()
self.fldigi_timer = time.time()
else: # key is up
if rxtx != 'rx':
self.fldigi_server.main.rx()
self.fldigi_timer = time.time()
def HamlibPoll(self): # Poll for Hamlib and K4 TCP commands
if self.hamlib_socket:
try: # Poll for new client connections.
conn, address = self.hamlib_socket.accept()
except socket.error:
pass
else:
# print ('Connection from', address)
self.hamlib_clients.append(HamlibHandlerRig2(self, conn, address))
for client in self.hamlib_clients: # Service existing clients
if not client.Process(): # False return indicates a closed connection; remove the handler for this client
self.hamlib_clients.remove(client)
# print ('Remove', client.address)
break
if self.k4_tcp_socket:
try: # Poll for new client connections.
conn, address = self.k4_tcp_socket.accept()
except socket.error:
pass
else:
# print ('Connection from', address)
self.k4_tcp_clients.append(ElecraftK4Handler(self, conf, conn, address))
for client in self.k4_tcp_clients: # Service existing clients
if not client.Process(): # False return indicates a closed connection; remove the handler for this client
self.k4_tcp_clients.remove(client)
# print ('Remove', client.address)
break
def OnKeyHook(self, event):
event.Skip()
if conf.hot_key_ptt_if_hidden: # Hot key PTT operates even if Quisk is hidden
return # Poll key with HotKeyPoll()
ptt1 = conf.hot_key_ptt1
if not ptt1:
return
if 97 <= ptt1 <= 122: # Lower case
ptt1 -= 32 # Convert to upper case
if event.GetKeyCode() == ptt1:
ptt2 = conf.hot_key_ptt2
hit = False
if ptt2 is None or ptt2 == wx.ACCEL_NORMAL:
hit = True
elif ptt2 == wx.ACCEL_SHIFT:
hit = event.ShiftDown()
elif ptt2 == wx.ACCEL_CTRL:
hit = event.ControlDown()
elif ptt2 == wx.ACCEL_ALT:
hit = event.AltDown()
elif ptt2 == wx.ACCEL_SHIFT | wx.ACCEL_CTRL:
hit = event.ShiftDown() and event.ControlDown()
else:
hit = True
if hit: # Repeated key press events will occur
self.hot_key_ptt_pressed = True
def HotKeyPoll(self): # This is only used if the hot key DOES work when Quisk is hidden.
if self.QuiskGetKeyState(conf.hot_key_ptt1):
ptt2 = conf.hot_key_ptt2
if ptt2 is None or ptt2 == wx.ACCEL_NORMAL:
self.hot_key_ptt_is_down = True
elif ptt2 == wx.ACCEL_SHIFT:
self.hot_key_ptt_is_down = wx.GetKeyState(wx.WXK_SHIFT)
elif ptt2 == wx.ACCEL_CTRL:
self.hot_key_ptt_is_down = wx.GetKeyState(wx.WXK_CONTROL)
elif ptt2 == wx.ACCEL_ALT:
self.hot_key_ptt_is_down = wx.GetKeyState(wx.WXK_ALT)
elif ptt2 == wx.ACCEL_SHIFT | wx.ACCEL_CTRL:
self.hot_key_ptt_is_down = wx.GetKeyState(wx.WXK_SHIFT) and wx.GetKeyState(wx.WXK_CONTROL)
else:
self.hot_key_ptt_is_down = True
else:
self.hot_key_ptt_is_down = False
if self.hot_key_ptt_is_down and not self.hot_key_ptt_was_down:
self.hot_key_ptt_pressed = True
def OnReadSound(self): # called at frequent intervals
#if sys.platform == 'win32':
# self.main_frame.Update()
if self.hamlib_com1_handler:
self.hamlib_com1_handler.Process()
if self.hamlib_com2_handler:
self.hamlib_com2_handler.Process()
if self.use_fast_heart_beat:
Hardware.FastHeartBeat()
if self.poll_gui_control:
Hardware.PollGuiControl()
if conf.do_repeater_offset:
hold = QS.tx_hold_state(-1)
if hold == 2: # Tx is being held for an FM repeater TX frequency shift
rdict = self.config_screen.favorites.RepeaterDict
freq = self.txFreq + self.VFO
freq = ((freq + 500) // 1000) * 1000
if freq in rdict:
offset, tone = rdict[freq]
QS.set_ctcss(tone)
Hardware.RepeaterOffset(offset)
for i in range(100):
time.sleep(0.010)
if Hardware.RepeaterOffset():
break
QS.tx_hold_state(3)
elif hold == 4: # No delay necessary on key up
Hardware.RepeaterOffset(0)
QS.set_ctcss(0)
QS.tx_hold_state(1)
if QS.is_key_down() and not self.tx_inhibit: # Tx indicator
if not self.tx_indicator:
self.tx_indicator = True
self.pttButton.Tx.TurnOn(True)
QS.tci_set_params(tci_trx=1)
else:
if self.tx_indicator:
self.tx_indicator = False
self.pttButton.Tx.TurnOn(False)
QS.tci_set_params(tci_trx=0)
if True: # Manage the PTT button using serial port, VOX, hot keys, WAV file play, PTT button, MIDI, CAT, TCI
ptt_button_down = self.pttButton.GetValue()
ptt = None
if conf.quisk_serial_cts[0:4] == "PTT " or conf.quisk_serial_dsr[0:4] == "PTT ":
old = self.serial_ptt_active
if self.file_play_state == 0:
if QS.get_params("serial_ptt"):
ptt = True
self.serial_ptt_active = True
elif self.serial_ptt_active:
ptt = False
self.serial_ptt_active = False
elif self.file_play_state == 2 and QS.get_params("serial_ptt"):
self.TurnOffFilePlay()
ptt = True
self.serial_ptt_active = True
if self.remote_control_head and old != self.serial_ptt_active:
Hardware.RemoteCtlSend('PTT;%d\n' % self.serial_ptt_active)
if self.useVOX:
if self.file_play_state == 0:
if QS.is_vox():
ptt = True
self.vox_ptt_active = True
elif self.vox_ptt_active:
ptt = False
self.vox_ptt_active = False
elif self.file_play_state == 2 and QS.is_vox(): # VOX tripped between file play repeats
self.TurnOffFilePlay()
ptt = True
self.vox_ptt_active = True
if self.file_play_state == 2 and QS.is_key_down(): # hardware key between file play repeats
if time.time() > self.file_play_timer - self.file_play_repeat + 0.25: # pause to allow key state to change
self.TurnOffFilePlay()
ptt = False
if conf.hot_key_ptt1 and self.screen != self.config_screen:
if conf.hot_key_ptt_if_hidden: # hot key PTT operates even if Quisk is hidden
self.HotKeyPoll()
if conf.hot_key_ptt_toggle:
if self.hot_key_ptt_pressed: # Key down event was received
ptt = not ptt_button_down
if ptt:
self.TurnOffFilePlay()
else:
if self.hot_key_ptt_pressed: # Multiple key down events are being received
ptt = True
self.hot_key_ptt_active = True
if not ptt_button_down:
self.TurnOffFilePlay()
elif self.hot_key_ptt_active and not self.QuiskGetKeyState(conf.hot_key_ptt1):
ptt = False
self.hot_key_ptt_active = False
self.hot_key_ptt_was_down = self.hot_key_ptt_is_down
self.hot_key_ptt_pressed = False
if self.tx_inhibit:
ptt = False
tci = QS.tci_get_params("tci_trx")
if tci is not None:
if tci:
self.tci_ptt_active = True
ptt = True
elif self.tci_ptt_active:
self.tci_ptt_active = False
ptt = False
if ptt is True and not ptt_button_down:
self.SetPTT(True)
elif ptt is False and ptt_button_down:
self.SetPTT(False)
if self.want_RxTx:
x = QS.is_key_down() # Must be 0 or 1
if self.old_RxTx != x:
self.old_RxTx = x
Hardware.OnChangeRxTx(x)
self.timer = time.time()
if self.bandscope_clock: # Hermes UDP protocol
data = QS.get_bandscope(self.bandscope_clock, self.bandscope_screen.zoom, float(self.bandscope_screen.zoom_deltaf))
if data and self.screen == self.bandscope_screen:
self.screen.OnGraphData(data)
if self.screen == self.scope:
# Get raw data, not FFT
data = QS.get_graph(0, 1.0, 0)
if data:
self.scope.OnGraphData(data) # Send message to draw new data
return 1 # we got new graph/scope data
elif self.screen == self.audio_fft_screen:
QS.get_graph(2, self.zoom, float(self.zoom_deltaf)) # discard data
audio_data = QS.get_audio_graph() # Display the audio FFT
if audio_data:
self.screen.OnGraphData(audio_data)
else:
data = QS.get_graph(1, self.zoom, float(self.zoom_deltaf)) # get FFT data
if data:
#T('')
if self.remote_control_slave:
Hardware.RemoteCtlSend("M;%s\n" % self.smeter.GetLabel())
if self.remote_control_head:
self.smeter.SetLabel(Hardware.GetSmeter())
elif self.screen == self.bandscope_screen:
d = QS.get_hermes_adc() # ADC level from bandscope, 0.0 to 1.0
if d < 1E-10:
d = 1E-10
self.smeter.SetLabel(" ADC %.0f%% %.0fdB" % (d * 100.0, 20 * math.log10(d)))
elif self.smeter_usage == "smeter": # update the S-meter
self.NewSmeter()
elif self.smeter_usage == "freq":
self.MeasureFrequency() # display measured frequency
else:
self.MeasureAudioVoltage() # display audio voltage
if self.screen == self.config_screen:
pass
elif self.screen == self.bandscope_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
data, index = QS.get_multirx_graph() # get FFT data for sub-receivers
if data:
self.multi_rx_screen.OnGraphData(data, index)
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 QS.tci_get_params("tci_clients_count") > 0:
# These are change requests from TCI clients. All must be echoed with the new value from Quisk.
# modulation
mode = QS.tci_get_params("tci_modulation")
if mode is not None: # TCI set new modulation
self.modeButns.SetLabel(mode, True)
self.tci_modulation = self.mode
QS.tci_set_params(tci_modulation=self.tci_modulation)
# split
split = QS.tci_get_params("tci_split_enable")
if split is not None: # TCI set split_enable
self.splitButton.SetValue(split, True)
self.tci_split_enable = self.split_rxtx
QS.tci_set_params(tci_split_enable=self.tci_split_enable)
# tci dds == self.VFO
tci_dds = QS.tci_get_params("tci_dds")
if tci_dds is not None:
self.ChangeHwFrequency(0, tci_dds, 'TCIrequest')
self.tci_dds = self.VFO
QS.tci_set_params(tci_dds=self.tci_dds)
# tci_vfo == self.VFO + self.rxFreq
tci_vfo = QS.tci_get_params("tci_vfo")
if tci_vfo is not None:
self.ChangeRxTxFrequency(tci_vfo, None)
self.tci_vfo = self.VFO + self.rxFreq
QS.tci_set_params(tci_vfo=self.tci_vfo)
# tci_trx handled elsewhere
if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks:
self.heart_time0 = self.timer
Hardware.HeartBeat()
if QS.tci_get_params("tci_clients_count") > 0: # limit the speed of frequency updates
# these are changes made in Quisk that must be broadcast to all clients.
# modulation
if self.tci_modulation != self.mode:
self.tci_modulation = self.mode
QS.tci_set_params(tci_modulation=self.tci_modulation)
# split
if self.tci_split_enable != self.split_rxtx:
self.tci_split_enable = self.split_rxtx
QS.tci_set_params(tci_split_enable=self.tci_split_enable)
# tci dds == self.VFO
if self.tci_dds != self.VFO:
self.tci_dds = self.VFO
QS.tci_set_params(tci_dds=self.tci_dds)
# tci_vfo == self.VFO + self.rxFreq
tci_vfo = self.VFO + self.rxFreq
if self.tci_vfo != tci_vfo:
self.tci_vfo = tci_vfo
QS.tci_set_params(tci_vfo=self.tci_vfo)
# tci_trx handled elsewhere
if self.is_HermesLite2:
self.tx_inhibit = QS.get_params('quisk_tx_inhibit')
else:
self.tx_inhibit = 0
if self.tx_inhibit != self.old_tx_inhibit:
self.old_tx_inhibit = self.tx_inhibit
self.graph.display.Refresh()
self.waterfall.pane1.display.Refresh()
if self.tx_inhibit and self.spotButton.GetValue():
self.spotButton.SetValue(0, True)
msg = QS.GetQuiskPrintf()
if msg:
print(msg, end='')
if self.screen == self.config_screen:
self.screen.OnGraphData() # Send message to draw new data
if self.add_version and Hardware.GetFirmwareVersion() is not None:
self.add_version = False
self.config_text = "%s, firmware version 1.%d" % (self.config_text, Hardware.GetFirmwareVersion())
self.main_frame.SetConfigText(self.config_text)
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.
tune, vfo = Hardware.ReturnFrequency()
if tune is not None and vfo is not None:
self.BandFromFreq(tune)
self.ChangeDisplayFrequency(tune - vfo, vfo)
self.FldigiPoll()
self.HamlibPoll()
if self.dxCluster:
if self.dxCluster.Poll():
self.station_screen.Refresh()
if self.timer - self.slowheart_time0 > 0.5:
self.slowheart_time0 = self.timer
if self.w_phase:
self.w_phase.SlowHeartBeat()
if self.timer - self.save_time0 > 20.0:
self.save_time0 = self.timer
if self.CheckState():
self.SaveState()
self.local_conf.SaveState()
if self.tmp_playing and QS.set_record_state(-1): # poll to see if playback is finished
self.btnTmpPlay.SetValue(False, True)
if self.file_play_state == 0:
pass
elif self.file_play_state == 1:
if QS.set_record_state(-1): # poll to see if playback is finished
if self.file_play_source == 12 and self.file_play_repeat: # repeat the CW message
self.file_play_state = 2 # Waiting for the timer to expire, and start another playback
self.file_play_timer = self.timer + self.file_play_repeat
self.SetPTT(False)
else:
self.btnFilePlay.SetValue(False, True)
elif self.file_play_state == 2:
if self.timer >= self.file_play_timer:
QS.set_record_state(5) # Start another playback
self.file_play_state = 1
self.SetPTT(True)
def OnReadMIDI(self, byts):
for byt in byts:
if byt & 0x80: # this is a status byte and the start of a new message
self.midi_message = [byt]
else:
self.midi_message.append(byt)
if len(self.midi_message) == 3:
if self.config_midi_window:
self.config_midi_window.OnNewMidiNote(self.midi_message)
if self.midi_handler is None:
if hasattr(conf, 'MidiHandler'):
self.midi_handler = conf.MidiHandler(self, conf)
else:
import midi_handler
self.midi_handler = midi_handler.MidiHandler(self, conf)
self.midi_handler.OnReadMIDI(byts)
def main():
"""If quisk is installed as a package, you can run it with quisk.main()."""
App()
application.MainLoop()
if __name__ == '__main__':
main()