3 Commits
Author SHA1 Message Date
James C. Ahlstrom 7e8a0846f7 Quisk version 4.2.40 2024-11-15 16:39:47 -05:00
James C. Ahlstrom fb888a2e08 Quisk version 4.2.39 2024-10-01 14:22:23 -04:00
James C. Ahlstrom 78a1ca3d60 Quisk version 4.2.38 2024-09-01 13:57:53 -04:00
9 changed files with 175 additions and 82 deletions
+21
View File
@@ -1,3 +1,24 @@
Quisk Version 4.2.40 November 2024
===================================
Python 3.13 eliminated the telnetlib module, so I wrote a replacement. I fixed a bug in window sizing.
If you use the Dx Cluster feature, please report any problems. Dx Cluster used to use the telnetlib module
but is now new code.
I updated the install to Python 3.13. Please report any problems with Python 3.13.
Quisk Version 4.2.39 October 2024
==================================
I added an optional Hardware method PollGuiControl(self). If you define this in your Hardware file,
the GUI thread will call it frequently. It can be used to set PTT from your hardware.
I am trying to work around a wxPython quirk that causes mouse wheel tuning to fail. Please test.
Quisk Version 4.2.38 September 2024
====================================
This is a bugfix release. I fixed a bug in the Hamlib serial port when using a physical TTY instead
of a pseudo-tty. The Hamlib ZZFA command now changes the receive frequency instead of the transmit
frequency. I fixed some window sizing issues.
Quisk Version 4.2.37 August 2024
=================================
Quisk now has a new option "Fixed tune offset" on the Config/radio/Options screen. This is meant for
+1 -1
View File
@@ -1,2 +1,2 @@
#Quisk version 4.2.37
#Quisk version 4.2.40
from .quisk import main
+87 -39
View File
@@ -1,9 +1,9 @@
from __future__ import absolute_import
# This code was contributed by Christof, DJ4CM. Many Thanks!!
# Modified November 2024 by N2ADR to remove deprecated telnetlib module.
import threading
import socket
import time
import telnetlib
import quisk_conf_defaults as conf
class DxEntry:
@@ -81,61 +81,109 @@ class DxEntry:
class DxCluster(threading.Thread):
def __init__(self):
self.do_init = 1
self.error = 'Starting'
self.dxSpots = []
threading.Thread.__init__(self)
self.doQuit = threading.Event()
self.dxSpots = []
self.doQuit.clear()
self.dxLock = threading.Lock()
self.addr = conf.dxClHost + ':' + str(conf.dxClPort)
self.msg_no_spots = "No DX Cluster data from " + self.addr
self.msg_one_spot = '1 DX spot received from ' + self.addr
self.msg_spots = ' DX spots received from ' + self.addr
def run(self):
self.telnetInit()
self.telnetConnect()
if self.error:
self.sock.close()
return
while not self.doQuit.isSet():
try:
self.telnetRead()
by = self.sock.recv(1024)
except TimeoutError:
continue
except:
self.tn.close()
time.sleep(20)
by = b''
if by:
with self.dxLock:
self.bytes += by
else:
self.sock.close()
self.error = "Restarting " + self.addr
time.sleep(2)
if not self.doQuit.isSet():
self.telnetConnect()
self.tn.close()
def setListener (self, listener):
self.listener = listener
if self.error:
self.sock.close()
return
self.sock.close()
def telnetInit(self):
self.tn = telnetlib.Telnet()
def telnetConnect(self):
self.bytes = bytearray(0)
self.error = 'Starting'
self.sock = socket.socket()
self.sock.settimeout(20)
try:
self.sock.connect( (conf.dxClHost, conf.dxClPort) )
except:
self.error = "Failed to connect to " + self.addr
return
self.sock.settimeout(1)
for i in range(10):
try:
self.tn.open(conf.dxClHost, conf.dxClPort, 10)
self.tn.read_until(b"login:", 10)
self.tn.write(conf.user_call_sign.encode('utf-8', errors='ignore') + b"\n") # user_call_sign may be Unicode
break
self.bytes += self.sock.recv(1024)
except:
time.sleep(0.5)
pass
if b"login:" in self.bytes:
break
else:
self.error = 'No "login:" prompt from ' + self.addr
return
self.sock.sendall(conf.user_call_sign.encode('utf-8', errors='ignore') + b"\r\n")
if conf.dxClPassword:
self.tn.read_until(b"Password: ")
self.tn.write(conf.dxClPassword.encode('utf-8', errors='ignore') + b"\n")
for i in range(10):
try:
self.bytes += self.sock.recv(1024)
except:
pass
if b"Password:" in self.bytes:
break
else:
self.error = 'No "Password:" prompt from ' + self.addr
return
self.sock.sendall(conf.dxClPassword.encode('utf-8', errors='ignore') + b"\r\n")
self.error = ''
self.bytes = bytearray(0)
def telnetRead(self):
message = self.tn.read_until(b'\n', 60).decode(encoding='utf-8', errors='replace')
if self.doQuit.isSet() == False:
dxEntry = DxEntry();
if dxEntry.parseMessage(message):
for i, listElement in enumerate(self.dxSpots):
if (listElement.equal(dxEntry)):
listElement.join (dxEntry)
return
if listElement.isExpired():
del (self.dxSpots[i])
self.dxSpots.append(dxEntry)
if self.listener:
self.listener()
def getHost(self):
return self.tn.host + ':' + str(self.tn.port)
def Poll(self):
with self.dxLock:
index = self.bytes.find(b"\n")
if index >= 0:
message = self.bytes[0:index + 1]
self.bytes = self.bytes[index + 1:]
else:
return
message = message.decode(encoding='utf-8', errors='replace')
dxEntry = DxEntry()
if dxEntry.parseMessage(message):
for i, listElement in enumerate(self.dxSpots):
if (listElement.equal(dxEntry)):
listElement.join (dxEntry)
return True
if listElement.isExpired():
del (self.dxSpots[i])
self.dxSpots.append(dxEntry)
return True
def stop(self):
self.doQuit.set()
def dxStatus(self):
if self.error:
return self.error
nSpots = len(self.dxSpots)
if nSpots == 0:
return self.msg_no_spots
elif nSpots == 1:
return self.msg_one_spot
return str(nSpots) + self.msg_spots
+2
View File
@@ -402,6 +402,8 @@ class Hardware(BaseHardware):
if do_tx:
try:
self.rx_udp_socket.send(s)
if DEBUG:
self.PrintStatus(' Send', self.want_udp_status)
except:
pass
def SetVNA(self, key_down=None, vna_start=None, vna_stop=None, vna_count=None, do_tx=False):
+22 -1
View File
@@ -91,6 +91,8 @@ class Hardware(BaseHardware):
conf.si570_direct_control = False
conf.si570_xtal_freq = 114285000
conf.repeater_delay = 0.25
self.ptt_count = 200
self.ptt_on = 0
BaseHardware.__init__(self, app, conf)
QS.set_sparams(multus_cw_samples=1)
self.use_softrock = True
@@ -104,6 +106,13 @@ class Hardware(BaseHardware):
self.usb_dev.ctrl_transfer(OUT, address, self.si570_i2c_address + 0x700, 0, message)
if DEBUG:
print ("USB send to 0x%X" % address, message)
def TransferIn(self, address, length):
if self.usb_dev:
recv = self.usb_dev.ctrl_transfer(IN, address, self.si570_i2c_address + 0x700, 0, length)
if DEBUG:
print ("USB receive from 0x%X" % address, recv)
return recv
return None
def open(self):
text = BaseHardware.open(self)
self.InitKeyer()
@@ -120,8 +129,20 @@ class Hardware(BaseHardware):
print ("Mode is not CW")
self.TransferOut(0x70, cw_mode)
return ret
def PollCwKey(self): # Called frequently by Quisk to check the CW key status
def PollCwKey(self): # Called frequently from the sound thread to check the CW key status
return # Quisk is always in Rx
def PollGuiControl(self): # Called frequently from the GUI thread
self.ptt_count -= 1
if self.ptt_count <= 0:
self.ptt_count = 200
reply = self.TransferIn(0xA5, 1)
if DEBUG:
print ("PollGuiControl got", reply)
if reply:
ptt = reply[0] # This is 255 for error
if ptt in (0, 1) and ptt != self.ptt_on:
self.ptt_on = ptt
self.application.pttButton.SetValue(ptt, True)
def InitKeyer(self):
# Initialize the keyer parameters
conf = self.conf
+37 -39
View File
@@ -334,6 +334,7 @@ class HamlibHandlerSerial:
self.public_name = public_name # the public name for the serial port
self.tune_step = 1000
if sys.platform == 'win32' or public_name.startswith("/dev/"):
self.port_is_mod_serial = True # self.port is from the serial module
try:
import serial
except:
@@ -346,6 +347,7 @@ class HamlibHandlerSerial:
print ("The serial port %s could not be opened." % public_name)
else:
import tty
self.port_is_mod_serial = False # self.port is a file descriptor
if os.path.lexists(public_name):
try:
os.remove(public_name)
@@ -369,7 +371,7 @@ class HamlibHandlerSerial:
def open(self):
return
def close(self):
if sys.platform == 'win32':
if self.port_is_mod_serial:
if self.port:
self.port.close()
self.port = None
@@ -382,7 +384,7 @@ class HamlibHandlerSerial:
def Read(self):
if self.port is None:
return
if sys.platform == 'win32':
if self.port_is_mod_serial:
text = self.port.read(99)
if not isinstance(text, Q3StringTypes):
text = text.decode('utf-8')
@@ -433,22 +435,14 @@ class HamlibHandlerSerial:
return
if isinstance(data, Q3StringTypes):
data = data.encode('utf-8', errors='ignore')
if sys.platform == 'win32':
if self.port_is_mod_serial:
self.port.write(data)
else:
r, w, x = select.select((), (self.port,), (), 0)
if w:
os.write(self.port, data)
def set_frequency(self, freq):
tune = freq - self.app.VFO
d = self.app.sample_rate * 45 // 100
if -d <= tune <= d: # Frequency is on-screen
vfo = self.app.VFO
else: # Change the VFO
vfo = (freq // 5000) * 5000 - 5000
tune = freq - vfo
self.app.BandFromFreq(freq)
self.app.ChangeHwFrequency(tune, vfo, 'FreqEntry')
def set_frequency(self, freq): # This changes the Rx frequency
self.app.ChangeRxTxFrequency(freq, None)
if HAMLIB_DEBUG: print("New Freq rx,tx", self.app.txFreq + self.app.VFO, self.app.rxFreq + self.app.VFO)
def ZZAC(self, cmd, data, length): # Set or read tune steps
if length == 0:
@@ -1440,12 +1434,8 @@ class ConfigStatus(wx.ScrolledCanvas):
self.MakeRow2("Sample interrupts", self.interrupts, cfile)
self.MakeRow2("Microphone or DGT level dB", level, qfile)
self.MakeRow2("FFT number of points", self.fft_size, err_msg)
if conf.dxClHost: # connection to dx cluster
nSpots = len(application.dxCluster.dxSpots)
if nSpots > 0:
msg = str(nSpots) + ' DX spot' + ('' if nSpots==1 else 's') + ' received from ' + application.dxCluster.getHost()
else:
msg = "No DX Cluster data from %s" % conf.dxClHost
if application.dxCluster:
msg = application.dxCluster.dxStatus()
self.MakeRow2("FFT number of errors", self.fft_error, msg)
else:
self.MakeRow2("FFT number of errors", self.fft_error)
@@ -2129,7 +2119,7 @@ class GraphScreen(wx.Window):
"""For mouse clicks in our display, translate to our screen coordinates."""
mouse_x, mouse_y = event.GetPosition()
win = event.GetEventObject()
if win is not self:
if win and win is not self:
x, y = win.GetPosition().Get()
mouse_x += x
mouse_y += y
@@ -2643,7 +2633,7 @@ class WaterfallScreen(wx.SplitterWindow):
self.y_zero = conf.waterfall_y_zero
self.zoom_control = 0
wx.SplitterWindow.__init__(self, frame)
self.SetSizeHints(width, -1, width)
self.SetSizeHints(width, -1)
self.SetSashGravity(0.50)
self.SetMinimumPaneSize(1)
self.SetSize((width, conf.waterfall_graph_size + 100)) # be able to set sash size
@@ -2997,7 +2987,7 @@ class MultiReceiverScreen(wx.SplitterWindow):
self.rx_zero = self.graph
self.Initialize(self.rx_zero)
self.waterfall.Hide()
self.SetSizeHints(self.width, -1, self.width)
self.SetSizeHints(self.width, -1)
# Calculate control width
rx_btn = QuiskPushbutton(self, None, "Rx 8....", style=wx.BU_EXACTFIT)
self.rx_btn_width, self.rx_btn_height = rx_btn.GetSize().Get()
@@ -3530,6 +3520,7 @@ class App(wx.App):
if not os.path.isdir(self.QuiskFilesDir):
self.QuiskFilesDir = DefaultConfigDir
self.std_out_err = StdOutput(self)
self.std_out_err.Logfile("WxPython " + wx.version())
QS.set_params(quisk_is_vna=0) # We are not the VNA program
# Read in configuration from the selected radio
self.BandPlan = []
@@ -3620,6 +3611,7 @@ class App(wx.App):
setattr(conf, 'widgets_file_name', '')
Hardware = self.Hardware
self.use_fast_heart_beat = hasattr(Hardware, "FastHeartBeat")
self.poll_gui_control = hasattr(Hardware, "PollGuiControl")
# Initialization - may be over-written by persistent state
self.local_conf.Initialize()
self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow
@@ -3830,9 +3822,10 @@ class App(wx.App):
self.std_out_err.Show(False)
else:
self.std_out_err.Show(True)
wClient, hClient = frame.GetClientSize()
#w, h = frame.GetSize().Get()
#print ('Main frame: size', w, h, 'client', wClient, hClient)
wClient, hClient = self.main_frame.GetClientSize()
wFrame, hFrame = self.main_frame.GetSize()
main_border_w = wFrame - wClient
#print ('Main frame: size', wFrame, hFrame, 'client', wClient, hClient, 'border', main_border_w)
# Find the data width from a list of preferred sizes; it is the width of returned graph data.
# The graph_width is the width of data_width that is displayed.
# The total width of the graph is graph_width + self.graph_borders
@@ -3963,14 +3956,14 @@ class App(wx.App):
frame.SetSizer(vertBox)
# Add the screens
vertBox.Add(self.config_screen, 1, wx.EXPAND)
vertBox.Add(self.multi_rx_screen, 1)
vertBox.Add(self.scope, 1)
vertBox.Add(self.bandscope_screen, 1)
vertBox.Add(self.filter_screen, 1)
vertBox.Add(self.multi_rx_screen, 1, wx.EXPAND)
vertBox.Add(self.scope, 1, wx.EXPAND)
vertBox.Add(self.bandscope_screen, 1, wx.EXPAND)
vertBox.Add(self.filter_screen, 1, wx.EXPAND)
if self.rate_audio_fft:
vertBox.Add(self.audio_fft_screen, 1)
vertBox.Add(self.help_screen, 1)
vertBox.Add(self.station_screen)
vertBox.Add(self.audio_fft_screen, 1, wx.EXPAND)
vertBox.Add(self.help_screen, 1, wx.EXPAND)
vertBox.Add(self.station_screen, 0, wx.EXPAND)
# Add the spacer
vertBox.Add(Spacer(frame), 0, wx.EXPAND)
# Add the sizer for the controls
@@ -3983,13 +3976,16 @@ class App(wx.App):
vertBox.AddSpacer(5) # Thanks to Christof, DJ4CM
# End of vertical box.
self.MakeButtons(frame, gbs)
minw = width = self.graph.width
maxw = maxh = -1
minh = 100
if conf.window_width > 0:
minw = width = maxw = conf.window_width
minw = maxw = conf.window_width
else:
minw = self.graph.width + main_border_w
maxw = -1
if conf.window_height > 0:
minh = maxh = self.height = conf.window_height
else:
minh = 100
maxh = -1
self.main_frame.SetSizeHints(minw, minh, maxw, maxh)
self.main_frame.SetClientSize(wx.Size(width, self.height))
if hasattr(Hardware, 'pre_open'): # pre_open() is called before open()
@@ -4031,7 +4027,6 @@ class App(wx.App):
if conf.dxClHost:
# create DX Cluster and register listener for change notification
self.dxCluster = dxcluster.DxCluster()
self.dxCluster.setListener(self.OnDxClChange)
self.dxCluster.start()
# Create shortcut keys for buttons
if conf.button_layout == 'Large screen':
@@ -4382,8 +4377,6 @@ class App(wx.App):
conf.tx_ip, conf.tx_audio_port,
conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q,
0.7, conf.mic_playback_rate)
def OnDxClChange(self):
self.station_screen.Refresh()
def OnIdle(self, event):
if self.screen:
self.screen.OnIdle(event)
@@ -6458,6 +6451,8 @@ class App(wx.App):
self.hamlib_com2_handler.Process()
if self.use_fast_heart_beat:
Hardware.FastHeartBeat()
if self.poll_gui_control:
Hardware.PollGuiControl()
if conf.do_repeater_offset:
hold = QS.tx_hold_state(-1)
if hold == 2: # Tx is being held for an FM repeater TX frequency shift
@@ -6638,6 +6633,9 @@ class App(wx.App):
self.ChangeDisplayFrequency(tune - vfo, vfo)
self.FldigiPoll()
self.HamlibPoll()
if self.dxCluster:
if self.dxCluster.Poll():
self.station_screen.Refresh()
if self.timer - self.slowheart_time0 > 0.5:
self.slowheart_time0 = self.timer
if self.w_phase:
+2 -1
View File
@@ -29,9 +29,10 @@ class Hardware(BaseHardware):
# self.hamlib_rigctld_port = 4532 # Standard rigctld control port
# self.hamlib_poll_seconds = 0.2 # Time interval to poll for changes
def open(self):
# Thanks to Peter, DC6AR, Rig ID changed from 229 to 2045.
ret = BaseHardware.open(self)
if not self.hamlib_connected: # rigctld is not started. Try to start it.
os.system("rigctld -m 229 -r /dev/ttyUSB0 -s 4800 & ") # Check the baud rate menu setting
os.system("rigctld -m 2045 -r /dev/ttyUSB0 -s 4800 & ") # Check the baud rate menu setting
# If this fails, start rigctld by hand.
return ret
+2
View File
@@ -120,6 +120,8 @@ class Hardware:
self.application.bandscope_clock = clock
#def PollCwKey(self): # Optional. Called frequently by the sound thread to check the CW key status.
# pass # Do not define if not needed.
#def PollGuiControl(self): # Optional. Called frequently by the GUI thread to change GUI settings (PTT etc.)
# pass # Do not define if not needed.
def StartSamples(self): # Quisk calls this from the sound thread to start sending samples.
# If you return a string, it replaces the string returned from hardware open()
pass
+1 -1
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.37'
Version = '4.2.40'
fp = open("__init__.py", "w") # write title string
fp.write("#Quisk version %s\n" % Version)