6007 lines
226 KiB
Python
Executable File
6007 lines
226 KiB
Python
Executable File
#! /usr/bin/python
|
|
|
|
# All QUISK software is Copyright (C) 2006-2018 by James C. Ahlstrom.
|
|
# This free software is licensed for use under the GNU General Public
|
|
# License (GPL), see http://www.opensource.org.
|
|
# Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!!
|
|
|
|
"""The main program for Quisk, 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
|
|
os.chdir(os.path.normpath(os.path.dirname(__file__))) # change directory to the location of this script
|
|
if sys.path[0] != '': # Make sure the current working directory is on path
|
|
sys.path.insert(0, '')
|
|
|
|
import wx, wx.html, wx.lib.stattext, wx.lib.colourdb, wx.grid, wx.richtext
|
|
import math, cmath, time, traceback, string, select, subprocess
|
|
import threading, pickle, webbrowser
|
|
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
|
|
|
|
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)
|
|
|
|
# Command line parsing: be able to specify the config file.
|
|
from optparse import OptionParser
|
|
parser = OptionParser()
|
|
parser.add_option('-c', '--config', dest='config_file_path',
|
|
help='Specify the configuration file path')
|
|
parser.add_option('', '--config2', dest='config_file_path2', default='',
|
|
help='Specify a second configuration file to read after the first')
|
|
parser.add_option('-a', '--ask', action="store_true", dest='AskMe', default=False,
|
|
help='Ask which radio to use when starting')
|
|
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 prefered 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.
|
|
Mo2CoKen = {'CWL':7, 'CWU':3, 'LSB':1, 'USB':2, 'AM':5, 'FM':4, 'DGT-U':9, 'DGT-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, 'DGT-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'}
|
|
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
|
|
if sys.platform == 'win32':
|
|
try:
|
|
import serial
|
|
except:
|
|
print ("Please install the pyserial module.")
|
|
else:
|
|
try:
|
|
self.port = serial.Serial(public_name, timeout=0, write_timeout=0)
|
|
except:
|
|
print ("The serial port %s could not be opened." % public_name)
|
|
else:
|
|
import tty
|
|
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 sys.platform != 'win32':
|
|
if self.public_name:
|
|
try:
|
|
os.remove(self.public_name)
|
|
except:
|
|
pass
|
|
def Read(self):
|
|
if self.port is None:
|
|
return
|
|
if sys.platform == 'win32':
|
|
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 sys.platform == 'win32':
|
|
self.port.write(data)
|
|
else:
|
|
r, w, x = select.select((), (self.port,), (), 0)
|
|
if w:
|
|
os.write(self.port, 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:
|
|
self.Write("%s050;" % cmd)
|
|
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)
|
|
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')
|
|
if HAMLIB_DEBUG: print ("New Freq rx,tx", self.app.txFreq + self.app.VFO, self.app.rxFreq + self.app.VFO)
|
|
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
|
|
info += '0000'
|
|
if ritFreq < 0: # RIT freq
|
|
info += "-%05d" % -ritFreq
|
|
else:
|
|
info += "+%05d" % 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.OnBtnMode(None, mode) # 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.OnBtnMode(None, mode) # 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:
|
|
if self.app.pttButton:
|
|
self.app.pttButton.SetValue(0, True)
|
|
else:
|
|
self.Error(cmd, data)
|
|
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:
|
|
if self.app.pttButton:
|
|
self.app.pttButton.SetValue(1, True)
|
|
else:
|
|
self.Error(cmd, data)
|
|
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 self.app.pttButton:
|
|
if data[0] == '0':
|
|
self.app.pttButton.SetValue(0, True)
|
|
else:
|
|
self.app.pttButton.SetValue(1, True)
|
|
else:
|
|
self.Error(cmd, data)
|
|
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:
|
|
"""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',
|
|
'm':'mode',
|
|
's':'split_vfo',
|
|
't':'ptt',
|
|
'v':'vfo',
|
|
}
|
|
# I don't understand the need for dump_state, nor what it means.
|
|
# A possible response to the "dump_state" request:
|
|
dump1 = """ 2
|
|
2
|
|
2
|
|
150000.000000 1500000000.000000 0x1ff -1 -1 0x10000003 0x3
|
|
0 0 0 0 0 0 0
|
|
0 0 0 0 0 0 0
|
|
0x1ff 1
|
|
0x1ff 0
|
|
0 0
|
|
0x1e 2400
|
|
0x2 500
|
|
0x1 8000
|
|
0x1 2400
|
|
0x20 15000
|
|
0x20 8000
|
|
0x40 230000
|
|
0 0
|
|
9990
|
|
9990
|
|
10000
|
|
0
|
|
10
|
|
10 20 30
|
|
0x3effffff
|
|
0x3effffff
|
|
0x7fffffff
|
|
0x7fffffff
|
|
0x7fffffff
|
|
0x7fffffff
|
|
"""
|
|
|
|
# Another possible response to the "dump_state" request:
|
|
dump2 = """ 0
|
|
2
|
|
2
|
|
150000.000000 30000000.000000 0x900af -1 -1 0x10 000003 0x3
|
|
0 0 0 0 0 0 0
|
|
150000.000000 30000000.000000 0x900af -1 -1 0x10 000003 0x3
|
|
0 0 0 0 0 0 0
|
|
0 0
|
|
0 0
|
|
0
|
|
0
|
|
0
|
|
0
|
|
|
|
|
|
0x0
|
|
0x0
|
|
0x0
|
|
0x0
|
|
0x0
|
|
0
|
|
"""
|
|
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 = ''
|
|
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['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
|
|
def Send(self, text):
|
|
"""Send text back to the client."""
|
|
if isinstance(text, Q3StringTypes):
|
|
text = text.encode('utf-8', errors='ignore')
|
|
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
|
|
t = "%s: %s" % (self.cmd, self.params) # Extended format echoes the command and parameters
|
|
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]
|
|
# print 'Reply', t
|
|
self.Send(t)
|
|
def ErrParam(self): # Invalid parameter
|
|
self.Reply(-1)
|
|
def UnImplemented(self): # Command not implemented
|
|
self.Reply(-4)
|
|
def ErrProtocol(self): # Protocol error
|
|
self.Reply(-8)
|
|
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 '\n' in self.received: # A complete command ending with newline is available
|
|
cmd, self.received = self.received.split('\n', 1) # Split off the command, save any further characters
|
|
else:
|
|
return 1
|
|
cmd = cmd.strip() # Here is our command
|
|
# print 'Get', cmd
|
|
if not cmd: # ??? Indicates a closed connection?
|
|
# print 'empty command'
|
|
self.sock.close()
|
|
self.sock = None
|
|
return 0
|
|
# Parse the command and call the appropriate handler
|
|
if cmd[0] == '+': # rigctld Extended Response Protocol
|
|
self.extended = '\n'
|
|
cmd = cmd[1:].strip()
|
|
elif cmd[0] in ';|,': # rigctld Extended Response Protocol
|
|
self.extended = cmd[0]
|
|
cmd = cmd[1:].strip()
|
|
else:
|
|
self.extended = None
|
|
if cmd[0:1] == '\\': # long form command starting with backslash
|
|
args = cmd[1:].split(None, 1)
|
|
self.cmd = args[0]
|
|
if len(args) == 1:
|
|
self.params = ''
|
|
else:
|
|
self.params = args[1]
|
|
self.Handlers.get(self.cmd, self.UnImplemented)()
|
|
else: # single-letter command
|
|
self.params = cmd[1:].strip()
|
|
cmd = cmd[0:1]
|
|
if cmd in 'Qq': # Quit command
|
|
return 0
|
|
try:
|
|
t = self.SingleLetters[cmd.lower()]
|
|
except KeyError:
|
|
self.UnImplemented()
|
|
else:
|
|
if cmd in string.ascii_uppercase:
|
|
self.cmd = 'set_' + t
|
|
else:
|
|
self.cmd = 'get_' + t
|
|
self.Handlers.get(self.cmd, self.UnImplemented)()
|
|
return 1
|
|
# These are the handlers for each request
|
|
def DumpState(self):
|
|
self.Send(self.dump2)
|
|
def ChkVfo(self):
|
|
self.Send('CHKVFO 0')
|
|
def GetFreq(self):
|
|
self.Reply('Frequency', self.app.rxFreq + self.app.VFO, 0)
|
|
def SetFreq(self):
|
|
try:
|
|
freq = float(self.params)
|
|
self.Reply(0)
|
|
except:
|
|
self.ErrParam()
|
|
else:
|
|
freq = int(freq + 0.5)
|
|
self.app.ChangeRxTxFrequency(freq, None)
|
|
def GetSplitFreq(self):
|
|
self.Reply('TX Frequency', self.app.txFreq + self.app.VFO, 0)
|
|
def SetSplitFreq(self):
|
|
try:
|
|
freq = float(self.params)
|
|
self.Reply(0)
|
|
except:
|
|
self.ErrParam()
|
|
else:
|
|
freq = int(freq + 0.5)
|
|
if self.app.split_rxtx and not self.app.split_hamlib_tx:
|
|
self.app.ChangeRxTxFrequency(freq, None)
|
|
else:
|
|
self.app.ChangeRxTxFrequency(None, freq)
|
|
def GetSplitVfo(self):
|
|
# I am not sure if "VFO" is a suitable response
|
|
if self.app.split_rxtx:
|
|
self.Reply('Split', 1, 'TX VFO', 'VFO', 0)
|
|
else:
|
|
self.Reply('Split', 0, 'TX VFO', 'VFO', 0)
|
|
def SetSplitVfo(self):
|
|
# Currently (Aug 2012) hamlib fails to send the "split" parameter, so this fails
|
|
try:
|
|
split, vfo = self.params.split()
|
|
split = int(split)
|
|
self.Reply(0)
|
|
except:
|
|
# traceback.print_exc()
|
|
self.ErrParam()
|
|
else:
|
|
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 == 'DGT-FM':
|
|
mode = 'FM'
|
|
elif mode[0:4] == 'DGT-':
|
|
mode = 'USB'
|
|
self.Reply('Mode', mode, 'Passband', self.app.filter_bandwidth, 0)
|
|
def SetMode(self):
|
|
try:
|
|
mode, bw = self.params.split()
|
|
bw = int(float(bw) + 0.5)
|
|
except:
|
|
self.ErrParam()
|
|
return
|
|
if mode in ('USB', 'LSB', 'AM', 'FM'):
|
|
self.Reply(0)
|
|
elif mode[0:4] == 'DGT-':
|
|
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.OnBtnMode(None, mode) # 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.OnBtnFilter(None, int(Lab))
|
|
def GetVfo(self):
|
|
self.Reply('VFO', "VFO", 0) # The type of VFO we have
|
|
def GetPtt(self):
|
|
if QS.is_key_down():
|
|
self.Reply('PTT', 1, 0)
|
|
else:
|
|
self.Reply('PTT', 0, 0)
|
|
def SetPtt(self):
|
|
if not self.app.pttButton:
|
|
self.UnImplemented()
|
|
return
|
|
try:
|
|
ptt = int(self.params)
|
|
self.Reply(0)
|
|
except:
|
|
self.ErrParam()
|
|
else:
|
|
self.app.pttButton.SetValue(ptt, 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
|
|
self.config_text = ''
|
|
threading.Thread.__init__(self)
|
|
self.doQuit = threading.Event()
|
|
self.doQuit.clear()
|
|
def run(self):
|
|
"""Read, process, play sound; then notify the GUI thread to check for FFT data."""
|
|
if self.do_init: # Open sound using this thread
|
|
self.do_init = 0
|
|
if self.samples_from_python:
|
|
self.config_text = Hardware.StartSamples()
|
|
QS.start_sound()
|
|
wx.CallAfter(application.PostStartup)
|
|
while not self.doQuit.isSet():
|
|
if self.samples_from_python:
|
|
samples = Hardware.GetRxSamples()
|
|
if samples:
|
|
QS.set_params(rx_samples=samples)
|
|
QS.read_sound()
|
|
wx.CallAfter(application.OnReadSound)
|
|
if self.samples_from_python:
|
|
Hardware.StopSamples()
|
|
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 = ConfigConfig(notebook, width)
|
|
notebook.AddPage(self.config, "Config")
|
|
self.sound = ConfigSound(notebook, width)
|
|
notebook.AddPage(self.sound, "Sound")
|
|
self.favorites = ConfigFavorites(notebook, width)
|
|
notebook.AddPage(self.favorites, "Favorites")
|
|
self.tx_audio = ConfigTxAudio(notebook, width)
|
|
notebook.AddPage(self.tx_audio, "Tx Audio")
|
|
self.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)
|
|
self.tx_audio.OnGraphData(data)
|
|
def InitBitmap(self): # Initial construction of bitmap
|
|
self.status.InitBitmap()
|
|
|
|
class ConfigStatus(wx.ScrolledWindow):
|
|
"""Display the status screen."""
|
|
def __init__(self, parent, width, fft_size):
|
|
wx.ScrolledWindow.__init__(self, parent)
|
|
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
|
self.bg_color = self.GetBackgroundColour()
|
|
self.tfg_color = wx.Colour(20, 20, 20) # use for text foreground
|
|
self.width = width
|
|
self.fft_size = fft_size
|
|
self.scroll_height = None
|
|
self.interupts = 0
|
|
self.read_error = -1
|
|
self.write_error = -1
|
|
self.underrun_error = -1
|
|
self.fft_error = -1
|
|
self.latencyCapt = -1
|
|
self.latencyPlay = -1
|
|
self.y_scale = 0
|
|
self.y_zero = 0
|
|
self.zoom_control = 0
|
|
self.rate_min = -1
|
|
self.rate_max = -1
|
|
self.chan_min = -1
|
|
self.chan_max = -1
|
|
self.mic_max_display = 0
|
|
self.err_msg = "No response"
|
|
self.msg1 = ""
|
|
self.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 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
|
|
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 not found %s" % conf.config_file_path
|
|
if conf.microphone_name:
|
|
level = "%3.0f" % self.mic_max_display
|
|
else:
|
|
level = "None"
|
|
if self.err_msg:
|
|
err_msg = self.err_msg
|
|
else:
|
|
err_msg = None
|
|
self.MakeRow2("Sample interrupts", self.interupts, cfile)
|
|
self.MakeRow2("Microphone or DGT level dB", level, application.config_text)
|
|
self.MakeRow2("FFT number of points", self.fft_size, err_msg)
|
|
if conf.dxClHost: # connection to dx cluster
|
|
nSpots = len(application.dxCluster.dxSpots)
|
|
if nSpots > 0:
|
|
msg = str(nSpots) + ' DX spot' + ('' if nSpots==1 else 's') + ' received from ' + application.dxCluster.getHost()
|
|
else:
|
|
msg = "No DX Cluster data from %s" % conf.dxClHost
|
|
self.MakeRow2("FFT number of errors", self.fft_error, msg)
|
|
else:
|
|
self.MakeRow2("FFT number of errors", self.fft_error)
|
|
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 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 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.interupts, self.fft_error, self.mic_max_display,
|
|
self.data_poll_usec
|
|
) = QS.get_state()
|
|
self.mic_max_display = 20.0 * math.log10((self.mic_max_display + 1) / 32767.0)
|
|
self.RefreshRect(self.mem_rect)
|
|
|
|
class ConfigConfig(wx.ScrolledWindow):
|
|
def __init__(self, parent, width):
|
|
wx.ScrolledWindow.__init__(self, parent)
|
|
self.width = width
|
|
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.SetBackgroundColour(parent.bg_color)
|
|
self.charx = charx = self.GetCharWidth()
|
|
self.chary = chary = self.GetCharHeight()
|
|
self.dy = self.chary
|
|
self.rx_phase = None
|
|
self.radio_group = None
|
|
# Make controls FIRST column
|
|
tab0 = charx * 4
|
|
# Receive phase
|
|
rx = wx.StaticText(self, -1, "Adjust receive amplitude and phase")
|
|
tx = wx.StaticText(self, -1, "Adjust transmit amplitude and phase")
|
|
x1, y1 = tx.GetSize().Get()
|
|
self.rx_phase = ctrl = QuiskPushbutton(self, self.OnBtnPhase, "Rx Phase...")
|
|
ctrl.SetColorGray()
|
|
if not conf.name_of_sound_capt:
|
|
ctrl.Enable(0)
|
|
x2, y2 = ctrl.GetSize().Get()
|
|
tab1 = tab0 + x1 + charx * 2
|
|
tab2 = tab1 + x2
|
|
tab3 = tab2 + charx * 8
|
|
self.y = self.yyy = y2 + self.chary
|
|
self.dy = y2 * 12 // 10
|
|
self.offset = (y2 - y1) // 2
|
|
rx.SetPosition((tab0, self.y))
|
|
ctrl.SetPosition((tab1, self.y - self.offset))
|
|
self.y += self.dy
|
|
# Transmit phase
|
|
self.tx_phase = ctrl = QuiskPushbutton(self, self.OnBtnPhase, "Tx Phase...")
|
|
ctrl.SetColorGray()
|
|
if not conf.name_of_mic_play:
|
|
ctrl.Enable(0)
|
|
tx.SetPosition((tab0, self.y))
|
|
ctrl.SetPosition((tab1, self.y - self.offset))
|
|
self.y += self.dy
|
|
# Choice (combo) box for decimation
|
|
lst = Hardware.VarDecimGetChoices()
|
|
if lst:
|
|
txt = Hardware.VarDecimGetLabel()
|
|
index = Hardware.VarDecimGetIndex()
|
|
else:
|
|
txt = "Variable decimation"
|
|
lst = ["None"]
|
|
index = 0
|
|
t = wx.StaticText(self, -1, txt)
|
|
ctrl = wx.Choice(self, -1, choices=lst, size=(x2, y2))
|
|
if lst:
|
|
self.Bind(wx.EVT_CHOICE, application.OnBtnDecimation, ctrl)
|
|
ctrl.SetSelection(index)
|
|
t.SetPosition((tab0, self.y))
|
|
ctrl.SetPosition((tab1, self.y - self.offset))
|
|
self.y += self.dy
|
|
# Transmit level controls
|
|
if hasattr(Hardware, "SetTxLevel"):
|
|
SliderBoxH(self, "Tx level %d%% ", 100, 0, 100, self.OnTxLevel, True, (tab0, self.y), tab2-tab0)
|
|
self.y += self.dy
|
|
level = conf.digital_tx_level
|
|
SliderBoxH(self, "Digital Tx level %d%% ", level, 0, level, self.OnDigitalTxLevel, True, (tab0, self.y), tab2-tab0)
|
|
self.y += self.dy
|
|
# mic_out_volume
|
|
if conf.name_of_mic_play:
|
|
level = int(conf.mic_out_volume * 100.0 + 0.1)
|
|
SliderBoxH(self, "SftRock Tx level %d%% ", level, 0, 100, self.OnSrTxLevel, True, (tab0, self.y), tab2-tab0)
|
|
self.y += self.dy
|
|
self.scroll_height = self.y
|
|
#### Make controls SECOND column
|
|
self.y = self.yyy
|
|
self.tab3 = tab3
|
|
self.charx = charx
|
|
## Record buttons
|
|
self.st = st = wx.StaticText(self, -1, "The file-record button will:", pos=(tab3 - charx * 2, self.y))
|
|
self.dy = st.GetSize().GetHeight() * 14 // 10
|
|
self.y += self.dy
|
|
# File for recording speaker audio
|
|
text = "Record Rx audio to WAV file "
|
|
path = conf.file_name_audio
|
|
self.file_button_rec_speaker = self.MakeFileButton(text, path, 0)
|
|
# File for recording samples
|
|
text = "Record I/Q samples to WAV file "
|
|
path = conf.file_name_samples
|
|
self.file_button_rec_iq = self.MakeFileButton(text, path, 1)
|
|
# File for recording the microphone
|
|
text = "Record the mic to make a CQ message"
|
|
path = ''
|
|
self.file_button_rec_mic = self.MakeFileButton(text, path, 2)
|
|
## Play buttons
|
|
wx.StaticText(self, -1, "The file-play button will:", pos=(tab3 - charx * 2, self.y))
|
|
self.y += self.dy
|
|
# File for playing speaker audio
|
|
text = "Play Rx audio from a WAV file"
|
|
path = ''
|
|
self.file_button_play_speaker = self.MakeFileButton(text, path, 10)
|
|
# file for playing samples
|
|
text = "Receive saved I/Q samples from a file"
|
|
path = ''
|
|
self.file_button_play_iq = self.MakeFileButton(text, path, 11)
|
|
# File for playing a file to the mic input for a CQ message
|
|
text = "Repeat a CQ message until a station answers"
|
|
path = conf.file_name_playback
|
|
self.file_button_play_mic = self.MakeFileButton(text, path, 12)
|
|
SliderBoxH(self, "Repeat secs %.1f ", 0, 0, 100, self.OnPlayFileRepeat, True, (tab3 + charx * 4, self.y), tab2-tab0, 0.1)
|
|
self.y += self.dy
|
|
if self.y > self.scroll_height:
|
|
self.scroll_height = self.y
|
|
self.SetScrollbars(1, 1, 100, self.scroll_height)
|
|
def MakeFileButton(self, text, path, index):
|
|
if index < 10: # record buttons
|
|
cb = wx.CheckBox(self, -1, text, pos=(self.tab3, self.y))
|
|
self.Bind(wx.EVT_CHECKBOX, self.OnCheckRecPlay, cb)
|
|
elif self.radio_group:
|
|
cb = wx.RadioButton(self, -1, text, pos=(self.tab3, self.y))
|
|
self.Bind(wx.EVT_RADIOBUTTON, self.OnCheckRecPlay, cb)
|
|
else:
|
|
self.radio_group = True
|
|
cb = wx.RadioButton(self, -1, text, pos=(self.tab3, self.y), style=wx.RB_GROUP)
|
|
self.Bind(wx.EVT_RADIOBUTTON, self.OnCheckRecPlay, cb)
|
|
x = self.tab3 + cb.GetSize().GetWidth()
|
|
bsz = wx.Size(self.charx * 3, cb.GetSize().GetHeight())
|
|
b = wx.Button(self, -1, "...", pos=(x, self.y), size=bsz)
|
|
b.check_box = cb
|
|
b.index = cb.index = index
|
|
b.path = cb.path = path
|
|
QS.set_file_name(b.index, name=path, enable=0)
|
|
self.Bind(wx.EVT_BUTTON, self.OnBtnFileName, b)
|
|
x = x + b.GetSize().GetWidth() + self.charx
|
|
dddy = (cb.GetSize().GetHeight() - self.st.GetSize().GetHeight()) // 2
|
|
if not path:
|
|
cb.Enable(False)
|
|
path = "(No file)"
|
|
b.txt_ctrl = wx.StaticText(self, -1, path, pos=(x, self.y + dddy))
|
|
self.y += self.dy
|
|
return b
|
|
def OnTxLevel(self, event):
|
|
application.tx_level = event.GetEventObject().GetValue()
|
|
Hardware.SetTxLevel()
|
|
def OnSrTxLevel(self, event):
|
|
level = event.GetEventObject().GetValue()
|
|
QS.set_mic_out_volume(level)
|
|
def OnDigitalTxLevel(self, event):
|
|
application.digital_tx_level = event.GetEventObject().GetValue()
|
|
Hardware.SetTxLevel()
|
|
def OnBtnPhase(self, event):
|
|
btn = event.GetEventObject()
|
|
if btn.GetLabel()[0:2] == 'Tx':
|
|
rx_tx = 'tx'
|
|
else:
|
|
rx_tx = 'rx'
|
|
application.screenBtnGroup.SetLabel('Graph', do_cmd=True)
|
|
if application.w_phase:
|
|
application.w_phase.Raise()
|
|
else:
|
|
application.w_phase = QAdjustPhase(self, self.width, rx_tx)
|
|
def OnBtnFileName(self, event):
|
|
btn = event.GetEventObject()
|
|
dr, fn = os.path.split(btn.path)
|
|
if btn.index < 10: # record buttons
|
|
dlg = wx.FileDialog(self, 'Choose WAV file', dr, fn, style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT, wildcard="Wave files (*.wav)|*.wav")
|
|
else:
|
|
dlg = wx.FileDialog(self, 'Choose WAV file', dr, fn, style=wx.FD_OPEN, wildcard="Wave files (*.wav)|*.wav")
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
path = dlg.GetPath()
|
|
if path[-4:].lower() != '.wav':
|
|
path = path + '.wav'
|
|
QS.set_file_name(btn.index, name=path)
|
|
btn.txt_ctrl.SetLabel(path)
|
|
btn.path = path
|
|
btn.check_box.path = path
|
|
btn.check_box.Enable(True)
|
|
if btn.index >= 10: # play buttons
|
|
btn.check_box.SetValue(True)
|
|
application.file_play_source = btn.index
|
|
QS.set_file_name(btn.index, enable=1)
|
|
QS.open_wav_file_play(path)
|
|
self.EnableRecPlay()
|
|
dlg.Destroy()
|
|
def EnableRecPlay(self):
|
|
enable_rec = (self.file_button_rec_speaker.check_box.GetValue() or
|
|
self.file_button_rec_iq.check_box.GetValue() or
|
|
self.file_button_rec_mic.check_box.GetValue())
|
|
enable_play = ((self.file_button_play_speaker.path and self.file_button_play_speaker.check_box.GetValue()) or
|
|
self.file_button_play_iq.check_box.GetValue() or
|
|
self.file_button_play_mic.check_box.GetValue())
|
|
application.btn_file_record.Enable(enable_rec)
|
|
application.btnFilePlay.Enable(enable_play)
|
|
def OnCheckRecPlay(self, event):
|
|
btn = event.GetEventObject()
|
|
if btn.GetValue():
|
|
if btn.index >= 10: # play button
|
|
QS.open_wav_file_play(btn.path)
|
|
application.file_play_source = btn.index
|
|
QS.set_file_name(btn.index, enable=1)
|
|
else:
|
|
QS.set_file_name(btn.index, enable=0)
|
|
self.EnableRecPlay()
|
|
def OnPlayFileRepeat(self, event):
|
|
application.file_play_repeat = event.GetEventObject().GetValue() * 0.1
|
|
|
|
class ConfigSound(wx.ScrolledWindow):
|
|
"""Display the available sound devices."""
|
|
def __init__(self, parent, width):
|
|
wx.ScrolledWindow.__init__(self, parent)
|
|
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
|
self.width = width
|
|
self.dev_capt, self.dev_play = QS.sound_devices()
|
|
self.tfg_color = parent.tfg_color
|
|
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.SetBackgroundColour(parent.bg_color)
|
|
self.charx = self.GetCharWidth()
|
|
self.chary = self.GetCharHeight()
|
|
self.dy = self.chary
|
|
height = self.chary * (3 + len(self.dev_capt) + len(self.dev_play))
|
|
if sys.platform != 'win32' and conf.show_pulse_audio_devices:
|
|
height += self.chary * (3 + 3 * len(application.pa_dev_capt) + 3 * len(application.pa_dev_play))
|
|
self.SetScrollbars(1, 1, 100, height)
|
|
def OnPaint(self, event):
|
|
dc = wx.PaintDC(self)
|
|
dc.Clear()
|
|
self.DoPrepareDC(dc)
|
|
dc.SetFont(self.font)
|
|
dc.SetTextForeground(self.tfg_color)
|
|
x0 = self.charx
|
|
self.y = self.chary // 3
|
|
dc.DrawText("Available devices for capture:", x0, self.y)
|
|
self.y += self.dy
|
|
for name in self.dev_capt:
|
|
dc.DrawText(' ' + name, x0, self.y)
|
|
self.y += self.dy
|
|
self.y += self.dy
|
|
dc.DrawText("Available devices for playback:", x0, self.y)
|
|
self.y += self.dy
|
|
for name in self.dev_play:
|
|
dc.DrawText(' ' + name, x0, self.y)
|
|
self.y += self.dy
|
|
if sys.platform != 'win32' and conf.show_pulse_audio_devices:
|
|
self.y += self.dy
|
|
dc.DrawText("Available PulseAudio devices for capture:", x0, self.y)
|
|
self.y += self.dy
|
|
for n0, n1, n2 in application.pa_dev_capt:
|
|
dc.DrawText(' ' * 4 + n1, x0, self.y)
|
|
self.y += self.dy
|
|
dc.DrawText(' ' * 8 + n0, x0, self.y)
|
|
self.y += self.dy
|
|
if n2:
|
|
dc.DrawText(' ' * 8 + n2, x0, self.y)
|
|
self.y += self.dy
|
|
self.y += self.dy
|
|
dc.DrawText("Available PulseAudio devices for playback:", x0, self.y)
|
|
self.y += self.dy
|
|
for n0, n1, n2 in application.pa_dev_play:
|
|
dc.DrawText(' ' * 4 + n1, x0, self.y)
|
|
self.y += self.dy
|
|
dc.DrawText(' ' * 8 + n0, x0, self.y)
|
|
self.y += self.dy
|
|
if n2:
|
|
dc.DrawText(' ' * 8 + n2, x0, self.y)
|
|
self.y += self.dy
|
|
|
|
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):
|
|
pass
|
|
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.OnBtnMode(None, mode)
|
|
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 ConfigTxAudio(wx.ScrolledWindow):
|
|
"""Display controls for the transmit audio."""
|
|
def __init__(self, parent, width):
|
|
wx.ScrolledWindow.__init__(self, parent)
|
|
self.width = width
|
|
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.SetBackgroundColour(parent.bg_color)
|
|
self.charx = charx = self.GetCharWidth()
|
|
self.chary = chary = self.GetCharHeight()
|
|
self.tmp_playing = False
|
|
# Make controls
|
|
tab0 = charx * 4
|
|
self.y = chary
|
|
t = "This is a test screen for transmit audio. SSB, AM and FM have separate settings."
|
|
wx.StaticText(self, -1, t, pos=(tab0, self.y))
|
|
self.btn_record = QuiskCheckbutton(self, self.OnBtnRecord, "Record")
|
|
self.btn_record.SetColorGray()
|
|
x2, y2 = self.btn_record.GetSize().Get()
|
|
self.dy = y2 * 12 // 10
|
|
self.offset = (y2 - chary) // 2
|
|
self.y += self.dy
|
|
# Record and Playback
|
|
ctl = wx.StaticText(self, -1, "Listen to transmit audio", pos=(tab0, self.y))
|
|
x1, y1 = ctl.GetSize().Get()
|
|
x = tab0 + x1 + charx * 3
|
|
y = self.y - self.offset
|
|
self.btn_record.SetPosition((x, y))
|
|
self.btn_playback = QuiskCheckbutton(self, self.OnBtnPlayback, "Playback")
|
|
self.btn_playback.SetColorGray()
|
|
self.btn_playback.SetPosition((x + x2 + charx * 3, y))
|
|
self.btn_playback.Enable(0)
|
|
if not conf.microphone_name:
|
|
self.btn_record.Enable(0)
|
|
tab1 = x + x2
|
|
tab2 = tab1 + charx * 3
|
|
self.y += self.dy
|
|
# mic level
|
|
self.mic_text = wx.StaticText(self, -1, "Peak microphone audio level None", pos=(tab0, self.y))
|
|
t = "Adjust the peak audio level to a few dB below zero."
|
|
wx.StaticText(self, -1, t, pos=(tab2, self.y))
|
|
self.y += self.dy
|
|
# Vox level
|
|
SliderBoxH(self, "VOX %d dB ", application.levelVOX, -40, 0, application.OnLevelVOX, True, (tab0, self.y), tab1-tab0)
|
|
t = "Audio level that triggers VOX (all modes)."
|
|
wx.StaticText(self, -1, t, pos=(tab2, self.y))
|
|
self.y += self.dy
|
|
# VOX hang
|
|
SliderBoxH(self, "VOX %0.2f ", application.timeVOX, 0, 4000, application.OnTimeVOX, True, (tab0, self.y), tab1-tab0, 0.001)
|
|
t = "Time to hold VOX after end of audio in seconds."
|
|
wx.StaticText(self, -1, t, pos=(tab2, self.y))
|
|
self.y += self.dy
|
|
# Tx Audio clipping
|
|
application.CtrlTxAudioClip = SliderBoxH(self, "Clip %2d ", 0, 0, 20, application.OnTxAudioClip, True, (tab0, self.y), tab1-tab0)
|
|
t = "Tx audio clipping level in dB for this mode."
|
|
wx.StaticText(self, -1, t, pos=(tab2, self.y))
|
|
self.y += self.dy
|
|
# Tx Audio preemphasis
|
|
application.CtrlTxAudioPreemph = SliderBoxH(self, "Preemphasis %4.2f ", 0, 0, 100, application.OnTxAudioPreemph, True, (tab0, self.y), tab1-tab0, 0.01)
|
|
t = "Tx audio preemphasis of high frequencies."
|
|
wx.StaticText(self, -1, t, pos=(tab2, self.y))
|
|
self.y += self.dy
|
|
self.SetScrollbars(1, 1, 100, self.y)
|
|
def OnGraphData(self, data=None):
|
|
if conf.microphone_name:
|
|
txt = "Peak microphone audio level %3.0f dB" % self.status.mic_max_display
|
|
self.mic_text.SetLabel(txt)
|
|
if self.tmp_playing and QS.set_record_state(-1): # poll to see if playback is finished
|
|
self.tmp_playing = False
|
|
self.btn_playback.SetValue(False)
|
|
self.btn_record.Enable(1)
|
|
def OnBtnRecord(self, event):
|
|
if event.GetEventObject().GetValue():
|
|
QS.set_kill_audio(1)
|
|
self.btn_playback.Enable(0)
|
|
QS.set_record_state(4)
|
|
else:
|
|
QS.set_kill_audio(0)
|
|
self.btn_playback.Enable(1)
|
|
QS.set_record_state(1)
|
|
def OnBtnPlayback(self, event):
|
|
if event.GetEventObject().GetValue():
|
|
self.btn_record.Enable(0)
|
|
QS.set_record_state(2)
|
|
self.tmp_playing = True
|
|
else:
|
|
self.btn_record.Enable(1)
|
|
QS.set_record_state(3)
|
|
self.tmp_playing = False
|
|
|
|
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 sys.platform == 'win32':
|
|
self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter)
|
|
if wxVersion in ('2', '3'):
|
|
self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
|
|
else:
|
|
self.SetBackgroundStyle(wx.BG_STYLE_PAINT)
|
|
def OnEnter(self, event):
|
|
if not application.w_phase:
|
|
self.SetFocus() # Set focus so we get mouse wheel events
|
|
def OnPaint(self, event):
|
|
#print 'GraphDisplay', self.GetUpdateRegion().GetBox()
|
|
dc = wx.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)
|
|
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, -130 to 0
|
|
y = self.zeroDB - int(y * self.scale / 10.0 + 0.5)
|
|
try:
|
|
y0 = self.line[x][1]
|
|
except IndexError:
|
|
self.line.append([x, y])
|
|
else:
|
|
if y > y0:
|
|
y = min(y, y0 + self.peak_hold)
|
|
self.line[x] = [x, y]
|
|
x = x + 1
|
|
self.Refresh()
|
|
def 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_label = wx.Pen(conf.color_graphlabels, 1)
|
|
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)
|
|
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 conf.BandPlan:
|
|
freq -= VFO
|
|
if f < freq:
|
|
xend = int(self.x0 + float(freq) * self.data_width / sample_rate + 0.5)
|
|
if c is not None:
|
|
dc.SetPen(wx.TRANSPARENT_PEN)
|
|
dc.SetBrush(wx.Brush(c))
|
|
dc.DrawRectangle(x, y, min(x3, xend) - x, tick0) # x axis
|
|
if xend >= x3:
|
|
break
|
|
x = xend
|
|
f = freq
|
|
c = color
|
|
# 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.DrawLine(self.x0, y_end, self.x0, application.screen_height)
|
|
def OnGraphData(self, data):
|
|
i1 = (self.data_width - self.graph_width) // 2
|
|
i2 = i1 + self.graph_width
|
|
self.display.OnGraphData(data[i1:i2])
|
|
def SetVFO(self, vfo):
|
|
self.VFO = vfo
|
|
self.doResize = True
|
|
def SetTxFreq(self, tx_freq, rx_freq):
|
|
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 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 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 self.mouse_is_rx:
|
|
application.rxFreq = freq
|
|
application.screen.SetTxFreq(self.txFreq, freq)
|
|
QS.set_tune(freq + application.ritFreq, self.txFreq)
|
|
else:
|
|
rnd = conf.freq_round_ssb
|
|
if rnd and not shift:
|
|
if application.mode in ('LSB', 'USB', 'AM', 'FM', 'FDV-U', 'FDV-L'):
|
|
freq = (freq + rnd//2) // rnd * rnd
|
|
else:
|
|
freq = self.FreqRound(freq, self.VFO)
|
|
self.ChangeHwFrequency(freq, self.VFO, 'MouseBtn1', event=event)
|
|
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 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
|
|
application.rxFreq += freq
|
|
application.screen.SetTxFreq(self.txFreq, application.rxFreq)
|
|
QS.set_tune(application.rxFreq + application.ritFreq, self.txFreq)
|
|
else: # Mouse motion changes the transmit frequency
|
|
self.ChangeHwFrequency(self.txFreq + freq, self.VFO, 'MouseMotion', event=event)
|
|
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 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
|
|
application.rxFreq = tune
|
|
application.screen.SetTxFreq(self.txFreq, tune)
|
|
QS.set_tune(tune + application.ritFreq, self.txFreq)
|
|
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):
|
|
application.ChangeHwFrequency(tune, vfo, source, band, event)
|
|
def PeakHold(self, name):
|
|
if name == 'GraphP1':
|
|
self.display.peak_hold = int(self.display.scale * conf.graph_peak_hold_1)
|
|
elif name == 'GraphP2':
|
|
self.display.peak_hold = int(self.display.scale * conf.graph_peak_hold_2)
|
|
else:
|
|
self.display.peak_hold = 9999
|
|
if self.display.peak_hold < 1:
|
|
self.display.peak_hold = 1
|
|
|
|
class StationScreen(wx.Window): # This code was contributed by Christof, DJ4CM. Many Thanks!!
|
|
"""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.richtext.RichTextCtrl(self.stationWindow)
|
|
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.OnBtnMode(None, mode)
|
|
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.nrStationInRange = 0
|
|
self.stationInfo.Clear()
|
|
found = True
|
|
self.nrStationInRange += 1
|
|
attr = self.stationInfo.GetBasicStyle()
|
|
attr.SetFlags(wx.TEXT_ATTR_TABS)
|
|
attr.SetTabs((40, 400, 700))
|
|
self.stationInfo.SetBasicStyle(attr)
|
|
self.stationInfo.BeginSymbolBullet(symbol, 0, 40)
|
|
self.stationInfo.BeginBold()
|
|
self.stationInfo.WriteText(statName + '\t')
|
|
self.stationInfo.EndBold()
|
|
self.stationInfo.WriteText (str(statFreq) + ' Hz\t' + statMode)
|
|
self.stationInfo.Newline()
|
|
self.stationInfo.EndSymbolBullet()
|
|
self.stationInfo.BeginLeftIndent(40)
|
|
if len(statDscr) > 0:
|
|
self.stationInfo.WriteText(statDscr)
|
|
self.stationInfo.Newline()
|
|
self.stationInfo.EndLeftIndent()
|
|
self.mouse_x = mouse_x
|
|
if self.firstStationInRange != None:
|
|
line = self.stationInfo.GetVisibleLineForCaretPosition(self.stationInfo.GetCaretPosition())
|
|
cy = line.GetAbsolutePosition()[1]
|
|
self.stationWindow.SetClientSize((340, cy+2))
|
|
self.stationInfo.SetClientSize((340, cy+2))
|
|
# convert coordinates to screen
|
|
sx, sy = self.ClientToScreen(wx.Point(mouse_x, mouse_y))
|
|
w, h = self.stationInfo.GetClientSize()
|
|
self.stationWindow.Move((sx - w * sx//graph.width, sy - h - 4))
|
|
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 == 'B':
|
|
pal2 = conf.waterfallPaletteB
|
|
elif conf.waterfall_palette == 'C':
|
|
pal2 = conf.waterfallPaletteC
|
|
else:
|
|
pal2 = conf.waterfallPalette
|
|
red = []
|
|
green = []
|
|
blue = []
|
|
n = 0
|
|
for i in range(256):
|
|
if i > pal2[n+1][0]:
|
|
n = n + 1
|
|
red.append((i - pal2[n][0]) *
|
|
(pal2[n+1][1] - pal2[n][1]) //
|
|
(pal2[n+1][0] - pal2[n][0]) + pal2[n][1])
|
|
green.append((i - pal2[n][0]) *
|
|
(pal2[n+1][2] - pal2[n][2]) //
|
|
(pal2[n+1][0] - pal2[n][0]) + pal2[n][2])
|
|
blue.append((i - pal2[n][0]) *
|
|
(pal2[n+1][3] - pal2[n][3]) //
|
|
(pal2[n+1][0] - pal2[n][0]) + pal2[n][3])
|
|
self.red = red
|
|
self.green = green
|
|
self.blue = blue
|
|
row = bytearray(4)
|
|
if wxVersion in ('2', '3'):
|
|
bmp = wx.BitmapFromBufferRGBA(1, 1, row)
|
|
else:
|
|
bmp = wx.Bitmap().FromBufferRGBA(1, 1, row)
|
|
bmp.x_origin = 0
|
|
self.bitmaps = [bmp] * application.screen_height
|
|
if sys.platform == 'win32':
|
|
self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter)
|
|
def OnEnter(self, event):
|
|
if not application.w_phase:
|
|
self.SetFocus() # Set focus so we get mouse wheel events
|
|
def OnPaint(self, event):
|
|
sample_rate = int(self.sample_rate * self.zoom)
|
|
dc = wx.BufferedPaintDC(self)
|
|
dc.SetTextForeground(conf.color_graphlabels)
|
|
dc.SetBackground(wx.Brush('Black'))
|
|
dc.Clear()
|
|
rit = self.DrawFilter(dc)
|
|
dc.SetLogicalFunction(wx.COPY)
|
|
x_origin = int(float(self.VFO) / sample_rate * self.data_width + 0.5)
|
|
y = self.margin
|
|
index = 0
|
|
if conf.waterfall_scroll_mode: # Draw the first few lines multiple times
|
|
for i in range(self.top_key, 1, -1):
|
|
b = self.bitmaps[index]
|
|
x = b.x_origin - x_origin
|
|
for j in range(0, i):
|
|
dc.DrawBitmap(b, x, y)
|
|
y += 1
|
|
index += 1
|
|
while y < self.height:
|
|
b = self.bitmaps[index]
|
|
x = b.x_origin - x_origin
|
|
dc.DrawBitmap(b, x, y)
|
|
y += 1
|
|
index += 1
|
|
dc.SetPen(self.tuningPen)
|
|
dc.SetLogicalFunction(wx.XOR)
|
|
dc.DrawLine(self.tune_tx, 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):
|
|
sample_rate = int(self.sample_rate * self.zoom)
|
|
#T('graph start')
|
|
row = bytearray(0) # Make a new row of pixels for a one-line image
|
|
gain = self.rf_gain
|
|
# 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.
|
|
for x in data: # x is -130 to 0, or so (dB)
|
|
yz = 40.0 + y_zero * 0.69 # -yz is the color center in dB
|
|
l = int((x - gain + yz) * (y_scale + 10) * 0.10 + 128)
|
|
l = max(l, 0)
|
|
l = min(l, 255)
|
|
row.append(self.red[l])
|
|
row.append(self.green[l])
|
|
row.append(self.blue[l])
|
|
row.append(255)
|
|
#print ('OnGraphData yz %.0f, slope %.3f, l %4d' % (yz, (y_scale + 10) * 0.10, l))
|
|
#T('graph string')
|
|
if wxVersion in ('2', '3'):
|
|
bmp = wx.BitmapFromBufferRGBA(len(row) // 4, 1, row)
|
|
else:
|
|
bmp = wx.Bitmap().FromBufferRGBA(len(row) // 4, 1, row)
|
|
bmp.x_origin = int(float(self.VFO) / sample_rate * self.data_width + 0.5)
|
|
self.bitmaps.insert(0, bmp)
|
|
del self.bitmaps[-1]
|
|
#self.ScrollWindow(0, 1, None)
|
|
#self.Refresh(False, (0, 0, self.graph_width, self.top_size + self.margin))
|
|
self.Refresh(False)
|
|
#T('graph end')
|
|
def SetTuningLine(self, tune_tx, tune_rx):
|
|
dc = wx.ClientDC(self)
|
|
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, width)
|
|
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 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)
|
|
|
|
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.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):
|
|
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):
|
|
self.SetTxFreq(tune, tune)
|
|
self.SetVFO(vfo)
|
|
Hardware.MultiRxFrequency(self.multirx_index, vfo)
|
|
QS.set_multirx_freq(self.multirx_index, tune)
|
|
|
|
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, self.width)
|
|
# 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):
|
|
index = len(self.receiver_list)
|
|
if index >= 7:
|
|
return
|
|
if index == 0:
|
|
pane2 = self.rx_zero
|
|
splitter = pane2.GetParent()
|
|
pane1 = MultiRxGraph(self, self.data_width, self.graph_width, index)
|
|
self.receiver_list.append(pane1)
|
|
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):
|
|
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):
|
|
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])
|
|
# read in text from file help.html in the directory of this module
|
|
self.LoadFile('help.html')
|
|
def OnGraphData(self, data):
|
|
pass
|
|
def ChangeYscale(self, y_scale):
|
|
pass
|
|
def ChangeYzero(self, y_zero):
|
|
pass
|
|
def OnIdle(self, event):
|
|
pass
|
|
def SetTxFreq(self, tx_freq, rx_freq):
|
|
pass
|
|
def OnLinkClicked(self, link):
|
|
webbrowser.open(link.GetHref(), new=2)
|
|
|
|
class QMainFrame(wx.Frame):
|
|
"""Create the main top-level window."""
|
|
def __init__(self, width, height):
|
|
fp = open('__init__.py') # Read in the title
|
|
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" % (configure.Settings[1], self.title, text))
|
|
|
|
## Note: The new amplitude/phase adjustments have ideas provided by Andrew Nilsson, VK6JBL
|
|
class QAdjustPhase(wx.Frame):
|
|
"""Create a window with amplitude and phase adjustment controls"""
|
|
f_ampl = "Amplitude adjustment %.6f"
|
|
f_phase = "Phase adjustment degrees %.6f"
|
|
def __init__(self, parent, width, rx_tx):
|
|
self.rx_tx = rx_tx # Must be "rx" or "tx"
|
|
if rx_tx == 'tx':
|
|
self.is_tx = 1
|
|
t = "Adjust Sound Card Transmit Amplitude and Phase"
|
|
else:
|
|
self.is_tx = 0
|
|
t = "Adjust Sound Card Receive Amplitude and Phase"
|
|
wx.Frame.__init__(self, application.main_frame, -1, t, pos=(50, 100), style=wx.CAPTION)
|
|
panel = wx.Panel(self)
|
|
self.MakeControls(panel, width)
|
|
self.Show()
|
|
def MakeControls(self, panel, width): # Make controls for phase/amplitude adjustment
|
|
self.old_amplitude, self.old_phase = application.GetAmplPhase(self.is_tx)
|
|
self.new_amplitude, self.new_phase = self.old_amplitude, self.old_phase
|
|
sl_max = width * 4 // 10 # maximum +/- value for slider
|
|
self.ampl_scale = float(conf.rx_max_amplitude_correct) / sl_max
|
|
self.phase_scale = float(conf.rx_max_phase_correct) / sl_max
|
|
font = wx.Font(conf.default_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
|
|
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
|
|
self.SetFont(font)
|
|
charx = self.GetCharWidth()
|
|
tab1 = charx
|
|
chary = self.GetCharHeight()
|
|
deltay = chary * 12 // 10
|
|
y = chary * 3 // 10
|
|
# Print available data points
|
|
if "panadapter" in conf.bandAmplPhase:
|
|
self.band = "panadapter"
|
|
else:
|
|
self.band = application.lastBand
|
|
app_vfo = (application.VFO + 500) // 1000
|
|
ap = application.bandAmplPhase
|
|
if self.band not in ap:
|
|
ap[self.band] = {}
|
|
if self.rx_tx not in ap[self.band]:
|
|
ap[self.band][self.rx_tx] = []
|
|
lst = ap[self.band][self.rx_tx]
|
|
freq_in_list = False
|
|
if lst:
|
|
t = "Band %s: VFO" % self.band
|
|
for l in lst:
|
|
vfo = (l[0] + 500) // 1000
|
|
if vfo == app_vfo:
|
|
freq_in_list = True
|
|
t = t + (" %d" % vfo)
|
|
else:
|
|
t = "Band %s: No data." % self.band
|
|
txt = wx.StaticText(panel, -1, t, pos=(tab1, y))
|
|
y += deltay * 14 // 10
|
|
self.t_ampl = wx.StaticText(panel, -1, self.f_ampl % self.old_amplitude, pos=(tab1, y))
|
|
y += deltay
|
|
fine = wx.StaticText(panel, -1, 'Fine', pos=(tab1, y))
|
|
coarse = wx.StaticText(panel, -1, 'Coarse', pos=(tab1, y + deltay))
|
|
tab2 = tab1 + coarse.GetSize().GetWidth() + charx
|
|
sliderX = width - tab2 - charx
|
|
self.ampl1 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1))
|
|
y += deltay
|
|
self.ampl2 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1))
|
|
y += deltay * 14 // 10
|
|
self.PosAmpl(self.old_amplitude)
|
|
self.t_phase = wx.StaticText(panel, -1, self.f_phase % self.old_phase, pos=(tab1, y))
|
|
y += deltay
|
|
fine = wx.StaticText(panel, -1, 'Fine', pos=(tab1, y))
|
|
coarse = wx.StaticText(panel, -1, 'Coarse', pos=(tab1, y + deltay))
|
|
self.phase1 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1))
|
|
y += deltay
|
|
self.phase2 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1))
|
|
y += deltay
|
|
sv = QuiskPushbutton(panel, self.OnBtnSave, 'Save %d' % app_vfo)
|
|
ds = QuiskPushbutton(panel, self.OnBtnDiscard, 'Destroy %d' % app_vfo)
|
|
cn = QuiskPushbutton(panel, self.OnBtnCancel, 'Cancel')
|
|
w, h = ds.GetSize().Get()
|
|
sv.SetSize((w, h))
|
|
cn.SetSize((w, h))
|
|
y += h // 4
|
|
x = (width - w * 3) // 4
|
|
sv.SetPosition((x, y))
|
|
ds.SetPosition((x*2 + w, y))
|
|
cn.SetPosition((x*3 + w*2, y))
|
|
sv.SetBackgroundColour('light blue')
|
|
ds.SetBackgroundColour('light blue')
|
|
cn.SetBackgroundColour('light blue')
|
|
if not freq_in_list:
|
|
ds.Disable()
|
|
y += h
|
|
y += h * 4 // 10
|
|
self.PosPhase(self.old_phase)
|
|
self.SetClientSize(wx.Size(width, y))
|
|
self.ampl1.Bind(wx.EVT_SCROLL, self.OnChange)
|
|
self.ampl2.Bind(wx.EVT_SCROLL, self.OnAmpl2)
|
|
self.phase1.Bind(wx.EVT_SCROLL, self.OnChange)
|
|
self.phase2.Bind(wx.EVT_SCROLL, self.OnPhase2)
|
|
def PosAmpl(self, ampl): # set pos1, pos2 for amplitude
|
|
pos2 = round(ampl / self.ampl_scale)
|
|
remain = ampl - pos2 * self.ampl_scale
|
|
pos1 = round(remain / self.ampl_scale * 50.0)
|
|
self.ampl1.SetValue(pos1)
|
|
self.ampl2.SetValue(pos2)
|
|
def PosPhase(self, phase): # set pos1, pos2 for phase
|
|
pos2 = round(phase / self.phase_scale)
|
|
remain = phase - pos2 * self.phase_scale
|
|
pos1 = round(remain / self.phase_scale * 50.0)
|
|
self.phase1.SetValue(pos1)
|
|
self.phase2.SetValue(pos2)
|
|
def OnChange(self, event):
|
|
ampl = self.ampl_scale * self.ampl1.GetValue() / 50.0 + self.ampl_scale * self.ampl2.GetValue()
|
|
if abs(ampl) < self.ampl_scale * 3.0 / 50.0:
|
|
ampl = 0.0
|
|
self.t_ampl.SetLabel(self.f_ampl % ampl)
|
|
phase = self.phase_scale * self.phase1.GetValue() / 50.0 + self.phase_scale * self.phase2.GetValue()
|
|
if abs(phase) < self.phase_scale * 3.0 / 50.0:
|
|
phase = 0.0
|
|
self.t_phase.SetLabel(self.f_phase % phase)
|
|
QS.set_ampl_phase(ampl, phase, self.is_tx)
|
|
self.new_amplitude, self.new_phase = ampl, phase
|
|
def OnAmpl2(self, event): # re-center the fine slider when the coarse slider is adjusted
|
|
ampl = self.ampl_scale * self.ampl1.GetValue() / 50.0 + self.ampl_scale * self.ampl2.GetValue()
|
|
self.PosAmpl(ampl)
|
|
self.OnChange(event)
|
|
def OnPhase2(self, event): # re-center the fine slider when the coarse slider is adjusted
|
|
phase = self.phase_scale * self.phase1.GetValue() / 50.0 + self.phase_scale * self.phase2.GetValue()
|
|
self.PosPhase(phase)
|
|
self.OnChange(event)
|
|
def DeleteEqual(self): # Remove entry with the same VFO
|
|
ap = application.bandAmplPhase
|
|
lst = ap[self.band][self.rx_tx]
|
|
vfo = (application.VFO + 500) // 1000
|
|
for i in range(len(lst)-1, -1, -1):
|
|
if (lst[i][0] + 500) // 1000 == vfo:
|
|
del lst[i]
|
|
def OnBtnSave(self, event):
|
|
data = (application.VFO, application.rxFreq, self.new_amplitude, self.new_phase)
|
|
self.DeleteEqual()
|
|
ap = application.bandAmplPhase
|
|
lst = ap[self.band][self.rx_tx]
|
|
lst.append(data)
|
|
lst.sort()
|
|
application.w_phase = None
|
|
self.Destroy()
|
|
def OnBtnDiscard(self, event):
|
|
self.DeleteEqual()
|
|
self.OnBtnCancel()
|
|
def OnBtnCancel(self, event=None):
|
|
QS.set_ampl_phase(self.old_amplitude, self.old_phase, self.is_tx)
|
|
application.w_phase = None
|
|
self.Destroy()
|
|
|
|
class Spacer(wx.Window):
|
|
"""Create a bar between the graph screen and the controls"""
|
|
def __init__(self, parent):
|
|
wx.Window.__init__(self, parent, pos = (0, 0),
|
|
size=(-1, 6), style = wx.NO_BORDER)
|
|
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
|
r, g, b = parent.GetBackgroundColour().Get(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', 'graphScaleZ', 'split_rxtx_play', 'modeFilter']
|
|
def __init__(self):
|
|
global application
|
|
application = self
|
|
self.init_path = None
|
|
self.bottom_widgets = None
|
|
self.dxCluster = None
|
|
self.main_frame = None
|
|
if sys.stdout.isatty():
|
|
wx.App.__init__(self, redirect=False)
|
|
else:
|
|
wx.App.__init__(self, redirect=True)
|
|
def QuiskText(self, *args, **kw): # Make our text control available to widget files
|
|
return QuiskText(*args, **kw)
|
|
def 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 OnInit(self):
|
|
"""Perform most initialization of the app here (called by wxPython on startup)."""
|
|
wx.lib.colourdb.updateColourDB() # Add additional color names
|
|
import quisk_widgets # quisk_widgets needs the application object
|
|
quisk_widgets.application = self
|
|
del quisk_widgets
|
|
global conf # conf is the module for all configuration data
|
|
import quisk_conf_defaults as conf
|
|
setattr(conf, 'config_file_path', ConfigPath)
|
|
setattr(conf, 'DefaultConfigDir', DefaultConfigDir)
|
|
if os.path.isfile(ConfigPath): # See if the user has a config file
|
|
setattr(conf, 'config_file_exists', True)
|
|
d = {}
|
|
d.update(conf.__dict__) # make items from conf available
|
|
exec(compile(open(ConfigPath).read(), ConfigPath, 'exec'), d) # execute the user's config file
|
|
if os.path.isfile(ConfigPath2): # See if the user has a second config file
|
|
exec(compile(open(ConfigPath2).read(), ConfigPath2, 'exec'), d) # execute the user's second config file
|
|
for k in d: # add user's config items to conf
|
|
v = d[k]
|
|
if k[0] != '_': # omit items starting with '_'
|
|
setattr(conf, k, v)
|
|
else:
|
|
setattr(conf, 'config_file_exists', False)
|
|
QS.set_params(quisk_is_vna=0) # We are not the VNA program
|
|
# Read in configuration from the selected radio
|
|
if self.main_frame:
|
|
self.local_conf = configure.Configuration(self, 'Same')
|
|
else:
|
|
self.local_conf = configure.Configuration(self, argv_options.AskMe)
|
|
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.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 = conf.bandAmplPhase
|
|
self.samples_from_python = False
|
|
self.NewIdList = [] # Hack: list of Ids in use for accelerator table
|
|
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
|
|
# 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.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.fewsec_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.hot_key_ptt_is_down = False
|
|
self.hot_key_ptt_was_down = False
|
|
self.hot_key_ptt_change = False
|
|
self.hardware_ptt_key_state = 0
|
|
self.fft_size = 1
|
|
self.accel_list = []
|
|
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 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.oldRxFreq = 0 # Last value of self.rxFreq
|
|
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 = None
|
|
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_hamlib_tx = True # Hamlib controls the Tx frequency when split; else the Rx frequency
|
|
self.split_rxtx_play = 2 # Play 1=both, high on Right; 2=both, high on Left; 3=only Rx; 4=only Tx
|
|
self.savedState = {}
|
|
self.pttButton = None
|
|
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
|
|
# 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
|
|
if conf.persistent_state:
|
|
self.init_path = os.path.join(os.path.dirname(ConfigPath), '.quisk_init.pkl')
|
|
try:
|
|
fp = open(self.init_path, "rb") # Pickle requires a bytes object
|
|
d = pickle.load(fp)
|
|
fp.close()
|
|
for k in d:
|
|
v = d[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()
|
|
for k, (vfo, tune, mode) in list(self.bandState.items()): # Historical: fix bad frequencies
|
|
try:
|
|
f1, f2 = conf.BandEdge[k]
|
|
if not f1 <= vfo + tune <= f2:
|
|
self.bandState[k] = conf.bandState[k]
|
|
except KeyError:
|
|
pass
|
|
if self.bandAmplPhase and not isinstance(list(self.bandAmplPhase.values())[0], dict):
|
|
print("""Old sound card amplitude and phase corrections must be re-entered (sorry).
|
|
The new code supports multiple corrections per band.""")
|
|
self.bandAmplPhase = {}
|
|
if Hardware.VarDecimGetChoices(): # Hardware can change the decimation.
|
|
self.sample_rate = Hardware.VarDecimSet() # Get the sample rate.
|
|
self.vardecim_set = self.sample_rate
|
|
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
|
|
# Check for PulseAudio names and substitute the actual device name for abbreviations
|
|
self.pulse_in_use = False
|
|
if sys.platform != 'win32' and conf.show_pulse_audio_devices:
|
|
self.pa_dev_capt, self.pa_dev_play = QS.pa_sound_devices()
|
|
for key in ("name_of_sound_play", "name_of_mic_play", "digital_output_name", "digital_rx1_name", "sample_playback_name"):
|
|
value = getattr(conf, key) # playback devices
|
|
if value[0:6] == "pulse:":
|
|
self.pulse_in_use = True
|
|
for n0, n1, n2 in self.pa_dev_play:
|
|
for n in (n0, n1, n2):
|
|
if value[6:] in n:
|
|
setattr(conf, key, "pulse:" + n0)
|
|
for key in ("name_of_sound_capt", "microphone_name", "digital_input_name"):
|
|
value = getattr(conf, key) # capture devices
|
|
if value[0:6] == "pulse:":
|
|
self.pulse_in_use = True
|
|
for n0, n1, n2 in self.pa_dev_capt:
|
|
for n in (n0, n1, n2):
|
|
if value[6:] in n:
|
|
setattr(conf, key, "pulse:" + n0)
|
|
# 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)
|
|
#w, h = frame.GetSize().Get()
|
|
#ww, hh = frame.GetClientSizeTuple()
|
|
#print ('Main frame: size', w, h, 'client', ww, hh)
|
|
# Find the data width from a list of prefered sizes; it is the width of returned graph data.
|
|
# The graph_width is the width of data_width that is displayed.
|
|
if conf.window_width > 0:
|
|
wFrame, h = frame.GetClientSize().Get() # client window width
|
|
graph = GraphScreen(frame, self.width//2, self.width//2, None) # make a GraphScreen to calculate borders
|
|
self.graph_width = wFrame - (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
|
|
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)
|
|
#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)
|
|
vertBox.Add(self.scope, 1)
|
|
vertBox.Add(self.bandscope_screen, 1)
|
|
vertBox.Add(self.filter_screen, 1)
|
|
if self.rate_audio_fft:
|
|
vertBox.Add(self.audio_fft_screen, 1)
|
|
vertBox.Add(self.help_screen, 1)
|
|
vertBox.Add(self.station_screen)
|
|
# 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)
|
|
minw = width = self.graph.width
|
|
maxw = maxh = -1
|
|
minh = 100
|
|
if conf.window_width > 0:
|
|
minw = width = maxw = conf.window_width
|
|
if conf.window_height > 0:
|
|
minh = maxh = self.height = conf.window_height
|
|
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()
|
|
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()
|
|
if QS.open_key(conf.key_method):
|
|
print('open_key failed for name "%s"' % conf.key_method)
|
|
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)
|
|
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 conf.dxClHost:
|
|
# create DX Cluster and register listener for change notification
|
|
self.dxCluster = dxcluster.DxCluster()
|
|
self.dxCluster.setListener(self.OnDxClChange)
|
|
self.dxCluster.start()
|
|
# Create shortcut keys for buttons
|
|
if conf.button_layout == 'Large screen':
|
|
for button in self.modeButns.GetButtons(): # mode buttons
|
|
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
|
|
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))
|
|
# Create a shortcut for the PTT key. This is only used if the hot key does NOT work when Quisk is hidden.
|
|
if conf.hot_key_ptt1 and not conf.hot_key_ptt_if_hidden:
|
|
rid = self.QuiskNewId()
|
|
frame.Bind(wx.EVT_MENU, self.OnHotKey, id=rid)
|
|
self.accel_list.append(wx.AcceleratorEntry(conf.hot_key_ptt2, conf.hot_key_ptt1, rid))
|
|
self.main_frame.SetAcceleratorTable(wx.AcceleratorTable(self.accel_list))
|
|
self.OnBtnMode(None, self.mode)
|
|
# self.OnTestTimer(None)
|
|
return True
|
|
#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 OpenSound(self):
|
|
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.
|
|
QS.open_sound(conf.name_of_sound_capt, conf.name_of_sound_play, 0,
|
|
conf.data_poll_usec, conf.latency_millisecs,
|
|
conf.microphone_name, conf.tx_ip, conf.tx_audio_port,
|
|
conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q,
|
|
conf.mic_out_volume, conf.name_of_mic_play, conf.mic_playback_rate)
|
|
def OnDxClChange(self):
|
|
self.station_screen.Refresh()
|
|
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):
|
|
QS.set_file_name(record_button=0) # Turn off file recording
|
|
time.sleep(0.1)
|
|
if self.sound_thread:
|
|
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
|
|
def OnExit(self):
|
|
if self.dxCluster:
|
|
self.dxCluster.stop()
|
|
QS.close_rx_udp()
|
|
Hardware.close()
|
|
self.SaveState()
|
|
self.local_conf.SaveState()
|
|
if self.hamlib_com1_handler:
|
|
self.hamlib_com1_handler.close()
|
|
if self.hamlib_com2_handler:
|
|
self.hamlib_com2_handler.close()
|
|
return 0
|
|
def OnBtnOnOff(self, event):
|
|
if event.GetEventObject().GetValue(): # Start samples
|
|
self.SaveState()
|
|
self.local_conf.SaveState()
|
|
self.OnInit()
|
|
else: # Stop samples
|
|
try:
|
|
wx.BeginBusyCursor()
|
|
self.OnBtnClose()
|
|
QS.close_rx_udp()
|
|
Hardware.close()
|
|
finally:
|
|
wx.EndBusyCursor()
|
|
def ImmediateChange(self, name):
|
|
value = getattr(conf, name)
|
|
if name == "keyupDelay" and conf.use_rx_udp == 10: # Hermes UDP protocol
|
|
if value > 1023:
|
|
value = 1023
|
|
Hardware.SetControlByte(0x10, 2, value & 0x3) # cw_hang_time
|
|
Hardware.SetControlByte(0x10, 1, (value >> 2) & 0xFF) # cw_hang_time
|
|
QS.ImmediateChange(name)
|
|
def CheckState(self): # check whether state has changed
|
|
changed = False
|
|
if self.init_path: # save current program state
|
|
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):
|
|
if self.init_path: # save current program state
|
|
d = {}
|
|
for n in self.StateNames:
|
|
d[n] = v = getattr(self, n)
|
|
self.savedState[n] = v
|
|
try:
|
|
fp = open(self.init_path, "wb") # Pickle requires a bytes object
|
|
pickle.dump(d, fp)
|
|
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 Hardware.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)
|
|
# Add sliders on the left
|
|
gbs.Add(self.sliderVol, (0, 0), (self.widget_row, 1), flag=wx.EXPAND|wx.LEFT, border=margin)
|
|
if Hardware.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, AGC
|
|
left_row2 = []
|
|
b = b_mute = QuiskCheckbutton(frame, self.OnBtnMute, text='Mute')
|
|
b.char_shortcut = 'u'
|
|
self.MakeAccel(b)
|
|
left_row2.append(b)
|
|
agc = QuiskCheckbutton(frame, self.OnBtnAGC, 'AGC')
|
|
agc.char_shortcut = 'G'
|
|
self.MakeAccel(agc)
|
|
b = WrapSlider(agc, self.OnBtnAGC, display=True)
|
|
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)
|
|
left_row2.append(self.sliderSquelch)
|
|
b = QuiskCycleCheckbutton(frame, self.OnBtnNB, ('NB', 'NB 1', 'NB 2', 'NB 3'))
|
|
b.char_shortcut = 'B'
|
|
self.MakeAccel(b)
|
|
left_row2.append(b)
|
|
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 = QuiskCheckbutton(frame, self.OnBtnSpot, 'Spot', color=conf.color_test)
|
|
b = WrapSlider(bt, self.OnBtnSpot, slider_value=self.levelSpot, display=True)
|
|
if hasattr(Hardware, 'OnSpot'):
|
|
bt.char_shortcut = 'o'
|
|
self.MakeAccel(bt)
|
|
else:
|
|
b.Enable(False)
|
|
left_row3.append(b)
|
|
# Split button
|
|
self.split_menu = wx.Menu()
|
|
item1 = self.split_menu.AppendRadioItem(-1, 'Play both, High Freq on R')
|
|
self.Bind(wx.EVT_MENU, self.OnMenuSplitPlay1, item1)
|
|
item2 = self.split_menu.AppendRadioItem(-1, 'Play both, High Freq on L')
|
|
self.Bind(wx.EVT_MENU, self.OnMenuSplitPlay2, item2)
|
|
item3 = self.split_menu.AppendRadioItem(-1, 'Play only Rx')
|
|
self.Bind(wx.EVT_MENU, self.OnMenuSplitPlay3, item3)
|
|
item4 = self.split_menu.AppendRadioItem(-1, 'Play only Tx')
|
|
self.Bind(wx.EVT_MENU, self.OnMenuSplitPlay4, item4)
|
|
if self.split_rxtx_play == 1:
|
|
item1.Check()
|
|
elif self.split_rxtx_play == 2:
|
|
item2.Check()
|
|
elif self.split_rxtx_play == 3:
|
|
item3.Check()
|
|
elif self.split_rxtx_play == 4:
|
|
item4.Check()
|
|
self.split_menu.AppendSeparator()
|
|
item = self.split_menu.Append(-1, 'Reverse Rx and Tx')
|
|
self.Bind(wx.EVT_MENU, self.OnMenuSplitRev, item)
|
|
item = self.split_menu.AppendCheckItem(-1, 'Lock Tx Frequency')
|
|
self.Bind(wx.EVT_MENU, self.OnMenuSplitLock, item)
|
|
self.split_menu.AppendSeparator()
|
|
item = self.split_menu.AppendRadioItem(-1, 'Hamlib control Tx')
|
|
self.Bind(wx.EVT_MENU, self.OnMenuSplitCtlTx, item)
|
|
item = self.split_menu.AppendRadioItem(-1, 'Hamlib control Rx')
|
|
self.Bind(wx.EVT_MENU, self.OnMenuSplitCtlRx, item)
|
|
b = QuiskCheckbutton(frame, self.OnBtnSplit, "Split")
|
|
b.char_shortcut = 'l'
|
|
self.MakeAccel(b)
|
|
self.splitButton = WrapMenu(b, self.split_menu)
|
|
if 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)
|
|
if hasattr(Hardware, 'OnButtonPTT'):
|
|
b = QuiskCheckbutton(frame, self.OnButtonPTT, 'PTT', color='red')
|
|
self.pttButton = b
|
|
left_row3.append(b)
|
|
b = QuiskCheckbutton(frame, self.OnButtonVOX, 'VOX')
|
|
b.char_shortcut = 'V'
|
|
self.MakeAccel(b)
|
|
left_row3.append(b)
|
|
else:
|
|
b = QuiskCheckbutton(frame, None, 'PTT')
|
|
b.Enable(False)
|
|
left_row3.append(b)
|
|
b = QuiskCheckbutton(frame, None, 'VOX')
|
|
b.Enable(False)
|
|
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)
|
|
b.Enable(0)
|
|
#left_row3.append(b)
|
|
self.btn_file_record = QuiskCheckbutton(frame, self.OnBtnFileRecord, conf.Xbtn_text_file_rec)
|
|
self.btn_file_record.Enable(0)
|
|
left_row3.append(self.btn_file_record)
|
|
self.btnFilePlay = QuiskCheckbutton(frame, self.OnBtnFilePlay, conf.Xbtn_text_file_play)
|
|
self.btnFilePlay.Enable(0)
|
|
left_row3.append(self.btnFilePlay)
|
|
### 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')]
|
|
shortcuts = ['C', 'S', 'A', 'M', 'D']
|
|
count = 5 # There is room for seven buttons
|
|
if conf.add_freedv_button:
|
|
n_freedv = count
|
|
count += 1
|
|
labels.append('FDV-U')
|
|
shortcuts.append('F')
|
|
if conf.add_imd_button:
|
|
n_imd = count
|
|
count += 1
|
|
labels.append('IMD')
|
|
shortcuts.append('I')
|
|
if count < 7 and conf.add_extern_demod:
|
|
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)
|
|
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)
|
|
self.freedv_menu_items = {}
|
|
if conf.add_freedv_button:
|
|
self.freedv_menu = wx.Menu()
|
|
item = self.freedv_menu.Append(-1, 'Upper sideband')
|
|
self.Bind(wx.EVT_MENU, self.OnFreedvMenu, item)
|
|
item = self.freedv_menu.Append(-1, 'Lower sideband')
|
|
self.Bind(wx.EVT_MENU, self.OnFreedvMenu, item)
|
|
self.freedv_menu.AppendSeparator()
|
|
msg = conf.freedv_tx_msg
|
|
QS.freedv_set_options(mode=conf.freedv_modes[0][1], tx_msg=msg, DEBUG=0, squelch=1)
|
|
for mode, index in conf.freedv_modes:
|
|
item = self.freedv_menu.AppendRadioItem(-1, mode)
|
|
self.freedv_menu_items[index] = item
|
|
self.Bind(wx.EVT_MENU, self.OnFreedvMenu, item)
|
|
if '700D' in mode:
|
|
item.Check()
|
|
QS.freedv_set_options(mode=index)
|
|
if conf.button_layout == 'Large screen':
|
|
b = QuiskCheckbutton(frame, self.OnBtnMode, 'FDV-U')
|
|
b.char_shortcut = 'F'
|
|
self.btnFreeDV = WrapMenu(b, self.freedv_menu)
|
|
self.modeButns.ReplaceButton(n_freedv, self.btnFreeDV)
|
|
else:
|
|
self.btnFreeDV = self.modeButns.AddMenu('FDV-U', self.freedv_menu)
|
|
try:
|
|
ok = QS.freedv_open()
|
|
except:
|
|
traceback.print_exc()
|
|
ok = 0
|
|
if not ok:
|
|
conf.add_freedv_button = False
|
|
if conf.button_layout == 'Large screen':
|
|
self.modeButns.GetButtons()[n_freedv].Enable(0)
|
|
else:
|
|
self.modeButns.Enable('FDV-U', False)
|
|
if conf.add_imd_button:
|
|
val = 500
|
|
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.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)
|
|
b = QuiskCheckbutton(frame, None, str(self.filterAdjBw1))
|
|
b = WrapSlider(b, self.OnBtnAdjFilter, slider_value=self.filterAdjBw1, wintype='filter')
|
|
self.filterButns.ReplaceButton(5, b)
|
|
right_row2 = self.filterButns.GetButtons()
|
|
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', ('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', 'Scope', 'Config', t)
|
|
self.screenBtnGroup = RadioBtnPopup(frame, self.OnBtnScreen, labels, conf.default_screen)
|
|
# 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)
|
|
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)
|
|
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")
|
|
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='#77DD77')
|
|
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_LEFT|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, wx.ALIGN_LEFT, True)
|
|
b = QuiskPushbutton(frame, self.OnSmeterRightDown, '..')
|
|
szr = wx.BoxSizer(wx.HORIZONTAL)
|
|
b_smeter = szr
|
|
szr.Add(self.smeter, 1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL)
|
|
szr.Add(b, 0, flag=wx.ALIGN_RIGHT|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 = wx.Menu()
|
|
item = self.smeter_menu.Append(-1, 'S-meter 1')
|
|
self.Bind(wx.EVT_MENU, self.OnSmeterMeterA, item)
|
|
item = self.smeter_menu.Append(-1, 'S-meter 5')
|
|
self.Bind(wx.EVT_MENU, self.OnSmeterMeterB, item)
|
|
item = self.smeter_menu.Append(-1, 'Frequency 2')
|
|
self.Bind(wx.EVT_MENU, self.OnSmeterFrequencyA, item)
|
|
item = self.smeter_menu.Append(-1, 'Frequency 10')
|
|
self.Bind(wx.EVT_MENU, self.OnSmeterFrequencyB, item)
|
|
item = self.smeter_menu.Append(-1, 'Audio 1')
|
|
self.Bind(wx.EVT_MENU, self.OnSmeterAudioA, item)
|
|
item = self.smeter_menu.Append(-1, 'Audio 5')
|
|
self.Bind(wx.EVT_MENU, self.OnSmeterAudioB, item)
|
|
# 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]:
|
|
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:
|
|
gbs.Add(b, (2, col), (1, 2), flag=flag)
|
|
col += 2
|
|
col = button_start_col
|
|
for b in left_row3:
|
|
gbs.Add(b, (3, col), (1, 2), flag=flag)
|
|
col += 2
|
|
col = 15
|
|
for b in self.modeButns.GetButtons():
|
|
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):
|
|
gbs.Add(right_row2[i], (2, col), (1, 2), flag=flag)
|
|
gbs.Add(right_row3[i], (3, col), (1, 2), flag=flag)
|
|
col += 2
|
|
gbs.Add(b_onoff, (0, button_start_col), (1, 1),
|
|
flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=self.freqDisplay.border)
|
|
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:
|
|
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():
|
|
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:
|
|
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 NewDVmeter(self):
|
|
if conf.add_freedv_button:
|
|
snr = QS.freedv_get_snr()
|
|
txt = QS.freedv_get_rx_char()
|
|
self.graph.ScrollMsg(txt)
|
|
self.waterfall.ScrollMsg(txt)
|
|
else:
|
|
snr = 0.0
|
|
t = " SNR %3.0f" % snr
|
|
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.
|
|
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:
|
|
bw = int(self.filterButns.buttons[index].GetLabel())
|
|
except:
|
|
bw = int(self.filterButns.buttons[0].GetLabel())
|
|
self.OnBtnFilter(None, bw)
|
|
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'):
|
|
center = max(1500, 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 OnFreedvMenu(self, event):
|
|
idd = event.GetId()
|
|
text = self.freedv_menu.GetLabel(idd)
|
|
if text[0:5] == 'Upper':
|
|
self.btnFreeDV.SetLabel('FDV-U')
|
|
self.btnFreeDV.Refresh()
|
|
self.OnBtnMode(None, 'FDV-U')
|
|
return
|
|
if text[0:5] == 'Lower':
|
|
self.btnFreeDV.SetLabel('FDV-L')
|
|
self.btnFreeDV.Refresh()
|
|
self.OnBtnMode(None, 'FDV-L')
|
|
return
|
|
for mode, index in conf.freedv_modes:
|
|
if mode == text:
|
|
break
|
|
else:
|
|
print ("Failure in OnFreedvMenu")
|
|
return
|
|
mode = QS.freedv_set_options(mode=index)
|
|
if mode != index: # change to new mode failed
|
|
self.freedv_menu_items[mode].Check(1)
|
|
pos = (self.width//2, self.height//2)
|
|
dlg = wx.MessageDialog(self.main_frame, "No codec2 support for mode " + text, "FreeDV Modes", wx.OK, pos)
|
|
dlg.ShowModal()
|
|
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 == '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)
|
|
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 == 'WFall':
|
|
self.screen.SetSashPosition(sash)
|
|
def OnBtnFileRecord(self, event):
|
|
if event.GetEventObject().GetValue():
|
|
QS.set_file_name(record_button=1)
|
|
else:
|
|
QS.set_file_name(record_button=0)
|
|
def ChangeYscale(self, event):
|
|
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)
|
|
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):
|
|
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)
|
|
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):
|
|
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, True)
|
|
Hardware.OnButtonPTT(event)
|
|
def SetPTT(self, value):
|
|
if self.pttButton:
|
|
self.pttButton.SetValue(value, False)
|
|
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:
|
|
QS.set_split_rxtx(self.split_rxtx_play)
|
|
self.rxFreq = self.oldRxFreq
|
|
d = self.sample_rate * 49 // 100 # Move rxFreq on-screen
|
|
if self.rxFreq < -d:
|
|
self.rxFreq = -d
|
|
elif self.rxFreq > d:
|
|
self.rxFreq = d
|
|
else:
|
|
QS.set_split_rxtx(0)
|
|
self.oldRxFreq = self.rxFreq
|
|
self.rxFreq = self.txFreq
|
|
self.screen.SetTxFreq(self.txFreq, self.rxFreq)
|
|
QS.set_tune(self.rxFreq + self.ritFreq, self.txFreq)
|
|
def 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):
|
|
if self.split_locktx:
|
|
self.split_locktx = False
|
|
self.splitButton.SetLabel("Split")
|
|
else:
|
|
self.split_locktx = True
|
|
self.splitButton.SetLabel("LkSplit")
|
|
self.splitButton.Refresh()
|
|
def OnMenuSplitRev(self, event): # Called when the Split Reverse button is pressed
|
|
if self.split_rxtx:
|
|
rx = self.rxFreq
|
|
self.rxFreq = self.txFreq
|
|
self.ChangeHwFrequency(rx, self.VFO, 'FreqEntry')
|
|
def OnMenuSplitCtlTx(self, event):
|
|
self.split_hamlib_tx = True
|
|
def OnMenuSplitCtlRx(self, event):
|
|
self.split_hamlib_tx = False
|
|
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 and self.pttButton:
|
|
Hardware.OnButtonPTT(event)
|
|
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.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
|
|
def OnBtnFilePlay(self, event):
|
|
btn = event.GetEventObject()
|
|
enable = btn.GetValue()
|
|
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 = event.GetEventObject()
|
|
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 OnBtnNB(self, event):
|
|
index = event.GetEventObject().index
|
|
QS.set_noise_blanker(index)
|
|
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):
|
|
"""Change the VFO and tuning frequencies, and notify the hardware.
|
|
|
|
tune: the new tuning frequency in +- sample_rate/2;
|
|
vfo: the new vfo frequency in Hertz; this is the RF frequency at zero Hz audio
|
|
source: a string indicating the source or widget requesting the change;
|
|
band: if source is "BtnBand", the band requested;
|
|
event: for a widget, the event (used to access control/shift key state).
|
|
|
|
Try to update the hardware by calling Hardware.ChangeFrequency().
|
|
The hardware will reply with the updated frequencies which may be different
|
|
from those requested; use and display the returned tune and vfo.
|
|
"""
|
|
if 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)
|
|
def ChangeDisplayFrequency(self, tune, vfo):
|
|
'Change the frequency displayed by Quisk'
|
|
change = 0
|
|
if tune != self.txFreq:
|
|
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 self.w_phase: # Phase adjustment screen can not change its VFO
|
|
self.w_phase.Destroy()
|
|
self.w_phase = None
|
|
ampl, phase = self.GetAmplPhase(0)
|
|
QS.set_ampl_phase(ampl, phase, 0)
|
|
ampl, phase = self.GetAmplPhase(1)
|
|
QS.set_ampl_phase(ampl, phase, 1)
|
|
if change:
|
|
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):
|
|
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)
|
|
self.ChangeHwFrequency(tune, vfo, 'FreqEntry')
|
|
if rx_freq and self.split_rxtx: # Frequency must be on-screen
|
|
tune = rx_freq - self.VFO
|
|
self.rxFreq = tune
|
|
self.screen.SetTxFreq(self.txFreq, tune)
|
|
QS.set_tune(tune + self.ritFreq, self.txFreq)
|
|
def OnBtnMode(self, event, mode=None):
|
|
if event is None: # called by application
|
|
self.modeButns.SetLabel(mode)
|
|
else: # called by button
|
|
mode = self.modeButns.GetLabel()
|
|
Hardware.ChangeMode(mode)
|
|
self.mode = mode
|
|
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.OnBtnMode(None, mode)
|
|
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.OnBtnMode(None, mode)
|
|
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(3)
|
|
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.OnBtnMode(None, mode)
|
|
self.txFreq = self.VFO = -1 # demand change
|
|
self.ChangeBand(band)
|
|
self.ChangeHwFrequency(tune, vfo, 'BtnBand', band=band)
|
|
if self.pttButton:
|
|
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): # Change to a new band based on the frequency
|
|
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')
|
|
self.OnBtnMode(None, mode)
|
|
self.ChangeBand(band)
|
|
break
|
|
def ChangeBand(self, band):
|
|
Hardware.ChangeBand(band)
|
|
self.waterfall.SetPane2(self.wfallScaleZ.get(band, (conf.waterfall_y_scale, conf.waterfall_y_zero)))
|
|
s, z = self.graphScaleZ.get(band, (conf.graph_y_scale, conf.graph_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 = 0 # Force an update
|
|
self.ChangeHwFrequency(tune, vfo, 'BtnUpDown')
|
|
def GetAmplPhase(self, is_tx):
|
|
if "panadapter" in conf.bandAmplPhase:
|
|
band = "panadapter"
|
|
else:
|
|
band = self.lastBand
|
|
try:
|
|
if is_tx:
|
|
lst = self.bandAmplPhase[band]["tx"]
|
|
else:
|
|
lst = self.bandAmplPhase[band]["rx"]
|
|
except KeyError:
|
|
return (0.0, 0.0)
|
|
length = len(lst)
|
|
if length == 0:
|
|
return (0.0, 0.0)
|
|
elif length == 1:
|
|
return lst[0][2], lst[0][3]
|
|
elif self.VFO < lst[0][0]: # before first data point
|
|
i1 = 0
|
|
i2 = 1
|
|
elif lst[length - 1][0] < self.VFO: # after last data point
|
|
i1 = length - 2
|
|
i2 = length - 1
|
|
else:
|
|
# Binary search for the bracket VFO
|
|
i1 = 0
|
|
i2 = length
|
|
index = (i1 + i2) // 2
|
|
for i in range(length):
|
|
diff = lst[index][0] - self.VFO
|
|
if diff < 0:
|
|
i1 = index
|
|
elif diff > 0:
|
|
i2 = index
|
|
else: # equal VFO's
|
|
return lst[index][2], lst[index][3]
|
|
if i2 - i1 <= 1:
|
|
break
|
|
index = (i1 + i2) // 2
|
|
d1 = self.VFO - lst[i1][0] # linear interpolation
|
|
d2 = lst[i2][0] - self.VFO
|
|
dx = d1 + d2
|
|
ampl = (d1 * lst[i2][2] + d2 * lst[i1][2]) / dx
|
|
phas = (d1 * lst[i2][3] + d2 * lst[i1][3]) / dx
|
|
return ampl, phas
|
|
def PostStartup(self): # called once after sound attempts to start
|
|
self.config_screen.OnGraphData(None) # update config in case sound is not running
|
|
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 self.pttButton:
|
|
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 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
|
|
def OnHotKey(self, event=None): # This is only used if the hot key does NOT work when Quisk is hidden.
|
|
if event:
|
|
event.Skip()
|
|
self.hot_key_ptt_change = True
|
|
self.hot_key_ptt_is_down = True
|
|
elif self.hot_key_ptt_is_down and not wx.GetKeyState(conf.hot_key_ptt1):
|
|
self.hot_key_ptt_is_down = False
|
|
def HotKeyPoll(self): # This is only used if the hot key DOES work when Quisk is hidden.
|
|
if wx.GetKeyState(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
|
|
self.hot_key_ptt_change = self.hot_key_ptt_is_down and not self.hot_key_ptt_was_down
|
|
def OnReadSound(self): # called at frequent intervals
|
|
if self.hamlib_com1_handler:
|
|
self.hamlib_com1_handler.Process()
|
|
if self.hamlib_com2_handler:
|
|
self.hamlib_com2_handler.Process()
|
|
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 self.pttButton: # Manage the PTT button using VOX, hardware switch, hot keys and WAV file play
|
|
ptt_button_down = self.pttButton.GetValue()
|
|
ptt = None
|
|
if self.hardware_ptt_key_state == 0 and QS.get_hardware_ptt() == 1: # Wait for PTT switch ON
|
|
ptt = True
|
|
self.hardware_ptt_key_state = 1
|
|
elif self.hardware_ptt_key_state == 1: # Wait for PTT switch OFF
|
|
if QS.get_hardware_ptt() == 1:
|
|
ptt = True
|
|
else:
|
|
ptt = False
|
|
self.hardware_ptt_key_state = 0
|
|
if self.useVOX:
|
|
if self.file_play_state == 0:
|
|
if QS.is_vox():
|
|
ptt = True
|
|
else:
|
|
ptt = False
|
|
elif self.file_play_state == 2 and QS.is_vox(): # VOX tripped between file play repeats
|
|
self.TurnOffFilePlay()
|
|
ptt = 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()
|
|
else:
|
|
self.OnHotKey()
|
|
if conf.hot_key_ptt_toggle:
|
|
if self.hot_key_ptt_change:
|
|
ptt = not ptt_button_down
|
|
if ptt:
|
|
self.TurnOffFilePlay()
|
|
else:
|
|
if self.hot_key_ptt_is_down:
|
|
ptt = True
|
|
if not ptt_button_down:
|
|
self.TurnOffFilePlay()
|
|
elif self.hot_key_ptt_was_down:
|
|
ptt = False
|
|
self.hot_key_ptt_was_down = self.hot_key_ptt_is_down
|
|
self.hot_key_ptt_change = False
|
|
if ptt is True and not ptt_button_down:
|
|
self.SetPTT(True)
|
|
elif ptt is False and ptt_button_down:
|
|
self.SetPTT(False)
|
|
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:
|
|
data = QS.get_graph(0, 1.0, 0) # get raw data
|
|
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:
|
|
data = QS.get_graph(1, self.zoom, float(self.zoom_deltaf)) # get FFT data and discard
|
|
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.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
|
|
if self.mode in ('FDV-U', 'FDV-L'):
|
|
self.NewDVmeter()
|
|
else:
|
|
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 self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks:
|
|
self.heart_time0 = self.timer
|
|
Hardware.HeartBeat()
|
|
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.timer - self.fewsec_time0 > 3.0:
|
|
# self.fewsec_time0 = self.timer
|
|
# print ('fewswc')
|
|
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 main():
|
|
"""If quisk is installed as a package, you can run it with quisk.main()."""
|
|
App()
|
|
application.MainLoop()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|