3 Commits
Author SHA1 Message Date
James C. Ahlstrom 1c209457dc Quisk version 4.2.41 2025-01-30 13:48:38 -05:00
James C. Ahlstrom 7e8a0846f7 Quisk version 4.2.40 2024-11-15 16:39:47 -05:00
James C. Ahlstrom fb888a2e08 Quisk version 4.2.39 2024-10-01 14:22:23 -04:00
10 changed files with 299 additions and 102 deletions
+21
View File
@@ -1,3 +1,24 @@
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 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 This is a bugfix release. I fixed a bug in the Hamlib serial port when using a physical TTY instead
+1 -1
View File
@@ -1,2 +1,2 @@
#Quisk version 4.2.38 #Quisk version 4.2.41
from .quisk import main from .quisk import main
+138 -47
View File
@@ -48,6 +48,7 @@ name2format = {
# Increasing the software version will display a message to re-read the soapy device. # Increasing the software version will display a message to re-read the soapy device.
soapy_software_version = 3 soapy_software_version = 3
wxpython_gtk3_bug = 0
def FormatKhz(dnum): # Round to 3 decimal places; remove ending ".000" def FormatKhz(dnum): # Round to 3 decimal places; remove ending ".000"
t = "%.3f" % dnum t = "%.3f" % dnum
@@ -384,10 +385,20 @@ class Configuration:
app.bottom_widgets = dct['BottomWidgets'](app, hardware, conf, frame, gbs, vertBox) app.bottom_widgets = dct['BottomWidgets'](app, hardware, conf, frame, gbs, vertBox)
return True return True
def OnPageChanging(self, event): # Called when the top level page changes (not RadioNotebook pages) def OnPageChanging(self, event): # Called when the top level page changes (not RadioNotebook pages)
global wxpython_gtk3_bug
event.Skip() event.Skip()
notebook = event.GetEventObject() notebook = event.GetEventObject()
index = event.GetSelection() index = event.GetSelection()
page = notebook.GetPage(index) page = notebook.GetPage(index)
if sys.platform != 'win32': # Work around a bug in wxPython 4.2.1 and gtk3
w, h = application.main_frame.GetSize() # Change main window size by +/- one pixel to recalculate sizes
if wxpython_gtk3_bug:
wxpython_gtk3_bug = 0
h += 1
else:
wxpython_gtk3_bug = 1
h -= 1
application.main_frame.SetSize((w, h))
if isinstance(page, RadioNotebook): if isinstance(page, RadioNotebook):
if not page.pages: if not page.pages:
page.MakePages() page.MakePages()
@@ -419,6 +430,14 @@ class Configuration:
elif udp > 0: elif udp > 0:
return 'HiQSDR' return 'HiQSDR'
return 'SoftRock USB' return 'SoftRock USB'
def FindNotebookPage(self, name):
for index in range(self.radios_page_start, self.notebk.GetPageCount()):
tab_text = self.notebk.GetPageText(index)
if tab_text[0] == '*':
tab_text = tab_text[1:-1]
if tab_text == name:
return index
return None
def AddRadio(self, radio_name, typ): def AddRadio(self, radio_name, typ):
radio_dict = {} radio_dict = {}
radio_dict['hardware_file_type'] = typ radio_dict['hardware_file_type'] = typ
@@ -435,13 +454,24 @@ class Configuration:
page = RadioNotebook(self.notebk, radio_name) page = RadioNotebook(self.notebk, radio_name)
self.notebk.AddPage(page, radio_name) self.notebk.AddPage(page, radio_name)
return True return True
def CopyRadio(self, old_name, new_name):
radio_dict = {}
index = Settings[2].index(old_name)
radio_dict.update(Settings[3][index])
Settings[2].append(new_name)
Settings[3].append(radio_dict)
page = RadioNotebook(self.notebk, new_name)
self.notebk.AddPage(page, new_name)
return True
def RenameRadio(self, old, new): def RenameRadio(self, old, new):
index = Settings[2].index(old) n = self.FindNotebookPage(old)
n = self.radios_page_start + index if n is None:
return
if old == Settings[1]: if old == Settings[1]:
self.notebk.SetPageText(n, "*%s*" % new) self.notebk.SetPageText(n, "*%s*" % new)
else: else:
self.notebk.SetPageText(n, new) self.notebk.SetPageText(n, new)
index = Settings[2].index(old)
Settings[2][index] = new Settings[2][index] = new
self.notebk.GetPage(n).NewName(new) self.notebk.GetPage(n).NewName(new)
if old == "ConfigFileRadio": if old == "ConfigFileRadio":
@@ -452,12 +482,10 @@ class Configuration:
index = Settings[2].index(name) index = Settings[2].index(name)
del Settings[2][index] del Settings[2][index]
del Settings[3][index] del Settings[3][index]
try: n = self.FindNotebookPage(name)
n = self.radios_page_start + index if n is not None:
self.notebk.DeletePage(n) self.notebk.DeletePage(n)
except: return True
pass
return True
def GetRadioDict(self, radio_name=None): # None radio_name means the current radio def GetRadioDict(self, radio_name=None): # None radio_name means the current radio
if radio_name: if radio_name:
index = Settings[2].index(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.SetBackgroundColour(parent.bg_color)
self.radio_name = radio_name self.radio_name = radio_name
self.pages = [] self.pages = []
self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanging)
def MakePages(self): def MakePages(self):
radio_name = self.radio_name radio_name = self.radio_name
radio_dict = local_conf.GetRadioDict(radio_name) radio_dict = local_conf.GetRadioDict(radio_name)
@@ -1325,6 +1354,20 @@ class RadioNotebook(wx.Notebook): # The second-level notebook for each radio nam
self.radio_name = new_name self.radio_name = new_name
for page in self.pages: for page in self.pages:
page.radio_name = new_name page.radio_name = new_name
def OnPageChanging(self, event):
global wxpython_gtk3_bug
event.Skip()
index = event.GetSelection()
page = self.GetPage(index)
if sys.platform != 'win32': # Work around a bug in wxPython 4.2.1 and gtk3
w, h = application.main_frame.GetSize() # Change main window size by +/- one pixel to recalculate sizes
if wxpython_gtk3_bug:
wxpython_gtk3_bug = 0
h += 1
else:
wxpython_gtk3_bug = 1
h -= 1
application.main_frame.SetSize((w, h))
class ChoiceCombo(wx.Choice): class ChoiceCombo(wx.Choice):
text_for_blank = "-blank-" text_for_blank = "-blank-"
@@ -1439,7 +1482,13 @@ class ComboCtrl(wxcombo.ComboCtrl):
wm = w wm = w
wm += charx * 5 wm += charx * 5
self.SetSizeHints(wm, self.height, 9999, self.height) self.SetSizeHints(wm, self.height, 9999, self.height)
def SetValue(self, value):
wxcombo.ComboCtrl.SetValue(self, value)
self.SetBackgroundColour(self.bgColor)
self.Refresh()
def SetSelection(self, n): # Set text to item in list box. Name conflict with wxcombo.ComboCtrl. def SetSelection(self, n): # Set text to item in list box. Name conflict with wxcombo.ComboCtrl.
self.SetBackgroundColour(self.bgColor)
self.Refresh()
try: try:
text = self.choices[n] text = self.choices[n]
except IndexError: except IndexError:
@@ -2328,9 +2377,9 @@ class ControlMixin:
else: else:
return value return value
class BaseWindow(wx.ScrolledCanvas, ControlMixin): class BaseWindow(wx.ScrolledWindow, ControlMixin):
def __init__(self, parent): def __init__(self, parent):
wx.ScrolledCanvas.__init__(self, parent) wx.ScrolledWindow.__init__(self, parent)
ControlMixin.__init__(self, parent) ControlMixin.__init__(self, parent)
class ConfigConfig(BaseWindow): class ConfigConfig(BaseWindow):
@@ -2653,17 +2702,21 @@ class Radios(BaseWindow): # The "Radios" first-level page
def __init__(self, parent): def __init__(self, parent):
BaseWindow.__init__(self, parent) BaseWindow.__init__(self, parent)
self.SetBackgroundColour(parent.bg_color) self.SetBackgroundColour(parent.bg_color)
self.num_cols = 8 self.num_cols = 5
self.radio_name = None self.radio_name = None
self.cur_radio_text = self.AddTextL(1, 'xx', self.num_cols - 1) self.index = 0
self.SetCurrentRadioText() txt = 'Each "radio" is a block of settings that describes a radio to use with Quisk.'
self.AddTextL(1, txt, self.num_cols - 1)
self.NextRow() self.NextRow()
self.NextRow() self.NextRow()
item = self.AddTextL(1, "When Quisk starts, use the radio") self.radio_buttons = []
rb = self.AddRadioButton(1, "When Quisk starts, use the radio", self.NewTop, start=True)
rb.SetValue(True)
self.start_radio = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True) self.start_radio = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True)
self.start_radio.handler = self.OnChoiceStartup self.start_radio.handler = self.OnChoiceStartup
self.radio_buttons.append((rb, self.start_radio))
self.NextRow() self.NextRow()
item = self.AddTextL(1, "Add a new radio with the general type") rb = self.AddRadioButton(1, "Add a new radio with the general type", self.NewTop)
choices = [] choices = []
for name, data in local_conf.receiver_data: for name, data in local_conf.receiver_data:
choices.append(name) choices.append(name)
@@ -2671,30 +2724,60 @@ class Radios(BaseWindow): # The "Radios" first-level page
self.add_type = self.AddComboCtrl(2, '', choices=choices, no_edit=True) self.add_type = self.AddComboCtrl(2, '', choices=choices, no_edit=True)
self.add_type.SetSelection(0) self.add_type.SetSelection(0)
item = self.AddTextL(3, "and name the new radio") item = self.AddTextL(3, "and name the new radio")
self.add_name = self.AddComboCtrl(4, '', choices=["My Radio", "SR with XVtr", "SoftRock"]) self.add_name = self.AddComboCtrl(4, '', choices=["My Radio", "Transverter", "SoftRock", "HL2"])
item = self.AddPushButton(5, "Add", self.OnBtnAdd) self.radio_buttons.append((rb, self.add_type, self.add_name))
self.NextRow() self.NextRow()
item = self.AddTextL(1, "Rename the radio named") rb = self.AddRadioButton(1, "Add a new radio copied from radio", self.NewTop)
self.add_from = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True)
item = self.AddTextL(3, "and name the new radio")
self.add_name2 = self.AddComboCtrl(4, '', choices=["My Radio", "Transverter", "SoftRock", "HL2"])
self.radio_buttons.append((rb, self.add_from, self.add_name2))
self.NextRow()
rb = self.AddRadioButton(1, "Rename the radio named", self.NewTop)
self.rename_old = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True) self.rename_old = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True)
item = self.AddTextL(3, "to the new name") item = self.AddTextL(3, "to the new name")
self.rename_new = self.AddComboCtrl(4, '', choices=["My Radio", "SR with XVtr", "SoftRock"]) self.rename_new = self.AddComboCtrl(4, '', choices=["My Radio", "Transverter", "SoftRock", "HL2"])
item = self.AddPushButton(5, "Rename", self.OnBtnRename) self.radio_buttons.append((rb, self.rename_old, self.rename_new))
self.NextRow() self.NextRow()
item = self.AddTextL(1, "Delete the radio named") rb = self.AddRadioButton(1, "Delete the radio named", self.NewTop)
self.delete_name = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True) self.delete_name = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True)
item = self.AddPushButton(3, "Delete", self.OnBtnDelete) self.radio_buttons.append((rb, self.delete_name))
self.NextRow() self.NextRow()
self.FitInside() self.NextRow()
self.SetScrollRate(1, 1) self.apply_btn = self.AddPushButton(3, "Apply", self.OnBtnApply)
self.NewRadioNames() self.NewRadioNames()
def SetCurrentRadioText(self): self.NewTop()
radio_dict = local_conf.GetRadioDict(self.radio_name) self.SetScrollRate(1, 1)
radio_type = radio_dict['hardware_file_type'] def NewTop(self, event=None):
if Settings[1] == "ConfigFileRadio": if event:
text = 'The current radio is ConfigFileRadio, so all settings come from the config file. The hardware type is %s.' % radio_type btn = event.GetEventObject()
else: # set first button
btn = self.radio_buttons[0][0]
for i in range(len(self.radio_buttons)):
data = self.radio_buttons[i]
rb = data[0]
if rb == btn:
self.index = i
for c in data[1:]:
c.Enable(rb == btn)
if self.index == 0:
self.apply_btn.Enable(False)
else: else:
text = "Quisk is running with settings from the radio %s. The hardware type is %s." % (Settings[1], radio_type) self.apply_btn.Enable(True)
self.cur_radio_text.SetLabel(text) def OnBtnApply(self, event):
index = self.index
ok = False
if index == 1: # add from hardware type
ok = self.OnBtnAdd()
elif index == 2: # add from existing radio
ok = self.OnBtnCopy()
elif index == 3: # rename
ok = self.OnBtnRename()
elif index == 4: # delete
ok = self.OnBtnDelete()
if ok:
self.radio_buttons[0][0].SetValue(True)
self.NewTop()
def DuplicateName(self, name): def DuplicateName(self, name):
if name in Settings[2] or name == "ConfigFileRadio": if name in Settings[2] or name == "ConfigFileRadio":
dlg = wx.MessageDialog(self, "The name already exists. Please choose a different name.", dlg = wx.MessageDialog(self, "The name already exists. Please choose a different name.",
@@ -2703,34 +2786,39 @@ class Radios(BaseWindow): # The "Radios" first-level page
dlg.Destroy() dlg.Destroy()
return True return True
return False return False
def OnBtnAdd(self, event): def OnBtnAdd(self):
name = self.add_name.GetValue().strip() name = self.add_name.GetValue().strip()
if not name or self.DuplicateName(name): if not name or self.DuplicateName(name):
return return
self.add_name.SetValue('') self.add_name.SetValue('')
typ = self.add_type.GetValue().strip() typ = self.add_type.GetValue().strip()
if local_conf.AddRadio(name, typ): if local_conf.AddRadio(name, typ):
if Settings[0] != "Ask me":
Settings[0] = name
self.NewRadioNames() self.NewRadioNames()
local_conf.settings_changed = True local_conf.settings_changed = True
def OnBtnRename(self, event): return True
def OnBtnCopy(self):
new_name = self.add_name2.GetValue().strip()
if not new_name or self.DuplicateName(new_name):
return
self.add_name2.SetValue('')
old_name = self.add_from.GetValue()
if local_conf.CopyRadio(old_name, new_name):
self.NewRadioNames()
local_conf.settings_changed = True
return True
def OnBtnRename(self):
old = self.rename_old.GetValue() old = self.rename_old.GetValue()
new = self.rename_new.GetValue().strip() new = self.rename_new.GetValue().strip()
if not old or not new or self.DuplicateName(new): if not old or not new or self.DuplicateName(new):
return return
self.rename_new.SetValue('') self.rename_new.SetValue('')
if local_conf.RenameRadio(old, new): if local_conf.RenameRadio(old, new):
if old == 'ConfigFileRadio' and Settings[1] == "ConfigFileRadio": if Settings[1] == old:
Settings[1] = new Settings[1] = new
elif Settings[1] == old:
Settings[1] = new
self.SetCurrentRadioText()
if Settings[0] != "Ask me":
Settings[0] = new
self.NewRadioNames() self.NewRadioNames()
local_conf.settings_changed = True local_conf.settings_changed = True
def OnBtnDelete(self, event): return True
def OnBtnDelete(self):
name = self.delete_name.GetValue() name = self.delete_name.GetValue()
if not name: if not name:
return return
@@ -2742,6 +2830,7 @@ class Radios(BaseWindow): # The "Radios" first-level page
if ret == wx.ID_OK and local_conf.DeleteRadio(name): if ret == wx.ID_OK and local_conf.DeleteRadio(name):
self.NewRadioNames() self.NewRadioNames()
local_conf.settings_changed = True local_conf.settings_changed = True
return True
def OnChoiceStartup(self, ctrl): def OnChoiceStartup(self, ctrl):
choice = self.start_radio.GetValue() choice = self.start_radio.GetValue()
if Settings[0] != choice: if Settings[0] != choice:
@@ -2751,6 +2840,8 @@ class Radios(BaseWindow): # The "Radios" first-level page
choices = Settings[2][:] # can rename any available radio choices = Settings[2][:] # can rename any available radio
self.rename_old.SetItems(choices) self.rename_old.SetItems(choices)
self.rename_old.SetSelection(0) self.rename_old.SetSelection(0)
self.add_from.SetItems(choices)
self.add_from.SetSelection(0)
if "ConfigFileRadio" in choices: # can not delete ConfigFileRadio if "ConfigFileRadio" in choices: # can not delete ConfigFileRadio
choices.remove("ConfigFileRadio") choices.remove("ConfigFileRadio")
self.delete_name.SetItems(choices) self.delete_name.SetItems(choices)
@@ -2762,13 +2853,13 @@ class Radios(BaseWindow): # The "Radios" first-level page
try: # Set text in control try: # Set text in control
index = choices.index(Settings[0]) # last used radio, or new or renamed radio index = choices.index(Settings[0]) # last used radio, or new or renamed radio
except: except:
num = len(Settings[2]) num = len(Settings[2]) # number of radios
if len == 0: if num == 0: # no radios
index = 1 index = 1 # use ConfigFileRadio
elif num == 1: elif num == 1: # one radio
index = 0 index = 0 # use the one radio
else: else:
index = len(choices) - 2 index = len(choices) - 2 # use the last radio
Settings[0] = choices[index] Settings[0] = choices[index]
self.start_radio.SetSelection(index) self.start_radio.SetSelection(index)
+14 -3
View File
@@ -194,7 +194,7 @@ Quisk is free open source software written in Python and C. It is hosted on the
<br> <br>
<p> <p>
If you have Python 3 installed on your computer, just continue to use that version. If you have Python 3 installed on your computer, just continue to use that version.
<b>On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11 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. If you want to install a more recent version of Python, uninstall the old version first.
Running multiple versions of Python is possible but can be confusing and is not necessary. Running multiple versions of Python is possible but can be confusing and is not necessary.
A Quisk installation is needed for each version of Python you have. A Quisk installation is needed for each version of Python you have.
@@ -204,7 +204,7 @@ A Quisk installation is needed for each version of Python you have.
<p> <p>
Windows does not include Python, so you must first install a recent version of 64-bit Python 3. Windows does not include Python, so you must first install a recent version of 64-bit Python 3.
If you already have 64-bit Python 3 installed, just keep using it. If you already have 64-bit Python 3 installed, just keep using it.
<b>On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11 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. There may be newer versions of Python, but until support is added, use the versions listed above.
Python is available from <a href="http://www.python.org/">http://www.python.org</a>. Python is available from <a href="http://www.python.org/">http://www.python.org</a>.
There is an option to <b>Add Python to PATH</b>. You MUST select There is an option to <b>Add Python to PATH</b>. You MUST select
@@ -256,6 +256,7 @@ If you change to that directory you can run Quisk with "pythonw quisk.py".
<br> <br>
<p> <p>
To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios. To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios.
Add a radio from the list of hardware radios available. Or add the best match.
Then set your sound devices for that radio; the device for the radio speakers, the microphone and so forth. All configuration Then set your sound devices for that radio; the device for the radio speakers, the microphone and so forth. All configuration
is (mostly) from the Config button. Ignore old directions and don't bother with a config file. is (mostly) from the Config button. Ignore old directions and don't bother with a config file.
</p> </p>
@@ -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. You can create a desktop icon to do that, but the method depends on your version of Linux.
<b>Never run Quisk with root privelege by using sudo. Fix the permission problem instead.</b> <b>Never run Quisk with root privelege by using sudo. Fix the permission problem instead.</b>
To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios. To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios.
Add a radio from the list of hardware radios available. Or add the best match.
Then set your sound devices for that radio; the device for the radio speakers, the microphone and so forth. All configuration Then set your sound devices for that radio; the device for the radio speakers, the microphone and so forth. All configuration
is (mostly) from the Config button. Ignore old directions and don't bother with a config file. is (mostly) from the Config button. Ignore old directions and don't bother with a config file.
To uninstall Quisk, just delete the quisk directory. To uninstall Quisk, just delete the quisk directory.
@@ -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. Press the Config button and go to the Radios screen.
Then create a radio by specifying the general hardware type and give it a name of your choosing. Then create a radio by specifying the general hardware type and give it a name of your choosing.
For a Hermes-Lite, specify "Hermes" as the hardware type and call it "HL2" (or some other name). For a Hermes-Lite, specify "Hermes" as the hardware type and call it "HL2" (or some other name).
Press "Add" and a new tab for your radio will appear. Look through the various settings on the HL2 tab. Press "Apply" and a new tab for your radio will appear. Look through the various settings on the HL2 tab.
The parameters for the radios are stored in the file quisk_settings.json. The parameters for the radios are stored in the file quisk_settings.json.
<br> <br>
<br> <br>
@@ -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, If your digital program only uses Alsa devices or you don't want to use PulseAudio,
you can use the ALSA loopback device. you can use the ALSA loopback device.
It works the same way as the Windows VAC. First create the loopback device with this command: It works the same way as the Windows VAC. First create the loopback device with this command:
<br>
<br>
<pre> <pre>
sudo modprobe snd-aloop index=19 sudo modprobe snd-aloop index=19
</pre> </pre>
<br>
<p>
The "index" is the card number, and it is best to use a number higher than your regular cards. The "index" is the card number, and it is best to use a number higher than your regular cards.
The loopback device will be gone on the next restart. The loopback device will be gone on the next restart.
You can create the loopback device every time the system starts, but the way to do that depends on your version of Linux. You can create the loopback device every time the system starts, but the way to do that depends on your version of Linux.
You could add this line to /etc/modprobe.d/alsa-base.conf: You could add this line to /etc/modprobe.d/alsa-base.conf:
</p>
<br>
<pre> <pre>
options snd-aloop index=19 options snd-aloop index=19
</pre> </pre>
<br>
<p>
Or you could add snd-aloop to /etc/modules or put the modprobe command in /etc/rc.local instead. Or you could add snd-aloop to /etc/modules or put the modprobe command in /etc/rc.local instead.
Restart Linux. Now you can enter "cat /proc/asound/cards" to Restart Linux. Now you can enter "cat /proc/asound/cards" to
print out your sound cards, and you should see a "Loopback" card listed. print out your sound cards, and you should see a "Loopback" card listed.
@@ -1517,6 +1527,7 @@ Your audio is now connected and you
should be able to receive digital signals. Be sure to test your should be able to receive digital signals. Be sure to test your
transmit signal off the air. You may need to reduce power to transmit signal off the air. You may need to reduce power to
improve linearity. improve linearity.
</p>
<br> <br>
<br> <br>
+87 -39
View File
@@ -1,9 +1,9 @@
from __future__ import absolute_import
# This code was contributed by Christof, DJ4CM. Many Thanks!! # This code was contributed by Christof, DJ4CM. Many Thanks!!
# Modified November 2024 by N2ADR to remove deprecated telnetlib module.
import threading import threading
import socket
import time import time
import telnetlib
import quisk_conf_defaults as conf import quisk_conf_defaults as conf
class DxEntry: class DxEntry:
@@ -81,61 +81,109 @@ class DxEntry:
class DxCluster(threading.Thread): class DxCluster(threading.Thread):
def __init__(self): def __init__(self):
self.do_init = 1 self.error = 'Starting'
self.dxSpots = []
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.doQuit = threading.Event() self.doQuit = threading.Event()
self.dxSpots = []
self.doQuit.clear() self.doQuit.clear()
self.dxLock = threading.Lock()
self.addr = conf.dxClHost + ':' + str(conf.dxClPort)
self.msg_no_spots = "No DX Cluster data from " + self.addr
self.msg_one_spot = '1 DX spot received from ' + self.addr
self.msg_spots = ' DX spots received from ' + self.addr
def run(self): def run(self):
self.telnetInit()
self.telnetConnect() self.telnetConnect()
if self.error:
self.sock.close()
return
while not self.doQuit.isSet(): while not self.doQuit.isSet():
try: try:
self.telnetRead() by = self.sock.recv(1024)
except TimeoutError:
continue
except: except:
self.tn.close() by = b''
time.sleep(20) if by:
with self.dxLock:
self.bytes += by
else:
self.sock.close()
self.error = "Restarting " + self.addr
time.sleep(2)
if not self.doQuit.isSet(): if not self.doQuit.isSet():
self.telnetConnect() self.telnetConnect()
self.tn.close() if self.error:
self.sock.close()
def setListener (self, listener): return
self.listener = listener self.sock.close()
def telnetInit(self):
self.tn = telnetlib.Telnet()
def telnetConnect(self): def telnetConnect(self):
self.bytes = bytearray(0)
self.error = 'Starting'
self.sock = socket.socket()
self.sock.settimeout(20)
try:
self.sock.connect( (conf.dxClHost, conf.dxClPort) )
except:
self.error = "Failed to connect to " + self.addr
return
self.sock.settimeout(1)
for i in range(10): for i in range(10):
try: try:
self.tn.open(conf.dxClHost, conf.dxClPort, 10) self.bytes += self.sock.recv(1024)
self.tn.read_until(b"login:", 10)
self.tn.write(conf.user_call_sign.encode('utf-8', errors='ignore') + b"\n") # user_call_sign may be Unicode
break
except: except:
time.sleep(0.5) pass
if b"login:" in self.bytes:
break
else:
self.error = 'No "login:" prompt from ' + self.addr
return
self.sock.sendall(conf.user_call_sign.encode('utf-8', errors='ignore') + b"\r\n")
if conf.dxClPassword: if conf.dxClPassword:
self.tn.read_until(b"Password: ") for i in range(10):
self.tn.write(conf.dxClPassword.encode('utf-8', errors='ignore') + b"\n") try:
self.bytes += self.sock.recv(1024)
except:
pass
if b"Password:" in self.bytes:
break
else:
self.error = 'No "Password:" prompt from ' + self.addr
return
self.sock.sendall(conf.dxClPassword.encode('utf-8', errors='ignore') + b"\r\n")
self.error = ''
self.bytes = bytearray(0)
def telnetRead(self): def Poll(self):
message = self.tn.read_until(b'\n', 60).decode(encoding='utf-8', errors='replace') with self.dxLock:
if self.doQuit.isSet() == False: index = self.bytes.find(b"\n")
dxEntry = DxEntry(); if index >= 0:
if dxEntry.parseMessage(message): message = self.bytes[0:index + 1]
for i, listElement in enumerate(self.dxSpots): self.bytes = self.bytes[index + 1:]
if (listElement.equal(dxEntry)): else:
listElement.join (dxEntry) return
return message = message.decode(encoding='utf-8', errors='replace')
if listElement.isExpired(): dxEntry = DxEntry()
del (self.dxSpots[i]) if dxEntry.parseMessage(message):
self.dxSpots.append(dxEntry) for i, listElement in enumerate(self.dxSpots):
if self.listener: if (listElement.equal(dxEntry)):
self.listener() listElement.join (dxEntry)
return True
def getHost(self): if listElement.isExpired():
return self.tn.host + ':' + str(self.tn.port) del (self.dxSpots[i])
self.dxSpots.append(dxEntry)
return True
def stop(self): def stop(self):
self.doQuit.set() self.doQuit.set()
def dxStatus(self):
if self.error:
return self.error
nSpots = len(self.dxSpots)
if nSpots == 0:
return self.msg_no_spots
elif nSpots == 1:
return self.msg_one_spot
return str(nSpots) + self.msg_spots
+2
View File
@@ -402,6 +402,8 @@ class Hardware(BaseHardware):
if do_tx: if do_tx:
try: try:
self.rx_udp_socket.send(s) self.rx_udp_socket.send(s)
if DEBUG:
self.PrintStatus(' Send', self.want_udp_status)
except: except:
pass pass
def SetVNA(self, key_down=None, vna_start=None, vna_stop=None, vna_count=None, do_tx=False): def SetVNA(self, key_down=None, vna_start=None, vna_stop=None, vna_count=None, do_tx=False):
+22 -1
View File
@@ -91,6 +91,8 @@ class Hardware(BaseHardware):
conf.si570_direct_control = False conf.si570_direct_control = False
conf.si570_xtal_freq = 114285000 conf.si570_xtal_freq = 114285000
conf.repeater_delay = 0.25 conf.repeater_delay = 0.25
self.ptt_count = 200
self.ptt_on = 0
BaseHardware.__init__(self, app, conf) BaseHardware.__init__(self, app, conf)
QS.set_sparams(multus_cw_samples=1) QS.set_sparams(multus_cw_samples=1)
self.use_softrock = True 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) self.usb_dev.ctrl_transfer(OUT, address, self.si570_i2c_address + 0x700, 0, message)
if DEBUG: if DEBUG:
print ("USB send to 0x%X" % address, message) 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): def open(self):
text = BaseHardware.open(self) text = BaseHardware.open(self)
self.InitKeyer() self.InitKeyer()
@@ -120,8 +129,20 @@ class Hardware(BaseHardware):
print ("Mode is not CW") print ("Mode is not CW")
self.TransferOut(0x70, cw_mode) self.TransferOut(0x70, cw_mode)
return ret 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 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): def InitKeyer(self):
# Initialize the keyer parameters # Initialize the keyer parameters
conf = self.conf conf = self.conf
+11 -10
View File
@@ -1434,12 +1434,8 @@ class ConfigStatus(wx.ScrolledCanvas):
self.MakeRow2("Sample interrupts", self.interrupts, cfile) self.MakeRow2("Sample interrupts", self.interrupts, cfile)
self.MakeRow2("Microphone or DGT level dB", level, qfile) self.MakeRow2("Microphone or DGT level dB", level, qfile)
self.MakeRow2("FFT number of points", self.fft_size, err_msg) self.MakeRow2("FFT number of points", self.fft_size, err_msg)
if conf.dxClHost: # connection to dx cluster if application.dxCluster:
nSpots = len(application.dxCluster.dxSpots) msg = application.dxCluster.dxStatus()
if nSpots > 0:
msg = str(nSpots) + ' DX spot' + ('' if nSpots==1 else 's') + ' received from ' + application.dxCluster.getHost()
else:
msg = "No DX Cluster data from %s" % conf.dxClHost
self.MakeRow2("FFT number of errors", self.fft_error, msg) self.MakeRow2("FFT number of errors", self.fft_error, msg)
else: else:
self.MakeRow2("FFT number of errors", self.fft_error) self.MakeRow2("FFT number of errors", self.fft_error)
@@ -2123,7 +2119,7 @@ class GraphScreen(wx.Window):
"""For mouse clicks in our display, translate to our screen coordinates.""" """For mouse clicks in our display, translate to our screen coordinates."""
mouse_x, mouse_y = event.GetPosition() mouse_x, mouse_y = event.GetPosition()
win = event.GetEventObject() win = event.GetEventObject()
if win is not self: if win and win is not self:
x, y = win.GetPosition().Get() x, y = win.GetPosition().Get()
mouse_x += x mouse_x += x
mouse_y += y mouse_y += y
@@ -3524,6 +3520,7 @@ class App(wx.App):
if not os.path.isdir(self.QuiskFilesDir): if not os.path.isdir(self.QuiskFilesDir):
self.QuiskFilesDir = DefaultConfigDir self.QuiskFilesDir = DefaultConfigDir
self.std_out_err = StdOutput(self) self.std_out_err = StdOutput(self)
self.std_out_err.Logfile("WxPython " + wx.version())
QS.set_params(quisk_is_vna=0) # We are not the VNA program QS.set_params(quisk_is_vna=0) # We are not the VNA program
# Read in configuration from the selected radio # Read in configuration from the selected radio
self.BandPlan = [] self.BandPlan = []
@@ -3614,6 +3611,7 @@ class App(wx.App):
setattr(conf, 'widgets_file_name', '') setattr(conf, 'widgets_file_name', '')
Hardware = self.Hardware Hardware = self.Hardware
self.use_fast_heart_beat = hasattr(Hardware, "FastHeartBeat") self.use_fast_heart_beat = hasattr(Hardware, "FastHeartBeat")
self.poll_gui_control = hasattr(Hardware, "PollGuiControl")
# Initialization - may be over-written by persistent state # Initialization - may be over-written by persistent state
self.local_conf.Initialize() self.local_conf.Initialize()
self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow
@@ -3989,6 +3987,7 @@ class App(wx.App):
minh = 100 minh = 100
maxh = -1 maxh = -1
self.main_frame.SetSizeHints(minw, minh, maxw, maxh) self.main_frame.SetSizeHints(minw, minh, maxw, maxh)
self.main_frame.SetClientSize(wx.Size(width, self.height))
if hasattr(Hardware, 'pre_open'): # pre_open() is called before open() if hasattr(Hardware, 'pre_open'): # pre_open() is called before open()
Hardware.pre_open() Hardware.pre_open()
if self.local_conf.GetWidgets(self, Hardware, conf, frame, gbs, vertBox): if self.local_conf.GetWidgets(self, Hardware, conf, frame, gbs, vertBox):
@@ -4028,7 +4027,6 @@ class App(wx.App):
if conf.dxClHost: if conf.dxClHost:
# create DX Cluster and register listener for change notification # create DX Cluster and register listener for change notification
self.dxCluster = dxcluster.DxCluster() self.dxCluster = dxcluster.DxCluster()
self.dxCluster.setListener(self.OnDxClChange)
self.dxCluster.start() self.dxCluster.start()
# Create shortcut keys for buttons # Create shortcut keys for buttons
if conf.button_layout == 'Large screen': if conf.button_layout == 'Large screen':
@@ -4379,8 +4377,6 @@ class App(wx.App):
conf.tx_ip, conf.tx_audio_port, conf.tx_ip, conf.tx_audio_port,
conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q, conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q,
0.7, conf.mic_playback_rate) 0.7, conf.mic_playback_rate)
def OnDxClChange(self):
self.station_screen.Refresh()
def OnIdle(self, event): def OnIdle(self, event):
if self.screen: if self.screen:
self.screen.OnIdle(event) self.screen.OnIdle(event)
@@ -6455,6 +6451,8 @@ class App(wx.App):
self.hamlib_com2_handler.Process() self.hamlib_com2_handler.Process()
if self.use_fast_heart_beat: if self.use_fast_heart_beat:
Hardware.FastHeartBeat() Hardware.FastHeartBeat()
if self.poll_gui_control:
Hardware.PollGuiControl()
if conf.do_repeater_offset: if conf.do_repeater_offset:
hold = QS.tx_hold_state(-1) hold = QS.tx_hold_state(-1)
if hold == 2: # Tx is being held for an FM repeater TX frequency shift if hold == 2: # Tx is being held for an FM repeater TX frequency shift
@@ -6635,6 +6633,9 @@ class App(wx.App):
self.ChangeDisplayFrequency(tune - vfo, vfo) self.ChangeDisplayFrequency(tune - vfo, vfo)
self.FldigiPoll() self.FldigiPoll()
self.HamlibPoll() self.HamlibPoll()
if self.dxCluster:
if self.dxCluster.Poll():
self.station_screen.Refresh()
if self.timer - self.slowheart_time0 > 0.5: if self.timer - self.slowheart_time0 > 0.5:
self.slowheart_time0 = self.timer self.slowheart_time0 = self.timer
if self.w_phase: if self.w_phase:
+2
View File
@@ -120,6 +120,8 @@ class Hardware:
self.application.bandscope_clock = clock self.application.bandscope_clock = clock
#def PollCwKey(self): # Optional. Called frequently by the sound thread to check the CW key status. #def PollCwKey(self): # Optional. Called frequently by the sound thread to check the CW key status.
# pass # Do not define if not needed. # pass # Do not define if not needed.
#def PollGuiControl(self): # Optional. Called frequently by the GUI thread to change GUI settings (PTT etc.)
# pass # Do not define if not needed.
def StartSamples(self): # Quisk calls this from the sound thread to start sending samples. def StartSamples(self): # Quisk calls this from the sound thread to start sending samples.
# If you return a string, it replaces the string returned from hardware open() # If you return a string, it replaces the string returned from hardware open()
pass pass
+1 -1
View File
@@ -7,7 +7,7 @@ import os
# You must define the version here. A title string including # You must define the version here. A title string including
# the version will be written to __init__.py and read by quisk.py. # the version will be written to __init__.py and read by quisk.py.
Version = '4.2.38' Version = '4.2.41'
fp = open("__init__.py", "w") # write title string fp = open("__init__.py", "w") # write title string
fp.write("#Quisk version %s\n" % Version) fp.write("#Quisk version %s\n" % Version)