Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5c4732eba | ||
|
|
1c209457dc | ||
|
|
7e8a0846f7 | ||
|
|
fb888a2e08 | ||
|
|
78a1ca3d60 |
@@ -1,3 +1,36 @@
|
||||
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
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
#Quisk version 4.2.37
|
||||
#Quisk version 4.2.42
|
||||
from .quisk import main
|
||||
|
||||
+242
-67
@@ -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
|
||||
@@ -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()
|
||||
@@ -419,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
|
||||
@@ -435,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":
|
||||
@@ -452,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)
|
||||
@@ -1295,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)
|
||||
@@ -1309,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)
|
||||
@@ -1325,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-"
|
||||
@@ -1439,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:
|
||||
@@ -1930,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)
|
||||
@@ -2328,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):
|
||||
@@ -2653,17 +2721,21 @@ 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)
|
||||
@@ -2671,30 +2743,60 @@ class Radios(BaseWindow): # The "Radios" first-level page
|
||||
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.",
|
||||
@@ -2703,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
|
||||
@@ -2742,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:
|
||||
@@ -2751,6 +2859,8 @@ 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)
|
||||
self.add_from.SetItems(choices)
|
||||
self.add_from.SetSelection(0)
|
||||
if "ConfigFileRadio" in choices: # can not delete ConfigFileRadio
|
||||
choices.remove("ConfigFileRadio")
|
||||
self.delete_name.SetItems(choices)
|
||||
@@ -2762,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)
|
||||
|
||||
@@ -2799,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
|
||||
@@ -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)
|
||||
@@ -4091,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)
|
||||
|
||||
@@ -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 or 3.12.</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 or 3.12.</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
|
||||
@@ -256,6 +256,7 @@ If you change to that directory you can run Quisk with "pythonw quisk.py".
|
||||
<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>
|
||||
@@ -385,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.
|
||||
@@ -532,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>
|
||||
@@ -1493,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.
|
||||
@@ -1517,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
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -91,6 +91,8 @@ class Hardware(BaseHardware):
|
||||
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
|
||||
@@ -104,6 +106,13 @@ class Hardware(BaseHardware):
|
||||
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()
|
||||
@@ -120,8 +129,20 @@ class Hardware(BaseHardware):
|
||||
print ("Mode is not CW")
|
||||
self.TransferOut(0x70, cw_mode)
|
||||
return ret
|
||||
def PollCwKey(self): # Called frequently by Quisk to check the CW key status
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -939,6 +940,8 @@ 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:
|
||||
@@ -1224,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):
|
||||
@@ -1440,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)
|
||||
@@ -2129,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
|
||||
@@ -2643,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
|
||||
@@ -2997,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()
|
||||
@@ -3530,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 = []
|
||||
@@ -3620,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
|
||||
@@ -3706,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
|
||||
@@ -3830,9 +4105,10 @@ class App(wx.App):
|
||||
self.std_out_err.Show(False)
|
||||
else:
|
||||
self.std_out_err.Show(True)
|
||||
wClient, hClient = frame.GetClientSize()
|
||||
#w, h = frame.GetSize().Get()
|
||||
#print ('Main frame: size', w, h, 'client', wClient, hClient)
|
||||
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
|
||||
@@ -3963,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
|
||||
@@ -3983,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()
|
||||
@@ -4031,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':
|
||||
@@ -4382,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)
|
||||
@@ -4413,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:
|
||||
@@ -6390,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()
|
||||
@@ -6404,6 +6683,19 @@ class App(wx.App):
|
||||
self.hamlib_clients.remove(client)
|
||||
# print ('Remove', client.address)
|
||||
break
|
||||
if self.k4_tcp_socket:
|
||||
try: # Poll for new client connections.
|
||||
conn, address = self.k4_tcp_socket.accept()
|
||||
except socket.error:
|
||||
pass
|
||||
else:
|
||||
# print ('Connection from', address)
|
||||
self.k4_tcp_clients.append(ElecraftK4Handler(self, conf, conn, address))
|
||||
for client in self.k4_tcp_clients: # Service existing clients
|
||||
if not client.Process(): # False return indicates a closed connection; remove the handler for this client
|
||||
self.k4_tcp_clients.remove(client)
|
||||
# print ('Remove', client.address)
|
||||
break
|
||||
def OnKeyHook(self, event):
|
||||
event.Skip()
|
||||
if conf.hot_key_ptt_if_hidden: # Hot key PTT operates even if Quisk is hidden
|
||||
@@ -6458,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
|
||||
@@ -6638,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:
|
||||
|
||||
+55
-41
@@ -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.
|
||||
@@ -1303,51 +1308,15 @@ fixed_tune_offset = 0
|
||||
|
||||
|
||||
################ 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.
|
||||
@@ -1398,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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.37'
|
||||
Version = '4.2.42'
|
||||
|
||||
fp = open("__init__.py", "w") # write title string
|
||||
fp.write("#Quisk version %s\n" % Version)
|
||||
|
||||
Reference in New Issue
Block a user