6 Commits
Author SHA1 Message Date
James C. Ahlstrom f5c4732eba Quisk version 4.2.42 2025-04-11 15:33:20 -04:00
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
14 changed files with 1093 additions and 235 deletions
+43
View File
@@ -1,3 +1,46 @@
Quisk Version 4.2.42 April 2025
================================
I fixed a bug in the Kenwood IF CAT command. Thanks Nick! I moved the Pulse server IP address to the Sound screen.
I added a CAT method to imitate the Elecraft K4 command set over a TCP port. Quisk now has four CAT controls. To
make this clearer I added more documentation to the Remote screen.
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
+1 -1
View File
@@ -1,2 +1,2 @@
#Quisk version 4.2.36
#Quisk version 4.2.42
from .quisk import main
+264 -87
View File
@@ -48,6 +48,7 @@ name2format = {
# Increasing the software version will display a message to re-read the soapy device.
soapy_software_version = 3
wxpython_gtk3_bug = 0
def FormatKhz(dnum): # Round to 3 decimal places; remove ending ".000"
t = "%.3f" % dnum
@@ -328,7 +329,7 @@ class Configuration:
path = self.NormPath(path)
if not os.path.isfile(path):
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)
ret = dlg.ShowModal()
dlg.Destroy()
@@ -384,10 +385,20 @@ class Configuration:
app.bottom_widgets = dct['BottomWidgets'](app, hardware, conf, frame, gbs, vertBox)
return True
def OnPageChanging(self, event): # Called when the top level page changes (not RadioNotebook pages)
global wxpython_gtk3_bug
event.Skip()
notebook = event.GetEventObject()
index = event.GetSelection()
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 not page.pages:
page.MakePages()
@@ -399,11 +410,12 @@ class Configuration:
notebk.AddPage(self.radio_page, "Radios")
self.radios_page_start = notebk.GetPageCount()
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]:
page = RadioNotebook(notebk, name)
if name == Settings[1]:
notebk.AddPage(page, "*%s*" % name)
else:
if name != Settings[1]:
page = RadioNotebook(notebk, name)
notebk.AddPage(page, name)
def GuessType(self):
udp = conf.use_rx_udp
@@ -418,6 +430,14 @@ class Configuration:
elif udp > 0:
return 'HiQSDR'
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):
radio_dict = {}
radio_dict['hardware_file_type'] = typ
@@ -434,13 +454,24 @@ class Configuration:
page = RadioNotebook(self.notebk, radio_name)
self.notebk.AddPage(page, radio_name)
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):
index = Settings[2].index(old)
n = self.radios_page_start + index
n = self.FindNotebookPage(old)
if n is None:
return
if old == Settings[1]:
self.notebk.SetPageText(n, "*%s*" % new)
else:
self.notebk.SetPageText(n, new)
index = Settings[2].index(old)
Settings[2][index] = new
self.notebk.GetPage(n).NewName(new)
if old == "ConfigFileRadio":
@@ -451,12 +482,10 @@ class Configuration:
index = Settings[2].index(name)
del Settings[2][index]
del Settings[3][index]
try:
n = self.radios_page_start + index
n = self.FindNotebookPage(name)
if n is not None:
self.notebk.DeletePage(n)
except:
pass
return True
return True
def GetRadioDict(self, radio_name=None): # None radio_name means the current radio
if radio_name:
index = Settings[2].index(radio_name)
@@ -477,7 +506,7 @@ class Configuration:
for rxname, data in self.receiver_data:
if rxname == receiver_name:
return data
return None
return ()
def GetReceiverDatum(self, receiver_name, item_name):
for rxname, data in self.receiver_data:
if rxname == receiver_name:
@@ -563,6 +592,8 @@ class Configuration:
# self.format4name with the items that Configuration understands.
# 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
# self.sections is a list of [section_name, section_data]
# section_data is a list of [data_name, text, fmt, help_text, values]
@@ -595,7 +626,7 @@ class Configuration:
except:
traceback.print_exc()
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
data_name = None
multi_line = False
@@ -1292,6 +1323,7 @@ class RadioNotebook(wx.Notebook): # The second-level notebook for each radio nam
self.SetBackgroundColour(parent.bg_color)
self.radio_name = radio_name
self.pages = []
self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanging)
def MakePages(self):
radio_name = self.radio_name
radio_dict = local_conf.GetRadioDict(radio_name)
@@ -1306,7 +1338,16 @@ class RadioNotebook(wx.Notebook): # The second-level notebook for each radio nam
self.AddPage(page, "Sound")
self.pages.append(page)
for section, names in local_conf.sections:
if section in ('Sound', 'Bands', 'Filters'): # There is a special page for these sections
if section == 'Sound': # There is a special page for these sections
continue
if section == 'Bands':
continue
if section == 'Filters':
continue
if section == 'Remote':
page = RadioRemote(self, radio_name, section, names)
self.AddPage(page, section)
self.pages.append(page)
continue
page = RadioSection(self, radio_name, section, names)
self.AddPage(page, section)
@@ -1322,6 +1363,20 @@ class RadioNotebook(wx.Notebook): # The second-level notebook for each radio nam
self.radio_name = new_name
for page in self.pages:
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):
text_for_blank = "-blank-"
@@ -1436,7 +1491,13 @@ class ComboCtrl(wxcombo.ComboCtrl):
wm = w
wm += charx * 5
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.
self.SetBackgroundColour(self.bgColor)
self.Refresh()
try:
text = self.choices[n]
except IndexError:
@@ -1927,6 +1988,16 @@ class ControlMixin:
btn.SetSizeHints(h, h, h, h)
self.gbs.Add(btn, (self.row, col), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx*border)
return sld, btn
def AddTextHeading(self, key, text, help_text):
if key in text: # Start of heading
self.NextRow()
self.SetFont(self.heading_font)
self.AddTextL(1, help_text, span=7)
self.SetFont(self.font)
self.NextRow()
self.col = 2
return key
return None
def AddPopupMenuHelp(self, Qclass, col, text, btn_text, help_text, span1=1, span2=1, border=1):
if text:
txt = wx.StaticText(self, -1, text)
@@ -2227,7 +2298,8 @@ class ControlMixin:
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',
'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)
application.ImmediateChange(name)
elif name[0:4] in ('lin_', 'win_'):
@@ -2258,6 +2330,9 @@ class ControlMixin:
elif hasattr(application.Hardware, "ImmediateChange"):
setattr(conf, name, x)
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
ok, v = self.EvalItem(value, fmt4)
return ok
@@ -2321,9 +2396,9 @@ class ControlMixin:
else:
return value
class BaseWindow(wx.ScrolledCanvas, ControlMixin):
class BaseWindow(wx.ScrolledWindow, ControlMixin):
def __init__(self, parent):
wx.ScrolledCanvas.__init__(self, parent)
wx.ScrolledWindow.__init__(self, parent)
ControlMixin.__init__(self, parent)
class ConfigConfig(BaseWindow):
@@ -2646,47 +2721,82 @@ class Radios(BaseWindow): # The "Radios" first-level page
def __init__(self, parent):
BaseWindow.__init__(self, parent)
self.SetBackgroundColour(parent.bg_color)
self.num_cols = 8
self.num_cols = 5
self.radio_name = None
self.cur_radio_text = self.AddTextL(1, 'xx', self.num_cols - 1)
self.SetCurrentRadioText()
self.index = 0
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()
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.handler = self.OnChoiceStartup
self.radio_buttons.append((rb, self.start_radio))
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 = []
for name, data in local_conf.receiver_data:
choices.append(name)
choices.sort()
self.add_type = self.AddComboCtrl(2, '', choices=choices, no_edit=True)
self.add_type.SetSelection(0)
item = self.AddTextL(3, "and name the new radio")
self.add_name = self.AddComboCtrl(4, '', choices=["My Radio", "SR with XVtr", "SoftRock"])
item = self.AddPushButton(5, "Add", self.OnBtnAdd)
self.add_name = self.AddComboCtrl(4, '', choices=["My Radio", "Transverter", "SoftRock", "HL2"])
self.radio_buttons.append((rb, self.add_type, self.add_name))
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)
item = self.AddTextL(3, "to the new name")
self.rename_new = self.AddComboCtrl(4, '', choices=["My Radio", "SR with XVtr", "SoftRock"])
item = self.AddPushButton(5, "Rename", self.OnBtnRename)
self.rename_new = self.AddComboCtrl(4, '', choices=["My Radio", "Transverter", "SoftRock", "HL2"])
self.radio_buttons.append((rb, self.rename_old, self.rename_new))
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)
item = self.AddPushButton(3, "Delete", self.OnBtnDelete)
self.radio_buttons.append((rb, self.delete_name))
self.NextRow()
self.FitInside()
self.SetScrollRate(1, 1)
self.NextRow()
self.apply_btn = self.AddPushButton(3, "Apply", self.OnBtnApply)
self.NewRadioNames()
def SetCurrentRadioText(self):
radio_dict = local_conf.GetRadioDict(self.radio_name)
radio_type = radio_dict['hardware_file_type']
if Settings[1] == "ConfigFileRadio":
text = 'The current radio is ConfigFileRadio, so all settings come from the config file. The hardware type is %s.' % radio_type
self.NewTop()
self.SetScrollRate(1, 1)
def NewTop(self, event=None):
if event:
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:
text = "Quisk is running with settings from the radio %s. The hardware type is %s." % (Settings[1], radio_type)
self.cur_radio_text.SetLabel(text)
self.apply_btn.Enable(True)
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):
if name in Settings[2] or name == "ConfigFileRadio":
dlg = wx.MessageDialog(self, "The name already exists. Please choose a different name.",
@@ -2695,34 +2805,39 @@ class Radios(BaseWindow): # The "Radios" first-level page
dlg.Destroy()
return True
return False
def OnBtnAdd(self, event):
def OnBtnAdd(self):
name = self.add_name.GetValue().strip()
if not name or self.DuplicateName(name):
return
self.add_name.SetValue('')
typ = self.add_type.GetValue().strip()
if local_conf.AddRadio(name, typ):
if Settings[0] != "Ask me":
Settings[0] = name
self.NewRadioNames()
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()
new = self.rename_new.GetValue().strip()
if not old or not new or self.DuplicateName(new):
return
self.rename_new.SetValue('')
if local_conf.RenameRadio(old, new):
if old == 'ConfigFileRadio' and Settings[1] == "ConfigFileRadio":
if Settings[1] == old:
Settings[1] = new
elif Settings[1] == old:
Settings[1] = new
self.SetCurrentRadioText()
if Settings[0] != "Ask me":
Settings[0] = new
self.NewRadioNames()
local_conf.settings_changed = True
def OnBtnDelete(self, event):
return True
def OnBtnDelete(self):
name = self.delete_name.GetValue()
if not name:
return
@@ -2734,6 +2849,7 @@ class Radios(BaseWindow): # The "Radios" first-level page
if ret == wx.ID_OK and local_conf.DeleteRadio(name):
self.NewRadioNames()
local_conf.settings_changed = True
return True
def OnChoiceStartup(self, ctrl):
choice = self.start_radio.GetValue()
if Settings[0] != choice:
@@ -2743,11 +2859,11 @@ class Radios(BaseWindow): # The "Radios" first-level page
choices = Settings[2][:] # can rename any available radio
self.rename_old.SetItems(choices)
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")
if Settings[1] in choices:
choices.remove(Settings[1])
self.delete_name.SetItems(choices) # can not delete ConfigFileRadio nor the current radio
self.delete_name.SetItems(choices)
self.delete_name.SetSelection(0)
choices = Settings[2] + ["Ask me"]
if "ConfigFileRadio" not in choices:
@@ -2756,13 +2872,13 @@ class Radios(BaseWindow): # The "Radios" first-level page
try: # Set text in control
index = choices.index(Settings[0]) # last used radio, or new or renamed radio
except:
num = len(Settings[2])
if len == 0:
index = 1
elif num == 1:
index = 0
num = len(Settings[2]) # number of radios
if num == 0: # no radios
index = 1 # use ConfigFileRadio
elif num == 1: # one radio
index = 0 # use the one radio
else:
index = len(choices) - 2
index = len(choices) - 2 # use the last radio
Settings[0] = choices[index]
self.start_radio.SetSelection(index)
@@ -2793,16 +2909,7 @@ class RadioSection(BaseWindow): # The pages for each section in the second-leve
radio_dict = local_conf.GetRadioDict(self.radio_name)
radio_type = radio_dict['hardware_file_type']
for name, text, fmt, help_text, values in self.names:
if name == 'remote_radio_password':
self.AddTextButtonHelp(col, text, "Change", self.OnChangePassword, help_text)
if col == 1:
col = 4
else:
col = 1
self.NextRow()
elif name == 'remote_radio_ip' and radio_type != "Control Head":
continue
elif name == 'favorites_file_path':
if name == 'favorites_file_path':
self.favorites_path = radio_dict.get('favorites_file_path', '')
row = self.row
self.row = 1
@@ -2892,12 +2999,6 @@ class RadioSection(BaseWindow): # The pages for each section in the second-leve
hlp = "To delete a row, select it and press this button."
self.AddTextButtonHelp(2, "Delete Midi row", "Delete", self.OnMidiDelete, hlp)
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:
self.AddColSpacer(2, 20)
self.AddColSpacer(5, 20)
@@ -2996,14 +3097,6 @@ class RadioSection(BaseWindow): # The pages for each section in the second-leve
index = self.list_ctrl.GetFirstSelected()
if index >= 0:
self.list_ctrl.Select(index, 0)
def OnChangePassword(self, event):
text = local_conf.globals.get('remote_radio_password', '')
dlg = wx.TextEntryDialog(self, "Please enter a password for remote access",
"Password Entry", text)
if dlg.ShowModal() == wx.ID_OK:
local_conf.globals["remote_radio_password"] = dlg.GetValue()
local_conf.settings_changed = True
local_conf.SaveState()
class RadioHardwareBase(BaseWindow): # The Hardware page in the second-level notebook for each radio
def __init__(self, parent, radio_name):
@@ -3656,7 +3749,7 @@ class RadioSound(BaseWindow): # The Sound page in the second-level notebook for
self.radio_dict = local_conf.GetRadioDict(self.radio_name)
self.num_cols = 8
for name, text, fmt, help_text, values in local_conf.GetSectionData('Sound'):
if name in ('digital_output_level', 'file_play_level'):
if name in ('digital_output_level', 'file_play_level', 'IQ_Server_IP'):
value = self.GetValue(name, self.radio_dict)
no_edit = "choice" in fmt or fmt == 'boolean'
txt, cb, btn = self.AddTextComboHelp(1, text, value, values, help_text, no_edit)
@@ -3688,8 +3781,10 @@ class RadioSound(BaseWindow): # The Sound page in the second-level notebook for
self.NextRow()
choices = (("48000", "96000", "192000"), ("0", "1"), ("0", "1"), (" ", "0", "1"))
r = 0
if "SoftRock" in self.radio_dict['hardware_file_type']: # Samples come from sound card
softrock = True
if "SoftRock" in self.radio_dict['hardware_file_type']:
softrock = True # Samples come from sound card
elif hasattr(application.Hardware, "use_softrock"):
softrock = application.Hardware.use_softrock
else:
softrock = False
last_row = 8
@@ -4089,6 +4184,88 @@ The Tx bits are sent as the "Alex" filters and are sent to the J16 interface.'
if hasattr(application.Hardware, "ImmediateChange"):
application.Hardware.ImmediateChange(name)
class RadioRemote(BaseWindow): # The Remote page in the second-level notebook for each radio
def __init__(self, parent, radio_name, section, names):
BaseWindow.__init__(self, parent)
self.radio_name = radio_name
self.section = section
self.names = names
self.heading_font = wx.Font(conf.config_font_size, wx.FONTFAMILY_SWISS, wx.ITALIC,
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
self.MakeControls()
def MakeControls(self):
self.num_cols = 8
#self.MarkCols()
self.AddColSpacer(1, 3)
self.NextRow()
self.col = 2
radio_dict = local_conf.GetRadioDict(self.radio_name)
radio_type = radio_dict['hardware_file_type']
heading = None
for name, text, fmt, help_text, values in self.names:
if heading:
if heading not in text: # End of heading
heading = None
self.NextRow()
self.col = 2
if not heading:
heading = self.AddTextHeading("Hamlib Rig 2", text,
'Rig control commands using "Hamlib Rig 2" are sent to a Quisk TCP port from multiple clients.')
if not heading:
heading = self.AddTextHeading("XML-RPC", text,
'XML-RPC is a protocol that allows clients to call methods on a remote server using XML passed via HTTP')
if not heading:
heading = self.AddTextHeading("CAT serial", text,
'"CAT serial" supports the FlexRadio PowerSDR 2.x command set sent over a serial port. Some Kenwood commands are included.')
if not heading:
heading = self.AddTextHeading("remote radio", text,
'Quisk can be used as a control head to control a real radio located remotely. This feature is due to AC2YD.')
if not heading:
heading = self.AddTextHeading("K4 TCP", text,
'Rig control commands using "K4 TCP" use the Elecraft K4 command set, and are sent to a Quisk TCP port from multiple clients.')
if not heading:
heading = self.AddTextHeading("Dx cluster", text,
'"Dx Cluster" is a telnet service used to announce Dx spots in real time.')
if name == 'remote_radio_password':
self.AddTextButtonHelp(self.col, text, "Change", self.OnChangePassword, help_text)
if self.col == 2:
self.col = 5
else:
self.col = 2
self.NextRow()
elif name == 'remote_radio_ip' and radio_type != "Control Head":
continue
else:
if fmt[0:4] in ('dict', 'list'):
continue
if name[0:4] == platform_ignore:
continue
value = self.GetValue(name, radio_dict)
no_edit = "choice" in fmt or fmt == 'boolean'
txt, cb, btn = self.AddTextComboHelp(self.col, text, value, values, help_text, no_edit)
cb.handler = self.OnChange
cb.quisk_data_name = name
if self.col == 2:
self.col = 5
else:
self.col = 2
self.NextRow()
self.NextRow()
self.AddTextL(1, "End of list", span=7)
self.AddColSpacer(3, 20)
self.AddColSpacer(6, 20)
self.FitInside()
self.SetScrollRate(1, 1)
def OnChangePassword(self, event):
text = local_conf.globals.get('remote_radio_password', '')
dlg = wx.TextEntryDialog(self, "Please enter a password for remote access",
"Password Entry", text)
if dlg.ShowModal() == wx.ID_OK:
local_conf.globals["remote_radio_password"] = dlg.GetValue()
local_conf.settings_changed = True
local_conf.SaveState()
class xxRadioFilters(BaseWindow): # The Filters page in the second-level notebook for each radio
def __init__(self, parent, radio_name):
BaseWindow.__init__(self, parent)
+36 -9
View File
@@ -194,7 +194,7 @@ Quisk is free open source software written in Python and C. It is hosted on the
<br>
<p>
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.
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.
@@ -204,7 +204,7 @@ A Quisk installation is needed for each version of Python you have.
<p>
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.
<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.
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
@@ -239,17 +239,24 @@ python -m pip install --upgrade pyserial
<br><br>
python -m pip install --upgrade quisk
<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".
Use "quisk.exe" as the command and "Quisk" as the name.
The "quisk.exe" program is in <b>your_install_directory</b>\Scripts.
Enter this as the location of the item:
</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 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>
<br>
<p>
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
is (mostly) from the Config button. Ignore old directions and don't bother with a config file.
</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.
<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.
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
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.
@@ -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.
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).
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.
<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
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.
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.
<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.
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.
@@ -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.
<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,
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".
@@ -1477,16 +1495,24 @@ at his site <a href="https://dk1mi.radio/quisk-fldigi/">https://dk1mi.radio/quis
If your digital program only uses Alsa devices or you don't want to use PulseAudio,
you can use the ALSA loopback device.
It works the same way as the Windows VAC. First create the loopback device with this command:
<br>
<br>
<pre>
sudo modprobe snd-aloop index=19
</pre>
<br>
<p>
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.
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:
</p>
<br>
<pre>
options snd-aloop index=19
</pre>
<br>
<p>
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
print out your sound cards, and you should see a "Loopback" card listed.
@@ -1501,6 +1527,7 @@ Your audio is now connected and you
should be able to receive digital signals. Be sure to test your
transmit signal off the air. You may need to reduce power to
improve linearity.
</p>
<br>
<br>
+87 -39
View File
@@ -1,9 +1,9 @@
from __future__ import absolute_import
# This code was contributed by Christof, DJ4CM. Many Thanks!!
# Modified November 2024 by N2ADR to remove deprecated telnetlib module.
import threading
import socket
import time
import telnetlib
import quisk_conf_defaults as conf
class DxEntry:
@@ -81,61 +81,109 @@ class DxEntry:
class DxCluster(threading.Thread):
def __init__(self):
self.do_init = 1
self.error = 'Starting'
self.dxSpots = []
threading.Thread.__init__(self)
self.doQuit = threading.Event()
self.dxSpots = []
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):
self.telnetInit()
self.telnetConnect()
if self.error:
self.sock.close()
return
while not self.doQuit.isSet():
try:
self.telnetRead()
by = self.sock.recv(1024)
except TimeoutError:
continue
except:
self.tn.close()
time.sleep(20)
by = b''
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():
self.telnetConnect()
self.tn.close()
def setListener (self, listener):
self.listener = listener
if self.error:
self.sock.close()
return
self.sock.close()
def telnetInit(self):
self.tn = telnetlib.Telnet()
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):
try:
self.tn.open(conf.dxClHost, conf.dxClPort, 10)
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
self.bytes += self.sock.recv(1024)
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:
self.tn.read_until(b"Password: ")
self.tn.write(conf.dxClPassword.encode('utf-8', errors='ignore') + b"\n")
for i in range(10):
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):
message = self.tn.read_until(b'\n', 60).decode(encoding='utf-8', errors='replace')
if self.doQuit.isSet() == False:
dxEntry = DxEntry();
if dxEntry.parseMessage(message):
for i, listElement in enumerate(self.dxSpots):
if (listElement.equal(dxEntry)):
listElement.join (dxEntry)
return
if listElement.isExpired():
del (self.dxSpots[i])
self.dxSpots.append(dxEntry)
if self.listener:
self.listener()
def getHost(self):
return self.tn.host + ':' + str(self.tn.port)
def Poll(self):
with self.dxLock:
index = self.bytes.find(b"\n")
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):
for i, listElement in enumerate(self.dxSpots):
if (listElement.equal(dxEntry)):
listElement.join (dxEntry)
return True
if listElement.isExpired():
del (self.dxSpots[i])
self.dxSpots.append(dxEntry)
return True
def stop(self):
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:
try:
self.rx_udp_socket.send(s)
if DEBUG:
self.PrintStatus(' Send', self.want_udp_status)
except:
pass
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)
+375 -55
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# All QUISK software is Copyright (C) 2006-2024 by James C. Ahlstrom.
# All QUISK software is Copyright (C) 2006-2025 by James C. Ahlstrom.
# This free software is licensed for use under the GNU General Public
# License (GPL), see http://www.opensource.org.
# Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!!
@@ -334,6 +334,7 @@ class HamlibHandlerSerial:
self.public_name = public_name # the public name for the serial port
self.tune_step = 1000
if sys.platform == 'win32' or public_name.startswith("/dev/"):
self.port_is_mod_serial = True # self.port is from the serial module
try:
import serial
except:
@@ -346,6 +347,7 @@ class HamlibHandlerSerial:
print ("The serial port %s could not be opened." % public_name)
else:
import tty
self.port_is_mod_serial = False # self.port is a file descriptor
if os.path.lexists(public_name):
try:
os.remove(public_name)
@@ -369,7 +371,7 @@ class HamlibHandlerSerial:
def open(self):
return
def close(self):
if sys.platform == 'win32':
if self.port_is_mod_serial:
if self.port:
self.port.close()
self.port = None
@@ -382,7 +384,7 @@ class HamlibHandlerSerial:
def Read(self):
if self.port is None:
return
if sys.platform == 'win32':
if self.port_is_mod_serial:
text = self.port.read(99)
if not isinstance(text, Q3StringTypes):
text = text.decode('utf-8')
@@ -433,22 +435,14 @@ class HamlibHandlerSerial:
return
if isinstance(data, Q3StringTypes):
data = data.encode('utf-8', errors='ignore')
if sys.platform == 'win32':
if self.port_is_mod_serial:
self.port.write(data)
else:
r, w, x = select.select((), (self.port,), (), 0)
if w:
os.write(self.port, data)
def set_frequency(self, freq):
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')
def set_frequency(self, freq): # This changes the Rx frequency
self.app.ChangeRxTxFrequency(freq, None)
if HAMLIB_DEBUG: print("New Freq rx,tx", self.app.txFreq + self.app.VFO, self.app.rxFreq + self.app.VFO)
def ZZAC(self, cmd, data, length): # Set or read tune steps
if length == 0:
@@ -591,11 +585,18 @@ class HamlibHandlerSerial:
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
if len(cmd) == 4: # Flex ZZIF
info += '0000'
if ritFreq < 0: # RIT freq
info += "-%05d" % -ritFreq
else:
info += "+%05d" % ritFreq
else: # Kenwood IF
info += '00000'
if ritFreq < 0: # RIT freq
info += "-%04d" % -ritFreq
else:
info += "+%04d" % ritFreq
info += "%d" % rit # RIT status
info += '0000'
if QS.is_key_down(): # MOX, key down
@@ -836,6 +837,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
"""Send text back to the client."""
if isinstance(text, Q3StringTypes):
text = text.encode('utf-8', errors='ignore')
if HAMLIB_DEBUG:
print ("Rig2 send", text)
try:
self.sock.sendall(text)
except socket.error:
@@ -937,11 +940,15 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
if not isinstance(text, Q3StringTypes):
text = text.decode('utf-8')
self.received += text
if HAMLIB_DEBUG and text:
print ("Raw rig2 received:", text)
if '\n' in self.received: # A complete command ending with newline is available
self.input, self.received = self.received.split('\n', 1) # Split off the command, save any further characters
else:
return 1
self.input = self.input.strip() # Here is our command line
if HAMLIB_DEBUG:
print ("Rig2 received", self.input)
while self.input:
# Parse the commands and call the appropriate handlers
self.params = ''
@@ -992,8 +999,12 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
if rx:
rx = rx[0]
self.Reply('Frequency', rx.txFreq + rx.VFO, 0)
if HAMLIB_DEBUG:
print ("GetFreq rx", rx.txFreq, rx.VFO)
else:
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
freq = self.GetParamNumber()
try:
@@ -1216,6 +1227,267 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
else:
self.ErrParam()
class CatControl:
def GetFreqVfo(self, vfoB=False):
if vfoB: # The Tx Frequency
return self.app.txFreq + self.app.VFO
else: # The Rx Frequency
rx = self.app.multi_rx_screen.receiver_list
if rx:
rx = rx[0]
return rx.txFreq + rx.VFO
else:
return self.app.rxFreq + self.app.VFO
def SetFreqVfo(self, freq, vfoB=False):
if vfoB: # The Tx Frequency
self.app.ChangeRxTxFrequency(None, freq)
else: # The Rx Frequency
# For multiple receivers this controls the first added receiver frequency; else it controls split Rx/Tx frequency.
rx = self.app.multi_rx_screen.receiver_list
if rx:
rx[0].ChangeRxTxFrequency(freq)
else:
self.app.ChangeRxTxFrequency(freq, None)
def GetFilterBW(self):
return self.app.filter_bandwidth
def SetFilterBW(self, bw):
# Choose button closest to requested bandwidth
buttons = self.app.filterButns.GetButtons()
Lab = buttons[0].GetLabel()
diff = abs(int(Lab) - bw)
for i in range(1, len(buttons) - 1):
label = buttons[i].GetLabel()
df = abs(int(label) - bw)
if df < diff:
Lab = label
diff = df
self.app.filterButns.SetLabel(Lab, True)
class ElecraftK4Handler(CatControl): # Test with telnet localhost 9200
Mo2CoElecraft = {'LSB':1, 'USB':2, 'CWU':3, 'FM':4, 'AM':5, 'DGT-U':6, 'CWL':7, 'DGT-L':9, 'DGT-FM':4, 'DGT-IQ':6}
Co2MoElecraft = {1:'LSB', 2:'USB', 3:'CWU', 4:'FM', 5:'AM', 6:'DGT-U', 7:'CWL', 9:'DGT-L'}
def __init__(self, app, conf, sock, address):
self.app = app
self.conf = conf
self.sock = sock
sock.settimeout(0.0)
self.address = address
self.params = '' # params is the string following the command
self.received = ''
self.cmds = []
self.K31Elecraft = False # K31 mode
h = self.Handlers = {} # Branch on the first two letters of the command
h['AI'] = self.AiAutoInfo
h['CW'] = self.CwCWPitch
h['DT'] = self.DtDataMode
h['FA'] = self.FaFbVfoFreq
h['FB'] = self.FaFbVfoFreq
h['FT'] = self.FtTxVfo
h['FW'] = self.FwFilterBW
h['ID'] = self.IdIdent
h['IF'] = self.IfInfo
h['IS'] = self.IsCenterPitch
h['KS'] = self.KsKeyerSpeed
h['K3'] = self.K3Elecraft
h['LN'] = self.LnLinkVfo
h['MD'] = self.MdMode
h['OM'] = self.OmOptionModules
h['RV'] = self.RvFirmware
h['RX'] = self.RxReceive
h['SB'] = self.SbSubReceiver
h['SM'] = self.SmSmeter
h['TX'] = self.TxTransmit
def Send(self, text):
"""Send text back to the client."""
if HAMLIB_DEBUG:
for cmd in self.cmds[0:-1]:
print ("CatTcp received: %-10s" % cmd)
print ("CatTcp received: %-10s ; sent %s" % (self.cmds[-1], text))
del self.cmds[:]
text = text.encode('utf-8', errors='ignore')
try:
self.sock.sendall(text)
except socket.error:
self.sock.close()
self.sock = None
def Process(self):
"""This is the main processing loop, and is called frequently. It reads and satisfies requests."""
if not self.sock:
return 0
try: # Read any data from the socket
text = self.sock.recv(1024)
except socket.timeout: # This does not work
pass
except socket.error: # Nothing to read
pass
else: # We got some characters
text = text.decode('utf-8', errors='ignore')
self.received += text
if HAMLIB_DEBUG and text:
pass #print ("Raw CatTcp received:", text)
if ';' in self.received: # A complete command ending with semicolon is available
cmd, self.received = self.received.split(';', 1) # Split off the command, save any further characters
else:
return 1
cmd = cmd.strip() # Here is our command line
if len(cmd) < 2:
return 1
if HAMLIB_DEBUG:
self.cmds.append(cmd)
base = cmd[0:2].upper()
try:
handler = self.Handlers[base]
except KeyError:
self.Send(cmd[0:2] + "?;")
#print ("Unknown command", cmd)
return 1
if len(cmd) == 2:
args = ''
elif cmd[2] == '$': # base is the first two letters upper case and any following "$"
base += '$'
args = cmd[3:]
else:
args = cmd[2:]
handler(base, args) # args is the remainder
return 1
def AiAutoInfo(self, base, args):
if not args:
self.Send("AI0;")
elif args != '0':
self.Send("AI0;")
def CwCWPitch(self, base, args):
pitch = self.conf.cwTone
pitch = (pitch + 5) // 10
if pitch < 25:
pitch = 25
elif pitch > 95:
pitch = 95
self.Send("CW%d;" % pitch)
def DtDataMode(self, base, args):
self.Send("%s0;" % base)
def IdIdent(self, base, args):
if args:
self.Send("ID?;")
else:
self.Send("ID017;")
#self.Send("IDQuisk;")
def IfInfo(self, base, args):
rxfreq = self.GetFreqVfo()
info = "%011d " % rxfreq # Rx frequency, five blanks
ritFreq = self.app.ritScale.GetValue()
if ritFreq < 0: # +/- RIT freq
info += "-%04d" % -ritFreq
else:
info += "+%04d" % ritFreq
if self.app.ritButton.GetValue(): # RIT status, XIT status, blank "00"
info += "10 00"
else:
info += "00 00"
if QS.is_key_down(): # MOX, key down
info += '1'
else:
info += '0'
mode = self.Mo2CoElecraft.get(self.app.mode, 2)
info += "%d" % mode # Mode 1-9
info += '00' # Rx on VFOA, no scan
if self.app.split_rxtx: # VFO split status
info += '1'
else:
info += '0'
info += '001 ;'
self.Send(info)
def FaFbVfoFreq(self, base, args):
if base[1] in ('B', 'b'):
vfoB = True
else:
vfoB = False
if not args: # Get command
freq = "%06d" % self.GetFreqVfo(vfoB)
self.Send(base + freq + ';')
else: # Put command
try:
freq = int(args)
except:
self.Send(base + "?;")
else:
l = len(args)
if l <= 2:
freq *= 1000000
elif l <= 5:
freq *= 1000
self.SetFreqVfo(freq, vfoB)
def FtTxVfo(self, base, args):
if not args:
if self.app.split_rxtx:
self.Send("FT1;")
else:
self.Send("FT0;")
else:
# Set split Rx/Tx mode.
is_split = self.app.splitButton.GetValue()
split = int(args)
if split and not is_split:
self.app.splitButton.SetValue(split, True)
elif is_split and not split:
self.app.splitButton.SetValue(split, True)
def FwFilterBW(self, base, args):
if not args:
code = self.GetFilterBW()
self.Send("%s%04d;" % (base, (code + 5) // 10))
else:
self.SetFilterBW(int(args) * 10)
def IsCenterPitch(self, base, args):
if not args:
code = self.GetFilterBW() // 2
if self.K31Elecraft:
self.Send("%s %04d;" % (base, code))
else:
code = (code + 5) // 10
self.Send("%s%04d;" % (base, code))
else:
if self.K31Elecraft:
self.SetFilterBW(int(args) * 2)
else:
self.SetFilterBW(int(args) * 2 * 10)
def K3Elecraft(self, base, args):
if args == '1':
self.K31Elecraft = True
else:
self.K31Elecraft = False
def KsKeyerSpeed(self, base, args):
if len(base) == 2:
self.Send("KS013;")
def LnLinkVfo(self, base, args):
if args != '0':
self.Send("LN0;")
def MdMode(self, base, args):
if args: # set the mode
code = int(args)
mode = self.Co2MoElecraft[code]
self.app.modeButns.SetLabel(mode, True)
else: # return the mode
code = self.Mo2CoElecraft.get(self.app.mode, 2)
self.Send("%s%d;" % (base, code))
def OmOptionModules(self, base, args):
self.Send("OM ------------;")
def RvFirmware(self, base, args):
self.Send("%s99.99;" % base)
def RxReceive(self, base, args):
self.app.pttButton.SetValue(0, True)
def SbSubReceiver(self, base, args):
self.Send("SB0;")
def SmSmeter(self, base, args):
if base[0:3].upper() == "SMH":
self.Send(base + "?;")
elif args:
self.Send(base + "?;")
elif self.K31Elecraft:
self.Send("SM0000;")
else:
self.Send("SM00;")
def TxTransmit(self, base, args):
self.app.pttButton.SetValue(1, True)
class SoundThread(threading.Thread):
"""Create a second (non-GUI) thread to read, process and play sound."""
def __init__(self, samples_from_python):
@@ -1432,12 +1704,8 @@ class ConfigStatus(wx.ScrolledCanvas):
self.MakeRow2("Sample interrupts", self.interrupts, cfile)
self.MakeRow2("Microphone or DGT level dB", level, qfile)
self.MakeRow2("FFT number of points", self.fft_size, err_msg)
if 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
if application.dxCluster:
msg = application.dxCluster.dxStatus()
self.MakeRow2("FFT number of errors", self.fft_error, msg)
else:
self.MakeRow2("FFT number of errors", self.fft_error)
@@ -2121,7 +2389,7 @@ class GraphScreen(wx.Window):
"""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:
if win and win is not self:
x, y = win.GetPosition().Get()
mouse_x += x
mouse_y += y
@@ -2218,7 +2486,8 @@ class GraphScreen(wx.Window):
self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseBtn1', event=event, rx_freq=freq)
else:
self.ChangeHwFrequency(freq, self.VFO, 'MouseBtn1', event=event)
self.CaptureMouse()
if not self.HasCapture():
self.CaptureMouse()
def OnLeftUp(self, event):
if self.HasCapture():
self.ReleaseMouse()
@@ -2231,7 +2500,7 @@ class GraphScreen(wx.Window):
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
if False: #conf.mouse_tune_method: # Mouse motion changes the VFO frequency
x = (mouse_x - self.mouse_x) # Thanks to VK6JBL
self.mouse_x = mouse_x
freq = float(x) * sample_rate / self.data_width
@@ -2634,7 +2903,7 @@ class WaterfallScreen(wx.SplitterWindow):
self.y_zero = conf.waterfall_y_zero
self.zoom_control = 0
wx.SplitterWindow.__init__(self, frame)
self.SetSizeHints(width, -1, width)
self.SetSizeHints(width, -1)
self.SetSashGravity(0.50)
self.SetMinimumPaneSize(1)
self.SetSize((width, conf.waterfall_graph_size + 100)) # be able to set sash size
@@ -2988,7 +3257,7 @@ class MultiReceiverScreen(wx.SplitterWindow):
self.rx_zero = self.graph
self.Initialize(self.rx_zero)
self.waterfall.Hide()
self.SetSizeHints(self.width, -1, self.width)
self.SetSizeHints(self.width, -1)
# Calculate control width
rx_btn = QuiskPushbutton(self, None, "Rx 8....", style=wx.BU_EXACTFIT)
self.rx_btn_width, self.rx_btn_height = rx_btn.GetSize().Get()
@@ -3521,6 +3790,7 @@ class App(wx.App):
if not os.path.isdir(self.QuiskFilesDir):
self.QuiskFilesDir = DefaultConfigDir
self.std_out_err = StdOutput(self)
self.std_out_err.Logfile("WxPython " + wx.version())
QS.set_params(quisk_is_vna=0) # We are not the VNA program
# Read in configuration from the selected radio
self.BandPlan = []
@@ -3611,6 +3881,7 @@ class App(wx.App):
setattr(conf, 'widgets_file_name', '')
Hardware = self.Hardware
self.use_fast_heart_beat = hasattr(Hardware, "FastHeartBeat")
self.poll_gui_control = hasattr(Hardware, "PollGuiControl")
# Initialization - may be over-written by persistent state
self.local_conf.Initialize()
self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow
@@ -3697,6 +3968,19 @@ class App(wx.App):
# traceback.print_exc()
else:
self.hamlib_socket = None
# Quisk control by K4 commands through a TCP port
self.k4_tcp_clients = [] # list of TCP connections to handle
if conf.k4_tcp_port:
try:
self.k4_tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.k4_tcp_socket.bind((conf.k4_tcp_ip, conf.k4_tcp_port))
self.k4_tcp_socket.settimeout(0.0)
self.k4_tcp_socket.listen(5) # listen for TCP connections from multiple clients
except:
self.k4_tcp_socket = None
# traceback.print_exc()
else:
self.k4_tcp_socket = None
# Quisk control by fldigi
self.fldigi_new_freq = None
self.fldigi_freq = None
@@ -3821,16 +4105,16 @@ class App(wx.App):
self.std_out_err.Show(False)
else:
self.std_out_err.Show(True)
#w, h = frame.GetSize().Get()
#ww, hh = frame.GetClientSizeTuple()
#print ('Main frame: size', w, h, 'client', ww, hh)
wClient, hClient = self.main_frame.GetClientSize()
wFrame, hFrame = self.main_frame.GetSize()
main_border_w = wFrame - wClient
#print ('Main frame: size', wFrame, hFrame, 'client', wClient, hClient, 'border', main_border_w)
# Find the data width from a list of preferred sizes; it is the width of returned graph data.
# The graph_width is the width of data_width that is displayed.
# The total width of the graph is graph_width + self.graph_borders
if conf.window_width > 0:
#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
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()
del graph
if self.graph_width % 2 == 1: # Both data_width and graph_width are even numbers
@@ -3955,14 +4239,14 @@ class App(wx.App):
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)
vertBox.Add(self.multi_rx_screen, 1, wx.EXPAND)
vertBox.Add(self.scope, 1, wx.EXPAND)
vertBox.Add(self.bandscope_screen, 1, wx.EXPAND)
vertBox.Add(self.filter_screen, 1, wx.EXPAND)
if self.rate_audio_fft:
vertBox.Add(self.audio_fft_screen, 1)
vertBox.Add(self.help_screen, 1)
vertBox.Add(self.station_screen)
vertBox.Add(self.audio_fft_screen, 1, wx.EXPAND)
vertBox.Add(self.help_screen, 1, wx.EXPAND)
vertBox.Add(self.station_screen, 0, wx.EXPAND)
# Add the spacer
vertBox.Add(Spacer(frame), 0, wx.EXPAND)
# Add the sizer for the controls
@@ -3975,13 +4259,16 @@ class App(wx.App):
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
minw = maxw = conf.window_width
else:
minw = self.graph.width + main_border_w
maxw = -1
if conf.window_height > 0:
minh = maxh = self.height = conf.window_height
else:
minh = 100
maxh = -1
self.main_frame.SetSizeHints(minw, minh, maxw, maxh)
self.main_frame.SetClientSize(wx.Size(width, self.height))
if hasattr(Hardware, 'pre_open'): # pre_open() is called before open()
@@ -4023,7 +4310,6 @@ class App(wx.App):
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':
@@ -4374,8 +4660,6 @@ class App(wx.App):
conf.tx_ip, conf.tx_audio_port,
conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q,
0.7, conf.mic_playback_rate)
def OnDxClChange(self):
self.station_screen.Refresh()
def OnIdle(self, event):
if self.screen:
self.screen.OnIdle(event)
@@ -4405,6 +4689,9 @@ class App(wx.App):
if self.hamlib_socket:
self.hamlib_socket.close()
self.hamlib_socket = None
if self.k4_tcp_socket:
self.k4_tcp_socket.close()
self.k4_tcp_socket = None
if self.dxCluster:
self.dxCluster.stop()
if self.hamlib_com1_handler:
@@ -4706,7 +4993,7 @@ class App(wx.App):
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
if False: #conf.mouse_tune_method: # Mouse motion changes the VFO frequency
self.splitButton.Enable(False)
left_row3.append(self.splitButton)
b = QuiskCheckbutton(frame, self.OnBtnFDX, 'FDX', color=conf.color_test)
@@ -5873,16 +6160,31 @@ class App(wx.App):
The hardware will reply with the updated frequencies which may be different
from those requested; use and display the returned tune and vfo.
"""
#print ("ChangeHW", source, tune, vfo, vfo + tune)
if not self.split_rxtx:
self.rxFreq = tune # rxFreq must be correct before the call to Hardware.ChangeFrequency()
elif rx_freq is not None:
self.rxFreq = rx_freq
rx_offset = self.rxFreq - tune
if self.screen == self.bandscope_screen:
freq = vfo + tune
tune = freq % 10000
vfo = freq - tune
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event)
self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None)
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event)
self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None)
elif conf.fixed_tune_offset:
# Tune with the VFO and keep a constant audio offset from the center. Tx tune is always conf.fixed_tune_offset.
tune_freq = vfo + tune
self.rxFreq = conf.fixed_tune_offset + rx_offset
if source == "BtnUpDown":
tune_freq = vfo + conf.fixed_tune_offset
else:
vfo = tune_freq - conf.fixed_tune_offset
tune_freq, vfo = Hardware.ChangeFrequency(tune_freq, vfo, source, band, event)
self.ChangeDisplayFrequency(tune_freq - vfo, vfo, True)
else:
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event)
self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None)
def ChangeDisplayFrequency(self, tune, vfo, new_rxfreq=False):
'Change the frequency displayed by Quisk'
change = 0
@@ -6367,7 +6669,7 @@ class App(wx.App):
if rxtx != 'rx':
self.fldigi_server.main.rx()
self.fldigi_timer = time.time()
def HamlibPoll(self): # Poll for Hamlib commands
def HamlibPoll(self): # Poll for Hamlib and K4 TCP commands
if self.hamlib_socket:
try: # Poll for new client connections.
conn, address = self.hamlib_socket.accept()
@@ -6379,7 +6681,20 @@ class App(wx.App):
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
# print ('Remove', client.address)
break
if self.k4_tcp_socket:
try: # Poll for new client connections.
conn, address = self.k4_tcp_socket.accept()
except socket.error:
pass
else:
# print ('Connection from', address)
self.k4_tcp_clients.append(ElecraftK4Handler(self, conf, conn, address))
for client in self.k4_tcp_clients: # Service existing clients
if not client.Process(): # False return indicates a closed connection; remove the handler for this client
self.k4_tcp_clients.remove(client)
# print ('Remove', client.address)
break
def OnKeyHook(self, event):
event.Skip()
@@ -6435,6 +6750,8 @@ class App(wx.App):
self.hamlib_com2_handler.Process()
if self.use_fast_heart_beat:
Hardware.FastHeartBeat()
if self.poll_gui_control:
Hardware.PollGuiControl()
if conf.do_repeater_offset:
hold = QS.tx_hold_state(-1)
if hold == 2: # Tx is being held for an FM repeater TX frequency shift
@@ -6615,6 +6932,9 @@ class App(wx.App):
self.ChangeDisplayFrequency(tune - vfo, vfo)
self.FldigiPoll()
self.HamlibPoll()
if self.dxCluster:
if self.dxCluster.Poll():
self.station_screen.Refresh()
if self.timer - self.slowheart_time0 > 0.5:
self.slowheart_time0 = self.timer
if self.w_phase:
+67 -41
View File
@@ -1094,6 +1094,11 @@ file_play_level = 1.000
#file_play_level = 0.500
#file_play_level = 0.100
## IQ_Server_IP Pulse server IP address, text
# IP Address for remote PulseAudio IQ server.
IQ_Server_IP = ""
#IQ_Server_IP = "192.168.230.102"
# Sound card names:
#
# In PortAudio, soundcards have an index number 0, 1, 2, ... and a name.
@@ -1287,55 +1292,31 @@ dc_remove_bw = 100
#dc_remove_bw = 200
#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
################ Remote
# DX cluster telent login data, thanks to DJ4CM. Must have station_display_lines > 0.
## dxClHost Dx cluster host name, text
# The Dx cluster options log into a Dx cluster server, and put station information
# on the station window under the graph and waterfall screens.
# dxClHost is the telnet host name.
dxClHost = ''
#dxClHost = 'example.host.net'
## dxClPort Dx cluster port number, integer
# The Dx cluster options log into a Dx cluster server, and put station information
# on the station window under the graph and waterfall screens.
# dxClPort is the telnet port number.
dxClPort = 7373
## user_call_sign Call sign for Dx cluster, text
# The Dx cluster options log into a Dx cluster server, and put station information
# on the station window under the graph and waterfall screens.
# user_call_sign is your call sign which may be needed for login.
user_call_sign = ''
## dxClPassword Password for Dx cluster, text
# The Dx cluster options log into a Dx cluster server, and put station information
# on the station window under the graph and waterfall screens.
# dxClPassword is the telnet password for the server.
dxClPassword = ''
#dxClPassword = 'getsomedx'
## dxClExpireTime Dx cluster expire minutes, integer
# The Dx cluster options log into a Dx cluster server, and put station information
# on the station window under the graph and waterfall screens.
# dxClExpireTime is the time in minutes until DX Cluster entries are removed.
dxClExpireTime = 20
## IQ_Server_IP Pulse server IP address, text
#IP Address for remote PulseAudio IQ server.
IQ_Server_IP = ""
## hamlib_ip IP address for Hamlib Rig 2, text
# You can control Quisk from Hamlib. Set the Hamlib rig to 2 and the device for rig 2 to
# localhost:4575. Or choose a different name and port here. Set the same name and port
# in the controlling program.
# hamlib_ip is the IP name or address.
hamlib_ip = "localhost"
hamlib_ip = ""
#hamlib_ip = "localhost"
## hamlib_port IP port for Hamlib, integer
## hamlib_port IP port for Hamlib Rig 2, integer
# You can control Quisk from Hamlib. For direct control, set the external program to rig 2
# "Hamlib NET rigctl", and set the Quisk hamlib port to 4532. To use the rigctld program to control
# Quisk, set the Quisk hamlib port to 4575. To turn off Hamlib control, set the Quisk port to zero.
@@ -1386,19 +1367,64 @@ win_hamlib_com2_name = ""
#win_hamlib_com2_name = "COM15"
#win_hamlib_com2_name = "COM16"
## remote_radio_ip Remote radio IP or name, text
## remote_radio_ip IP or name of the remote radio, text
# Quisk can be used as a control head to control a real radio located remotely.
# This is the IP adddress or the host name of the remote radio.
remote_radio_ip = ""
#remote_radio_ip = "192.168.1.56"
## remote_radio_password Password for remote radio, text
## remote_radio_password Password for the remote radio, text
# Quisk can be used as a control head to control a real radio located remotely.
# For security it requires the same password to be entered on both computers.
# Enter a fairly long pass phrase of 20 or more characters.
# It is only necessary to enter it once on each computer.
remote_radio_password = ""
## k4_tcp_ip IP address for K4 TCP, text
# This is the Quisk IP address for the TCP server implementing K4 commands.
k4_tcp_ip = ""
#k4_tcp_ip = "localhost"
#k4_tcp_ip = "127.0.0.1"
## k4_tcp_port Port number for K4 TCP, integer
# This is the Quisk port for the TCP server implementing K4 commands.
k4_tcp_port = 0
#k4_tcp_port = 9200
#k4_tcp_port = 50004
# DX cluster telent login data, thanks to DJ4CM. Must have station_display_lines > 0.
## dxClHost Dx cluster host name, text
# The Dx cluster options log into a Dx cluster server, and put station information
# on the station window under the graph and waterfall screens.
# dxClHost is the telnet host name.
dxClHost = ''
#dxClHost = 'example.host.net'
## dxClPort Dx cluster port number, integer
# The Dx cluster options log into a Dx cluster server, and put station information
# on the station window under the graph and waterfall screens.
# dxClPort is the telnet port number.
dxClPort = 7373
## user_call_sign Call sign for Dx cluster, text
# The Dx cluster options log into a Dx cluster server, and put station information
# on the station window under the graph and waterfall screens.
# user_call_sign is your call sign which may be needed for login.
user_call_sign = ''
## dxClPassword Password for Dx cluster, text
# The Dx cluster options log into a Dx cluster server, and put station information
# on the station window under the graph and waterfall screens.
# dxClPassword is the telnet password for the server.
dxClPassword = ''
#dxClPassword = 'getsomedx'
## dxClExpireTime Dx cluster expire minutes, integer
# The Dx cluster options log into a Dx cluster server, and put station information
# on the station window under the graph and waterfall screens.
# dxClExpireTime is the time in minutes until DX Cluster entries are removed.
dxClExpireTime = 20
hamlib_com1_name = ""
+2 -1
View File
@@ -29,9 +29,10 @@ class Hardware(BaseHardware):
# self.hamlib_rigctld_port = 4532 # Standard rigctld control port
# self.hamlib_poll_seconds = 0.2 # Time interval to poll for changes
def open(self):
# Thanks to Peter, DC6AR, Rig ID changed from 229 to 2045.
ret = BaseHardware.open(self)
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.
return ret
+2
View File
@@ -120,6 +120,8 @@ class Hardware:
self.application.bandscope_clock = clock
#def PollCwKey(self): # Optional. Called frequently by the sound thread to check the CW key status.
# 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.
# If you return a string, it replaces the string returned from hardware open()
pass
+2 -2
View File
@@ -7,7 +7,7 @@ import os
# You must define the version here. A title string including
# the version will be written to __init__.py and read by quisk.py.
Version = '4.2.36'
Version = '4.2.42'
fp = open("__init__.py", "w") # write title string
fp.write("#Quisk version %s\n" % Version)
@@ -85,7 +85,7 @@ N1MM+ and software that uses Hamlib.
url = 'http://james.ahlstrom.name/quisk/',
packages = ['quisk', 'quisk.n2adr', 'quisk.softrock', 'quisk.freedvpkg',
'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_data = {'' : ['*.txt', '*.html', '*.so', '*.dll']},
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
self.ChangeFrequency(vfo, vfo, 'repeater')
return False
def ImmediateChange(self, name):
return
def OnSpot(self, level):
pass
def HeartBeat(self): # Called at about 10 Hz by the main