3 Commits
Author SHA1 Message Date
James C. Ahlstrom 14a80f09d5 Quisk version 4.2.37 2024-08-21 15:25:10 -04:00
James C. Ahlstrom 1c9f81254b Quisk version 4.2.36 2024-08-03 13:56:30 -04:00
James C. Ahlstrom 76208643a0 Quisk version 4.2.35 2024-06-28 13:54:55 -04:00
12 changed files with 1039 additions and 252 deletions
+27
View File
@@ -1,3 +1,30 @@
Quisk Version 4.2.37 August 2024
=================================
Quisk now has a new option "Fixed tune offset" on the Config/radio/Options screen. This is meant for
Softrock radios that need a constant audio frequency to simplify amplitude and phase corrections.
The value zero means no change to Quisk tuning. Otherwise enter the fixed frequency offset from center.
If you are using the obsolete "mouse_tune_method = 1" please delete it and use this new feature.
If you use softrock_tune_vfo.py as your hardware file, you should replace it with softrock/hardware_usb.py.
Quisk now works on Python 3.12. And I fixed a bug in the small screen graph width.
Quisk Version 4.2.36 August 2024
=================================
I added two new color schemes by Michael, DK1MI. The On/Off button color and text color can
now be configured. (Michael, send me the final colors). There are a few other small changes.
Quisk Version 4.2.35 June 2024
===============================
There is a new item "File play level" on the Config/radio/Sound screen to reduce the volume when playing files.
Use it to reduce the volume of the CQ message relative to radio sound.
Dan, AI2M, provided a patch to compile in MacOS on Apple Silicon. Thanks Dan!
I am working on better amplitude and phase adjustments when using SoftRock radios. The new corrections
screen has a spreadsheet to display the VFO and tune offset. And there is a feature to automatically
measure the corrections when using a signal generator as a source. If you use Quisk with SoftRock or similar
radios, please let me know what you think.
Quisk Version 4.2.34 May 2024
==============================
For the Hermes-Lite2 the inhibit Tx function (CN8) now releases the Spot button.
+1 -1
View File
@@ -1,2 +1,2 @@
#Quisk version 4.2.34
#Quisk version 4.2.37
from .quisk import main
+416 -195
View File
@@ -1,13 +1,9 @@
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
import sys, wx, wx.lib, os, re, pickle, traceback, json
import sys, wx, wx.lib, os, re, pickle, traceback, json, copy
# Quisk will alter quisk_conf_defaults to include the user's config file.
import quisk_conf_defaults as conf
import _quisk as QS
from quisk_widgets import QuiskPushbutton, QuiskCheckbutton, QuiskBitField, SliderBoxH, SliderBoxHH
from quisk_widgets import FreqFormatter
from quisk_widgets import wxVersion
if wxVersion in ('2', '3'):
import wx.combo as wxcombo
@@ -288,10 +284,8 @@ class Configuration:
self.settings_changed = True
errors = errors + "Failed to set %s to %s using format %s\n" % (k, v, fmt)
#traceback.print_exc()
if conf.color_scheme == 'B':
conf.__dict__.update(conf.color_scheme_B)
elif conf.color_scheme == 'C':
conf.__dict__.update(conf.color_scheme_C)
if conf.color_scheme != 'A':
conf.__dict__.update(getattr(conf, 'color_scheme_' + conf.color_scheme))
self.RequiredValues(radio_dict) # Why not update conf too??? This only updates the radio_dict.
if errors:
dlg = wx.MessageDialog(None, errors,
@@ -334,7 +328,7 @@ class Configuration:
path = self.NormPath(path)
if not os.path.isfile(path):
dlg = wx.MessageDialog(None,
"Failure for hardware file %s!" % path,
"Can not find the hardware file %s!" % path,
'Hardware File', wx.OK|wx.ICON_ERROR)
ret = dlg.ShowModal()
dlg.Destroy()
@@ -405,11 +399,12 @@ class Configuration:
notebk.AddPage(self.radio_page, "Radios")
self.radios_page_start = notebk.GetPageCount()
notebk.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanging, notebk)
if Settings[1] in Settings[2]:
page = RadioNotebook(notebk, Settings[1])
notebk.AddPage(page, "*%s*" % Settings[1])
for name in Settings[2]:
page = RadioNotebook(notebk, name)
if name == Settings[1]:
notebk.AddPage(page, "*%s*" % name)
else:
if name != Settings[1]:
page = RadioNotebook(notebk, name)
notebk.AddPage(page, name)
def GuessType(self):
udp = conf.use_rx_udp
@@ -483,7 +478,7 @@ class Configuration:
for rxname, data in self.receiver_data:
if rxname == receiver_name:
return data
return None
return ()
def GetReceiverDatum(self, receiver_name, item_name):
for rxname, data in self.receiver_data:
if rxname == receiver_name:
@@ -569,6 +564,8 @@ class Configuration:
# self.format4name with the items that Configuration understands.
# Dicts and lists are Python objects. All other items are text, not Python objects.
#
# This parses the quisk_conf_defaults.py file and user-defined radios in ./*pkg/quisk_hardware.py.
#
# Sections start with 16 #, section name
# self.sections is a list of [section_name, section_data]
# section_data is a list of [data_name, text, fmt, help_text, values]
@@ -601,11 +598,11 @@ class Configuration:
except:
traceback.print_exc()
def _ParserConf(self, filename):
re_AeqB = re.compile("^#?(\w+)\s*=\s*([^#]+)#*(.*)") # item values "a = b"
re_AeqB = re.compile(r"^#?(\w+)\s*=\s*([^#]+)#*(.*)") # item values "a = b"
section = None
data_name = None
multi_line = False
fp = open(filename, "r")
fp = open(filename, "r", encoding="utf8")
for line in fp:
line = line.strip()
if not line:
@@ -835,12 +832,10 @@ class QPowerMeterCalibration(wx.Frame):
## Note: The amplitude/phase adjustments have ideas provided by Andrew Nilsson, VK6JBL.
## October 2020: changed to make RxTx frequency and VFO two independent variables.
## June 2024: Added a Grid control and automatic receive measurement.
class QAdjustPhase(wx.Frame):
"""Create a window with amplitude and phase adjustment controls"""
f_ampl = "Amplitude adjustment %.6f"
f_phase = "Phase adjustment degrees %.6f"
def __init__(self, parent, width, rx_tx):
self.width = width
self.rx_tx = rx_tx # Must be "rx" or "tx"
if rx_tx == 'tx':
self.is_tx = 1
@@ -848,121 +843,232 @@ class QAdjustPhase(wx.Frame):
else:
self.is_tx = 0
t = "Adjust Sound Card Receive Amplitude and Phase"
wx.Frame.__init__(self, application.main_frame, -1, t, pos=(50, 100), style=wx.CAPTION)
wx.Frame.__init__(self, application.main_frame, -1, t, pos=(50, 100), style=wx.CAPTION|wx.RESIZE_BORDER)
self.client_width = application.screen_width * 5 // 10
self.SetClientSize((self.client_width, application.screen_height * 5 // 10))
self.new_amplitude = self.new_phase = 0.0
self.new_tune = 0
self.bandAmplPhase = copy.deepcopy(application.bandAmplPhase)
self.new_cell = None
self.dirty = False
self.manual = True
self.panel = wx.Panel(self)
self.MakeControls()
self.Redraw()
self.Show()
self.Bind(wx.EVT_CLOSE, self.OnBtnExit)
QS.softrock_corrections(1)
QS.set_ampl_phase(0.0, 0.0, self.is_tx)
self.grid.GoToCell(0, 2)
def MakeControls(self): # Make controls for phase/amplitude adjustment
panel = self.panel
width = self.width
self.old_amplitude, self.old_phase = application.GetAmplPhase(self.rx_tx)
self.new_amplitude, self.new_phase = self.old_amplitude, self.old_phase
sl_max = width * 4 // 10 # maximum +/- value for slider
sizer = wx.BoxSizer(wx.VERTICAL)
panel.SetSizer(sizer)
sl_max = application.screen_width * 4 // 10 # maximum +/- value for slider
self.ampl_scale = float(conf.rx_max_amplitude_correct) / sl_max
self.phase_scale = float(conf.rx_max_phase_correct) / sl_max
font = wx.Font(conf.default_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
main_font = wx.Font(conf.default_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
self.SetFont(font)
self.SetFont(main_font)
panel.SetFont(main_font)
charx = self.GetCharWidth()
self.tab1 = tab1 = charx
chary = self.GetCharHeight()
deltay = chary * 12 // 10
y = chary * 3 // 10
self.controls = []
txt = wx.StaticText(panel, -1, "No data", pos=(tab1, y))
self.controls.append(txt)
y += txt.GetSize().GetHeight()
#y += deltay * 14 // 10
self.t_ampl = wx.StaticText(panel, -1, self.f_ampl % self.old_amplitude, pos=(tab1, y))
self.controls.append(self.t_ampl)
y += deltay
fine = wx.StaticText(panel, -1, 'Fine', pos=(tab1, y))
self.controls.append(fine)
coarse = wx.StaticText(panel, -1, 'Coarse', pos=(tab1, y + deltay))
self.controls.append(coarse)
tab2 = tab1 + coarse.GetSize().GetWidth() + charx
sliderX = width - tab2 - charx
self.ampl1 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1))
self.controls.append(self.ampl1)
y += deltay
self.ampl2 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1))
self.controls.append(self.ampl2)
y += deltay * 14 // 10
self.PosAmpl(self.old_amplitude)
self.t_phase = wx.StaticText(panel, -1, self.f_phase % self.old_phase, pos=(tab1, y))
self.controls.append(self.t_phase)
y += deltay
fine = wx.StaticText(panel, -1, 'Fine', pos=(tab1, y))
self.controls.append(fine)
coarse = wx.StaticText(panel, -1, 'Coarse', pos=(tab1, y + deltay))
self.controls.append(coarse)
self.phase1 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1))
self.controls.append(self.phase1)
y += deltay
self.phase2 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1))
self.controls.append(self.phase2)
y += deltay
sv = QuiskPushbutton(panel, self.OnBtnSave, 'Save')
self.controls.append(sv)
dv = QuiskPushbutton(panel, self.OnBtnDestroyVFO, 'Destroy VFO')
self.controls.append(dv)
ds = QuiskPushbutton(panel, self.OnBtnDestroyALL, 'Destroy ALL')
self.controls.append(ds)
cn = QuiskPushbutton(panel, self.OnBtnFinished, 'Finished')
self.controls.append(cn)
hl = QuiskPushbutton(panel, self.OnBtnHelp, 'Help')
self.controls.append(hl)
w, h = ds.GetSize().Get()
sv.SetSize((w, h))
cn.SetSize((w, h))
hl.SetSize((w, h))
y += h * 5 // 10
x = (width - w * 5) // 6
sv.SetPosition((x, y))
dv.SetPosition((x*2 + w, y))
ds.SetPosition((x*3 + w*2, y))
cn.SetPosition((x*4 + w*3, y))
hl.SetPosition((x*5 + w*4, y))
sv.SetBackgroundColour('light blue')
dv.SetBackgroundColour('light blue')
ds.SetBackgroundColour('light blue')
cn.SetBackgroundColour('light blue')
hl.SetBackgroundColour('light blue')
y += h
y += h * 4 // 10
self.PosPhase(self.old_phase)
self.SetClientSize(wx.Size(width, y))
self.ampl1.Bind(wx.EVT_SCROLL, self.OnChange)
# Create the grid heading and Destroy button
self.cell_amph = wx.StaticText(panel, -1, "")
font = wx.Font(conf.default_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_BOLD, False, conf.quisk_typeface)
self.cell_amph.SetFont(font)
self.cell_amph.SetLabel("Cell frequency %s, gain %.3f, phase %.3f\u00B0 " % (FreqFormatter(144000000), -1.123, -12.456))
dv = wx.lib.buttons.GenButton(panel, label=" Destroy ")
dv.SetBezelWidth(3)
dv.SetBackgroundColour("#DDD")
dv.Bind(wx.EVT_BUTTON, self.OnBtnDestroy)
hbox = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(hbox, flag=wx.BOTTOM|wx.EXPAND, border=chary * 3 // 10)
hbox.Add(self.cell_amph, flag=wx.ALL|wx.ALIGN_CENTER_VERTICAL, border=chary // 2)
hbox.Add(dv, flag=wx.LEFT|wx.RIGHT|wx.ALIGN_CENTER_VERTICAL, border=charx * 5)
# Create the grid
self.grid = wx.grid.Grid(panel, style=wx.BORDER_SIMPLE)
self.grid.CreateGrid(4, 3)
self.grid.HideRowLabels()
self.selected_row = 0
self.selected_col = 0
self.cell_bg_color = self.grid.GetDefaultCellBackgroundColour()
self.grid.SetDefaultCellFont(main_font)
width, h = self.GetTextExtent("987-24 000")
self.grid.SetDefaultColSize(width, True)
self.grid.EnableEditing(False)
self.grid.SetColLabelValue(0, "Band")
self.grid.SetColLabelValue(1, "VFO")
self.grid.SetColLabelValue(2, "Tune")
self.grid.SetColSize(0, width // 2)
self.grid.SetColSize(1, width * 14 // 10)
num_tune = (self.client_width - width * 2) // width
#print ("num_tune", num_tune)
for col in range(3, num_tune + 2):
self.grid.AppendCols()
self.grid.SetColLabelValue(col, "Tune")
#self.grid.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.OnGridLeftClick)
self.grid.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.OnGridSelectCell)
gbox = wx.BoxSizer(wx.VERTICAL)
sizer.Add(gbox, proportion=1, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=charx)
gbox.Add(self.grid, proportion=1)
# Create the New Cell text, Manual/Measure, AddCell button
sbs = wx.StaticBoxSizer(wx.VERTICAL, panel, " New Cell")
sizer.Add(sbs, flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT|wx.BOTTOM, border=charx*3)
self.add_cell_text = wx.StaticText(panel, -1, "Band")
self.add_cell_text.SetFont(main_font)
sbs.Add(self.add_cell_text, flag=wx.LEFT|wx.TOP, border=charx)
box0 = wx.BoxSizer(wx.HORIZONTAL)
sbs.Add(1, charx)
sbs.Add(box0, flag=wx.EXPAND)
rb_manual = wx.RadioButton(panel, -1, "Manual adjustment", style=wx.RB_GROUP)
self.Bind(wx.EVT_RADIOBUTTON, self.OnBtnManMeasure)
rb_measure = wx.RadioButton(panel, -1, "Measure")
if self.is_tx:
rb_manual.Enable(False)
rb_measure.Enable(False)
b = wx.lib.buttons.GenButton(panel, label=" Add cell ")
b.Bind(wx.EVT_BUTTON, self.OnBtnAddCell)
b.SetBezelWidth(3)
b.SetBackgroundColour("#DDD")
box0.Add(rb_manual, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx)
box0.Add(rb_measure, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*5)
box0.Add(b, flag=wx.LEFT, border=charx*5)
# Create the amplitude slider controls
box1 = wx.BoxSizer(wx.HORIZONTAL)
box2 = wx.BoxSizer(wx.HORIZONTAL)
sbs.Add(box1, flag=wx.EXPAND)
sbs.Add(box2, flag=wx.EXPAND)
# ST_ELLIPSIZE_END needed to work around a layout bug
fine = wx.StaticText(panel, -1, 'Gain Fine', style=wx.ST_ELLIPSIZE_END)
coarse = wx.StaticText(panel, -1, 'Gain Coarse', style=wx.ST_ELLIPSIZE_END)
box1.Add(fine, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*3)
box2.Add(coarse, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*3)
self.ampl1 = wx.Slider(panel, -1, 0, -sl_max, sl_max)
self.ampl2 = wx.Slider(panel, -1, 0, -sl_max, sl_max)
box1.Add(self.ampl1, flag=wx.LEFT, proportion=1, border=0)
box2.Add(self.ampl2, flag=wx.LEFT, proportion=1, border=0)
self.ampl1.Bind(wx.EVT_SCROLL, self.OnAmpl1)
self.ampl2.Bind(wx.EVT_SCROLL, self.OnAmpl2)
self.phase1.Bind(wx.EVT_SCROLL, self.OnChange)
self.phase2.Bind(wx.EVT_SCROLL, self.OnPhase2)
# Create the phase slider controls
box3 = wx.BoxSizer(wx.HORIZONTAL)
box4 = wx.BoxSizer(wx.HORIZONTAL)
sbs.Add(box3, flag=wx.EXPAND)
sbs.Add(box4, flag=wx.EXPAND)
fine = wx.StaticText(panel, -1, 'Phase Fine', style=wx.ST_ELLIPSIZE_END)
coarse = wx.StaticText(panel, -1, 'Phase Coarse', style=wx.ST_ELLIPSIZE_END)
box3.Add(fine, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*3)
box4.Add(coarse, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*3)
self.phas1 = wx.Slider(panel, -1, 0, -sl_max, sl_max)
self.phas2 = wx.Slider(panel, -1, 0, -sl_max, sl_max)
box3.Add(self.phas1, flag=wx.LEFT, proportion=1, border=0)
box4.Add(self.phas2, flag=wx.LEFT, proportion=1, border=0)
self.phas1.Bind(wx.EVT_SCROLL, self.OnPhase1)
self.phas2.Bind(wx.EVT_SCROLL, self.OnPhase2)
# Create the button row
btnbox = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(btnbox, flag=wx.EXPAND)
btns = []
btnbox.Add(1, 1, proportion=10)
b = wx.lib.buttons.GenButton(panel, label=" Save ")
btnbox.Add(b)
btnbox.Add(1, 1, proportion=5)
btns.append(b)
size = b.GetSize()
b.Bind(wx.EVT_BUTTON, self.OnBtnSave)
b = wx.lib.buttons.GenButton(panel, label=" Exit ")
btnbox.Add(b)
btnbox.Add(1, 1, proportion=5)
btns.append(b)
b.Bind(wx.EVT_BUTTON, self.OnBtnExit)
b = wx.lib.buttons.GenButton(panel, label=" Help ")
btnbox.Add(b)
btnbox.Add(1, 1, proportion=10)
btns.append(b)
b.Bind(wx.EVT_BUTTON, self.OnBtnHelp)
#b.Bind(wx.EVT_BUTTON, self.OnGraphData)
for b in btns:
b.SetBezelWidth(3)
b.SetBackgroundColour("#DDD")
b.SetSize(size)
sizer.Add(charx, chary)
def Redraw(self):
# Print available data points
self.band = application.lastBand
data = application.bandAmplPhase.get(self.band, {})
data = data.get(self.rx_tx, [])
if data:
t = "Band %s VFO and frequency list\n" % self.band
for vfo, items in data:
t += "%10d:" % vfo
for freq, ampl, phase in items:
t += " %7d" % freq
t += "\n"
self.grid.ClearGrid()
# Print available data cells
row = 0
bands = []
self.cell_dict = {}
for band in self.bandAmplPhase:
if band != "Version" and self.rx_tx in self.bandAmplPhase[band]:
bands.append(band)
bands.sort(reverse=True)
for band in bands:
vfos_tunes = self.bandAmplPhase[band][self.rx_tx]
if vfos_tunes:
if self.grid.GetNumberRows() <= row:
self.grid.AppendRows()
self.grid.SetCellValue(row, 0, band)
self.grid.SetCellAlignment(row, 0, wx.ALIGN_CENTER, wx.ALIGN_CENTER)
for vfo, tunes in vfos_tunes:
if self.grid.GetNumberRows() <= row:
self.grid.AppendRows()
self.cell_dict[(row, 0)] = band, vfo, 0.0
self.grid.SetCellValue(row, 1, FreqFormatter(vfo))
self.grid.SetCellAlignment(row, 1, wx.ALIGN_RIGHT, wx.ALIGN_CENTER)
col = 2
for tune, ampl, phase in tunes:
if self.grid.GetNumberCols() <= col:
self.grid.AppendCols()
self.grid.SetColLabelValue(col, "Tune")
self.grid.SetCellValue(row, col, FreqFormatter(tune))
self.grid.SetCellAlignment(row, col, wx.ALIGN_RIGHT, wx.ALIGN_CENTER)
self.cell_dict[(row, col)] = tune, ampl, phase
if (vfo, tune) == self.new_cell:
self.grid.GoToCell(row, col)
col += 1
row += 1
self.new_cell = None
self.grid.ForceRefresh()
def SetNewCellText(self):
vfo = FreqFormatter(application.VFO)
self.add_cell_text.SetLabel("Band %s, Center (VFO) %s, Tune %d, Gain %5.3f, Phase %5.3f\u00B0" % (
application.lastBand, vfo, self.new_tune, self.new_amplitude + 1.0, self.new_phase))
def SlowHeartBeat(self):
if self.manual:
self.new_tune = application.txFreq
else:
t = "Band %s: No data." % self.band
x, y = self.controls[0].GetPosition()
height = self.controls[0].GetSize().GetHeight()
self.controls[0].Destroy()
txt = wx.StaticText(self.panel, -1, t, pos=(self.tab1, y))
self.controls[0] = txt
delta = txt.GetSize().GetHeight() - height
for ctrl in self.controls[1:]:
x, y = ctrl.GetPosition()
y += delta
ctrl.Move(x, y)
h = self.GetClientSize().GetHeight()
self.SetClientSize(wx.Size(self.width, h + delta))
self.new_tune, gain, phase = QS.softrock_corrections(2)
ampl = gain - 1.0
self.new_amplitude = ampl
phase *= 57.2958
self.new_phase = phase
self.PosAmpl(ampl)
self.PosPhase(phase)
self.SetNewCellText()
def OnGridSelectCell(self, event=None):
self.grid.SetCellBackgroundColour(self.selected_row, self.selected_col, self.cell_bg_color)
if event:
event.Skip()
row = self.selected_row = event.GetRow()
col = self.selected_col = event.GetCol()
else:
row = self.selected_row
col = self.selected_col
self.grid.SetCellBackgroundColour(row, col, '#FEA')
self.grid.ForceRefresh()
band, vfo, z = self.cell_dict.get((row, 0), ('', None, 0))
tune, am, ph = self.cell_dict.get((row, col), (None, None, None))
if not band or vfo is None:
self.cell_amph.SetLabel("Cell is empty")
elif col == 0:
self.cell_amph.SetLabel("Band %s" % band)
elif col == 1:
self.cell_amph.SetLabel("Center (VFO) frequency %s" % FreqFormatter(vfo))
elif tune is None:
self.cell_amph.SetLabel("Cell is empty")
else:
self.cell_amph.SetLabel("Cell frequency %s, gain %.3f, phase %.3f\u00B0" % (FreqFormatter(vfo + tune), am + 1.0, ph))
def PosAmpl(self, ampl): # set pos1, pos2 for amplitude
pos2 = round(ampl / self.ampl_scale)
remain = ampl - pos2 * self.ampl_scale
@@ -973,83 +1079,196 @@ class QAdjustPhase(wx.Frame):
pos2 = round(phase / self.phase_scale)
remain = phase - pos2 * self.phase_scale
pos1 = round(remain / self.phase_scale * 50.0)
self.phase1.SetValue(pos1)
self.phase2.SetValue(pos2)
def OnChange(self, event):
ampl = self.ampl_scale * self.ampl1.GetValue() / 50.0 + self.ampl_scale * self.ampl2.GetValue()
if abs(ampl) < self.ampl_scale * 3.0 / 50.0:
ampl = 0.0
self.t_ampl.SetLabel(self.f_ampl % ampl)
phase = self.phase_scale * self.phase1.GetValue() / 50.0 + self.phase_scale * self.phase2.GetValue()
if abs(phase) < self.phase_scale * 3.0 / 50.0:
phase = 0.0
self.t_phase.SetLabel(self.f_phase % phase)
QS.set_ampl_phase(ampl, phase, self.is_tx)
self.new_amplitude, self.new_phase = ampl, phase
self.phas1.SetValue(pos1)
self.phas2.SetValue(pos2)
def OnAmpl2(self, event): # re-center the fine slider when the coarse slider is adjusted
ampl = self.ampl_scale * self.ampl1.GetValue() / 50.0 + self.ampl_scale * self.ampl2.GetValue()
self.PosAmpl(ampl)
self.OnChange(event)
self.OnAmpl1(event)
def OnAmpl1(self, event):
ampl = self.ampl_scale * self.ampl1.GetValue() / 50.0 + self.ampl_scale * self.ampl2.GetValue()
self.new_amplitude = ampl
self.SetNewCellText()
QS.set_ampl_phase(self.new_amplitude, self.new_phase, self.is_tx)
def OnPhase2(self, event): # re-center the fine slider when the coarse slider is adjusted
phase = self.phase_scale * self.phase1.GetValue() / 50.0 + self.phase_scale * self.phase2.GetValue()
phase = self.phase_scale * self.phas1.GetValue() / 50.0 + self.phase_scale * self.phas2.GetValue()
self.PosPhase(phase)
self.OnChange(event)
def OnBtnDestroyVFO(self, event): # Remove entry with the application VFO
data = application.bandAmplPhase.get(self.band, {})
data = data.get(self.rx_tx, [])
for i in range(len(data)):
if data[i][0] == application.VFO:
del data[i]
break
self.Redraw()
def OnBtnSave(self, event):
data = application.bandAmplPhase
if self.band not in data:
data[self.band] = {}
data = data[self.band]
if self.rx_tx not in data:
data[self.rx_tx] = []
data = data[self.rx_tx]
for vfo, items in data: # All items must be lists, not tuples
if vfo == application.VFO:
for i in range(len(items)):
item = items[i]
if item[0] == application.rxFreq:
item[1] = self.new_amplitude
item[2] = self.new_phase
self.OnPhase1(event)
def OnPhase1(self, event):
phase = self.phase_scale * self.phas1.GetValue() / 50.0 + self.phase_scale * self.phas2.GetValue()
self.new_phase = phase
self.SetNewCellText()
QS.set_ampl_phase(self.new_amplitude, self.new_phase, self.is_tx)
def OnBtnAddCell(self, event):
band = application.lastBand
vfo = application.VFO
self.new_cell = (vfo, self.new_tune)
ampl = self.new_amplitude
phase = self.new_phase
tune = self.new_tune
if band not in self.bandAmplPhase:
self.bandAmplPhase[band] = {}
if self.rx_tx not in self.bandAmplPhase[band]:
self.bandAmplPhase[band][self.rx_tx] = []
vfos_tunes = self.bandAmplPhase[band][self.rx_tx]
for i in range(0, len(vfos_tunes)):
vfo_tunes = vfos_tunes[i]
if vfo_tunes[0] == vfo:
tunes = vfo_tunes[1]
for j in range(0, len(tunes)):
if tunes[j][0] == tune:
tunes[j] = [tune, ampl, phase]
break
else:
items.append([application.rxFreq, self.new_amplitude, self.new_phase])
items.sort()
break
else:
data.append([application.VFO, [[application.rxFreq, self.new_amplitude, self.new_phase]]])
data.sort()
tunes.append([tune, ampl, phase])
tunes.sort()
self.dirty = True
self.Redraw()
return
vfos_tunes.append([vfo, [[tune, ampl, phase]]])
vfos_tunes = vfos_tunes.sort()
self.dirty = True
self.Redraw()
def OnBtnDestroyALL(self, event):
dlg = wx.MessageDialog(self, "This will destroy all data for band %s!" % self.band,
"Destroy Data", style = wx.YES|wx.NO)
if dlg.ShowModal() == wx.ID_YES:
if self.band in application.bandAmplPhase:
del application.bandAmplPhase[self.band]
self.grid.ClearSelection()
def OnBtnDestroy(self, event):
# Create a list of cells to be deleted
col_row = []
# print("Rows", self.grid.GetSelectedRows())
# print("Cols", self.grid.GetSelectedCols())
# print("Cells", self.grid.GetSelectedCells())
# print("Left", self.grid.GetSelectionBlockTopLeft())
# print("Right", self.grid.GetSelectionBlockBottomRight())
#
# delete the cell at the grid cursor
row = self.grid.GetGridCursorRow()
col = self.grid.GetGridCursorCol()
cr = col, row
if cr not in col_row:
col_row.append(cr)
#rows = self.grid.GetSelectedRows() # Not used
# delete whole columns
cols = self.grid.GetSelectedCols()
for col in cols:
for row in range(0, self.grid.GetNumberRows()):
cr = col, row
if cr not in col_row:
col_row.append(cr)
# delete cells
cells = self.grid.GetSelectedCells()
for cell in cells:
row, col = cell.Get()
cr = col, row
if cr not in col_row:
col_row.append(cr)
# delete blocks
top_left_list = self.grid.GetSelectionBlockTopLeft()
bottom_right_list = self.grid.GetSelectionBlockBottomRight()
for i in range(len(top_left_list)):
row1, col1 = top_left_list[i].Get()
row2, col2 = bottom_right_list[i].Get()
for row in range(row1, row2 + 1):
for col in range(col1, col2 + 1):
cr = col, row
if cr not in col_row:
col_row.append(cr)
col_row.sort(reverse=True)
changed = False
for col, row in col_row:
try:
band, vfo, z = self.cell_dict[(row, 0)]
vfos_tunes = self.bandAmplPhase[band][self.rx_tx]
except:
continue
if col in (0, 1): # Destroy row
for i in range(0, len(vfos_tunes)):
if vfos_tunes[i][0] == vfo:
del vfos_tunes[i]
changed = True
break
else: # Destroy cell
try:
tune, am, ph = self.cell_dict[(row, col)]
except:
continue
for i in range(0, len(vfos_tunes)):
if vfos_tunes[i][0] != vfo:
continue
tunes = vfos_tunes[i][1]
for j in range(0, len(tunes)):
if tunes[j][0] == tune:
del tunes[j]
changed = True
break
if changed:
self.dirty = True
self.Redraw()
def OnBtnFinished(self, event=None):
self.selected_row = self.grid.GetGridCursorRow()
self.selected_col = self.grid.GetGridCursorCol()
self.OnGridSelectCell()
self.grid.ClearSelection()
def OnBtnManMeasure(self, event):
self.manual = event.GetEventObject().GetLabel()[0:3] == "Man"
self.ampl1.Enable(self.manual)
self.ampl2.Enable(self.manual)
self.phas1.Enable(self.manual)
self.phas2.Enable(self.manual)
if self.manual:
QS.softrock_corrections(1)
QS.set_ampl_phase(self.new_amplitude, self.new_phase, self.is_tx)
else:
QS.softrock_corrections(2)
QS.set_ampl_phase(0.0, 0.0, self.is_tx)
def OnBtnSave(self, event):
application.bandAmplPhase = copy.deepcopy(self.bandAmplPhase)
self.dirty = False
def OnBtnExit(self, event):
#self.OnGraphData(None)
if self.dirty:
dlg = wx.MessageDialog(self,
"Your changes are not saved. Do you want to save them?", "Changes Were Made",
wx.OK|wx.CANCEL|wx.CANCEL_DEFAULT|wx.ICON_WARNING)
dlg.SetOKLabel("Discard Changes")
ret = dlg.ShowModal()
dlg.Destroy()
if ret == wx.ID_CANCEL:
return
QS.softrock_corrections(0)
ampl, phase = application.GetAmplPhase(self.rx_tx)
QS.set_ampl_phase(ampl, phase, self.is_tx)
application.w_phase = None
self.Destroy()
def OnBtnHelp(self, event=None):
dlg = wx.MessageDialog(self,
'The "VFO" is the frequency at the center of the graph screen. The Rx or Tx frequency is the offset from the VFO. \
Adjust the VFO and the frequency as desired. Then adjust the sliders to minimize the image. Press "Save" when satisfied. \
To adjust the VFO, use the band Up/Down buttons, or right click the graph at the desired VFO. \
'For manual adjustment attach a signal generator or look at a strong signal in the band. Set the Quisk frequency to the signal frequency. \
Adjust the amplitude and phase sliders to minimize the image. To save the adjustment, press Add Cell. \
For automatic measurement, attach a signal generator and choose some frequencies. Press Add Cell for each one. \
Adjustments must be made for both receive and transmit on each band. \
No changes are made to the table of adjustments until you press Save. \
The maximum slider adjustment range can be changed on the radio Hardware screen. \
The other buttons will delete the data for the current VFO, or for the whole band. \
For more information, press the main "Help" button, \
then "Documentation", then "SoftRock".'
For more information, press the main "Help" button, then "Documentation", then "SoftRock".'
, "Adjustment Help", style=wx.OK)
dlg.ShowModal()
def OnGraphData(self, event):
if not hasattr(self, "VFO"):
self.VFO = []
self.Tune = []
self.Gain = []
self.Phase = []
if event:
self.VFO.append(application.VFO)
self.Tune.append(self.new_tune)
self.Gain.append(self.new_amplitude + 1.0)
self.Phase.append(self.new_phase)
else:
for name in ("VFO", "Tune", "Gain", "Phase"):
print ("%s = [" % name)
lst = getattr(self, name)
for i in range(0, len(lst)):
if name[0] in "VT":
print("%d," % lst[i])
else:
print("%.5f," % lst[i])
print ("]")
class ListEditDialog(wx.Dialog): # Display a dialog with a List-Edit control, plus Ok/Cancel
def __init__(self, parent, title, choice, choices, width):
@@ -2011,7 +2230,8 @@ class ControlMixin:
if self.radio_name == Settings[1]: # changed for current radio
if name in ('hot_key_ptt_toggle', 'hot_key_ptt_if_hidden', 'keyupDelay', 'cwTone', 'pulse_audio_verbose_output',
'start_cw_delay', 'start_ssb_delay', 'maximum_tx_secs', 'quisk_serial_cts', 'quisk_serial_dsr',
'hot_key_ptt1', 'hot_key_ptt2', 'midi_ptt_toggle', 'TxRxSilenceMsec', 'hermes_lite2_enable'):
'hot_key_ptt1', 'hot_key_ptt2', 'midi_ptt_toggle', 'TxRxSilenceMsec', 'hermes_lite2_enable',
'fixed_tune_offset'):
setattr(conf, name, x)
application.ImmediateChange(name)
elif name[0:4] in ('lin_', 'win_'):
@@ -2028,6 +2248,9 @@ class ControlMixin:
elif name == "digital_output_level":
setattr(conf, name, x)
QS.set_sparams(digital_output_level=x)
elif name == "file_play_level":
setattr(conf, name, x)
QS.set_sparams(file_play_level=x)
elif name[0:7] == 'hermes_':
if name == 'hermes_TxLNA_dB':
application.Hardware.ChangeTxLNA(x)
@@ -2039,6 +2262,9 @@ class ControlMixin:
elif hasattr(application.Hardware, "ImmediateChange"):
setattr(conf, name, x)
application.Hardware.ImmediateChange(name)
elif name[0:6] == 'keyer_':
setattr(conf, name, x)
application.ImmediateChange(name)
def FormatOK(self, value, fmt4): # Check formats integer, number, boolean
ok, v = self.EvalItem(value, fmt4)
return ok
@@ -2441,6 +2667,7 @@ class Radios(BaseWindow): # The "Radios" first-level page
choices = []
for name, data in local_conf.receiver_data:
choices.append(name)
choices.sort()
self.add_type = self.AddComboCtrl(2, '', choices=choices, no_edit=True)
self.add_type.SetSelection(0)
item = self.AddTextL(3, "and name the new radio")
@@ -2524,11 +2751,9 @@ class Radios(BaseWindow): # The "Radios" first-level page
choices = Settings[2][:] # can rename any available radio
self.rename_old.SetItems(choices)
self.rename_old.SetSelection(0)
if "ConfigFileRadio" in choices:
if "ConfigFileRadio" in choices: # can not delete ConfigFileRadio
choices.remove("ConfigFileRadio")
if Settings[1] in choices:
choices.remove(Settings[1])
self.delete_name.SetItems(choices) # can not delete ConfigFileRadio nor the current radio
self.delete_name.SetItems(choices)
self.delete_name.SetSelection(0)
choices = Settings[2] + ["Ask me"]
if "ConfigFileRadio" not in choices:
@@ -2673,12 +2898,6 @@ class RadioSection(BaseWindow): # The pages for each section in the second-leve
hlp = "To delete a row, select it and press this button."
self.AddTextButtonHelp(2, "Delete Midi row", "Delete", self.OnMidiDelete, hlp)
self.NextRow()
if self.section == "Timing and CW":
if col != 1:
col = 1
self.NextRow()
self.NextRow()
self.AddTextL(1, 'Midi CW key moved to "Keys"', span=4)
if not Keys:
self.AddColSpacer(2, 20)
self.AddColSpacer(5, 20)
@@ -3437,13 +3656,13 @@ 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 == 'digital_output_level':
if name in ('digital_output_level', 'file_play_level'):
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)
cb.handler = self.OnChange
cb.quisk_data_name = name
break
self.NextRow()
self.NextRow()
# Add the grid for the sound settings
sizer = wx.GridBagSizer(2, 2)
@@ -3469,8 +3688,10 @@ class RadioSound(BaseWindow): # The Sound page in the second-level notebook for
self.NextRow()
choices = (("48000", "96000", "192000"), ("0", "1"), ("0", "1"), (" ", "0", "1"))
r = 0
if "SoftRock" in self.radio_dict['hardware_file_type']: # Samples come from sound card
softrock = True
if "SoftRock" in self.radio_dict['hardware_file_type']:
softrock = True # Samples come from sound card
elif hasattr(application.Hardware, "use_softrock"):
softrock = application.Hardware.use_softrock
else:
softrock = False
last_row = 8
+32 -12
View File
@@ -194,7 +194,7 @@ Quisk is free open source software written in Python and C. It is hosted on the
<br>
<p>
If you have Python 3 installed on your computer, just continue to use that version.
<b>On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11.</b>
<b>On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11 or 3.12.</b>
If you want to install a more recent version of Python, uninstall the old version first.
Running multiple versions of Python is possible but can be confusing and is not necessary.
A Quisk installation is needed for each version of Python you have.
@@ -204,7 +204,7 @@ A Quisk installation is needed for each version of Python you have.
<p>
Windows does not include Python, so you must first install a recent version of 64-bit Python 3.
If you already have 64-bit Python 3 installed, just keep using it.
<b>On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11.</b>
<b>On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11 or 3.12.</b>
There may be newer versions of Python, but until support is added, use the versions listed above.
Python is available from <a href="http://www.python.org/">http://www.python.org</a>.
There is an option to <b>Add Python to PATH</b>. You MUST select
@@ -239,13 +239,19 @@ python -m pip install --upgrade pyserial
<br><br>
python -m pip install --upgrade quisk
<br><br>
You should then be able to start Quisk with the command "quisk".
You can also start Quisk with "python -m quisk".
To create a Quisk shortcut on your desktop, right-click an empty space and select "New" and "Shortcut".
Use "quisk.exe" as the command and "Quisk" as the name.
The "quisk.exe" program is in <b>your_install_directory</b>\Scripts.
Enter this as the location of the item:
</p>
<br>
<pre>
pythonw -m quisk
</pre>
<br>
<p>
and then name the shortcut, perhaps "Quisk".
If you check the properties you will see that Windows expanded the location and start directory to the full path.
If you are curious, Quisk and its Python source files are installed in "<b>your_install_directory</b>\Lib\site-packages\quisk".
If you change to that directory you can run Quisk with "python quisk.py".
If you change to that directory you can run Quisk with "pythonw quisk.py".
</p>
<br>
<p>
@@ -824,8 +830,14 @@ SoftRock radios use an analog mixer to change the RF signal to stereo audio, and
A high quality sound card is advisable. The analog mixer is not perfect, and it will be necessary to adjust the I and Q
signals to equal amplitude and 90 degrees phase difference. Use the Config/Config screen buttons to
bring up an adjustment screen. Adjustments must be made for each band, and separately for transmit and receive.
The adjustment depends on both the VFO frequency and the tuning offset from the VFO. See my paper
The adjustment depends on both the VFO frequency and the tuning offset from the VFO.
Quisk has an option to fix the tuning offset to a constant and to tune with the hardware.
The tuning offset then has a constant correction and it is only necessary to correct for the VFO,
a much less sensitive correction.
See my paper
<a href="http://james.ahlstrom.name/phase_corr.html">http://james.ahlstrom.name/phase_corr.html</a> for more information.
<br>
<br>
A good strategy is to pick a VFO near the band center, and record corrections at an Rx frequency of -15000, -1000, 1000 and 15000 Hertz.
If the corrections are sensitive to VFO, record the corrections for these same Rx frequencies at VFOs equal to
or slightly outside the upper and lower band edges.
@@ -834,15 +846,20 @@ You can add as many correction points as desired. The corrections are saved in t
by hand if you are a Python expert. Otherwise you can just read the values.
<br>
<br>
If you fix the tuning offset to a constant, you can create a correction for VFO at the band center, or
the center and both edges.
<br>
<br>
To create a Receive correction point for a given VFO and frequency, attach a signal generator to the SoftRock through an attenuator,
and look at the image on the graph screen. If the signal is 3500 Hertz above the VFO, the image is 3500 Hertz below it.
If you don't have a signal generator, find a strong station on the band and look at its image. Minimize the image by adjusting
the amplitude and phase sliders on the adjustment screen, and then press "Save".
Choose "Measure" to measure the corrections and then "Add Cell".
If you don't have a signal generator, find a strong station on the band and look at its image. Choose "Manual adjustment".
Minimize the image by adjusting the amplitude and phase sliders on the adjustment screen, and then press "Add cell".
<br>
<br>
To create a Transmit correction point for a given VFO and frequency, attach the SoftRock RF output to a spectrum analyzer through an attenuator,
and look at the image. If you don't have a spectrum analyzer use a second receiver tuned to the image. Minimize the image by adjusting
the amplitude and phase sliders on the adjustment screen, and then press "Save".
the amplitude and phase sliders on the adjustment screen, and then press "Add cell".
<br>
<br>
</p>
@@ -1468,7 +1485,10 @@ show the devices being used. Change the Fldigi playback to QuiskDigitalInput, an
Monitor of QuiskDigitalOutput. Then everything should work. You do not need to use pavucontrol again because PulseAudio will
remember the settings.
<br>
<br>
Michael, DK1MI/N1BSD, has written a nice description of how to set up Fldigi. It is available
at his site <a href="https://dk1mi.radio/quisk-fldigi/">https://dk1mi.radio/quisk-fldigi/</a>.
<br>
<br>
If your digital program only uses Alsa devices or you don't want to use PulseAudio,
you can use the ALSA loopback device.
+1
View File
@@ -0,0 +1 @@
#
+188
View File
@@ -0,0 +1,188 @@
# This file is part of the Multus project. See https://www.multus-sdr.com/.
# It controls what items appear on the Hardware configuration screen and
# provides logic for each radio.
################ Receivers Multus CW, A transceiver from the Multus project.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = "multuspkg/quisk_hardware.py"
## widgets_file_name Widget file path, rfile
# This optional file adds additional controls for the radio.
#widgets_file_name = ""
## rx_max_amplitude_correct Max ampl correct, number
# If you get your I/Q samples from a sound card, you will need to correct the
# amplitude and phase for inaccuracies in the analog hardware. The correction is
# entered using the controls from the "Rx Phase" button on the config screen.
# You must enter a positive number. This controls the range of the control.
#rx_max_amplitude_correct = 0.2
## rx_max_phase_correct Max phase correct, number
# If you get your I/Q samples from a sound card, you will need to correct the
# amplitude and phase for inaccuracies in the analog hardware. The correction is
# entered using the controls from the "Rx Phase" button on the config screen.
# You must enter a positive number. This controls the range of the control in degrees.
#rx_max_phase_correct = 10.0
## tx_level Tx Level, dict
# This is the level of the Tx audio sent to SoftRock hardware after all processing as a percentage
# number from 0 to 100.
# The level should be below 100 to allow headroom for amplitude and phase adjustments.
# Changes are immediate (no need to restart).
#tx_level = {}
## digital_tx_level Digital Tx power %, integer
# Digital modes reduce power by the percentage on the config screen.
# This is the maximum value of the slider.
#digital_tx_level = 100
## keyer_speed Keyer speed WPM, integer
# This sets the keyer speed in words per minute.
#keyer_speed = 8
#keyer_speed = 13
#keyer_speed = 18
#keyer_speed = 20
#keyer_speed = 30
#keyer_speed = 40
## keyer_type Type of keyer, text choice
# Use "Straight" mode for a straight key or a bug. Connect the tip of the key to the transceiver.
# Otherwise, select Iambic-A or Iambic-B mode of operation.
#keyer_type = "Straight"
#keyer_type = "Iambic-A"
#keyer_type = "Iambic-B"
## keyer_space Keyer space, text choice
# This sets the type of spacing between elements or letters.
# "Element" attempts to provide proper spacing between the elements of a character.
# (The keyer always provides proper spacing between elements).
# "Letter" turns on letter spacing. This attempts to provide proper spacing between characters.
#keyer_space = "Element"
#keyer_space = "Letter"
## keyer_weight Keyer weight, integer choice
# A weight of 50% means that the dit time equals the inter-element time, which is standard.
# Reducing the weight increases the inter-element time, increasing the weight reduces it.
#keyer_weight = 50
#keyer_weight = 25
#keyer_weight = 75
## keyer_paddle Keyer paddle, text choice
# "Normal" means the left paddle is DIT. "Reverse" means the left paddle is DAH.
#keyer_paddle = "Normal"
#keyer_paddle = "Reverse"
import configure
import softrock
from softrock.hardware_usb import Hardware as BaseHardware
from softrock.hardware_usb import IN, OUT, UBYTE2, UBYTE4
import _quisk as QS
DEBUG = 0
class Hardware(BaseHardware):
def __init__(self, app, conf):
# Set constant parameters here:
conf.usb_vendor_id = 0x16c0
conf.usb_product_id = 0x05dc
conf.si570_i2c_address = 0x55
conf.si570_direct_control = False
conf.si570_xtal_freq = 114285000
conf.repeater_delay = 0.25
BaseHardware.__init__(self, app, conf)
QS.set_sparams(multus_cw_samples=1)
self.use_softrock = True
if DEBUG:
print ("The name of this radio is", configure.Settings[1])
print ("The hardware file type is", conf.hardware_file_type)
def TransferOut(self, address, message): # message is a bytes array or an integer 0 to 255
if isinstance(message, int):
message = message.to_bytes(1, 'big')
if self.usb_dev:
self.usb_dev.ctrl_transfer(OUT, address, self.si570_i2c_address + 0x700, 0, message)
if DEBUG:
print ("USB send to 0x%X" % address, message)
def open(self):
text = BaseHardware.open(self)
self.InitKeyer()
return text
def ChangeMode(self, mode):
ret = BaseHardware.ChangeMode(self, mode)
if mode in ("CWL", "CWU"):
cw_mode = b'C' # Adding "b" does not affect the value sent
if DEBUG:
print ("Mode is CW")
else:
cw_mode = b'U'
if DEBUG:
print ("Mode is not CW")
self.TransferOut(0x70, cw_mode)
return ret
def PollCwKey(self): # Called frequently by Quisk to check the CW key status
return # Quisk is always in Rx
def InitKeyer(self):
# Initialize the keyer parameters
conf = self.conf
if not hasattr(conf, "keyer_speed"):
conf.keyer_speed = 18
if not hasattr(conf, "keyer_type"):
conf.keyer_type = "Straight"
if not hasattr(conf, "keyer_space"):
conf.keyer_space = "Element"
if not hasattr(conf, "keyer_weight"):
conf.keyer_weight = 50
if not hasattr(conf, "keyer_paddle"):
conf.keyer_paddle = "Normal"
# We need to initialize in case the persistent hardware values differ from the Quisk values.
for name in ("keyer_speed", "keyer_type", "keyer_space", "keyer_weight", "keyer_paddle", "cwTone"):
self.ImmediateChange(name)
def ImmediateChange(self, name):
BaseHardware.ImmediateChange(self, name)
value = getattr(self.conf, name)
if DEBUG:
print ("ImmediateChange", name, value)
if name == "keyer_speed":
self.TransferOut(0x7B, value)
elif name == "keyer_type":
if value == "Straight":
mode = 0
elif value == "Iambic-A":
mode = 1
elif value == "Iambic-B":
mode = 2
else:
mode = 0
self.TransferOut(0x71, mode)
elif name == "keyer_space":
if value == "Element":
spacing = 0
elif value == "Letter":
spacing = 1
else:
spacing = 0
self.TransferOut(0x75, spacing)
elif name == "keyer_weight":
weight = int(value)
self.TransferOut(0x77, weight)
elif name == "keyer_paddle":
if value == "Normal":
paddle = 0
elif value == "Reverse":
paddle = 1
else:
paddle = 0
self.TransferOut(0x73, paddle)
elif name == "cwTone":
if value < 500:
tone_index = 0 # 400 Hz
elif 500 <= value < 700:
tone_index = 1 # 600 Hz
elif 700 <= value < 900:
tone_index = 2 # 800 Hz
else:
tone_index = 3 # 1000 Hz
self.TransferOut(0x7F, tone_index)
+150 -1
View File
@@ -152,6 +152,10 @@ static int quisk_use_sidetone; // is there a sidetone volume control?
static int hl2_txbuf_errors; // errors in the Hermes-Lite2 Tx buffer
static int hl2_txbuf_state; // state machine for errors in the Hermes-Lite2 Tx buffer
static int freedv_monitor; // pass the Freedv audio to the speakers instead of to the demodulator
static int softrock_correct_freq; // gain and phase corrections for SoftRock
static double softrock_correct_gain;
static double softrock_correct_phase;
static int softrock_correct_active; // 0 No correction adjustment; 1 Manual adjustment; 2 Calculate Rx corrections
static complex double PySampleBuf[SAMP_BUFFER_SIZE]; // buffer for samples returned from Python
static int PySampleCount; // count of samples in buffer
@@ -1972,7 +1976,7 @@ static int quisk_process_demodulate(complex double * cSamples, double * dsamples
ssb_squelch(dsamples, nSamples, quisk_filter_srate, MeasureSquelch + bank);
d_delay(dsamples, nSamples, bank, SQUELCH_FFT_SIZE);
}
quisk_calc_audio_graph(pow(2, 31) - 1, NULL, dsamples, nSamples, 1);
//quisk_calc_audio_graph(pow(2, 31) - 1, NULL, dsamples, nSamples, 1);
nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio24p4, 2);
nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7);
//quisk_calc_audio_graph(pow(2, 31) - 1, NULL, dsamples, nSamples, 1);
@@ -3157,6 +3161,8 @@ static PyObject * get_params(PyObject * self, PyObject * args)
return PyInt_FromLong(quisk_serial_ptt);
if (strcmp(name, "hl2_txbuf_errors") == 0)
return PyInt_FromLong(hl2_txbuf_errors);
if (strcmp(name, "softrock_correct_active") == 0)
return PyInt_FromLong((long)softrock_correct_active);
if (strcmp(name, "quisk_tx_inhibit") == 0)
return PyInt_FromLong((long)quisk_tx_inhibit);
Py_INCREF (Py_None);
@@ -4260,6 +4266,13 @@ static PyObject * change_scan(PyObject * self, PyObject * args) // Called from G
return Py_None;
}
static PyObject * softrock_corrections(PyObject * self, PyObject * args) // Called from GUI thread
{
if (!PyArg_ParseTuple (args, "i", &softrock_correct_active))
return NULL;
return Py_BuildValue("idd", softrock_correct_freq, softrock_correct_gain, softrock_correct_phase);
}
static PyObject * change_rates(PyObject * self, PyObject * args) // Called from GUI thread
{ // Change to new sample rates
@@ -5047,6 +5060,135 @@ static PyObject * get_bandscope(PyObject * self, PyObject * args) // Called by t
return Py_None;
}
#if 0
static void check_channel_delay(fft_data * ptFft)
{
int i, maxi, freq;
static int count=0, freq0;
static double gain=0, phase=0;
double d, ampl, maxa;
static double complex * samples1=NULL, * samples2;
if (samples1 == NULL) {
samples1 = (double complex *)malloc(fft_size * sizeof(double complex));
samples2 = (double complex *)malloc(fft_size * sizeof(double complex));
}
for (i = 0; i < fft_size; i++) {
samples1[i] = creal(ptFft->samples[i]);
samples2[i] = cimag(ptFft->samples[i]);
}
fftw_execute_dft(quisk_fft_plan, samples1, samples1);
fftw_execute_dft(quisk_fft_plan, samples2, samples2);
maxa = 0;
maxi = 0;
for (i = 0; i < fft_size / 2; i++) {
ampl = cabs(samples1[i]);
if (ampl > maxa) {
maxa = ampl;
maxi = i;
}
}
freq = maxi * fft_sample_rate / fft_size;
//freq = maxi * 44100 / fft_size;
gain += cabs(samples1[maxi]) / cabs(samples2[maxi]);
d = carg(samples1[maxi]) - carg(samples2[maxi]);
if (d > M_PI)
d -= M_PI * 2;
else if(d < - M_PI)
d += M_PI * 2;
phase += d;
count += 1;
if (count == 1) {
freq0 = freq;
}
else if (freq != freq0) {
gain = phase = count = 0;
return;
}
if (count > 10) {
gain /= count;
phase /= count;
//printf("Channel delay freq %6d, gain %6.3lf, phase %8.5lf radians, %6.3lf degrees\n",
// freq, gain, phase, phase * 57.29578);
printf("%6d, %6.3lf, %6.3lf\n", freq, gain, phase * 57.29578);
gain = phase = count = 0;
}
}
#endif
static void softrock_correct_fft(fft_data * ptFft, int fixit)
{ // See i/Q balance in Rocky: https://www.dxatlas.com/Rocky/Advanced.asp
double ampl, r1, r2, i1, i2, Zr, Zi, Pwr, Gain, Phase, OutR, OutI, S1R, S1I, S2R, S2I;
static double tot_ga=0, tot_ph=0;
static int count=0;
double maxa = 0;
int i, maxi = 0, freq;
double complex Z, s1, s2, Hr, Hi, S1, S2;
double tim;
static double time0 = 0;
for (i = 0; i < fft_size; i++) {
ampl = cabs(ptFft->samples[i]);
if (ampl > maxa) {
maxa = ampl;
maxi = i;
}
}
if (maxi < fft_size / 2)
freq = maxi;
else
freq = maxi - fft_size;
freq = freq * fft_sample_rate / fft_size;
s1 = ptFft->samples[maxi];
s2 = ptFft->samples[fft_size - maxi];
Z = s1 * s2;
r1 = creal(s1);
r2 = creal(s2);
i1 = cimag(s1);
i2 = cimag(s2);
Pwr = r1 * r1 + i1 * i1 + r2 * r2 + i2 * i2;
Zr = creal(Z) / Pwr;
Zi = cimag(Z) / Pwr;
Gain = sqrt((1.0 + 2 * Zr) / (1.0 - 2 * Zr));
Phase = asin(Zi * (Gain * Gain + 1.0) / Gain);
tot_ga += Gain;
tot_ph += Phase;
count += 1;
tim = QuiskTimeSec();
if (tim - time0 >= 0.5) {
time0 = tim;
softrock_correct_freq = freq;
softrock_correct_gain = tot_ga / count;
softrock_correct_phase = tot_ph / count;
//if (softrock_correct_active)
// printf("Freq domain Gain %10.7lf, Phase %10.7lf, %10.7lf, freq %d\n",
// softrock_correct_gain, softrock_correct_phase, softrock_correct_phase * 57.29578, freq);
tot_ga = tot_ph = count = 0;
}
if (fixit) {
Hr = softrock_correct_gain * cos(softrock_correct_phase);
Hi = softrock_correct_gain * sin(softrock_correct_phase);
for (i = 1; i < fft_size / 2; i++) {
S1 = ptFft->samples[fft_size - i];
S2 = ptFft->samples[i];
S1R = creal(S1);
S1I = cimag(S1);
S2R = creal(S2);
S2I = cimag(S2);
OutR = S2R + S1R - (S2I + S1I) * Hi + (S2R - S1R) * Hr;
OutI = S2I - S1I + (S2I + S1I) * Hr + (S2R - S1R) * Hi;
ptFft->samples[i] = OutR + I * OutI;
S1R = creal(S2);
S1I = cimag(S2);
S2R = creal(S1);
S2I = cimag(S1);
OutR = S2R + S1R - (S2I + S1I) * Hi + (S2R - S1R) * Hr;
OutI = S2I - S1I + (S2I + S1I) * Hr + (S2R - S1R) * Hi;
ptFft->samples[fft_size - i] = OutR + I * OutI;
}
}
}
static PyObject * get_graph(PyObject * self, PyObject * args) // Called by the GUI thread
{
int i, j, k, m, n, index, ffts, ii, mm, m0, deltam;
@@ -5119,7 +5261,10 @@ static PyObject * get_graph(PyObject * self, PyObject * args) // Called by the G
// Continue with FFT calculation
for (i = 0; i < fft_size; i++) // multiply by window
ptFft->samples[i] *= fft_window[i];
//check_channel_delay(ptFft);
fftw_execute_dft(quisk_fft_plan, ptFft->samples, ptFft->samples); // Calculate FFT
if (softrock_correct_active == 2)
softrock_correct_fft(ptFft, 0);
// Create RMS s-meter value at known bandwidth
// The pass band is (rx_tune_freq + filter_start_offset) to += bandwidth
// d1 is the tune frequency
@@ -5170,6 +5315,9 @@ static PyObject * get_graph(PyObject * self, PyObject * args) // Called by the G
//QuiskPrintf(" %d %.4lf At %5d to %5d place %5d to %5d for block %d\n", fft_size, scan_valid, mm, m, ii, i, ptFft->block);
}
else {
// Zero frequency is at index fft_size / 2.
// There are fft_size/2 positive frequencies and fft_size/2-1 negative frequencies.
// The frequency at index k and (fft_size - k) are equal except for sign.
count_fft++;
k = 0;
for (i = fft_size / 2; i < fft_size; i++) // Negative frequencies
@@ -5995,6 +6143,7 @@ static PyMethodDef QuiskMethods[] = {
{"get_filter_rate", get_filter_rate, METH_VARARGS, "Return the sample rate used for the filters."},
{"get_tx_filter", quisk_get_tx_filter, METH_VARARGS, "Return the frequency response of the transmit filter."},
{"get_audio_graph", get_audio_graph, METH_VARARGS, "Return a tuple of the audio graph data."},
{"softrock_corrections", softrock_corrections, METH_VARARGS, "Control and return SoftRock amplitude and phase corrections."},
{"measure_frequency", measure_frequency, METH_VARARGS, "Set the method, return the measured frequency."},
{"measure_audio", measure_audio, METH_VARARGS, "Set the method, return the measured audio voltage."},
{"get_overrange", get_overrange, METH_VARARGS, "Return the count of overrange (clip) for the ADC."},
+65 -24
View File
@@ -836,6 +836,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
"""Send text back to the client."""
if isinstance(text, Q3StringTypes):
text = text.encode('utf-8', errors='ignore')
if HAMLIB_DEBUG:
print ("Rig2 send", text)
try:
self.sock.sendall(text)
except socket.error:
@@ -942,6 +944,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
else:
return 1
self.input = self.input.strip() # Here is our command line
if HAMLIB_DEBUG:
print ("Rig2 received", self.input)
while self.input:
# Parse the commands and call the appropriate handlers
self.params = ''
@@ -992,8 +996,12 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
if rx:
rx = rx[0]
self.Reply('Frequency', rx.txFreq + rx.VFO, 0)
if HAMLIB_DEBUG:
print ("GetFreq rx", rx.txFreq, rx.VFO)
else:
self.Reply('Frequency', self.app.rxFreq + self.app.VFO, 0)
if HAMLIB_DEBUG:
print ("GetFreq app", self.app.rxFreq, self.app.txFreq, self.app.VFO)
def SetFreq(self): # The Rx frequency
freq = self.GetParamNumber()
try:
@@ -2218,7 +2226,8 @@ class GraphScreen(wx.Window):
self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseBtn1', event=event, rx_freq=freq)
else:
self.ChangeHwFrequency(freq, self.VFO, 'MouseBtn1', event=event)
self.CaptureMouse()
if not self.HasCapture():
self.CaptureMouse()
def OnLeftUp(self, event):
if self.HasCapture():
self.ReleaseMouse()
@@ -2231,7 +2240,7 @@ class GraphScreen(wx.Window):
mouse_x, mouse_y = self.GetMousePosition(event)
if wx.GetKeyState(wx.WXK_SHIFT):
mouse_x -= self.filter_center * self.data_width / sample_rate
if conf.mouse_tune_method: # Mouse motion changes the VFO frequency
if False: #conf.mouse_tune_method: # Mouse motion changes the VFO frequency
x = (mouse_x - self.mouse_x) # Thanks to VK6JBL
self.mouse_x = mouse_x
freq = float(x) * sample_rate / self.data_width
@@ -2537,12 +2546,10 @@ class WaterfallDisplay(wx.Window):
self.top_key = 8
self.top_size = (self.top_key + 2) * (self.top_key - 1) // 2
# Make the palette
if conf.waterfall_palette == 'B':
pal2 = conf.waterfallPaletteB
elif conf.waterfall_palette == 'C':
pal2 = conf.waterfallPaletteC
else:
if conf.waterfall_palette == 'A':
pal2 = conf.waterfallPalette
else:
pal2 = getattr(conf, "waterfallPalette" + conf.waterfall_palette)
red = bytearray(256)
green = bytearray(256)
blue = bytearray(256)
@@ -3628,7 +3635,7 @@ class App(wx.App):
self.save_time0 = self.timer
self.smeter_db_time0 = self.timer
self.smeter_sunits_time0 = self.timer
self.fewsec_time0 = self.timer
self.slowheart_time0 = self.timer
self.multi_rx_index = 0
self.multi_rx_timer = self.timer
self.band_up_down = 0 # Are band Up/Down buttons in use?
@@ -3784,6 +3791,7 @@ class App(wx.App):
pass #traceback.print_exc()
if "Version" not in self.bandAmplPhase:
self.FixAmplPhase() # Convert to new format Oct 2020
self.FixAmplPhase2() # New logic June 2024
if Hardware.VarDecimGetChoices(): # Hardware can change the decimation.
self.sample_rate = Hardware.VarDecimSet() # Get the sample rate.
self.vardecim_set = self.sample_rate
@@ -3822,16 +3830,15 @@ 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()
#ww, hh = frame.GetClientSizeTuple()
#print ('Main frame: size', w, h, 'client', ww, hh)
#print ('Main frame: size', w, h, 'client', wClient, hClient)
# Find the data width from a list of preferred sizes; it is the width of returned graph data.
# The graph_width is the width of data_width that is displayed.
# The total width of the graph is graph_width + self.graph_borders
if conf.window_width > 0:
#wFrame, h = frame.GetClientSize().Get() # client window width
wFrame = conf.window_width - 15 # Thanks to Nick Abramenko, RA3PKJ
graph = GraphScreen(frame, self.width//2, self.width//2, None) # make a GraphScreen to calculate borders
self.graph_width = wFrame - (graph.width - graph.graph_width) # less graph borders equals actual graph_width
self.graph_width = wClient - (graph.width - graph.graph_width) # less graph borders equals actual graph_width
graph.Destroy()
del graph
if self.graph_width % 2 == 1: # Both data_width and graph_width are even numbers
@@ -4450,6 +4457,25 @@ class App(wx.App):
except:
traceback.print_exc()
self.bandAmplPhase[band] = {}
def FixAmplPhase2(self): # New definition of phase
if self.bandAmplPhase["Version"]["rx"] == [1]:
return
degrad = 360.0 / 2 / math.pi
for band in self.bandAmplPhase:
if band == "Version":
continue
for rx_tx in self.bandAmplPhase[band]:
if rx_tx in ('rx', 'tx'):
data = self.bandAmplPhase[band][rx_tx]
for item in data:
vfo = item[0]
lst_framph = item[1]
for framph in lst_framph:
freq, am, ph = framph
p = math.atan(math.tan(ph / degrad) / (1.0 + am))
#print ("%5s %3s %8d %8d %8.4f %8.3f %8.3f" % (band, rx_tx, vfo, freq, am, ph, p * degrad))
framph[2] = p * degrad
self.bandAmplPhase["Version"]["rx"] = [1]
def ImmediateChange(self, name):
if name == "pulse_audio_verbose_output":
value = getattr(conf, name)
@@ -4688,7 +4714,7 @@ class App(wx.App):
b.char_shortcut = 'l'
self.MakeAccel(b)
self.splitButton = WrapMenu(b, self.split_menu)
if conf.mouse_tune_method: # Mouse motion changes the VFO frequency
if False: #conf.mouse_tune_method: # Mouse motion changes the VFO frequency
self.splitButton.Enable(False)
left_row3.append(self.splitButton)
b = QuiskCheckbutton(frame, self.OnBtnFDX, 'FDX', color=conf.color_test)
@@ -4898,7 +4924,8 @@ class App(wx.App):
self.freqDisplay.Display(self.txFreq + self.VFO)
# On/Off button
if conf.button_layout == 'Large screen':
b_onoff = QuiskCheckbutton(frame, self.OnBtnOnOff, "On", color='#77DD77')
b_onoff = QuiskCheckbutton(frame, self.OnBtnOnOff, "On", color=conf.color_onoff)
b_onoff.text_color = conf.color_onoff_text
b_onoff.SetValue(True, do_cmd=False)
h = b_freqdisp.height
b_onoff.SetSizeHints(h, h, h, h)
@@ -5854,16 +5881,31 @@ class App(wx.App):
The hardware will reply with the updated frequencies which may be different
from those requested; use and display the returned tune and vfo.
"""
#print ("ChangeHW", source, tune, vfo, vfo + tune)
if not self.split_rxtx:
self.rxFreq = tune # rxFreq must be correct before the call to Hardware.ChangeFrequency()
elif rx_freq is not None:
self.rxFreq = rx_freq
rx_offset = self.rxFreq - tune
if self.screen == self.bandscope_screen:
freq = vfo + tune
tune = freq % 10000
vfo = freq - tune
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event)
self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None)
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event)
self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None)
elif conf.fixed_tune_offset:
# Tune with the VFO and keep a constant audio offset from the center. Tx tune is always conf.fixed_tune_offset.
tune_freq = vfo + tune
self.rxFreq = conf.fixed_tune_offset + rx_offset
if source == "BtnUpDown":
tune_freq = vfo + conf.fixed_tune_offset
else:
vfo = tune_freq - conf.fixed_tune_offset
tune_freq, vfo = Hardware.ChangeFrequency(tune_freq, vfo, source, band, event)
self.ChangeDisplayFrequency(tune_freq - vfo, vfo, True)
else:
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event)
self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None)
def ChangeDisplayFrequency(self, tune, vfo, new_rxfreq=False):
'Change the frequency displayed by Quisk'
change = 0
@@ -5883,10 +5925,8 @@ class App(wx.App):
self.graph.SetVFO(vfo)
self.waterfall.SetVFO(vfo)
self.station_screen.Refresh()
if self.w_phase:
self.w_phase.Redraw()
if change:
if conf.name_of_sound_capt or conf.name_of_mic_play:
if (conf.name_of_sound_capt or conf.name_of_mic_play) and QS.get_params("softrock_correct_active") == 0:
ampl, phase = self.GetAmplPhase('rx')
QS.set_ampl_phase(ampl, phase, 0)
ampl, phase = self.GetAmplPhase('tx')
@@ -6362,7 +6402,7 @@ class App(wx.App):
for client in self.hamlib_clients: # Service existing clients
if not client.Process(): # False return indicates a closed connection; remove the handler for this client
self.hamlib_clients.remove(client)
# print 'Remove', client.address
# print ('Remove', client.address)
break
def OnKeyHook(self, event):
event.Skip()
@@ -6598,9 +6638,10 @@ class App(wx.App):
self.ChangeDisplayFrequency(tune - vfo, vfo)
self.FldigiPoll()
self.HamlibPoll()
#if self.timer - self.fewsec_time0 > 3.0:
# self.fewsec_time0 = self.timer
# print ('fewswc')
if self.timer - self.slowheart_time0 > 0.5:
self.slowheart_time0 = self.timer
if self.w_phase:
self.w_phase.SlowHeartBeat()
if self.timer - self.save_time0 > 20.0:
self.save_time0 = self.timer
if self.CheckState():
+104 -4
View File
@@ -1084,7 +1084,15 @@ digital_rx1_name = ""
## digital_output_level Digital output level, number
# This is the volume control 0.0 to 1.0 for digital playback to fldigi, etc.
# Changes are immediate (no need to restart).
digital_output_level = 0.7
digital_output_level = 0.700
## file_play_level File play level, number
# Use this to reduce the volume when playing files. For example, if the volume of the CQ
# message is too loud, reduce the level relative to radio sound.
# Changes are immediate (no need to restart).
file_play_level = 1.000
#file_play_level = 0.500
#file_play_level = 0.100
# Sound card names:
#
@@ -1279,6 +1287,18 @@ dc_remove_bw = 100
#dc_remove_bw = 200
#dc_remove_bw = 400
## fixed_tune_offset Fixed tune offset, integer
# If this is zero, Quisk tunes as usual. The hardware frequency (the VFO or center frequency)
# is fixed and Quisk tunes
# within the spectrum returned by the hardware. Otherwise enter a fixed offset between the
# center frequency and the tuning frequency. Then tuning is done by the hardware.
# This is useful with Softrock hardware as
# it simplifies the corrections for amplitude and phase imbalance.
# Changes are immediate (no need to restart).
fixed_tune_offset = 0
#fixed_tune_offset = 5000
#fixed_tune_offset = 10000
@@ -1908,6 +1928,8 @@ btn_text_switch = u"\u21C4" # Character to switch left-right
color_scheme = 'A'
#color_scheme = 'B'
#color_scheme = 'C'
#color_scheme = 'DK1MI_GR'
#color_scheme = 'DK1MI_AM'
## waterfall_palette Waterfall colors, text choice
# This controls the colors used in the waterfall. The default color scheme is A, and you can change this scheme
@@ -1915,6 +1937,7 @@ color_scheme = 'A'
waterfall_palette = 'A'
#waterfall_palette = 'B'
#waterfall_palette = 'C'
#waterfall_palette = 'DK1MI_AM'
@@ -1949,6 +1972,8 @@ color_txline = 'red' # vertical line color for tx in graph
color_rxline = 'green' # vertical line color for rx in graph
color_graph_msg_fg = 'black' # text messages on the graph screen
color_graph_msg_bg = 'lemonchiffon2' # background of text messages on the graph screen
color_onoff = '#77DD77' # on/off power button
color_onoff_text = 'black' # on/off power button text
# This color scheme B, a dark color scheme designed by Steve Murphy, KB8RWQ.
# Additional colors added by N2ADR.
@@ -1978,6 +2003,8 @@ color_scheme_B = {
'color_rxline' : 'green',
'color_graph_msg_fg' : 'white',
'color_graph_msg_bg' : '#111111',
'color_onoff' : '#77DD77',
'color_onoff_text' : 'black',
}
# This is color scheme C:
@@ -2015,12 +2042,73 @@ color_scheme_C = {
'color_rxline' : '#3CC918',
'color_graph_msg_fg' : '#000000',
'color_graph_msg_bg' : 'lemonchiffon2',
'color_onoff' : '#77DD77',
'color_onoff_text' : 'black',
}
#############################################################################################
# This is a green dark color scheme designed by Michael
color_scheme_DK1MI_GR = {
'color_bg' : '#0F0F0F', # Lower screen background
'color_bg_txt' : '#00CC00', # Lower screen text color
'color_graph' : '#0F0F0F', # Graph background
'color_config2' : '#008000', # color in tab row of config screen
'color_gl' : '#2F4F4F', # Lines on the graph
'color_graphticks' : '#CCCCCC', # Graph ticks
'color_graphline' : '#00FF00', # graph data line color
'color_graphlabels' : '#00FF00', # graph label color
'color_btn' : '#141414', # button color
'color_check_btn' : '#005500', # color of a check button when it is checked
'color_cycle_btn' : '#005500', # color of a cycle button when it is checked
'color_adjust_btn' : '#005500', # color of an adjustable button when it is checked
'color_test' : '#005500', # color of a button used for test (turn off for tx)
'color_freq' : '#208B57', # background color of frequency and s-meter
'color_freq_txt' : '#00FF00', # text color of frequency display
'color_entry' : '#2E2E2E', # frequency entry box
'color_entry_txt' : '#00FF00', # text color of entry box
'color_enable' : '#00CC00', # text color for an enabled button
'color_disable' : '#FF0000', # text color for a disabled button
'color_popchoice' : '#555555', # text color for button that pops up a row of buttons
'color_bandwidth' : '#003300', # color for bandwidth display; thanks to WB4JFI
'color_txline' : '#00CC00', # vertical line color for tx in graph
'color_rxline' : '#00FF00', # vertical line color for rx in graph
'color_graph_msg_fg' : '#008800', # text messages on the graph screen
'color_graph_msg_bg' : '#1E1E1E', # background of text messages on the graph screen
'color_onoff' : '#005500', # On/Off button
'color_onoff_text' : '#FF0000', # On/Off text
}
#############################################################################################
# This is an amber dark color scheme designed by Michael
color_scheme_DK1MI_AM = {
'color_bg' : '#0F0F0F', # Lower screen background
'color_bg_txt' : '#FFBF00', # Lower screen text color
'color_graph' : '#0F0F0F', # Graph background
'color_config2' : '#FF8000', # Color in tab row of config screen
'color_gl' : '#2F4F4F', # Lines on the graph
'color_graphticks' : '#CCCCCC', # Graph ticks
'color_graphline' : '#FFBF00', # Graph data line color
'color_graphlabels' : '#FFBF00', # Graph label color
'color_btn' : '#141414', # Button color
'color_check_btn' : '#865E2E', # Color of a check button when it is checked
'color_cycle_btn' : '#865E2E', # Color of a cycle button when it is checked
'color_adjust_btn' : '#865E2E', # Color of an adjustable button when it is checked
'color_test' : '#865E2E', # Color of a button used for test (turn off for tx)
'color_freq' : '#FF9933', # Background color of frequency and s-meter
'color_freq_txt' : '#000000', # Text color of frequency display
'color_entry' : '#2E2E2E', # Frequency entry box
'color_entry_txt' : '#FFBF00', # Text color of entry box
'color_enable' : '#FFBF00', # Text color for an enabled button
'color_disable' : '#FF0000', # Text color for a disabled button
'color_popchoice' : '#555555', # Text color for button that pops up a row of buttons
'color_bandwidth' : '#4B2E0F', # Color for bandwidth display
'color_txline' : '#FFBF00', # Vertical line color for tx in graph
'color_rxline' : '#FF8000', # Vertical line color for rx in graph
'color_graph_msg_fg' : '#FF8000', # Text messages on the graph screen
'color_graph_msg_bg' : '#1E1E1E', # Background of text messages on the graph screen
'color_onoff' : '#865E2E', # On/Off button
'color_onoff_text' : '#FF0000', # On/Off text
}
# These are the palettes for the waterfall. The one used is named waterfallPallette,
# so to use a different one, overwrite this name in your configuration file.
# These are the palettes for the waterfall.
waterfallPalette = (
( 0, 0, 0, 0),
( 36, 85, 0, 255),
@@ -2078,6 +2166,18 @@ waterfallPaletteC = ( # from David Fainitski
(255, 245, 99, 3)
)
waterfallPaletteDK1MI_AM = ( # from Michael
(0, 0, 0, 0),
(32, 25, 20, 0),
(64, 58, 40, 6),
(96, 78, 50, 16),
(128, 120, 85, 29),
(160, 144, 100, 51),
(192, 195, 140, 43),
(224, 198, 150, 35),
(255, 255, 191, 0)
)
# This is the data used to draw colored lines on the frequency X axis to
# indicate CW and Phone sub-bands. You can make it anything you want.
# These are the colors used for sub-bands:
+5 -3
View File
@@ -7,7 +7,7 @@ import os
# You must define the version here. A title string including
# the version will be written to __init__.py and read by quisk.py.
Version = '4.2.34'
Version = '4.2.37'
fp = open("__init__.py", "w") # write title string
fp.write("#Quisk version %s\n" % Version)
@@ -39,8 +39,10 @@ if sys.platform == "darwin": # Build for Macintosh
libraries = ['portaudio', 'fftw3', 'm']
if os.path.isdir('/opt/local/include'): # MacPorts
base_dir = '/opt/local'
elif os.path.isdir('/usr/local/include'): # HomeBrew
elif os.path.isdir('/usr/local/include'): # HomeBrew on macOS Intel
base_dir = '/usr/local'
elif os.path.isdir('/opt/homebrew/include'): # HomeBrew on Apple Silicon
base_dir = '/opt/homebrew'
else: # Regular build?
base_dir = '/usr'
if os.path.isfile(base_dir + "/include/pulse/pulseaudio.h"):
@@ -83,7 +85,7 @@ N1MM+ and software that uses Hamlib.
url = 'http://james.ahlstrom.name/quisk/',
packages = ['quisk', 'quisk.n2adr', 'quisk.softrock', 'quisk.freedvpkg',
'quisk.hermes', 'quisk.hiqsdr', 'quisk.afedrinet', 'quisk.soapypkg',
'quisk.sdrmicronpkg', 'quisk.perseuspkg', 'quisk.ac2yd'],
'quisk.sdrmicronpkg', 'quisk.perseuspkg', 'quisk.ac2yd', 'quisk.multuspkg'],
package_dir = {'quisk' : '.'},
package_data = {'' : ['*.txt', '*.html', '*.so', '*.dll']},
entry_points = {'gui_scripts' : ['quisk = quisk.quisk:main', 'quisk_vna = quisk.quisk_vna:main']},
+2
View File
@@ -178,6 +178,8 @@ class Hardware(BaseHardware):
vfo = self.vfo + int(offset * 1000) # Convert kHz to Hz
self.ChangeFrequency(vfo, vfo, 'repeater')
return False
def ImmediateChange(self, name):
return
def OnSpot(self, level):
pass
def HeartBeat(self): # Called at about 10 Hz by the main
+48 -12
View File
@@ -86,7 +86,9 @@ static struct wav_file file_rec_audio, file_rec_samples, file_rec_mic;
static int close_file_rec;
double digital_output_level = 0.7;
static double file_play_level = 1.0;
static int dc_remove_bw=100; // bandwidth of DC removal filter
static int multus_cw_samples = 0; // always send CW Tx samples to the Multus radio, even in Rx
static ty_sample_start pt_sample_start;
static ty_sample_stop pt_sample_stop;
@@ -175,9 +177,10 @@ static void correct_sample (struct sound_dev * dev, complex double * cSamples, i
for (i = 0; i < nSamples; i++) {
re = creal(cSamples[i]);
im = cimag(cSamples[i]);
re = re * dev->AmPhAAAA;
im = re * dev->AmPhCCCC + im * dev->AmPhDDDD;
cSamples[i] = re + I * im;
cSamples[i] = re * dev->AmPhAAAA + I * (re * dev->AmPhCCCC + im * dev->AmPhDDDD);
//re = re * dev->AmPhAAAA;
//im = re * dev->AmPhCCCC + im * dev->AmPhDDDD;
//cSamples[i] = re + I * im;
}
}
}
@@ -730,8 +733,11 @@ void * quisk_make_txIQ(struct sound_dev * dev, int rewind)
}
// amplitude and phase corrections
if (dev->doAmplPhase) {
dSamp0 = dSamp0 * dev->AmPhAAAA;
dSamp1 = dSamp0 * dev->AmPhCCCC + dSamp1 * dev->AmPhDDDD;
//dSamp0 = dSamp0 * dev->AmPhAAAA;
//dSamp1 = dSamp0 * dev->AmPhCCCC + dSamp1 * dev->AmPhDDDD;
d = dSamp0;
dSamp0 = d * dev->AmPhAAAA;
dSamp1 = d * dev->AmPhCCCC + dSamp1 * dev->AmPhDDDD;
}
switch (dev->sound_format) {
case Int16:
@@ -836,6 +842,34 @@ int quisk_play_sidetone(struct sound_dev * dev)
return 0;
}
#if 0
static void testing(double complex * cSamples, int nSamples)
{
int i;
static double AvgR=0, AvgQ=0, AvgRQ=0;
double d, p, Gain, Phase;
time_t tim;
static time_t time0;
for (i = 0; i < nSamples; i++) {
d = creal(cSamples[i]);
AvgR += d * d;
p = cimag(cSamples[i]);
AvgQ += p * p;
AvgRQ += d * p;
}
tim = time(NULL);
if (tim - time0 >= 2) {
time0 = tim;
Gain = sqrt(AvgR / AvgQ);
Phase = AvgRQ / AvgQ / Gain;
Phase = asin(Phase);
printf("Time domain Gain %10.7lf, Phase %10.7lf, %10.7lf degrees\n", Gain, Phase, Phase * 57.29578);
AvgR = AvgQ = AvgRQ = 0;
}
}
#endif
int quisk_read_sound(void) // Called from sound thread
{ // called in an infinite loop by the main program
int i, nSamples, mic_count, mic_interp, retval, is_cw, mic_sample_rate;
@@ -913,9 +947,10 @@ int quisk_read_sound(void) // Called from sound thread
//QuiskPrintTime("quisk_read_sound end", 0);
if (Capture.channel_Delay >= 0) // delay the I or Q channel by one sample
delay_sample(&Capture, (double *)cSamples, nSamples);
DCremove(cSamples, nSamples, quisk_sound_state.sample_rate, key_state);
//testing(cSamples, nSamples);
if (Capture.doAmplPhase) // amplitude and phase corrections
correct_sample(&Capture, cSamples, nSamples);
DCremove(cSamples, nSamples, quisk_sound_state.sample_rate, key_state);
if (nSamples <= 0)
QuiskSleepMicrosec(2000);
#if DEBUG_IO > 1
@@ -969,7 +1004,7 @@ int quisk_read_sound(void) // Called from sound thread
if (quisk_record_state == TMP_PLAY_SPKR_MIC)
quisk_tmp_playback(cSamples, nSamples, 1.0); // replace radio sound
else if (quisk_record_state == FILE_PLAY_SPKR_MIC)
quisk_file_playback(cSamples, nSamples, 1.0); // replace radio sound
quisk_file_playback(cSamples, nSamples, file_play_level); // replace radio sound
// Play the demodulated audio
#if DEBUG_MIC != 2
if ( ! quisk_play_sidetone(&quisk_Playback)) // play sidetone
@@ -1075,7 +1110,7 @@ int quisk_read_sound(void) // Called from sound thread
if (is_cw) { // Transmit CW; use capture device for timing, not microphone
cwCount += (double)retval * quisk_MicPlayback.sample_rate / quisk_sound_state.sample_rate;
mic_count = 0;
if (QUISK_CWKEY_DOWN || quiskSpotLevel >= 0) {
if (QUISK_CWKEY_DOWN || quiskSpotLevel >= 0 || multus_cw_samples) {
while (cwCount >= 1.0) {
if (cwEnvelope < 1.0) {
cwEnvelope += 1. / (quisk_MicPlayback.sample_rate * 5e-3); // 5 milliseconds
@@ -1401,6 +1436,8 @@ void quisk_open_sound(void) // Called from GUI thread
RawSamplePlayback.channel_I = 0;
RawSamplePlayback.channel_Q = 1;
file_play_level = QuiskGetConfigDouble("file_play_level", 1.0);
set_num_channels (&Capture);
set_num_channels (&quisk_Playback);
set_num_channels (&MicCapture);
@@ -1565,10 +1602,9 @@ PyObject * quisk_micplay_channels(PyObject * self, PyObject * args) // Called fr
PyObject * quisk_set_sparams(PyObject * self, PyObject * args, PyObject * keywds)
{ /* Call with keyword arguments ONLY; change local parameters */
static char * kwlist[] = {"dc_remove_bw", "digital_output_level", "remote_control_slave", "remote_control_head",
NULL} ;
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|idii", kwlist, &dc_remove_bw, &digital_output_level,
&remote_control_slave, &remote_control_head))
"file_play_level", "multus_cw_samples", NULL} ;
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|idiidi", kwlist, &dc_remove_bw, &digital_output_level,
&remote_control_slave, &remote_control_head, &file_play_level, &multus_cw_samples))
return NULL;
Py_INCREF (Py_None);
return Py_None;