6 Commits
Author SHA1 Message Date
James C. Ahlstrom 1c209457dc Quisk version 4.2.41 2025-01-30 13:48:38 -05:00
James C. Ahlstrom 7e8a0846f7 Quisk version 4.2.40 2024-11-15 16:39:47 -05:00
James C. Ahlstrom fb888a2e08 Quisk version 4.2.39 2024-10-01 14:22:23 -04:00
James C. Ahlstrom 78a1ca3d60 Quisk version 4.2.38 2024-09-01 13:57:53 -04:00
James C. Ahlstrom 14a80f09d5 Quisk version 4.2.37 2024-08-21 15:25:10 -04:00
James C. Ahlstrom 1c9f81254b Quisk version 4.2.36 2024-08-03 13:56:30 -04:00
16 changed files with 808 additions and 188 deletions
+42
View File
@@ -1,3 +1,45 @@
Quisk Version 4.2.41 January 2025
==================================
I made some improvements to the top-level Config/Radios screen. There is now an option to
make a new radio by copying settings from an existing radio. I worked around a bug in wxPython
and gtk3 that caused drawing problems on the config screens. This problem is not present on Windows.
Quisk Version 4.2.40 November 2024
===================================
Python 3.13 eliminated the telnetlib module, so I wrote a replacement. I fixed a bug in window sizing.
If you use the Dx Cluster feature, please report any problems. Dx Cluster used to use the telnetlib module
but is now new code.
I updated the install to Python 3.13. Please report any problems with Python 3.13.
Quisk Version 4.2.39 October 2024
==================================
I added an optional Hardware method PollGuiControl(self). If you define this in your Hardware file,
the GUI thread will call it frequently. It can be used to set PTT from your hardware.
I am trying to work around a wxPython quirk that causes mouse wheel tuning to fail. Please test.
Quisk Version 4.2.38 September 2024
====================================
This is a bugfix release. I fixed a bug in the Hamlib serial port when using a physical TTY instead
of a pseudo-tty. The Hamlib ZZFA command now changes the receive frequency instead of the transmit
frequency. I fixed some window sizing issues.
Quisk Version 4.2.37 August 2024
=================================
Quisk now has a new option "Fixed tune offset" on the Config/radio/Options screen. This is meant for
Softrock radios that need a constant audio frequency to simplify amplitude and phase corrections.
The value zero means no change to Quisk tuning. Otherwise enter the fixed frequency offset from center.
If you are using the obsolete "mouse_tune_method = 1" please delete it and use this new feature.
If you use softrock_tune_vfo.py as your hardware file, you should replace it with softrock/hardware_usb.py.
Quisk now works on Python 3.12. And I fixed a bug in the small screen graph width.
Quisk Version 4.2.36 August 2024
=================================
I added two new color schemes by Michael, DK1MI. The On/Off button color and text color can
now be configured. (Michael, send me the final colors). There are a few other small changes.
Quisk Version 4.2.35 June 2024 Quisk Version 4.2.35 June 2024
=============================== ===============================
There is a new item "File play level" on the Config/radio/Sound screen to reduce the volume when playing files. There is a new item "File play level" on the Config/radio/Sound screen to reduce the volume when playing files.
+1 -1
View File
@@ -1,2 +1,2 @@
#Quisk version 4.2.35 #Quisk version 4.2.41
from .quisk import main from .quisk import main
+184 -70
View File
@@ -48,6 +48,7 @@ name2format = {
# Increasing the software version will display a message to re-read the soapy device. # Increasing the software version will display a message to re-read the soapy device.
soapy_software_version = 3 soapy_software_version = 3
wxpython_gtk3_bug = 0
def FormatKhz(dnum): # Round to 3 decimal places; remove ending ".000" def FormatKhz(dnum): # Round to 3 decimal places; remove ending ".000"
t = "%.3f" % dnum t = "%.3f" % dnum
@@ -284,10 +285,8 @@ class Configuration:
self.settings_changed = True self.settings_changed = True
errors = errors + "Failed to set %s to %s using format %s\n" % (k, v, fmt) errors = errors + "Failed to set %s to %s using format %s\n" % (k, v, fmt)
#traceback.print_exc() #traceback.print_exc()
if conf.color_scheme == 'B': if conf.color_scheme != 'A':
conf.__dict__.update(conf.color_scheme_B) conf.__dict__.update(getattr(conf, 'color_scheme_' + conf.color_scheme))
elif conf.color_scheme == 'C':
conf.__dict__.update(conf.color_scheme_C)
self.RequiredValues(radio_dict) # Why not update conf too??? This only updates the radio_dict. self.RequiredValues(radio_dict) # Why not update conf too??? This only updates the radio_dict.
if errors: if errors:
dlg = wx.MessageDialog(None, errors, dlg = wx.MessageDialog(None, errors,
@@ -330,7 +329,7 @@ class Configuration:
path = self.NormPath(path) path = self.NormPath(path)
if not os.path.isfile(path): if not os.path.isfile(path):
dlg = wx.MessageDialog(None, dlg = wx.MessageDialog(None,
"Failure for hardware file %s!" % path, "Can not find the hardware file %s!" % path,
'Hardware File', wx.OK|wx.ICON_ERROR) 'Hardware File', wx.OK|wx.ICON_ERROR)
ret = dlg.ShowModal() ret = dlg.ShowModal()
dlg.Destroy() dlg.Destroy()
@@ -386,10 +385,20 @@ class Configuration:
app.bottom_widgets = dct['BottomWidgets'](app, hardware, conf, frame, gbs, vertBox) app.bottom_widgets = dct['BottomWidgets'](app, hardware, conf, frame, gbs, vertBox)
return True return True
def OnPageChanging(self, event): # Called when the top level page changes (not RadioNotebook pages) def OnPageChanging(self, event): # Called when the top level page changes (not RadioNotebook pages)
global wxpython_gtk3_bug
event.Skip() event.Skip()
notebook = event.GetEventObject() notebook = event.GetEventObject()
index = event.GetSelection() index = event.GetSelection()
page = notebook.GetPage(index) page = notebook.GetPage(index)
if sys.platform != 'win32': # Work around a bug in wxPython 4.2.1 and gtk3
w, h = application.main_frame.GetSize() # Change main window size by +/- one pixel to recalculate sizes
if wxpython_gtk3_bug:
wxpython_gtk3_bug = 0
h += 1
else:
wxpython_gtk3_bug = 1
h -= 1
application.main_frame.SetSize((w, h))
if isinstance(page, RadioNotebook): if isinstance(page, RadioNotebook):
if not page.pages: if not page.pages:
page.MakePages() page.MakePages()
@@ -401,11 +410,12 @@ class Configuration:
notebk.AddPage(self.radio_page, "Radios") notebk.AddPage(self.radio_page, "Radios")
self.radios_page_start = notebk.GetPageCount() self.radios_page_start = notebk.GetPageCount()
notebk.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanging, notebk) notebk.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanging, notebk)
if Settings[1] in Settings[2]:
page = RadioNotebook(notebk, Settings[1])
notebk.AddPage(page, "*%s*" % Settings[1])
for name in Settings[2]: for name in Settings[2]:
if name != Settings[1]:
page = RadioNotebook(notebk, name) page = RadioNotebook(notebk, name)
if name == Settings[1]:
notebk.AddPage(page, "*%s*" % name)
else:
notebk.AddPage(page, name) notebk.AddPage(page, name)
def GuessType(self): def GuessType(self):
udp = conf.use_rx_udp udp = conf.use_rx_udp
@@ -420,6 +430,14 @@ class Configuration:
elif udp > 0: elif udp > 0:
return 'HiQSDR' return 'HiQSDR'
return 'SoftRock USB' return 'SoftRock USB'
def FindNotebookPage(self, name):
for index in range(self.radios_page_start, self.notebk.GetPageCount()):
tab_text = self.notebk.GetPageText(index)
if tab_text[0] == '*':
tab_text = tab_text[1:-1]
if tab_text == name:
return index
return None
def AddRadio(self, radio_name, typ): def AddRadio(self, radio_name, typ):
radio_dict = {} radio_dict = {}
radio_dict['hardware_file_type'] = typ radio_dict['hardware_file_type'] = typ
@@ -436,13 +454,24 @@ class Configuration:
page = RadioNotebook(self.notebk, radio_name) page = RadioNotebook(self.notebk, radio_name)
self.notebk.AddPage(page, radio_name) self.notebk.AddPage(page, radio_name)
return True return True
def CopyRadio(self, old_name, new_name):
radio_dict = {}
index = Settings[2].index(old_name)
radio_dict.update(Settings[3][index])
Settings[2].append(new_name)
Settings[3].append(radio_dict)
page = RadioNotebook(self.notebk, new_name)
self.notebk.AddPage(page, new_name)
return True
def RenameRadio(self, old, new): def RenameRadio(self, old, new):
index = Settings[2].index(old) n = self.FindNotebookPage(old)
n = self.radios_page_start + index if n is None:
return
if old == Settings[1]: if old == Settings[1]:
self.notebk.SetPageText(n, "*%s*" % new) self.notebk.SetPageText(n, "*%s*" % new)
else: else:
self.notebk.SetPageText(n, new) self.notebk.SetPageText(n, new)
index = Settings[2].index(old)
Settings[2][index] = new Settings[2][index] = new
self.notebk.GetPage(n).NewName(new) self.notebk.GetPage(n).NewName(new)
if old == "ConfigFileRadio": if old == "ConfigFileRadio":
@@ -453,11 +482,9 @@ class Configuration:
index = Settings[2].index(name) index = Settings[2].index(name)
del Settings[2][index] del Settings[2][index]
del Settings[3][index] del Settings[3][index]
try: n = self.FindNotebookPage(name)
n = self.radios_page_start + index if n is not None:
self.notebk.DeletePage(n) self.notebk.DeletePage(n)
except:
pass
return True return True
def GetRadioDict(self, radio_name=None): # None radio_name means the current radio def GetRadioDict(self, radio_name=None): # None radio_name means the current radio
if radio_name: if radio_name:
@@ -479,7 +506,7 @@ class Configuration:
for rxname, data in self.receiver_data: for rxname, data in self.receiver_data:
if rxname == receiver_name: if rxname == receiver_name:
return data return data
return None return ()
def GetReceiverDatum(self, receiver_name, item_name): def GetReceiverDatum(self, receiver_name, item_name):
for rxname, data in self.receiver_data: for rxname, data in self.receiver_data:
if rxname == receiver_name: if rxname == receiver_name:
@@ -565,6 +592,8 @@ class Configuration:
# self.format4name with the items that Configuration understands. # self.format4name with the items that Configuration understands.
# Dicts and lists are Python objects. All other items are text, not Python objects. # Dicts and lists are Python objects. All other items are text, not Python objects.
# #
# This parses the quisk_conf_defaults.py file and user-defined radios in ./*pkg/quisk_hardware.py.
#
# Sections start with 16 #, section name # Sections start with 16 #, section name
# self.sections is a list of [section_name, section_data] # self.sections is a list of [section_name, section_data]
# section_data is a list of [data_name, text, fmt, help_text, values] # section_data is a list of [data_name, text, fmt, help_text, values]
@@ -597,11 +626,11 @@ class Configuration:
except: except:
traceback.print_exc() traceback.print_exc()
def _ParserConf(self, filename): def _ParserConf(self, filename):
re_AeqB = re.compile("^#?(\w+)\s*=\s*([^#]+)#*(.*)") # item values "a = b" re_AeqB = re.compile(r"^#?(\w+)\s*=\s*([^#]+)#*(.*)") # item values "a = b"
section = None section = None
data_name = None data_name = None
multi_line = False multi_line = False
fp = open(filename, "r") fp = open(filename, "r", encoding="utf8")
for line in fp: for line in fp:
line = line.strip() line = line.strip()
if not line: if not line:
@@ -986,6 +1015,7 @@ class QAdjustPhase(wx.Frame):
btnbox.Add(1, 1, proportion=10) btnbox.Add(1, 1, proportion=10)
btns.append(b) btns.append(b)
b.Bind(wx.EVT_BUTTON, self.OnBtnHelp) b.Bind(wx.EVT_BUTTON, self.OnBtnHelp)
#b.Bind(wx.EVT_BUTTON, self.OnGraphData)
for b in btns: for b in btns:
b.SetBezelWidth(3) b.SetBezelWidth(3)
b.SetBackgroundColour("#DDD") b.SetBackgroundColour("#DDD")
@@ -1220,6 +1250,7 @@ class QAdjustPhase(wx.Frame):
application.bandAmplPhase = copy.deepcopy(self.bandAmplPhase) application.bandAmplPhase = copy.deepcopy(self.bandAmplPhase)
self.dirty = False self.dirty = False
def OnBtnExit(self, event): def OnBtnExit(self, event):
#self.OnGraphData(None)
if self.dirty: if self.dirty:
dlg = wx.MessageDialog(self, dlg = wx.MessageDialog(self,
"Your changes are not saved. Do you want to save them?", "Changes Were Made", "Your changes are not saved. Do you want to save them?", "Changes Were Made",
@@ -1245,6 +1276,27 @@ The maximum slider adjustment range can be changed on the radio Hardware screen.
For more information, press the main "Help" button, then "Documentation", then "SoftRock".' For more information, press the main "Help" button, then "Documentation", then "SoftRock".'
, "Adjustment Help", style=wx.OK) , "Adjustment Help", style=wx.OK)
dlg.ShowModal() dlg.ShowModal()
def OnGraphData(self, event):
if not hasattr(self, "VFO"):
self.VFO = []
self.Tune = []
self.Gain = []
self.Phase = []
if event:
self.VFO.append(application.VFO)
self.Tune.append(self.new_tune)
self.Gain.append(self.new_amplitude + 1.0)
self.Phase.append(self.new_phase)
else:
for name in ("VFO", "Tune", "Gain", "Phase"):
print ("%s = [" % name)
lst = getattr(self, name)
for i in range(0, len(lst)):
if name[0] in "VT":
print("%d," % lst[i])
else:
print("%.5f," % lst[i])
print ("]")
class ListEditDialog(wx.Dialog): # Display a dialog with a List-Edit control, plus Ok/Cancel class ListEditDialog(wx.Dialog): # Display a dialog with a List-Edit control, plus Ok/Cancel
def __init__(self, parent, title, choice, choices, width): def __init__(self, parent, title, choice, choices, width):
@@ -1271,6 +1323,7 @@ class RadioNotebook(wx.Notebook): # The second-level notebook for each radio nam
self.SetBackgroundColour(parent.bg_color) self.SetBackgroundColour(parent.bg_color)
self.radio_name = radio_name self.radio_name = radio_name
self.pages = [] self.pages = []
self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanging)
def MakePages(self): def MakePages(self):
radio_name = self.radio_name radio_name = self.radio_name
radio_dict = local_conf.GetRadioDict(radio_name) radio_dict = local_conf.GetRadioDict(radio_name)
@@ -1301,6 +1354,20 @@ class RadioNotebook(wx.Notebook): # The second-level notebook for each radio nam
self.radio_name = new_name self.radio_name = new_name
for page in self.pages: for page in self.pages:
page.radio_name = new_name page.radio_name = new_name
def OnPageChanging(self, event):
global wxpython_gtk3_bug
event.Skip()
index = event.GetSelection()
page = self.GetPage(index)
if sys.platform != 'win32': # Work around a bug in wxPython 4.2.1 and gtk3
w, h = application.main_frame.GetSize() # Change main window size by +/- one pixel to recalculate sizes
if wxpython_gtk3_bug:
wxpython_gtk3_bug = 0
h += 1
else:
wxpython_gtk3_bug = 1
h -= 1
application.main_frame.SetSize((w, h))
class ChoiceCombo(wx.Choice): class ChoiceCombo(wx.Choice):
text_for_blank = "-blank-" text_for_blank = "-blank-"
@@ -1415,7 +1482,13 @@ class ComboCtrl(wxcombo.ComboCtrl):
wm = w wm = w
wm += charx * 5 wm += charx * 5
self.SetSizeHints(wm, self.height, 9999, self.height) self.SetSizeHints(wm, self.height, 9999, self.height)
def SetValue(self, value):
wxcombo.ComboCtrl.SetValue(self, value)
self.SetBackgroundColour(self.bgColor)
self.Refresh()
def SetSelection(self, n): # Set text to item in list box. Name conflict with wxcombo.ComboCtrl. def SetSelection(self, n): # Set text to item in list box. Name conflict with wxcombo.ComboCtrl.
self.SetBackgroundColour(self.bgColor)
self.Refresh()
try: try:
text = self.choices[n] text = self.choices[n]
except IndexError: except IndexError:
@@ -2206,7 +2279,8 @@ class ControlMixin:
if self.radio_name == Settings[1]: # changed for current radio if self.radio_name == Settings[1]: # changed for current radio
if name in ('hot_key_ptt_toggle', 'hot_key_ptt_if_hidden', 'keyupDelay', 'cwTone', 'pulse_audio_verbose_output', if name in ('hot_key_ptt_toggle', 'hot_key_ptt_if_hidden', 'keyupDelay', 'cwTone', 'pulse_audio_verbose_output',
'start_cw_delay', 'start_ssb_delay', 'maximum_tx_secs', 'quisk_serial_cts', 'quisk_serial_dsr', 'start_cw_delay', 'start_ssb_delay', 'maximum_tx_secs', 'quisk_serial_cts', 'quisk_serial_dsr',
'hot_key_ptt1', 'hot_key_ptt2', 'midi_ptt_toggle', 'TxRxSilenceMsec', 'hermes_lite2_enable'): 'hot_key_ptt1', 'hot_key_ptt2', 'midi_ptt_toggle', 'TxRxSilenceMsec', 'hermes_lite2_enable',
'fixed_tune_offset'):
setattr(conf, name, x) setattr(conf, name, x)
application.ImmediateChange(name) application.ImmediateChange(name)
elif name[0:4] in ('lin_', 'win_'): elif name[0:4] in ('lin_', 'win_'):
@@ -2237,6 +2311,9 @@ class ControlMixin:
elif hasattr(application.Hardware, "ImmediateChange"): elif hasattr(application.Hardware, "ImmediateChange"):
setattr(conf, name, x) setattr(conf, name, x)
application.Hardware.ImmediateChange(name) application.Hardware.ImmediateChange(name)
elif name[0:6] == 'keyer_':
setattr(conf, name, x)
application.ImmediateChange(name)
def FormatOK(self, value, fmt4): # Check formats integer, number, boolean def FormatOK(self, value, fmt4): # Check formats integer, number, boolean
ok, v = self.EvalItem(value, fmt4) ok, v = self.EvalItem(value, fmt4)
return ok return ok
@@ -2300,9 +2377,9 @@ class ControlMixin:
else: else:
return value return value
class BaseWindow(wx.ScrolledCanvas, ControlMixin): class BaseWindow(wx.ScrolledWindow, ControlMixin):
def __init__(self, parent): def __init__(self, parent):
wx.ScrolledCanvas.__init__(self, parent) wx.ScrolledWindow.__init__(self, parent)
ControlMixin.__init__(self, parent) ControlMixin.__init__(self, parent)
class ConfigConfig(BaseWindow): class ConfigConfig(BaseWindow):
@@ -2625,47 +2702,82 @@ class Radios(BaseWindow): # The "Radios" first-level page
def __init__(self, parent): def __init__(self, parent):
BaseWindow.__init__(self, parent) BaseWindow.__init__(self, parent)
self.SetBackgroundColour(parent.bg_color) self.SetBackgroundColour(parent.bg_color)
self.num_cols = 8 self.num_cols = 5
self.radio_name = None self.radio_name = None
self.cur_radio_text = self.AddTextL(1, 'xx', self.num_cols - 1) self.index = 0
self.SetCurrentRadioText() txt = 'Each "radio" is a block of settings that describes a radio to use with Quisk.'
self.AddTextL(1, txt, self.num_cols - 1)
self.NextRow() self.NextRow()
self.NextRow() self.NextRow()
item = self.AddTextL(1, "When Quisk starts, use the radio") self.radio_buttons = []
rb = self.AddRadioButton(1, "When Quisk starts, use the radio", self.NewTop, start=True)
rb.SetValue(True)
self.start_radio = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True) self.start_radio = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True)
self.start_radio.handler = self.OnChoiceStartup self.start_radio.handler = self.OnChoiceStartup
self.radio_buttons.append((rb, self.start_radio))
self.NextRow() self.NextRow()
item = self.AddTextL(1, "Add a new radio with the general type") rb = self.AddRadioButton(1, "Add a new radio with the general type", self.NewTop)
choices = [] choices = []
for name, data in local_conf.receiver_data: for name, data in local_conf.receiver_data:
choices.append(name) choices.append(name)
choices.sort()
self.add_type = self.AddComboCtrl(2, '', choices=choices, no_edit=True) self.add_type = self.AddComboCtrl(2, '', choices=choices, no_edit=True)
self.add_type.SetSelection(0) self.add_type.SetSelection(0)
item = self.AddTextL(3, "and name the new radio") item = self.AddTextL(3, "and name the new radio")
self.add_name = self.AddComboCtrl(4, '', choices=["My Radio", "SR with XVtr", "SoftRock"]) self.add_name = self.AddComboCtrl(4, '', choices=["My Radio", "Transverter", "SoftRock", "HL2"])
item = self.AddPushButton(5, "Add", self.OnBtnAdd) self.radio_buttons.append((rb, self.add_type, self.add_name))
self.NextRow() self.NextRow()
item = self.AddTextL(1, "Rename the radio named") rb = self.AddRadioButton(1, "Add a new radio copied from radio", self.NewTop)
self.add_from = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True)
item = self.AddTextL(3, "and name the new radio")
self.add_name2 = self.AddComboCtrl(4, '', choices=["My Radio", "Transverter", "SoftRock", "HL2"])
self.radio_buttons.append((rb, self.add_from, self.add_name2))
self.NextRow()
rb = self.AddRadioButton(1, "Rename the radio named", self.NewTop)
self.rename_old = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True) self.rename_old = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True)
item = self.AddTextL(3, "to the new name") item = self.AddTextL(3, "to the new name")
self.rename_new = self.AddComboCtrl(4, '', choices=["My Radio", "SR with XVtr", "SoftRock"]) self.rename_new = self.AddComboCtrl(4, '', choices=["My Radio", "Transverter", "SoftRock", "HL2"])
item = self.AddPushButton(5, "Rename", self.OnBtnRename) self.radio_buttons.append((rb, self.rename_old, self.rename_new))
self.NextRow() self.NextRow()
item = self.AddTextL(1, "Delete the radio named") rb = self.AddRadioButton(1, "Delete the radio named", self.NewTop)
self.delete_name = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True) self.delete_name = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True)
item = self.AddPushButton(3, "Delete", self.OnBtnDelete) self.radio_buttons.append((rb, self.delete_name))
self.NextRow() self.NextRow()
self.FitInside() self.NextRow()
self.SetScrollRate(1, 1) self.apply_btn = self.AddPushButton(3, "Apply", self.OnBtnApply)
self.NewRadioNames() self.NewRadioNames()
def SetCurrentRadioText(self): self.NewTop()
radio_dict = local_conf.GetRadioDict(self.radio_name) self.SetScrollRate(1, 1)
radio_type = radio_dict['hardware_file_type'] def NewTop(self, event=None):
if Settings[1] == "ConfigFileRadio": if event:
text = 'The current radio is ConfigFileRadio, so all settings come from the config file. The hardware type is %s.' % radio_type btn = event.GetEventObject()
else: # set first button
btn = self.radio_buttons[0][0]
for i in range(len(self.radio_buttons)):
data = self.radio_buttons[i]
rb = data[0]
if rb == btn:
self.index = i
for c in data[1:]:
c.Enable(rb == btn)
if self.index == 0:
self.apply_btn.Enable(False)
else: else:
text = "Quisk is running with settings from the radio %s. The hardware type is %s." % (Settings[1], radio_type) self.apply_btn.Enable(True)
self.cur_radio_text.SetLabel(text) def OnBtnApply(self, event):
index = self.index
ok = False
if index == 1: # add from hardware type
ok = self.OnBtnAdd()
elif index == 2: # add from existing radio
ok = self.OnBtnCopy()
elif index == 3: # rename
ok = self.OnBtnRename()
elif index == 4: # delete
ok = self.OnBtnDelete()
if ok:
self.radio_buttons[0][0].SetValue(True)
self.NewTop()
def DuplicateName(self, name): def DuplicateName(self, name):
if name in Settings[2] or name == "ConfigFileRadio": if name in Settings[2] or name == "ConfigFileRadio":
dlg = wx.MessageDialog(self, "The name already exists. Please choose a different name.", dlg = wx.MessageDialog(self, "The name already exists. Please choose a different name.",
@@ -2674,34 +2786,39 @@ class Radios(BaseWindow): # The "Radios" first-level page
dlg.Destroy() dlg.Destroy()
return True return True
return False return False
def OnBtnAdd(self, event): def OnBtnAdd(self):
name = self.add_name.GetValue().strip() name = self.add_name.GetValue().strip()
if not name or self.DuplicateName(name): if not name or self.DuplicateName(name):
return return
self.add_name.SetValue('') self.add_name.SetValue('')
typ = self.add_type.GetValue().strip() typ = self.add_type.GetValue().strip()
if local_conf.AddRadio(name, typ): if local_conf.AddRadio(name, typ):
if Settings[0] != "Ask me":
Settings[0] = name
self.NewRadioNames() self.NewRadioNames()
local_conf.settings_changed = True local_conf.settings_changed = True
def OnBtnRename(self, event): return True
def OnBtnCopy(self):
new_name = self.add_name2.GetValue().strip()
if not new_name or self.DuplicateName(new_name):
return
self.add_name2.SetValue('')
old_name = self.add_from.GetValue()
if local_conf.CopyRadio(old_name, new_name):
self.NewRadioNames()
local_conf.settings_changed = True
return True
def OnBtnRename(self):
old = self.rename_old.GetValue() old = self.rename_old.GetValue()
new = self.rename_new.GetValue().strip() new = self.rename_new.GetValue().strip()
if not old or not new or self.DuplicateName(new): if not old or not new or self.DuplicateName(new):
return return
self.rename_new.SetValue('') self.rename_new.SetValue('')
if local_conf.RenameRadio(old, new): if local_conf.RenameRadio(old, new):
if old == 'ConfigFileRadio' and Settings[1] == "ConfigFileRadio": if Settings[1] == old:
Settings[1] = new Settings[1] = new
elif Settings[1] == old:
Settings[1] = new
self.SetCurrentRadioText()
if Settings[0] != "Ask me":
Settings[0] = new
self.NewRadioNames() self.NewRadioNames()
local_conf.settings_changed = True local_conf.settings_changed = True
def OnBtnDelete(self, event): return True
def OnBtnDelete(self):
name = self.delete_name.GetValue() name = self.delete_name.GetValue()
if not name: if not name:
return return
@@ -2713,6 +2830,7 @@ class Radios(BaseWindow): # The "Radios" first-level page
if ret == wx.ID_OK and local_conf.DeleteRadio(name): if ret == wx.ID_OK and local_conf.DeleteRadio(name):
self.NewRadioNames() self.NewRadioNames()
local_conf.settings_changed = True local_conf.settings_changed = True
return True
def OnChoiceStartup(self, ctrl): def OnChoiceStartup(self, ctrl):
choice = self.start_radio.GetValue() choice = self.start_radio.GetValue()
if Settings[0] != choice: if Settings[0] != choice:
@@ -2722,11 +2840,11 @@ class Radios(BaseWindow): # The "Radios" first-level page
choices = Settings[2][:] # can rename any available radio choices = Settings[2][:] # can rename any available radio
self.rename_old.SetItems(choices) self.rename_old.SetItems(choices)
self.rename_old.SetSelection(0) self.rename_old.SetSelection(0)
if "ConfigFileRadio" in choices: self.add_from.SetItems(choices)
self.add_from.SetSelection(0)
if "ConfigFileRadio" in choices: # can not delete ConfigFileRadio
choices.remove("ConfigFileRadio") choices.remove("ConfigFileRadio")
if Settings[1] in choices: self.delete_name.SetItems(choices)
choices.remove(Settings[1])
self.delete_name.SetItems(choices) # can not delete ConfigFileRadio nor the current radio
self.delete_name.SetSelection(0) self.delete_name.SetSelection(0)
choices = Settings[2] + ["Ask me"] choices = Settings[2] + ["Ask me"]
if "ConfigFileRadio" not in choices: if "ConfigFileRadio" not in choices:
@@ -2735,13 +2853,13 @@ class Radios(BaseWindow): # The "Radios" first-level page
try: # Set text in control try: # Set text in control
index = choices.index(Settings[0]) # last used radio, or new or renamed radio index = choices.index(Settings[0]) # last used radio, or new or renamed radio
except: except:
num = len(Settings[2]) num = len(Settings[2]) # number of radios
if len == 0: if num == 0: # no radios
index = 1 index = 1 # use ConfigFileRadio
elif num == 1: elif num == 1: # one radio
index = 0 index = 0 # use the one radio
else: else:
index = len(choices) - 2 index = len(choices) - 2 # use the last radio
Settings[0] = choices[index] Settings[0] = choices[index]
self.start_radio.SetSelection(index) self.start_radio.SetSelection(index)
@@ -2871,12 +2989,6 @@ class RadioSection(BaseWindow): # The pages for each section in the second-leve
hlp = "To delete a row, select it and press this button." hlp = "To delete a row, select it and press this button."
self.AddTextButtonHelp(2, "Delete Midi row", "Delete", self.OnMidiDelete, hlp) self.AddTextButtonHelp(2, "Delete Midi row", "Delete", self.OnMidiDelete, hlp)
self.NextRow() self.NextRow()
if self.section == "Timing and CW":
if col != 1:
col = 1
self.NextRow()
self.NextRow()
self.AddTextL(1, 'Midi CW key moved to "Keys"', span=4)
if not Keys: if not Keys:
self.AddColSpacer(2, 20) self.AddColSpacer(2, 20)
self.AddColSpacer(5, 20) self.AddColSpacer(5, 20)
@@ -3667,8 +3779,10 @@ class RadioSound(BaseWindow): # The Sound page in the second-level notebook for
self.NextRow() self.NextRow()
choices = (("48000", "96000", "192000"), ("0", "1"), ("0", "1"), (" ", "0", "1")) choices = (("48000", "96000", "192000"), ("0", "1"), ("0", "1"), (" ", "0", "1"))
r = 0 r = 0
if "SoftRock" in self.radio_dict['hardware_file_type']: # Samples come from sound card if "SoftRock" in self.radio_dict['hardware_file_type']:
softrock = True softrock = True # Samples come from sound card
elif hasattr(application.Hardware, "use_softrock"):
softrock = application.Hardware.use_softrock
else: else:
softrock = False softrock = False
last_row = 8 last_row = 8
+40 -10
View File
@@ -194,7 +194,7 @@ Quisk is free open source software written in Python and C. It is hosted on the
<br> <br>
<p> <p>
If you have Python 3 installed on your computer, just continue to use that version. If you have Python 3 installed on your computer, just continue to use that version.
<b>On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11.</b> <b>On Windows Quisk requires Python 3.8, 3.9, 3.10, 3.11, 3.12 or 3.13.</b>
If you want to install a more recent version of Python, uninstall the old version first. If you want to install a more recent version of Python, uninstall the old version first.
Running multiple versions of Python is possible but can be confusing and is not necessary. Running multiple versions of Python is possible but can be confusing and is not necessary.
A Quisk installation is needed for each version of Python you have. A Quisk installation is needed for each version of Python you have.
@@ -204,7 +204,7 @@ A Quisk installation is needed for each version of Python you have.
<p> <p>
Windows does not include Python, so you must first install a recent version of 64-bit Python 3. Windows does not include Python, so you must first install a recent version of 64-bit Python 3.
If you already have 64-bit Python 3 installed, just keep using it. If you already have 64-bit Python 3 installed, just keep using it.
<b>On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11.</b> <b>On Windows Quisk requires Python 3.8, 3.9, 3.10, 3.11, 3.12 or 3.13.</b>
There may be newer versions of Python, but until support is added, use the versions listed above. There may be newer versions of Python, but until support is added, use the versions listed above.
Python is available from <a href="http://www.python.org/">http://www.python.org</a>. Python is available from <a href="http://www.python.org/">http://www.python.org</a>.
There is an option to <b>Add Python to PATH</b>. You MUST select There is an option to <b>Add Python to PATH</b>. You MUST select
@@ -239,17 +239,24 @@ python -m pip install --upgrade pyserial
<br><br> <br><br>
python -m pip install --upgrade quisk python -m pip install --upgrade quisk
<br><br> <br><br>
You should then be able to start Quisk with the command "quisk".
You can also start Quisk with "python -m quisk".
To create a Quisk shortcut on your desktop, right-click an empty space and select "New" and "Shortcut". To create a Quisk shortcut on your desktop, right-click an empty space and select "New" and "Shortcut".
Use "quisk.exe" as the command and "Quisk" as the name. Enter this as the location of the item:
The "quisk.exe" program is in <b>your_install_directory</b>\Scripts. </p>
<br>
<pre>
pythonw -m quisk
</pre>
<br>
<p>
and then name the shortcut, perhaps "Quisk".
If you check the properties you will see that Windows expanded the location and start directory to the full path.
If you are curious, Quisk and its Python source files are installed in "<b>your_install_directory</b>\Lib\site-packages\quisk". If you are curious, Quisk and its Python source files are installed in "<b>your_install_directory</b>\Lib\site-packages\quisk".
If you change to that directory you can run Quisk with "python quisk.py". If you change to that directory you can run Quisk with "pythonw quisk.py".
</p> </p>
<br> <br>
<p> <p>
To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios. To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios.
Add a radio from the list of hardware radios available. Or add the best match.
Then set your sound devices for that radio; the device for the radio speakers, the microphone and so forth. All configuration Then set your sound devices for that radio; the device for the radio speakers, the microphone and so forth. All configuration
is (mostly) from the Config button. Ignore old directions and don't bother with a config file. is (mostly) from the Config button. Ignore old directions and don't bother with a config file.
</p> </p>
@@ -379,6 +386,7 @@ You run Quisk by running the file quisk.py with python3. For example, "python3 /
You can create a desktop icon to do that, but the method depends on your version of Linux. You can create a desktop icon to do that, but the method depends on your version of Linux.
<b>Never run Quisk with root privelege by using sudo. Fix the permission problem instead.</b> <b>Never run Quisk with root privelege by using sudo. Fix the permission problem instead.</b>
To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios. To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios.
Add a radio from the list of hardware radios available. Or add the best match.
Then set your sound devices for that radio; the device for the radio speakers, the microphone and so forth. All configuration Then set your sound devices for that radio; the device for the radio speakers, the microphone and so forth. All configuration
is (mostly) from the Config button. Ignore old directions and don't bother with a config file. is (mostly) from the Config button. Ignore old directions and don't bother with a config file.
To uninstall Quisk, just delete the quisk directory. To uninstall Quisk, just delete the quisk directory.
@@ -526,7 +534,7 @@ When you first install Quisk, you will not have any settings for your radio.
Press the Config button and go to the Radios screen. Press the Config button and go to the Radios screen.
Then create a radio by specifying the general hardware type and give it a name of your choosing. Then create a radio by specifying the general hardware type and give it a name of your choosing.
For a Hermes-Lite, specify "Hermes" as the hardware type and call it "HL2" (or some other name). For a Hermes-Lite, specify "Hermes" as the hardware type and call it "HL2" (or some other name).
Press "Add" and a new tab for your radio will appear. Look through the various settings on the HL2 tab. Press "Apply" and a new tab for your radio will appear. Look through the various settings on the HL2 tab.
The parameters for the radios are stored in the file quisk_settings.json. The parameters for the radios are stored in the file quisk_settings.json.
<br> <br>
<br> <br>
@@ -824,8 +832,14 @@ SoftRock radios use an analog mixer to change the RF signal to stereo audio, and
A high quality sound card is advisable. The analog mixer is not perfect, and it will be necessary to adjust the I and Q A high quality sound card is advisable. The analog mixer is not perfect, and it will be necessary to adjust the I and Q
signals to equal amplitude and 90 degrees phase difference. Use the Config/Config screen buttons to signals to equal amplitude and 90 degrees phase difference. Use the Config/Config screen buttons to
bring up an adjustment screen. Adjustments must be made for each band, and separately for transmit and receive. bring up an adjustment screen. Adjustments must be made for each band, and separately for transmit and receive.
The adjustment depends on both the VFO frequency and the tuning offset from the VFO. See my paper The adjustment depends on both the VFO frequency and the tuning offset from the VFO.
Quisk has an option to fix the tuning offset to a constant and to tune with the hardware.
The tuning offset then has a constant correction and it is only necessary to correct for the VFO,
a much less sensitive correction.
See my paper
<a href="http://james.ahlstrom.name/phase_corr.html">http://james.ahlstrom.name/phase_corr.html</a> for more information. <a href="http://james.ahlstrom.name/phase_corr.html">http://james.ahlstrom.name/phase_corr.html</a> for more information.
<br>
<br>
A good strategy is to pick a VFO near the band center, and record corrections at an Rx frequency of -15000, -1000, 1000 and 15000 Hertz. A good strategy is to pick a VFO near the band center, and record corrections at an Rx frequency of -15000, -1000, 1000 and 15000 Hertz.
If the corrections are sensitive to VFO, record the corrections for these same Rx frequencies at VFOs equal to If the corrections are sensitive to VFO, record the corrections for these same Rx frequencies at VFOs equal to
or slightly outside the upper and lower band edges. or slightly outside the upper and lower band edges.
@@ -834,6 +848,10 @@ You can add as many correction points as desired. The corrections are saved in t
by hand if you are a Python expert. Otherwise you can just read the values. by hand if you are a Python expert. Otherwise you can just read the values.
<br> <br>
<br> <br>
If you fix the tuning offset to a constant, you can create a correction for VFO at the band center, or
the center and both edges.
<br>
<br>
To create a Receive correction point for a given VFO and frequency, attach a signal generator to the SoftRock through an attenuator, To create a Receive correction point for a given VFO and frequency, attach a signal generator to the SoftRock through an attenuator,
and look at the image on the graph screen. If the signal is 3500 Hertz above the VFO, the image is 3500 Hertz below it. and look at the image on the graph screen. If the signal is 3500 Hertz above the VFO, the image is 3500 Hertz below it.
Choose "Measure" to measure the corrections and then "Add Cell". Choose "Measure" to measure the corrections and then "Add Cell".
@@ -1469,21 +1487,32 @@ show the devices being used. Change the Fldigi playback to QuiskDigitalInput, an
Monitor of QuiskDigitalOutput. Then everything should work. You do not need to use pavucontrol again because PulseAudio will Monitor of QuiskDigitalOutput. Then everything should work. You do not need to use pavucontrol again because PulseAudio will
remember the settings. remember the settings.
<br> <br>
<br>
Michael, DK1MI/N1BSD, has written a nice description of how to set up Fldigi. It is available
at his site <a href="https://dk1mi.radio/quisk-fldigi/">https://dk1mi.radio/quisk-fldigi/</a>.
<br>
<br> <br>
If your digital program only uses Alsa devices or you don't want to use PulseAudio, If your digital program only uses Alsa devices or you don't want to use PulseAudio,
you can use the ALSA loopback device. you can use the ALSA loopback device.
It works the same way as the Windows VAC. First create the loopback device with this command: It works the same way as the Windows VAC. First create the loopback device with this command:
<br>
<br>
<pre> <pre>
sudo modprobe snd-aloop index=19 sudo modprobe snd-aloop index=19
</pre> </pre>
<br>
<p>
The "index" is the card number, and it is best to use a number higher than your regular cards. The "index" is the card number, and it is best to use a number higher than your regular cards.
The loopback device will be gone on the next restart. The loopback device will be gone on the next restart.
You can create the loopback device every time the system starts, but the way to do that depends on your version of Linux. You can create the loopback device every time the system starts, but the way to do that depends on your version of Linux.
You could add this line to /etc/modprobe.d/alsa-base.conf: You could add this line to /etc/modprobe.d/alsa-base.conf:
</p>
<br>
<pre> <pre>
options snd-aloop index=19 options snd-aloop index=19
</pre> </pre>
<br>
<p>
Or you could add snd-aloop to /etc/modules or put the modprobe command in /etc/rc.local instead. Or you could add snd-aloop to /etc/modules or put the modprobe command in /etc/rc.local instead.
Restart Linux. Now you can enter "cat /proc/asound/cards" to Restart Linux. Now you can enter "cat /proc/asound/cards" to
print out your sound cards, and you should see a "Loopback" card listed. print out your sound cards, and you should see a "Loopback" card listed.
@@ -1498,6 +1527,7 @@ Your audio is now connected and you
should be able to receive digital signals. Be sure to test your should be able to receive digital signals. Be sure to test your
transmit signal off the air. You may need to reduce power to transmit signal off the air. You may need to reduce power to
improve linearity. improve linearity.
</p>
<br> <br>
<br> <br>
+80 -32
View File
@@ -1,9 +1,9 @@
from __future__ import absolute_import
# This code was contributed by Christof, DJ4CM. Many Thanks!! # This code was contributed by Christof, DJ4CM. Many Thanks!!
# Modified November 2024 by N2ADR to remove deprecated telnetlib module.
import threading import threading
import socket
import time import time
import telnetlib
import quisk_conf_defaults as conf import quisk_conf_defaults as conf
class DxEntry: class DxEntry:
@@ -81,61 +81,109 @@ class DxEntry:
class DxCluster(threading.Thread): class DxCluster(threading.Thread):
def __init__(self): def __init__(self):
self.do_init = 1 self.error = 'Starting'
self.dxSpots = []
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.doQuit = threading.Event() self.doQuit = threading.Event()
self.dxSpots = []
self.doQuit.clear() self.doQuit.clear()
self.dxLock = threading.Lock()
self.addr = conf.dxClHost + ':' + str(conf.dxClPort)
self.msg_no_spots = "No DX Cluster data from " + self.addr
self.msg_one_spot = '1 DX spot received from ' + self.addr
self.msg_spots = ' DX spots received from ' + self.addr
def run(self): def run(self):
self.telnetInit()
self.telnetConnect() self.telnetConnect()
if self.error:
self.sock.close()
return
while not self.doQuit.isSet(): while not self.doQuit.isSet():
try: try:
self.telnetRead() by = self.sock.recv(1024)
except TimeoutError:
continue
except: except:
self.tn.close() by = b''
time.sleep(20) if by:
with self.dxLock:
self.bytes += by
else:
self.sock.close()
self.error = "Restarting " + self.addr
time.sleep(2)
if not self.doQuit.isSet(): if not self.doQuit.isSet():
self.telnetConnect() self.telnetConnect()
self.tn.close() if self.error:
self.sock.close()
def setListener (self, listener): return
self.listener = listener self.sock.close()
def telnetInit(self):
self.tn = telnetlib.Telnet()
def telnetConnect(self): def telnetConnect(self):
self.bytes = bytearray(0)
self.error = 'Starting'
self.sock = socket.socket()
self.sock.settimeout(20)
try:
self.sock.connect( (conf.dxClHost, conf.dxClPort) )
except:
self.error = "Failed to connect to " + self.addr
return
self.sock.settimeout(1)
for i in range(10): for i in range(10):
try: try:
self.tn.open(conf.dxClHost, conf.dxClPort, 10) self.bytes += self.sock.recv(1024)
self.tn.read_until(b"login:", 10)
self.tn.write(conf.user_call_sign.encode('utf-8', errors='ignore') + b"\n") # user_call_sign may be Unicode
break
except: except:
time.sleep(0.5) pass
if b"login:" in self.bytes:
break
else:
self.error = 'No "login:" prompt from ' + self.addr
return
self.sock.sendall(conf.user_call_sign.encode('utf-8', errors='ignore') + b"\r\n")
if conf.dxClPassword: if conf.dxClPassword:
self.tn.read_until(b"Password: ") for i in range(10):
self.tn.write(conf.dxClPassword.encode('utf-8', errors='ignore') + b"\n") try:
self.bytes += self.sock.recv(1024)
except:
pass
if b"Password:" in self.bytes:
break
else:
self.error = 'No "Password:" prompt from ' + self.addr
return
self.sock.sendall(conf.dxClPassword.encode('utf-8', errors='ignore') + b"\r\n")
self.error = ''
self.bytes = bytearray(0)
def telnetRead(self): def Poll(self):
message = self.tn.read_until(b'\n', 60).decode(encoding='utf-8', errors='replace') with self.dxLock:
if self.doQuit.isSet() == False: index = self.bytes.find(b"\n")
dxEntry = DxEntry(); if index >= 0:
message = self.bytes[0:index + 1]
self.bytes = self.bytes[index + 1:]
else:
return
message = message.decode(encoding='utf-8', errors='replace')
dxEntry = DxEntry()
if dxEntry.parseMessage(message): if dxEntry.parseMessage(message):
for i, listElement in enumerate(self.dxSpots): for i, listElement in enumerate(self.dxSpots):
if (listElement.equal(dxEntry)): if (listElement.equal(dxEntry)):
listElement.join (dxEntry) listElement.join (dxEntry)
return return True
if listElement.isExpired(): if listElement.isExpired():
del (self.dxSpots[i]) del (self.dxSpots[i])
self.dxSpots.append(dxEntry) self.dxSpots.append(dxEntry)
if self.listener: return True
self.listener()
def getHost(self):
return self.tn.host + ':' + str(self.tn.port)
def stop(self): def stop(self):
self.doQuit.set() self.doQuit.set()
def dxStatus(self):
if self.error:
return self.error
nSpots = len(self.dxSpots)
if nSpots == 0:
return self.msg_no_spots
elif nSpots == 1:
return self.msg_one_spot
return str(nSpots) + self.msg_spots
+2
View File
@@ -402,6 +402,8 @@ class Hardware(BaseHardware):
if do_tx: if do_tx:
try: try:
self.rx_udp_socket.send(s) self.rx_udp_socket.send(s)
if DEBUG:
self.PrintStatus(' Send', self.want_udp_status)
except: except:
pass pass
def SetVNA(self, key_down=None, vna_start=None, vna_stop=None, vna_count=None, do_tx=False): def SetVNA(self, key_down=None, vna_start=None, vna_stop=None, vna_count=None, do_tx=False):
+1
View File
@@ -0,0 +1 @@
#
+209
View File
@@ -0,0 +1,209 @@
# This file is part of the Multus project. See https://www.multus-sdr.com/.
# It controls what items appear on the Hardware configuration screen and
# provides logic for each radio.
################ Receivers Multus CW, A transceiver from the Multus project.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = "multuspkg/quisk_hardware.py"
## widgets_file_name Widget file path, rfile
# This optional file adds additional controls for the radio.
#widgets_file_name = ""
## rx_max_amplitude_correct Max ampl correct, number
# If you get your I/Q samples from a sound card, you will need to correct the
# amplitude and phase for inaccuracies in the analog hardware. The correction is
# entered using the controls from the "Rx Phase" button on the config screen.
# You must enter a positive number. This controls the range of the control.
#rx_max_amplitude_correct = 0.2
## rx_max_phase_correct Max phase correct, number
# If you get your I/Q samples from a sound card, you will need to correct the
# amplitude and phase for inaccuracies in the analog hardware. The correction is
# entered using the controls from the "Rx Phase" button on the config screen.
# You must enter a positive number. This controls the range of the control in degrees.
#rx_max_phase_correct = 10.0
## tx_level Tx Level, dict
# This is the level of the Tx audio sent to SoftRock hardware after all processing as a percentage
# number from 0 to 100.
# The level should be below 100 to allow headroom for amplitude and phase adjustments.
# Changes are immediate (no need to restart).
#tx_level = {}
## digital_tx_level Digital Tx power %, integer
# Digital modes reduce power by the percentage on the config screen.
# This is the maximum value of the slider.
#digital_tx_level = 100
## keyer_speed Keyer speed WPM, integer
# This sets the keyer speed in words per minute.
#keyer_speed = 8
#keyer_speed = 13
#keyer_speed = 18
#keyer_speed = 20
#keyer_speed = 30
#keyer_speed = 40
## keyer_type Type of keyer, text choice
# Use "Straight" mode for a straight key or a bug. Connect the tip of the key to the transceiver.
# Otherwise, select Iambic-A or Iambic-B mode of operation.
#keyer_type = "Straight"
#keyer_type = "Iambic-A"
#keyer_type = "Iambic-B"
## keyer_space Keyer space, text choice
# This sets the type of spacing between elements or letters.
# "Element" attempts to provide proper spacing between the elements of a character.
# (The keyer always provides proper spacing between elements).
# "Letter" turns on letter spacing. This attempts to provide proper spacing between characters.
#keyer_space = "Element"
#keyer_space = "Letter"
## keyer_weight Keyer weight, integer choice
# A weight of 50% means that the dit time equals the inter-element time, which is standard.
# Reducing the weight increases the inter-element time, increasing the weight reduces it.
#keyer_weight = 50
#keyer_weight = 25
#keyer_weight = 75
## keyer_paddle Keyer paddle, text choice
# "Normal" means the left paddle is DIT. "Reverse" means the left paddle is DAH.
#keyer_paddle = "Normal"
#keyer_paddle = "Reverse"
import configure
import softrock
from softrock.hardware_usb import Hardware as BaseHardware
from softrock.hardware_usb import IN, OUT, UBYTE2, UBYTE4
import _quisk as QS
DEBUG = 0
class Hardware(BaseHardware):
def __init__(self, app, conf):
# Set constant parameters here:
conf.usb_vendor_id = 0x16c0
conf.usb_product_id = 0x05dc
conf.si570_i2c_address = 0x55
conf.si570_direct_control = False
conf.si570_xtal_freq = 114285000
conf.repeater_delay = 0.25
self.ptt_count = 200
self.ptt_on = 0
BaseHardware.__init__(self, app, conf)
QS.set_sparams(multus_cw_samples=1)
self.use_softrock = True
if DEBUG:
print ("The name of this radio is", configure.Settings[1])
print ("The hardware file type is", conf.hardware_file_type)
def TransferOut(self, address, message): # message is a bytes array or an integer 0 to 255
if isinstance(message, int):
message = message.to_bytes(1, 'big')
if self.usb_dev:
self.usb_dev.ctrl_transfer(OUT, address, self.si570_i2c_address + 0x700, 0, message)
if DEBUG:
print ("USB send to 0x%X" % address, message)
def TransferIn(self, address, length):
if self.usb_dev:
recv = self.usb_dev.ctrl_transfer(IN, address, self.si570_i2c_address + 0x700, 0, length)
if DEBUG:
print ("USB receive from 0x%X" % address, recv)
return recv
return None
def open(self):
text = BaseHardware.open(self)
self.InitKeyer()
return text
def ChangeMode(self, mode):
ret = BaseHardware.ChangeMode(self, mode)
if mode in ("CWL", "CWU"):
cw_mode = b'C' # Adding "b" does not affect the value sent
if DEBUG:
print ("Mode is CW")
else:
cw_mode = b'U'
if DEBUG:
print ("Mode is not CW")
self.TransferOut(0x70, cw_mode)
return ret
def PollCwKey(self): # Called frequently from the sound thread to check the CW key status
return # Quisk is always in Rx
def PollGuiControl(self): # Called frequently from the GUI thread
self.ptt_count -= 1
if self.ptt_count <= 0:
self.ptt_count = 200
reply = self.TransferIn(0xA5, 1)
if DEBUG:
print ("PollGuiControl got", reply)
if reply:
ptt = reply[0] # This is 255 for error
if ptt in (0, 1) and ptt != self.ptt_on:
self.ptt_on = ptt
self.application.pttButton.SetValue(ptt, True)
def InitKeyer(self):
# Initialize the keyer parameters
conf = self.conf
if not hasattr(conf, "keyer_speed"):
conf.keyer_speed = 18
if not hasattr(conf, "keyer_type"):
conf.keyer_type = "Straight"
if not hasattr(conf, "keyer_space"):
conf.keyer_space = "Element"
if not hasattr(conf, "keyer_weight"):
conf.keyer_weight = 50
if not hasattr(conf, "keyer_paddle"):
conf.keyer_paddle = "Normal"
# We need to initialize in case the persistent hardware values differ from the Quisk values.
for name in ("keyer_speed", "keyer_type", "keyer_space", "keyer_weight", "keyer_paddle", "cwTone"):
self.ImmediateChange(name)
def ImmediateChange(self, name):
BaseHardware.ImmediateChange(self, name)
value = getattr(self.conf, name)
if DEBUG:
print ("ImmediateChange", name, value)
if name == "keyer_speed":
self.TransferOut(0x7B, value)
elif name == "keyer_type":
if value == "Straight":
mode = 0
elif value == "Iambic-A":
mode = 1
elif value == "Iambic-B":
mode = 2
else:
mode = 0
self.TransferOut(0x71, mode)
elif name == "keyer_space":
if value == "Element":
spacing = 0
elif value == "Letter":
spacing = 1
else:
spacing = 0
self.TransferOut(0x75, spacing)
elif name == "keyer_weight":
weight = int(value)
self.TransferOut(0x77, weight)
elif name == "keyer_paddle":
if value == "Normal":
paddle = 0
elif value == "Reverse":
paddle = 1
else:
paddle = 0
self.TransferOut(0x73, paddle)
elif name == "cwTone":
if value < 500:
tone_index = 0 # 400 Hz
elif 500 <= value < 700:
tone_index = 1 # 600 Hz
elif 700 <= value < 900:
tone_index = 2 # 800 Hz
else:
tone_index = 3 # 1000 Hz
self.TransferOut(0x7F, tone_index)
+58 -1
View File
@@ -1976,7 +1976,7 @@ static int quisk_process_demodulate(complex double * cSamples, double * dsamples
ssb_squelch(dsamples, nSamples, quisk_filter_srate, MeasureSquelch + bank); ssb_squelch(dsamples, nSamples, quisk_filter_srate, MeasureSquelch + bank);
d_delay(dsamples, nSamples, bank, SQUELCH_FFT_SIZE); d_delay(dsamples, nSamples, bank, SQUELCH_FFT_SIZE);
} }
quisk_calc_audio_graph(pow(2, 31) - 1, NULL, dsamples, nSamples, 1); //quisk_calc_audio_graph(pow(2, 31) - 1, NULL, dsamples, nSamples, 1);
nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio24p4, 2); nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio24p4, 2);
nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7); nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7);
//quisk_calc_audio_graph(pow(2, 31) - 1, NULL, dsamples, nSamples, 1); //quisk_calc_audio_graph(pow(2, 31) - 1, NULL, dsamples, nSamples, 1);
@@ -5060,6 +5060,62 @@ static PyObject * get_bandscope(PyObject * self, PyObject * args) // Called by t
return Py_None; return Py_None;
} }
#if 0
static void check_channel_delay(fft_data * ptFft)
{
int i, maxi, freq;
static int count=0, freq0;
static double gain=0, phase=0;
double d, ampl, maxa;
static double complex * samples1=NULL, * samples2;
if (samples1 == NULL) {
samples1 = (double complex *)malloc(fft_size * sizeof(double complex));
samples2 = (double complex *)malloc(fft_size * sizeof(double complex));
}
for (i = 0; i < fft_size; i++) {
samples1[i] = creal(ptFft->samples[i]);
samples2[i] = cimag(ptFft->samples[i]);
}
fftw_execute_dft(quisk_fft_plan, samples1, samples1);
fftw_execute_dft(quisk_fft_plan, samples2, samples2);
maxa = 0;
maxi = 0;
for (i = 0; i < fft_size / 2; i++) {
ampl = cabs(samples1[i]);
if (ampl > maxa) {
maxa = ampl;
maxi = i;
}
}
freq = maxi * fft_sample_rate / fft_size;
//freq = maxi * 44100 / fft_size;
gain += cabs(samples1[maxi]) / cabs(samples2[maxi]);
d = carg(samples1[maxi]) - carg(samples2[maxi]);
if (d > M_PI)
d -= M_PI * 2;
else if(d < - M_PI)
d += M_PI * 2;
phase += d;
count += 1;
if (count == 1) {
freq0 = freq;
}
else if (freq != freq0) {
gain = phase = count = 0;
return;
}
if (count > 10) {
gain /= count;
phase /= count;
//printf("Channel delay freq %6d, gain %6.3lf, phase %8.5lf radians, %6.3lf degrees\n",
// freq, gain, phase, phase * 57.29578);
printf("%6d, %6.3lf, %6.3lf\n", freq, gain, phase * 57.29578);
gain = phase = count = 0;
}
}
#endif
static void softrock_correct_fft(fft_data * ptFft, int fixit) static void softrock_correct_fft(fft_data * ptFft, int fixit)
{ // See i/Q balance in Rocky: https://www.dxatlas.com/Rocky/Advanced.asp { // See i/Q balance in Rocky: https://www.dxatlas.com/Rocky/Advanced.asp
double ampl, r1, r2, i1, i2, Zr, Zi, Pwr, Gain, Phase, OutR, OutI, S1R, S1I, S2R, S2I; double ampl, r1, r2, i1, i2, Zr, Zi, Pwr, Gain, Phase, OutR, OutI, S1R, S1I, S2R, S2I;
@@ -5205,6 +5261,7 @@ static PyObject * get_graph(PyObject * self, PyObject * args) // Called by the G
// Continue with FFT calculation // Continue with FFT calculation
for (i = 0; i < fft_size; i++) // multiply by window for (i = 0; i < fft_size; i++) // multiply by window
ptFft->samples[i] *= fft_window[i]; ptFft->samples[i] *= fft_window[i];
//check_channel_delay(ptFft);
fftw_execute_dft(quisk_fft_plan, ptFft->samples, ptFft->samples); // Calculate FFT fftw_execute_dft(quisk_fft_plan, ptFft->samples, ptFft->samples); // Calculate FFT
if (softrock_correct_active == 2) if (softrock_correct_active == 2)
softrock_correct_fft(ptFft, 0); softrock_correct_fft(ptFft, 0);
+71 -51
View File
@@ -334,6 +334,7 @@ class HamlibHandlerSerial:
self.public_name = public_name # the public name for the serial port self.public_name = public_name # the public name for the serial port
self.tune_step = 1000 self.tune_step = 1000
if sys.platform == 'win32' or public_name.startswith("/dev/"): if sys.platform == 'win32' or public_name.startswith("/dev/"):
self.port_is_mod_serial = True # self.port is from the serial module
try: try:
import serial import serial
except: except:
@@ -346,6 +347,7 @@ class HamlibHandlerSerial:
print ("The serial port %s could not be opened." % public_name) print ("The serial port %s could not be opened." % public_name)
else: else:
import tty import tty
self.port_is_mod_serial = False # self.port is a file descriptor
if os.path.lexists(public_name): if os.path.lexists(public_name):
try: try:
os.remove(public_name) os.remove(public_name)
@@ -369,7 +371,7 @@ class HamlibHandlerSerial:
def open(self): def open(self):
return return
def close(self): def close(self):
if sys.platform == 'win32': if self.port_is_mod_serial:
if self.port: if self.port:
self.port.close() self.port.close()
self.port = None self.port = None
@@ -382,7 +384,7 @@ class HamlibHandlerSerial:
def Read(self): def Read(self):
if self.port is None: if self.port is None:
return return
if sys.platform == 'win32': if self.port_is_mod_serial:
text = self.port.read(99) text = self.port.read(99)
if not isinstance(text, Q3StringTypes): if not isinstance(text, Q3StringTypes):
text = text.decode('utf-8') text = text.decode('utf-8')
@@ -433,22 +435,14 @@ class HamlibHandlerSerial:
return return
if isinstance(data, Q3StringTypes): if isinstance(data, Q3StringTypes):
data = data.encode('utf-8', errors='ignore') data = data.encode('utf-8', errors='ignore')
if sys.platform == 'win32': if self.port_is_mod_serial:
self.port.write(data) self.port.write(data)
else: else:
r, w, x = select.select((), (self.port,), (), 0) r, w, x = select.select((), (self.port,), (), 0)
if w: if w:
os.write(self.port, data) os.write(self.port, data)
def set_frequency(self, freq): def set_frequency(self, freq): # This changes the Rx frequency
tune = freq - self.app.VFO self.app.ChangeRxTxFrequency(freq, None)
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) if HAMLIB_DEBUG: print("New Freq rx,tx", self.app.txFreq + self.app.VFO, self.app.rxFreq + self.app.VFO)
def ZZAC(self, cmd, data, length): # Set or read tune steps def ZZAC(self, cmd, data, length): # Set or read tune steps
if length == 0: if length == 0:
@@ -836,6 +830,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
"""Send text back to the client.""" """Send text back to the client."""
if isinstance(text, Q3StringTypes): if isinstance(text, Q3StringTypes):
text = text.encode('utf-8', errors='ignore') text = text.encode('utf-8', errors='ignore')
if HAMLIB_DEBUG:
print ("Rig2 send", text)
try: try:
self.sock.sendall(text) self.sock.sendall(text)
except socket.error: except socket.error:
@@ -942,6 +938,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
else: else:
return 1 return 1
self.input = self.input.strip() # Here is our command line self.input = self.input.strip() # Here is our command line
if HAMLIB_DEBUG:
print ("Rig2 received", self.input)
while self.input: while self.input:
# Parse the commands and call the appropriate handlers # Parse the commands and call the appropriate handlers
self.params = '' self.params = ''
@@ -992,8 +990,12 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
if rx: if rx:
rx = rx[0] rx = rx[0]
self.Reply('Frequency', rx.txFreq + rx.VFO, 0) self.Reply('Frequency', rx.txFreq + rx.VFO, 0)
if HAMLIB_DEBUG:
print ("GetFreq rx", rx.txFreq, rx.VFO)
else: else:
self.Reply('Frequency', self.app.rxFreq + self.app.VFO, 0) self.Reply('Frequency', self.app.rxFreq + self.app.VFO, 0)
if HAMLIB_DEBUG:
print ("GetFreq app", self.app.rxFreq, self.app.txFreq, self.app.VFO)
def SetFreq(self): # The Rx frequency def SetFreq(self): # The Rx frequency
freq = self.GetParamNumber() freq = self.GetParamNumber()
try: try:
@@ -1432,12 +1434,8 @@ class ConfigStatus(wx.ScrolledCanvas):
self.MakeRow2("Sample interrupts", self.interrupts, cfile) self.MakeRow2("Sample interrupts", self.interrupts, cfile)
self.MakeRow2("Microphone or DGT level dB", level, qfile) self.MakeRow2("Microphone or DGT level dB", level, qfile)
self.MakeRow2("FFT number of points", self.fft_size, err_msg) self.MakeRow2("FFT number of points", self.fft_size, err_msg)
if conf.dxClHost: # connection to dx cluster if application.dxCluster:
nSpots = len(application.dxCluster.dxSpots) msg = application.dxCluster.dxStatus()
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) self.MakeRow2("FFT number of errors", self.fft_error, msg)
else: else:
self.MakeRow2("FFT number of errors", self.fft_error) self.MakeRow2("FFT number of errors", self.fft_error)
@@ -2121,7 +2119,7 @@ class GraphScreen(wx.Window):
"""For mouse clicks in our display, translate to our screen coordinates.""" """For mouse clicks in our display, translate to our screen coordinates."""
mouse_x, mouse_y = event.GetPosition() mouse_x, mouse_y = event.GetPosition()
win = event.GetEventObject() win = event.GetEventObject()
if win is not self: if win and win is not self:
x, y = win.GetPosition().Get() x, y = win.GetPosition().Get()
mouse_x += x mouse_x += x
mouse_y += y mouse_y += y
@@ -2218,6 +2216,7 @@ class GraphScreen(wx.Window):
self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseBtn1', event=event, rx_freq=freq) self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseBtn1', event=event, rx_freq=freq)
else: else:
self.ChangeHwFrequency(freq, self.VFO, 'MouseBtn1', event=event) self.ChangeHwFrequency(freq, self.VFO, 'MouseBtn1', event=event)
if not self.HasCapture():
self.CaptureMouse() self.CaptureMouse()
def OnLeftUp(self, event): def OnLeftUp(self, event):
if self.HasCapture(): if self.HasCapture():
@@ -2231,7 +2230,7 @@ class GraphScreen(wx.Window):
mouse_x, mouse_y = self.GetMousePosition(event) mouse_x, mouse_y = self.GetMousePosition(event)
if wx.GetKeyState(wx.WXK_SHIFT): if wx.GetKeyState(wx.WXK_SHIFT):
mouse_x -= self.filter_center * self.data_width / sample_rate mouse_x -= self.filter_center * self.data_width / sample_rate
if conf.mouse_tune_method: # Mouse motion changes the VFO frequency if False: #conf.mouse_tune_method: # Mouse motion changes the VFO frequency
x = (mouse_x - self.mouse_x) # Thanks to VK6JBL x = (mouse_x - self.mouse_x) # Thanks to VK6JBL
self.mouse_x = mouse_x self.mouse_x = mouse_x
freq = float(x) * sample_rate / self.data_width freq = float(x) * sample_rate / self.data_width
@@ -2537,12 +2536,10 @@ class WaterfallDisplay(wx.Window):
self.top_key = 8 self.top_key = 8
self.top_size = (self.top_key + 2) * (self.top_key - 1) // 2 self.top_size = (self.top_key + 2) * (self.top_key - 1) // 2
# Make the palette # Make the palette
if conf.waterfall_palette == 'B': if conf.waterfall_palette == 'A':
pal2 = conf.waterfallPaletteB
elif conf.waterfall_palette == 'C':
pal2 = conf.waterfallPaletteC
else:
pal2 = conf.waterfallPalette pal2 = conf.waterfallPalette
else:
pal2 = getattr(conf, "waterfallPalette" + conf.waterfall_palette)
red = bytearray(256) red = bytearray(256)
green = bytearray(256) green = bytearray(256)
blue = bytearray(256) blue = bytearray(256)
@@ -2636,7 +2633,7 @@ class WaterfallScreen(wx.SplitterWindow):
self.y_zero = conf.waterfall_y_zero self.y_zero = conf.waterfall_y_zero
self.zoom_control = 0 self.zoom_control = 0
wx.SplitterWindow.__init__(self, frame) wx.SplitterWindow.__init__(self, frame)
self.SetSizeHints(width, -1, width) self.SetSizeHints(width, -1)
self.SetSashGravity(0.50) self.SetSashGravity(0.50)
self.SetMinimumPaneSize(1) self.SetMinimumPaneSize(1)
self.SetSize((width, conf.waterfall_graph_size + 100)) # be able to set sash size self.SetSize((width, conf.waterfall_graph_size + 100)) # be able to set sash size
@@ -2990,7 +2987,7 @@ class MultiReceiverScreen(wx.SplitterWindow):
self.rx_zero = self.graph self.rx_zero = self.graph
self.Initialize(self.rx_zero) self.Initialize(self.rx_zero)
self.waterfall.Hide() self.waterfall.Hide()
self.SetSizeHints(self.width, -1, self.width) self.SetSizeHints(self.width, -1)
# Calculate control width # Calculate control width
rx_btn = QuiskPushbutton(self, None, "Rx 8....", style=wx.BU_EXACTFIT) 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, self.rx_btn_height = rx_btn.GetSize().Get()
@@ -3523,6 +3520,7 @@ class App(wx.App):
if not os.path.isdir(self.QuiskFilesDir): if not os.path.isdir(self.QuiskFilesDir):
self.QuiskFilesDir = DefaultConfigDir self.QuiskFilesDir = DefaultConfigDir
self.std_out_err = StdOutput(self) self.std_out_err = StdOutput(self)
self.std_out_err.Logfile("WxPython " + wx.version())
QS.set_params(quisk_is_vna=0) # We are not the VNA program QS.set_params(quisk_is_vna=0) # We are not the VNA program
# Read in configuration from the selected radio # Read in configuration from the selected radio
self.BandPlan = [] self.BandPlan = []
@@ -3613,6 +3611,7 @@ class App(wx.App):
setattr(conf, 'widgets_file_name', '') setattr(conf, 'widgets_file_name', '')
Hardware = self.Hardware Hardware = self.Hardware
self.use_fast_heart_beat = hasattr(Hardware, "FastHeartBeat") self.use_fast_heart_beat = hasattr(Hardware, "FastHeartBeat")
self.poll_gui_control = hasattr(Hardware, "PollGuiControl")
# Initialization - may be over-written by persistent state # Initialization - may be over-written by persistent state
self.local_conf.Initialize() self.local_conf.Initialize()
self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow
@@ -3823,16 +3822,16 @@ class App(wx.App):
self.std_out_err.Show(False) self.std_out_err.Show(False)
else: else:
self.std_out_err.Show(True) self.std_out_err.Show(True)
#w, h = frame.GetSize().Get() wClient, hClient = self.main_frame.GetClientSize()
#ww, hh = frame.GetClientSizeTuple() wFrame, hFrame = self.main_frame.GetSize()
#print ('Main frame: size', w, h, 'client', ww, hh) main_border_w = wFrame - wClient
#print ('Main frame: size', wFrame, hFrame, 'client', wClient, hClient, 'border', main_border_w)
# Find the data width from a list of preferred sizes; it is the width of returned graph data. # Find the data width from a list of preferred sizes; it is the width of returned graph data.
# The graph_width is the width of data_width that is displayed. # The graph_width is the width of data_width that is displayed.
# The total width of the graph is graph_width + self.graph_borders
if conf.window_width > 0: if conf.window_width > 0:
#wFrame, h = frame.GetClientSize().Get() # client window width
wFrame = conf.window_width - 15 # Thanks to Nick Abramenko, RA3PKJ
graph = GraphScreen(frame, self.width//2, self.width//2, None) # make a GraphScreen to calculate borders 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 self.graph_width = wClient - (graph.width - graph.graph_width) # less graph borders equals actual graph_width
graph.Destroy() graph.Destroy()
del graph del graph
if self.graph_width % 2 == 1: # Both data_width and graph_width are even numbers if self.graph_width % 2 == 1: # Both data_width and graph_width are even numbers
@@ -3957,14 +3956,14 @@ class App(wx.App):
frame.SetSizer(vertBox) frame.SetSizer(vertBox)
# Add the screens # Add the screens
vertBox.Add(self.config_screen, 1, wx.EXPAND) vertBox.Add(self.config_screen, 1, wx.EXPAND)
vertBox.Add(self.multi_rx_screen, 1) vertBox.Add(self.multi_rx_screen, 1, wx.EXPAND)
vertBox.Add(self.scope, 1) vertBox.Add(self.scope, 1, wx.EXPAND)
vertBox.Add(self.bandscope_screen, 1) vertBox.Add(self.bandscope_screen, 1, wx.EXPAND)
vertBox.Add(self.filter_screen, 1) vertBox.Add(self.filter_screen, 1, wx.EXPAND)
if self.rate_audio_fft: if self.rate_audio_fft:
vertBox.Add(self.audio_fft_screen, 1) vertBox.Add(self.audio_fft_screen, 1, wx.EXPAND)
vertBox.Add(self.help_screen, 1) vertBox.Add(self.help_screen, 1, wx.EXPAND)
vertBox.Add(self.station_screen) vertBox.Add(self.station_screen, 0, wx.EXPAND)
# Add the spacer # Add the spacer
vertBox.Add(Spacer(frame), 0, wx.EXPAND) vertBox.Add(Spacer(frame), 0, wx.EXPAND)
# Add the sizer for the controls # Add the sizer for the controls
@@ -3977,13 +3976,16 @@ class App(wx.App):
vertBox.AddSpacer(5) # Thanks to Christof, DJ4CM vertBox.AddSpacer(5) # Thanks to Christof, DJ4CM
# End of vertical box. # End of vertical box.
self.MakeButtons(frame, gbs) self.MakeButtons(frame, gbs)
minw = width = self.graph.width
maxw = maxh = -1
minh = 100
if conf.window_width > 0: if conf.window_width > 0:
minw = width = maxw = conf.window_width minw = maxw = conf.window_width
else:
minw = self.graph.width + main_border_w
maxw = -1
if conf.window_height > 0: if conf.window_height > 0:
minh = maxh = self.height = conf.window_height minh = maxh = self.height = conf.window_height
else:
minh = 100
maxh = -1
self.main_frame.SetSizeHints(minw, minh, maxw, maxh) self.main_frame.SetSizeHints(minw, minh, maxw, maxh)
self.main_frame.SetClientSize(wx.Size(width, self.height)) self.main_frame.SetClientSize(wx.Size(width, self.height))
if hasattr(Hardware, 'pre_open'): # pre_open() is called before open() if hasattr(Hardware, 'pre_open'): # pre_open() is called before open()
@@ -4025,7 +4027,6 @@ class App(wx.App):
if conf.dxClHost: if conf.dxClHost:
# create DX Cluster and register listener for change notification # create DX Cluster and register listener for change notification
self.dxCluster = dxcluster.DxCluster() self.dxCluster = dxcluster.DxCluster()
self.dxCluster.setListener(self.OnDxClChange)
self.dxCluster.start() self.dxCluster.start()
# Create shortcut keys for buttons # Create shortcut keys for buttons
if conf.button_layout == 'Large screen': if conf.button_layout == 'Large screen':
@@ -4376,8 +4377,6 @@ class App(wx.App):
conf.tx_ip, conf.tx_audio_port, conf.tx_ip, conf.tx_audio_port,
conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q, conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q,
0.7, conf.mic_playback_rate) 0.7, conf.mic_playback_rate)
def OnDxClChange(self):
self.station_screen.Refresh()
def OnIdle(self, event): def OnIdle(self, event):
if self.screen: if self.screen:
self.screen.OnIdle(event) self.screen.OnIdle(event)
@@ -4708,7 +4707,7 @@ class App(wx.App):
b.char_shortcut = 'l' b.char_shortcut = 'l'
self.MakeAccel(b) self.MakeAccel(b)
self.splitButton = WrapMenu(b, self.split_menu) self.splitButton = WrapMenu(b, self.split_menu)
if conf.mouse_tune_method: # Mouse motion changes the VFO frequency if False: #conf.mouse_tune_method: # Mouse motion changes the VFO frequency
self.splitButton.Enable(False) self.splitButton.Enable(False)
left_row3.append(self.splitButton) left_row3.append(self.splitButton)
b = QuiskCheckbutton(frame, self.OnBtnFDX, 'FDX', color=conf.color_test) b = QuiskCheckbutton(frame, self.OnBtnFDX, 'FDX', color=conf.color_test)
@@ -4918,7 +4917,8 @@ class App(wx.App):
self.freqDisplay.Display(self.txFreq + self.VFO) self.freqDisplay.Display(self.txFreq + self.VFO)
# On/Off button # On/Off button
if conf.button_layout == 'Large screen': if conf.button_layout == 'Large screen':
b_onoff = QuiskCheckbutton(frame, self.OnBtnOnOff, "On", color='#77DD77') b_onoff = QuiskCheckbutton(frame, self.OnBtnOnOff, "On", color=conf.color_onoff)
b_onoff.text_color = conf.color_onoff_text
b_onoff.SetValue(True, do_cmd=False) b_onoff.SetValue(True, do_cmd=False)
h = b_freqdisp.height h = b_freqdisp.height
b_onoff.SetSizeHints(h, h, h, h) b_onoff.SetSizeHints(h, h, h, h)
@@ -5874,16 +5874,31 @@ class App(wx.App):
The hardware will reply with the updated frequencies which may be different The hardware will reply with the updated frequencies which may be different
from those requested; use and display the returned tune and vfo. from those requested; use and display the returned tune and vfo.
""" """
#print ("ChangeHW", source, tune, vfo, vfo + tune)
if not self.split_rxtx: if not self.split_rxtx:
self.rxFreq = tune # rxFreq must be correct before the call to Hardware.ChangeFrequency() self.rxFreq = tune # rxFreq must be correct before the call to Hardware.ChangeFrequency()
elif rx_freq is not None: elif rx_freq is not None:
self.rxFreq = rx_freq self.rxFreq = rx_freq
rx_offset = self.rxFreq - tune
if self.screen == self.bandscope_screen: if self.screen == self.bandscope_screen:
freq = vfo + tune freq = vfo + tune
tune = freq % 10000 tune = freq % 10000
vfo = freq - tune vfo = freq - tune
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event) tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event)
self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None) self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None)
elif conf.fixed_tune_offset:
# Tune with the VFO and keep a constant audio offset from the center. Tx tune is always conf.fixed_tune_offset.
tune_freq = vfo + tune
self.rxFreq = conf.fixed_tune_offset + rx_offset
if source == "BtnUpDown":
tune_freq = vfo + conf.fixed_tune_offset
else:
vfo = tune_freq - conf.fixed_tune_offset
tune_freq, vfo = Hardware.ChangeFrequency(tune_freq, vfo, source, band, event)
self.ChangeDisplayFrequency(tune_freq - vfo, vfo, True)
else:
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event)
self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None)
def ChangeDisplayFrequency(self, tune, vfo, new_rxfreq=False): def ChangeDisplayFrequency(self, tune, vfo, new_rxfreq=False):
'Change the frequency displayed by Quisk' 'Change the frequency displayed by Quisk'
change = 0 change = 0
@@ -6380,7 +6395,7 @@ class App(wx.App):
for client in self.hamlib_clients: # Service existing clients 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 if not client.Process(): # False return indicates a closed connection; remove the handler for this client
self.hamlib_clients.remove(client) self.hamlib_clients.remove(client)
# print 'Remove', client.address # print ('Remove', client.address)
break break
def OnKeyHook(self, event): def OnKeyHook(self, event):
event.Skip() event.Skip()
@@ -6436,6 +6451,8 @@ class App(wx.App):
self.hamlib_com2_handler.Process() self.hamlib_com2_handler.Process()
if self.use_fast_heart_beat: if self.use_fast_heart_beat:
Hardware.FastHeartBeat() Hardware.FastHeartBeat()
if self.poll_gui_control:
Hardware.PollGuiControl()
if conf.do_repeater_offset: if conf.do_repeater_offset:
hold = QS.tx_hold_state(-1) hold = QS.tx_hold_state(-1)
if hold == 2: # Tx is being held for an FM repeater TX frequency shift if hold == 2: # Tx is being held for an FM repeater TX frequency shift
@@ -6616,6 +6633,9 @@ class App(wx.App):
self.ChangeDisplayFrequency(tune - vfo, vfo) self.ChangeDisplayFrequency(tune - vfo, vfo)
self.FldigiPoll() self.FldigiPoll()
self.HamlibPoll() self.HamlibPoll()
if self.dxCluster:
if self.dxCluster.Poll():
self.station_screen.Refresh()
if self.timer - self.slowheart_time0 > 0.5: if self.timer - self.slowheart_time0 > 0.5:
self.slowheart_time0 = self.timer self.slowheart_time0 = self.timer
if self.w_phase: if self.w_phase:
+95 -3
View File
@@ -1287,6 +1287,18 @@ dc_remove_bw = 100
#dc_remove_bw = 200 #dc_remove_bw = 200
#dc_remove_bw = 400 #dc_remove_bw = 400
## fixed_tune_offset Fixed tune offset, integer
# If this is zero, Quisk tunes as usual. The hardware frequency (the VFO or center frequency)
# is fixed and Quisk tunes
# within the spectrum returned by the hardware. Otherwise enter a fixed offset between the
# center frequency and the tuning frequency. Then tuning is done by the hardware.
# This is useful with Softrock hardware as
# it simplifies the corrections for amplitude and phase imbalance.
# Changes are immediate (no need to restart).
fixed_tune_offset = 0
#fixed_tune_offset = 5000
#fixed_tune_offset = 10000
@@ -1916,6 +1928,8 @@ btn_text_switch = u"\u21C4" # Character to switch left-right
color_scheme = 'A' color_scheme = 'A'
#color_scheme = 'B' #color_scheme = 'B'
#color_scheme = 'C' #color_scheme = 'C'
#color_scheme = 'DK1MI_GR'
#color_scheme = 'DK1MI_AM'
## waterfall_palette Waterfall colors, text choice ## waterfall_palette Waterfall colors, text choice
# This controls the colors used in the waterfall. The default color scheme is A, and you can change this scheme # This controls the colors used in the waterfall. The default color scheme is A, and you can change this scheme
@@ -1923,6 +1937,7 @@ color_scheme = 'A'
waterfall_palette = 'A' waterfall_palette = 'A'
#waterfall_palette = 'B' #waterfall_palette = 'B'
#waterfall_palette = 'C' #waterfall_palette = 'C'
#waterfall_palette = 'DK1MI_AM'
@@ -1957,6 +1972,8 @@ color_txline = 'red' # vertical line color for tx in graph
color_rxline = 'green' # vertical line color for rx in graph color_rxline = 'green' # vertical line color for rx in graph
color_graph_msg_fg = 'black' # text messages on the graph screen color_graph_msg_fg = 'black' # text messages on the graph screen
color_graph_msg_bg = 'lemonchiffon2' # background of text messages on the graph screen color_graph_msg_bg = 'lemonchiffon2' # background of text messages on the graph screen
color_onoff = '#77DD77' # on/off power button
color_onoff_text = 'black' # on/off power button text
# This color scheme B, a dark color scheme designed by Steve Murphy, KB8RWQ. # This color scheme B, a dark color scheme designed by Steve Murphy, KB8RWQ.
# Additional colors added by N2ADR. # Additional colors added by N2ADR.
@@ -1986,6 +2003,8 @@ color_scheme_B = {
'color_rxline' : 'green', 'color_rxline' : 'green',
'color_graph_msg_fg' : 'white', 'color_graph_msg_fg' : 'white',
'color_graph_msg_bg' : '#111111', 'color_graph_msg_bg' : '#111111',
'color_onoff' : '#77DD77',
'color_onoff_text' : 'black',
} }
# This is color scheme C: # This is color scheme C:
@@ -2023,12 +2042,73 @@ color_scheme_C = {
'color_rxline' : '#3CC918', 'color_rxline' : '#3CC918',
'color_graph_msg_fg' : '#000000', 'color_graph_msg_fg' : '#000000',
'color_graph_msg_bg' : 'lemonchiffon2', 'color_graph_msg_bg' : 'lemonchiffon2',
'color_onoff' : '#77DD77',
'color_onoff_text' : 'black',
} }
############################################################################################# #############################################################################################
# This is a green dark color scheme designed by Michael
color_scheme_DK1MI_GR = {
'color_bg' : '#0F0F0F', # Lower screen background
'color_bg_txt' : '#00CC00', # Lower screen text color
'color_graph' : '#0F0F0F', # Graph background
'color_config2' : '#008000', # color in tab row of config screen
'color_gl' : '#2F4F4F', # Lines on the graph
'color_graphticks' : '#CCCCCC', # Graph ticks
'color_graphline' : '#00FF00', # graph data line color
'color_graphlabels' : '#00FF00', # graph label color
'color_btn' : '#141414', # button color
'color_check_btn' : '#005500', # color of a check button when it is checked
'color_cycle_btn' : '#005500', # color of a cycle button when it is checked
'color_adjust_btn' : '#005500', # color of an adjustable button when it is checked
'color_test' : '#005500', # color of a button used for test (turn off for tx)
'color_freq' : '#208B57', # background color of frequency and s-meter
'color_freq_txt' : '#00FF00', # text color of frequency display
'color_entry' : '#2E2E2E', # frequency entry box
'color_entry_txt' : '#00FF00', # text color of entry box
'color_enable' : '#00CC00', # text color for an enabled button
'color_disable' : '#FF0000', # text color for a disabled button
'color_popchoice' : '#555555', # text color for button that pops up a row of buttons
'color_bandwidth' : '#003300', # color for bandwidth display; thanks to WB4JFI
'color_txline' : '#00CC00', # vertical line color for tx in graph
'color_rxline' : '#00FF00', # vertical line color for rx in graph
'color_graph_msg_fg' : '#008800', # text messages on the graph screen
'color_graph_msg_bg' : '#1E1E1E', # background of text messages on the graph screen
'color_onoff' : '#005500', # On/Off button
'color_onoff_text' : '#FF0000', # On/Off text
}
#############################################################################################
# This is an amber dark color scheme designed by Michael
color_scheme_DK1MI_AM = {
'color_bg' : '#0F0F0F', # Lower screen background
'color_bg_txt' : '#FFBF00', # Lower screen text color
'color_graph' : '#0F0F0F', # Graph background
'color_config2' : '#FF8000', # Color in tab row of config screen
'color_gl' : '#2F4F4F', # Lines on the graph
'color_graphticks' : '#CCCCCC', # Graph ticks
'color_graphline' : '#FFBF00', # Graph data line color
'color_graphlabels' : '#FFBF00', # Graph label color
'color_btn' : '#141414', # Button color
'color_check_btn' : '#865E2E', # Color of a check button when it is checked
'color_cycle_btn' : '#865E2E', # Color of a cycle button when it is checked
'color_adjust_btn' : '#865E2E', # Color of an adjustable button when it is checked
'color_test' : '#865E2E', # Color of a button used for test (turn off for tx)
'color_freq' : '#FF9933', # Background color of frequency and s-meter
'color_freq_txt' : '#000000', # Text color of frequency display
'color_entry' : '#2E2E2E', # Frequency entry box
'color_entry_txt' : '#FFBF00', # Text color of entry box
'color_enable' : '#FFBF00', # Text color for an enabled button
'color_disable' : '#FF0000', # Text color for a disabled button
'color_popchoice' : '#555555', # Text color for button that pops up a row of buttons
'color_bandwidth' : '#4B2E0F', # Color for bandwidth display
'color_txline' : '#FFBF00', # Vertical line color for tx in graph
'color_rxline' : '#FF8000', # Vertical line color for rx in graph
'color_graph_msg_fg' : '#FF8000', # Text messages on the graph screen
'color_graph_msg_bg' : '#1E1E1E', # Background of text messages on the graph screen
'color_onoff' : '#865E2E', # On/Off button
'color_onoff_text' : '#FF0000', # On/Off text
}
# These are the palettes for the waterfall.
# These are the palettes for the waterfall. The one used is named waterfallPallette,
# so to use a different one, overwrite this name in your configuration file.
waterfallPalette = ( waterfallPalette = (
( 0, 0, 0, 0), ( 0, 0, 0, 0),
( 36, 85, 0, 255), ( 36, 85, 0, 255),
@@ -2086,6 +2166,18 @@ waterfallPaletteC = ( # from David Fainitski
(255, 245, 99, 3) (255, 245, 99, 3)
) )
waterfallPaletteDK1MI_AM = ( # from Michael
(0, 0, 0, 0),
(32, 25, 20, 0),
(64, 58, 40, 6),
(96, 78, 50, 16),
(128, 120, 85, 29),
(160, 144, 100, 51),
(192, 195, 140, 43),
(224, 198, 150, 35),
(255, 255, 191, 0)
)
# This is the data used to draw colored lines on the frequency X axis to # This is the data used to draw colored lines on the frequency X axis to
# indicate CW and Phone sub-bands. You can make it anything you want. # indicate CW and Phone sub-bands. You can make it anything you want.
# These are the colors used for sub-bands: # These are the colors used for sub-bands:
+2 -1
View File
@@ -29,9 +29,10 @@ class Hardware(BaseHardware):
# self.hamlib_rigctld_port = 4532 # Standard rigctld control port # self.hamlib_rigctld_port = 4532 # Standard rigctld control port
# self.hamlib_poll_seconds = 0.2 # Time interval to poll for changes # self.hamlib_poll_seconds = 0.2 # Time interval to poll for changes
def open(self): def open(self):
# Thanks to Peter, DC6AR, Rig ID changed from 229 to 2045.
ret = BaseHardware.open(self) ret = BaseHardware.open(self)
if not self.hamlib_connected: # rigctld is not started. Try to start it. if not self.hamlib_connected: # rigctld is not started. Try to start it.
os.system("rigctld -m 229 -r /dev/ttyUSB0 -s 4800 & ") # Check the baud rate menu setting os.system("rigctld -m 2045 -r /dev/ttyUSB0 -s 4800 & ") # Check the baud rate menu setting
# If this fails, start rigctld by hand. # If this fails, start rigctld by hand.
return ret return ret
+2
View File
@@ -120,6 +120,8 @@ class Hardware:
self.application.bandscope_clock = clock self.application.bandscope_clock = clock
#def PollCwKey(self): # Optional. Called frequently by the sound thread to check the CW key status. #def PollCwKey(self): # Optional. Called frequently by the sound thread to check the CW key status.
# pass # Do not define if not needed. # pass # Do not define if not needed.
#def PollGuiControl(self): # Optional. Called frequently by the GUI thread to change GUI settings (PTT etc.)
# pass # Do not define if not needed.
def StartSamples(self): # Quisk calls this from the sound thread to start sending samples. def StartSamples(self): # Quisk calls this from the sound thread to start sending samples.
# If you return a string, it replaces the string returned from hardware open() # If you return a string, it replaces the string returned from hardware open()
pass pass
+2 -2
View File
@@ -7,7 +7,7 @@ import os
# You must define the version here. A title string including # You must define the version here. A title string including
# the version will be written to __init__.py and read by quisk.py. # the version will be written to __init__.py and read by quisk.py.
Version = '4.2.35' Version = '4.2.41'
fp = open("__init__.py", "w") # write title string fp = open("__init__.py", "w") # write title string
fp.write("#Quisk version %s\n" % Version) fp.write("#Quisk version %s\n" % Version)
@@ -85,7 +85,7 @@ N1MM+ and software that uses Hamlib.
url = 'http://james.ahlstrom.name/quisk/', url = 'http://james.ahlstrom.name/quisk/',
packages = ['quisk', 'quisk.n2adr', 'quisk.softrock', 'quisk.freedvpkg', packages = ['quisk', 'quisk.n2adr', 'quisk.softrock', 'quisk.freedvpkg',
'quisk.hermes', 'quisk.hiqsdr', 'quisk.afedrinet', 'quisk.soapypkg', 'quisk.hermes', 'quisk.hiqsdr', 'quisk.afedrinet', 'quisk.soapypkg',
'quisk.sdrmicronpkg', 'quisk.perseuspkg', 'quisk.ac2yd'], 'quisk.sdrmicronpkg', 'quisk.perseuspkg', 'quisk.ac2yd', 'quisk.multuspkg'],
package_dir = {'quisk' : '.'}, package_dir = {'quisk' : '.'},
package_data = {'' : ['*.txt', '*.html', '*.so', '*.dll']}, package_data = {'' : ['*.txt', '*.html', '*.so', '*.dll']},
entry_points = {'gui_scripts' : ['quisk = quisk.quisk:main', 'quisk_vna = quisk.quisk_vna:main']}, entry_points = {'gui_scripts' : ['quisk = quisk.quisk:main', 'quisk_vna = quisk.quisk_vna:main']},
+2
View File
@@ -178,6 +178,8 @@ class Hardware(BaseHardware):
vfo = self.vfo + int(offset * 1000) # Convert kHz to Hz vfo = self.vfo + int(offset * 1000) # Convert kHz to Hz
self.ChangeFrequency(vfo, vfo, 'repeater') self.ChangeFrequency(vfo, vfo, 'repeater')
return False return False
def ImmediateChange(self, name):
return
def OnSpot(self, level): def OnSpot(self, level):
pass pass
def HeartBeat(self): # Called at about 10 Hz by the main def HeartBeat(self): # Called at about 10 Hz by the main
+5 -5
View File
@@ -88,6 +88,7 @@ static int close_file_rec;
double digital_output_level = 0.7; double digital_output_level = 0.7;
static double file_play_level = 1.0; static double file_play_level = 1.0;
static int dc_remove_bw=100; // bandwidth of DC removal filter static int dc_remove_bw=100; // bandwidth of DC removal filter
static int multus_cw_samples = 0; // always send CW Tx samples to the Multus radio, even in Rx
static ty_sample_start pt_sample_start; static ty_sample_start pt_sample_start;
static ty_sample_stop pt_sample_stop; static ty_sample_stop pt_sample_stop;
@@ -1109,7 +1110,7 @@ int quisk_read_sound(void) // Called from sound thread
if (is_cw) { // Transmit CW; use capture device for timing, not microphone if (is_cw) { // Transmit CW; use capture device for timing, not microphone
cwCount += (double)retval * quisk_MicPlayback.sample_rate / quisk_sound_state.sample_rate; cwCount += (double)retval * quisk_MicPlayback.sample_rate / quisk_sound_state.sample_rate;
mic_count = 0; mic_count = 0;
if (QUISK_CWKEY_DOWN || quiskSpotLevel >= 0) { if (QUISK_CWKEY_DOWN || quiskSpotLevel >= 0 || multus_cw_samples) {
while (cwCount >= 1.0) { while (cwCount >= 1.0) {
if (cwEnvelope < 1.0) { if (cwEnvelope < 1.0) {
cwEnvelope += 1. / (quisk_MicPlayback.sample_rate * 5e-3); // 5 milliseconds cwEnvelope += 1. / (quisk_MicPlayback.sample_rate * 5e-3); // 5 milliseconds
@@ -1601,10 +1602,9 @@ PyObject * quisk_micplay_channels(PyObject * self, PyObject * args) // Called fr
PyObject * quisk_set_sparams(PyObject * self, PyObject * args, PyObject * keywds) PyObject * quisk_set_sparams(PyObject * self, PyObject * args, PyObject * keywds)
{ /* Call with keyword arguments ONLY; change local parameters */ { /* Call with keyword arguments ONLY; change local parameters */
static char * kwlist[] = {"dc_remove_bw", "digital_output_level", "remote_control_slave", "remote_control_head", static char * kwlist[] = {"dc_remove_bw", "digital_output_level", "remote_control_slave", "remote_control_head",
"file_play_level", NULL} ; "file_play_level", "multus_cw_samples", NULL} ;
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|idiidi", kwlist, &dc_remove_bw, &digital_output_level,
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|idiid", kwlist, &dc_remove_bw, &digital_output_level, &remote_control_slave, &remote_control_head, &file_play_level, &multus_cw_samples))
&remote_control_slave, &remote_control_head, &file_play_level))
return NULL; return NULL;
Py_INCREF (Py_None); Py_INCREF (Py_None);
return Py_None; return Py_None;