2 Commits
Author SHA1 Message Date
James C. Ahlstrom c97ffa155e Quisk version 4.2.55 2026-10-12 13:37:47 -04:00
jimahlstrom 745882a506 Add README for Quisk Software Defined Radio
Added introductory information about Quisk software defined radio, including its features and support links.
2026-09-27 16:19:11 -04:00
15 changed files with 197 additions and 566 deletions
+6
View File
@@ -1,3 +1,9 @@
Quisk Version 4.2.55 October 2026
==================================
Karl Heinz, K5KHK, provided a patche to fix the AC2YD remote control head. He provided another patch
to fix Hamlib control when using WSJT-X improved. He added a mic jitter buffer to the remote control
head so it works over WiFi. Thanks Karl!
Quisk Version 4.2.54 August 2026
=================================
Quisk can now accept audio samples for Tx from TCI at rates 48000, 24000, 12000 and 8000. Please test.
+12
View File
@@ -0,0 +1,12 @@
# Quisk Software Defined Radio
Quisk is a software defined radio transceiver that runs on a PC or on a single board computer such as a Raspberry Pi.
It is written in Python and C.
It works with various kinds of radio hardware such as SoftRock, Hermes Lite 2,
the SDR-IQ by RfSpace, and with network UDP samples.
Quisk is easily extensible by writing your own hardware file in Python.
I wrote Quisk back in 2008, and I have been maintaining it ever since.
For questions and support, please visit the group https://groups.io/g/n2adr-sdr.
For more information see https://james.ahlstrom.name/quisk.
+1 -1
View File
@@ -1,2 +1,2 @@
#Quisk version 4.2.54
#Quisk version 4.2.55
from .quisk import main
BIN
View File
Binary file not shown.
+101 -8
View File
@@ -53,6 +53,41 @@ static int control_head_data_width; // app.data_width of control head for gr
static int packets_sent;
static int packets_recd;
///// The jitter buffer is thanks to Karl Heinz - K5KHK
// Jitter buffer for the microphone samples received from the control head. The network can hold
// packets for 100 msec or more (Wi-Fi power saving and scanning) and then deliver them in a burst,
// but the Hermes Tx buffer only has 50 msec of slack. Without a buffer here, each late burst is an
// underflow (a gap on the air) followed by an overflow (discarded samples). So buffer mic_jb_target
// frames before playing, and then release them at a steady MIC_JB_RATE.
// The buffer adds its delay to the transmit audio, so it is off unless the configuration item
// remote_radio_mic_jitter_ms is set. When it is off, samples are passed on as they arrive.
#define MIC_JB_RATE 8000 // frames per second sent by the control head
#define MIC_JB_SIZE 8000 // ring buffer size in stereo frames
#define MIC_JB_MAX_MS 500 // largest configurable delay; MIC_JB_SIZE must hold this plus MIC_JB_EXTRA
#define MIC_JB_EXTRA 2000 // trim back to mic_jb_target above mic_jb_target + this (clock drift): 250 msec
#define MIC_JB_IDLE 0.5 // seconds without packets that end a transmission
static int16_t mic_jb[MIC_JB_SIZE * 2];
static int mic_jb_target; // frames to buffer before playing, from remote_radio_mic_jitter_ms; zero for off
static int mic_jb_read, mic_jb_count, mic_jb_playing, mic_jb_starved;
static int mic_jb_underruns, mic_jb_overruns;
static double mic_jb_time, mic_jb_frac, mic_jb_last_packet;
static void mic_jb_reset(void)
{
int msec;
msec = QuiskGetConfigInt("remote_radio_mic_jitter_ms", 0);
if (msec < 0)
msec = 0;
else if (msec > MIC_JB_MAX_MS)
msec = MIC_JB_MAX_MS;
mic_jb_target = msec * MIC_JB_RATE / 1000;
mic_jb_read = mic_jb_count = mic_jb_playing = mic_jb_starved = 0;
mic_jb_underruns = mic_jb_overruns = 0;
mic_jb_frac = 0;
}
// Receive stereo 16-bit pcm radio speaker sound on the control head via UDP
int read_remote_radio_sound_socket(complex double * cSamples)
{
@@ -117,9 +152,9 @@ int read_remote_radio_sound_socket(complex double * cSamples)
// Receive stereo 16-bit pcm microphone samples at the remote radio via UDP
int read_remote_mic_sound_socket(complex double * cSamples)
{
int i, bytes, nInt16, nSamples;
int i, j, bytes, nInt16, nSamples, nFrames;
int16_t buf[UDP_SEND_INT16];
double samp_r, samp_l;
double now, due;
struct timeval tm_wait;
fd_set fds;
static struct quisk_cHB45Filter HalfBand;
@@ -134,8 +169,8 @@ int read_remote_mic_sound_socket(complex double * cSamples)
quisk_filt_cInit(&cFiltInterp3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double));
}
// read all available packets, one per loop
nSamples = 0;
// read all available packets into the jitter buffer, one per loop
now = QuiskTimeSec();
while (1) {
tm_wait.tv_sec = 0;
tm_wait.tv_usec = 0;
@@ -152,15 +187,71 @@ int read_remote_mic_sound_socket(complex double * cSamples)
break;
}
if (bytes > 0) {
if (mic_jb_starved) { // the buffer ran dry; count it unless the transmission had ended
mic_jb_starved = 0;
if (now - mic_jb_last_packet < MIC_JB_IDLE)
mic_jb_underruns++;
}
mic_jb_last_packet = now;
nInt16 = bytes / 2;
for (i = 0; i < nInt16; i += 2) {
samp_r = buf[i];
samp_l = buf[i + 1];
cSamples[nSamples++] = (samp_r + I * samp_l) / CLIP16 * CLIP32;
for (i = 0; i + 1 < nInt16; i += 2) {
if (mic_jb_count >= MIC_JB_SIZE) { // full: drop the oldest frame
mic_jb_read = (mic_jb_read + 1) % MIC_JB_SIZE;
mic_jb_count--;
}
j = (mic_jb_read + mic_jb_count) % MIC_JB_SIZE * 2;
mic_jb[j] = buf[i];
mic_jb[j + 1] = buf[i + 1];
mic_jb_count++;
}
}
} // while(1)
// release the frames that are due since the last call
nSamples = 0;
if (mic_jb_target == 0) { // the jitter buffer is off: pass on everything received
nFrames = mic_jb_count;
}
else if ( ! mic_jb_playing) {
nFrames = 0;
if (mic_jb_count >= mic_jb_target) {
mic_jb_playing = 1;
mic_jb_time = now;
mic_jb_frac = 0;
}
}
else {
due = (now - mic_jb_time) * MIC_JB_RATE + mic_jb_frac;
if (due < 0) // the clock was set back
due = 0;
else if (due > MIC_JB_RATE / 2) // limit to 0.5 sec, well within SAMP_BUFFER_SIZE after interpolation
due = MIC_JB_RATE / 2;
mic_jb_time = now;
nFrames = (int)due;
mic_jb_frac = due - nFrames;
if (nFrames > mic_jb_count) { // ran dry: play what is left, then buffer up again
nFrames = mic_jb_count;
mic_jb_playing = 0;
mic_jb_starved = 1;
}
}
for (i = 0; i < nFrames; i++) {
j = mic_jb_read * 2;
cSamples[nSamples++] = ((double)mic_jb[j] + I * (double)mic_jb[j + 1]) / CLIP16 * CLIP32;
mic_jb_read = (mic_jb_read + 1) % MIC_JB_SIZE;
}
mic_jb_count -= nFrames;
if (mic_jb_target && mic_jb_count > mic_jb_target + MIC_JB_EXTRA) { // the control head's clock is faster than ours
i = mic_jb_count - mic_jb_target;
mic_jb_read = (mic_jb_read + i) % MIC_JB_SIZE;
mic_jb_count -= i;
mic_jb_overruns++;
}
if ((mic_jb_underruns || mic_jb_overruns) && now - mic_jb_last_packet > MIC_JB_IDLE) {
QuiskPrintf("Remote mic jitter buffer: %d underruns, %d overruns\n", mic_jb_underruns, mic_jb_overruns);
mic_jb_underruns = mic_jb_overruns = 0;
}
nSamples = quisk_cInterpolate(cSamples, nSamples, &cFiltInterp3, 3);
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand);
@@ -716,6 +807,7 @@ PyObject * quisk_start_remote_radio_remote_sound(PyObject * self, PyObject * arg
packets_sent = 0;
packets_recd = 0;
mic_jb_reset();
return Py_None;
}
@@ -742,6 +834,7 @@ PyObject * quisk_stop_remote_radio_remote_sound(PyObject * self, PyObject * args
control_head_graph_socket_started = 0;
remote_radio_graph_socket_started = 0;
control_head_data_width = 0;
mic_jb_reset();
QuiskPrintf("total packets sent = %i, recd = %i\n", packets_sent, packets_recd);
+22 -3
View File
@@ -190,10 +190,20 @@ class Remot: # Remote comtrol base class
return
try: # Read any data from the socket
text = self.remote_ctl_connection.recv(1024)
except:
#traceback.print_exc()
# Thanks to Karl Heinz - K5KHK
except (BlockingIOError, InterruptedError, socket.timeout): # No data available now
return
except OSError: # For example ConnectionResetError: the Control Head is gone
print('Closing Remote Control connection: recv() failed')
self.RemoteCtlClose(False)
return
else: # We got some characters
if not text: # recv() returns nothing only when the Control Head has closed the connection
# Close now rather than waiting for the heartbeat timeout: a Control Head that
# crashed or lost its network may have left the transmitter keyed.
print('Closing Remote Control connection: closed by Control Head')
self.RemoteCtlClose(False)
return
if not isinstance(text, str):
text = text.decode('utf-8')
self.received += text
@@ -375,13 +385,22 @@ class Remot: # Remote comtrol base class
self.cw_next_keydown = None
def StopTransmit(self):
# TODO: Add code for modes other than CW
# PTT code added thanks to Karl Heinz - K5KHK
# Called whenever the remote session ends (RemoteCtlClose). Release everything the
# Control Head may have keyed: the CW key and, for voice and digital modes, PTT.
# Without the PTT release, a Control Head that disappears while transmitting (a crash,
# a lost network, a dead battery) leaves the transmitter keyed until someone unkeys it
# at the radio.
while len(self.cw_event_queue):
self.cw_event_queue.popleft()
self.cw_next_event_ts = None
self.cw_next_keydown = None
self.cw_key_down = 0
QS.set_remote_cwkey(0)
ptt = getattr(self.app, 'pttButton', None)
if ptt and ptt.GetValue():
print('Remote Control connection closed while transmitting: releasing PTT')
ptt.SetValue(0, True) # Same as releasing PTT at the radio; runs OnButtonPTT
def ProcessRadioBtn(self, command, cmd_text):
# Large and Small format screens send different button events for radio buttons.
+2 -1
View File
@@ -2302,7 +2302,8 @@ class ControlMixin:
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',
'fixed_tune_offset', 'calibrate_temp_20', 'calibrate_temp_40', 'calibrate_current_0', 'calibrate_current_1'):
'fixed_tune_offset', 'calibrate_temp_20', 'calibrate_temp_40', 'calibrate_current_0', 'calibrate_current_1',
'remote_radio_mic_jitter_ms'):
setattr(conf, name, x)
application.ImmediateChange(name)
elif name[0:4] in ('lin_', 'win_'):
-1
View File
@@ -1 +0,0 @@
#
-418
View File
@@ -1,418 +0,0 @@
# This provides support for the SDR Micron TRX by David Fainitski, N7DDC
# On Windows this uses Python module ftd2xx. Install with pip.
# On Linux it uses ftdi1. Install python3-ftdi1 with your package manager.
import sys, wx, traceback, time
from quisk_hardware_model import Hardware as BaseHardware
DEBUG = 1 # print debug messages during operation
WRITE_LOG = 1 # write the data received from the MicronTRX during self.log_time to the file self.log_name
#WRITE_LOG = 2 # receive data from the file self.log_name instead of from USB
# https://github.com/Dfinitski/SDR-Micron
# Short Control Packet type "SCP" from PC to Micron TRX, 16 bytes, MSB first (big endian)
#
# (0 - 7) Preamble, seven 0x55 and 0xD5
# (8, 9, 10) "SCP"
#(11) RX0 enable, 0 - 1
#(12) RX1 enable, 0 - 1
#(13) BS0 enable, 0 - 1
#(14) TX0 enable, 0 - 1
#(15) CDC enable, 0 - 1
# Universal control packet type "UCP" from PC to TRX or from TRX to PC, 64 bytes, MSB first (big-endian)
#
# (0 - 7) Preamble, seven 0x55 and 0xD5
# (8, 9, 10) "UCP"
# (11) read only FW prefix MSB
# (12) read only FW prefix
# (13) read only FW prefix LSB
# (14) read only char '-'
# (15) Not used
# (16) Not used
# (17) Not used
# (18, 19, 20, 21) - 4 bytes RX0 frequency
# (22, 23, 24, 25) - 4 bytes RX1 frequency
# (26, 27, 28, 29) - 4 bytes BS0 frequency
# (30, 31, 32, 33) - 4 bytes TX0 frequency
# (34) - Attenuator, 0 - 31 (dB value)
# (35) - BS0 Rate
# 0 - 48 kHz,
# 1 - 96 kHz,
# 2 - 192 kHz,
# 3 - 384 kHz,
# 4 - 768 kHz,
# 5 - 1536 kHz
# (36) LNA enable
# (37) TX level 0 - 255
# (38) RX BPF bypass 0 - 1
# (39) BS0 mode
# 0 - 4096 buffered samples
# 1 - 8192 buffered samples
# 2 - 16384 buffered samples
# (40) Not used
# (41) Not used
# (42) read-only PTT state from device 0-1
# (43) Mic Boost, 0 - 1
# (44) TX LPF Bypass 0 - 1
# (45) PTT to device 1 - 0
# (46) CW mode on 0 - 1
# (47) CW key manipulation from PC to device 1 - 0
# (48) Read only PWR, Output power in Watts
# (49) Read only SWR x.x, from 1.0 to 9.9 as a natural number without decimal point
# (50) Key Speed 10 - 63 ppm (20 by default)
# (51) Paddle swap 0, 1
# (52) Key mode, 0 = straight, 01 = Mode A, 10 = Mode B
# (53) Key weight, 33 to 66, (50 by default)
# (54) Key auto spacing, 0, 1
# (55) ST Level, 0 - 255 sight tone level
# (56) ST Freq in tens Hertz, 40 - 100 (400 - 1000Hz), 70 by default
# (57) Not used
# (58) Delay ON, 8 - 255 ms (25 by default) - time for TX carrier delay is needed for transmitter preparing to TX
# (59) Hang out time in tens ms 0 - 255 (0 to 2550 msec) (250 ms by default) needed to keep transmitter ready after PTT
# (60) Read only CW key manipulation from device to PC
# (61) Read only FW2 - firmware version MSB
# (62) Read only FW1
# (63) Read only FW0 - firmware version LSB
# Define the name of the hardware and the items on the hardware screen (see quisk_conf_defaults.py):
################ Receivers SdrMicronTRX, The SDR Micron TRX project by David Fainitski.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'microntrxpkg/quisk_hardware.py'
class Hardware(BaseHardware):
sample_rates = [48] #[48, 96, 192, 384, 768, 1536]
BS0_samples = {0:4096, 1:8192, 2:16384}
BS0_bytes = {0:4096 * 2, 1:8192 * 2, 2:16384 * 2} # Two bytes per sample
def __init__(self, app, conf):
BaseHardware.__init__(self, app, conf)
self.usb_windows = None
self.usb_linux = None
self.prefix = b'\x55\x55\x55\x55\x55\x55\x55\xD5'
self.read_data = bytearray()
self.tx_samples = b''
self.rf_gain_labels = ('RF 0', 'RF -10', 'RF -20', 'RF -30')
self.index = 1
self.old_vfo = 0
self.sdrmicron_clock = 76800000
self.sdrmicron_decim = 1600
self.bscope_data = bytearray()
self.have_version = False
self.frame_msg = ''
self.remaining_BS0 = -1
self.log_name = "microntrxpkg/microntrx_log.dat"
self.log_time = 5.0 # seconds of log data
self.log_time0 = None # starting time.time() to start log file
self.log_fp = None
self.pc_control = bytearray(64)
self.pc_control[0:8] = self.prefix
self.pc_control[8:11] = b'UCP'
self.got_defaults = 0
self.pc_enables = bytearray(16)
self.pc_enables[0:8] = self.prefix
self.pc_enables[8:11] = b'SCP'
self.fw_version = bytearray
if conf.fft_size_multiplier == 0:
conf.fft_size_multiplier = 3 # Set size needed by VarDecim
def open(self): # This method must return a string showing whether the open succeeded or failed.
if WRITE_LOG == 1:
self.log_fp = open(self.log_name, "wb")
elif WRITE_LOG == 2:
self.log_fp = open(self.log_name, "rb")
rx_bytes = 3 # rx_bytes is the number of bytes in each I or Q sample: 1, 2, 3, or 4
rx_endian = 1 # rx_endian is the order of bytes in the sample array: 0 == little endian; 1 == big endian
self.InitSamples(rx_bytes, rx_endian) # Initialize: read samples from this hardware file and send them to Quisk
self.InitTxSamples(2, 1, 48000) # Initialize Tx samples to send to the hardware
bs_bytes = 2
bs_endian = 1
#self.InitBscope(bs_bytes, bs_endian, self.sdrmicron_clock, self.BS1_samples[self.pc_control[40]]) # Initialize bandscope
if WRITE_LOG == 2:
return "Read data from %s" % self.log_name
elif sys.platform == 'win32':
return self.open_win()
else:
return self.open_linux()
def open_win(self):
try:
import ftd2xx as d2xx
except:
return 'The ftd2xx module is missing'
enum = d2xx.createDeviceInfoList() # quantity of FTDI devices
if enum == 0:
return 'There are no FTDI devices available'
for i in range(enum): # Searching and opening needed device
a = d2xx.getDeviceInfoDetail(i)
if DEBUG:
print ("%d: description %s, serial %s" % (i, a['description'], a['serial']))
if a['description']==b'SDR-Micron':
try:
usb = d2xx.openEx(a['serial'])
except:
return 'Device SDR-Micron could not be opened'
usb.setBitMode(255, 0) # reset
time.sleep(0.1)
usb.setBitMode(255, 64) # Configure FT2232H into Sync FIFO mode
usb.setTimeouts(100, 100) # read, write
usb.setLatencyTimer(2)
usb.setUSBParameters(32, 32) # in_tx_size, out_tx_size
time.sleep(1.5) # waiting for device initialisation
usb.read(usb.getQueueStatus()) # clean the usb data buffer
self.frame_msg = a['description'].decode('utf-8') + ' S/N - ' + a['serial'].decode('utf-8')
self.usb_windows = usb
return self.frame_msg
return 'Device SDR-Micron was not found'
def open_linux(self): # Not finished
try:
import ftdi1
except:
return 'The ftdi1 module is missing'
usb = ftdi1.new()
# Open Channel A (interface 1 in libftdi)
ftdi1.set_interface(usb, ftdi1.INTERFACE_A)
rc = ftdi1.usb_open(usb, 0x0403, 0x6014) # 0x6014 is the standard FT232H PID
if rc == 0:
ftdi1.usb_reset(usb)
ftdi1.set_usb_read_timout(usb, 1)
ftdi1.set_usb_write_timout(usb, 5)
ftdi1.set_latency_timer(usb, 2)
# Enable Sync FIFO mode
# 0xFF = pin mask, 0x40 = BITMODE_SYNCFF
ftdi1.set_bitmode(usb, 0xFF, ftdi1.BITMODE_SYNCFF)
if DEBUG:
print("Sync FIFO active via ftdi1.")
self.usb_linux = usb
return "Device SDR-Micron opened"
return 'Device SDR-Micron was not found'
def usb_read(self):
if WRITE_LOG == 2:
data = self.log_fp.read(120)
return data
elif self.usb_windows:
count = self.usb_windows.getQueueStatus() # Read all available bytes
if count > 0:
return self.usb_windows.read(count) # return Python bytes
elif self.usb_linux:
count, data = ftdi1.read_data(self.usb_linux, 1024)
if count > 0:
return data # data is Python bytes
elif count < 0 and DEBUG:
print("Error %d in usb_read", count)
return None
def usb_write(self, data):
data = bytes(data) # Convert bytearray to bytes
if self.usb_windows:
try:
self.usb_windows.write(data)
except:
if DEBUG:
print('Error while usb_write')
elif self.usb_linux:
count = ftdi1.write_data(self.usb_linux, data)
if count != len(data) and DEBUG:
print("Failure in usb_write_data %d %d", count, len(data))
def close(self):
if self.usb_windows:
self.usb_windows.setBitMode(255, 0) # reset
time.sleep(0.5)
self.usb_windows.close()
self.usb_windows = None
elif self.usb_linux:
ftdi1.usb_close(self.usb_linux)
ftdi1.free(self.usb_linux)
self.usb_linux = None
def OnButtonRfGain(self, event):
btn = event.GetEventObject()
n = btn.index
if n == 0:
self.pc_control[34] = 0
elif n == 1:
self.pc_control[34] = 10
elif n == 2:
self.pc_control[34] = 20
elif n == 3:
self.pc_control[34] = 30
if DEBUG:
print ('pc_control[34]', self.pc_control[34])
self.rx_control_upd()
def ChangeFrequency(self, tx_freq, vfo, source='', band='', event=None):
if DEBUG:
print('ChangeFrequency tx_freq', tx_freq, 'vfo', vfo)
if tx_freq and tx_freq > 0:
tx_freq = int(tx_freq - self.transverter_offset)
self.pc_control[30] = self.pc_control[18] = (tx_freq >> 24) & 0xFF # MSB
self.pc_control[31] = self.pc_control[19] = (tx_freq >> 16) & 0xFF
self.pc_control[32] = self.pc_control[20] = (tx_freq >> 8) & 0xFF
self.pc_control[33] = self.pc_control[21] = (tx_freq ) & 0xFF #LSB
self.rx_control_upd()
if vfo:
vfo = int(vfo - self.transverter_offset)
if vfo != self.old_vfo:
self.old_vfo = vfo
self.pc_control[18] = (vfo >> 24) & 0xFF # MSB
self.pc_control[19] = (vfo >> 16) & 0xFF
self.pc_control[20] = (vfo >> 8) & 0xFF
self.pc_control[21] = (vfo ) & 0xFF #LSB
self.rx_control_upd()
return tx_freq, vfo
def OnSpot(self, level):
pass
def OnChangeRxTx(self, is_rx):
self.pc_control[45] = is_rx
self.rx_control_upd()
def VarDecimGetChoices(self): # Return a list/tuple of strings for the decimation control.
return list(map(str, self.sample_rates)) # convert integer to string
def VarDecimGetLabel(self): # return a text label for the control
return "Sample rate ksps"
def VarDecimGetIndex(self): # return the current index
return self.index
def VarDecimSet(self, index=None): # return sample rate
if index is None: # initial call to set the sample rate before the call to open()
rate = self.application.vardecim_set
try:
self.index = self.sample_rates.index(rate // 1000)
except:
self.index = 0
else:
self.index = index
self.pc_control[35] = self.index
rate = self.sample_rates[self.index] * 1000
if DEBUG:
print("varDecimSet index", self.index, "rate", rate)
self.rx_control_upd()
return rate
def VarDecimRange(self): # Return the lowest and highest sample rate.
#return (48000, 1536000)
return (48000, 48000)
def StartSamples(self): # called by the sound thread
self.pc_enables[11] = 1 # RX0 enable
#self.pc_enables[13] = 1 # BS0 enable
self.usb_write(self.pc_enables)
if DEBUG:
print ("Start samples")
def StopSamples(self): # called by the sound thread
self.pc_enables[11] = 0
self.pc_enables[13] = 0
self.usb_write(self.pc_enables)
if DEBUG:
print ("Stop Samples")
def rx_control_upd(self):
if self.got_defaults:
self.usb_write(self.pc_control)
def GetRxSamples(self): # Read from the MicronTRX. Called frequently from the sound thread.
# Send Tx samples to the hardware
self.tx_samples += self.GetTxSamples()
while len(self.tx_samples) >= 492:
data = self.prefix + b'TX0\x00\x00\x00\x00\x00' + self.tx_samples[0:492]
self.usb_write(data)
#print(data[0:24], len(data))
#i = data[16] << 8 | data[17]
#print ("%8d" % i)
self.tx_samples = self.tx_samples[492:]
# Read messages from USB
data = self.usb_read()
if data:
self.read_data += data
if DEBUG > 1:
print("Read %s bytes, buffer length %d" % (len(data), len(self.read_data)))
if WRITE_LOG == 1:
if self.log_fp:
if self.log_time0 is None:
self.log_time0 = time.time()
if time.time() - self.log_time0 < self.log_time:
self.log_fp.write(data)
else:
self.log_fp.close()
self.log_fp = None
while True:
start = self.read_data.find(self.prefix)
if start < 0: # No prefix
return
if start > 0:
del self.read_data[0:start]
if DEBUG:
print ("USB blocks lost sync")
if len(self.read_data) < 11:
return
block_type = self.read_data[8:11]
if block_type == b'BS0':
if len(self.read_data) < 508:
return
packet_number = self.read_data[11]
if packet_number == 0: # start of a block of data
self.remaining_BS0 = self.BS0_bytes[self.pc_control[39]]
self.bscope_data = self.read_data[16:508] # 492 bytes
self.remaining_BS0 -= 492
elif self.remaining_BS0 >= 492:
self.bscope_data += self.read_data[16:508] # 492 bytes
self.remaining_BS0 -= 492
elif self.remaining_BS0 <= 0:
pass
else:
self.bscope_data += self.read_data[16:self.remaining_BS0 + 16]
self.remaining_BS0 = -1
# self.AddBscopeSamples(self.bscope_data)
if DEBUG > 1:
print("BS0 buffer length %d" % len(self.bscope_data))
del self.read_data[0:508]
elif block_type == b'CDM':
if len(self.read_data) < 180:
return
if DEBUG > 1:
print("CDM")
del self.read_data[0:180]
elif block_type == b'RX0':
if len(self.read_data) < 508:
return
if self.read_data[12]:
self.GotClip()
self.AddRxSamples(self.read_data[16:508])
if DEBUG > 1:
print("RX0")
del self.read_data[0:508]
elif block_type == b'RX1':
if len(self.read_data) < 508:
return
if DEBUG > 1:
print("RX1")
del self.read_data[0:508]
elif block_type == b'UCP':
if len(self.read_data) < 64:
return
if self.have_version is False:
self.have_version = True
self.fw_version = self.read_data[11:15] + self.read_data[61:64]
self.frame_msg += ' F/W version - ' + self.fw_version.decode('utf-8')
self.application.main_frame.SetConfigText(self.frame_msg)
self.pc_control[11:64] = self.read_data[11:64] # save UCP content with defaults
self.got_defaults = 1;
if DEBUG > 1:
print("UCP")
del self.read_data[0:64]
else:
del self.read_data[0:11]
if DEBUG:
print("Unknown USB block type")
+2 -2
View File
@@ -73,7 +73,7 @@ static double modulation_index = 1.6; // For FM transmit, the modulation index
static int is_vox = 0; // Is the VOX level exceeded?
static int vox_level = CLIP16; // VOX trigger level as a number 0 to CLIP16
static int timeVOX = 2000; // VOX hang time in milliseconds
static int tx_sample_rate = 48000; // Used for SoapySDR and Tx samples from Python
static int tx_sample_rate = 48000; // Used for SoapySDR
static int reverse_tx_sideband;
static int PsEnable = 0;
static int PsCal = 0;
@@ -1305,7 +1305,7 @@ int quisk_process_microphone(int mic_sample_rate, complex double * cSamples, int
; // hermes handles this itself
else if (quisk_play_state == SOFTWARE_CWKEY)
serial_key_samples(cSamples, count);
if(quisk_pt_sample_write && key_down) { // Used for SoapySDR and samples from Python
if(quisk_pt_sample_write) { // Used for SoapySDR
// Interpolate the mic samples to the Tx sample rate
//printf("Tx sample rate %i\n", tx_sample_rate);
switch (tx_sample_rate) {
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "quisk"
version = "4.2.54"
version = "4.2.55"
requires-python = ">=3.13"
description = 'QUISK is a Software Defined Radio (SDR) transceiver that can control various radio hardware.'
license = "GPL-2.0-or-later"
+3 -100
View File
@@ -115,8 +115,6 @@ int quiskTxHoldState; // hold Tx until the repeater frequency shift is complet
int quisk_is_vna; // zero for normal program, one for the VNA program
static int py_sample_rx_bytes=2; // number of bytes in each I or Q sample: 1, 2, 3, or 4
static int py_sample_rx_endian; // order of sample array: 0 == little endian; 1 == big endian
static int py_sample_tx_bytes; // number of bytes in each I or Q sample: 2, 3, or 4
static int py_sample_tx_endian; // order of sample array: 0 == little endian; 1 == big endian
static int py_bscope_bytes;
static int py_bscope_endian;
static int quisk_auto_notch; // auto notch control
@@ -161,8 +159,6 @@ static int softrock_correct_active; // 0 No correction adjustment; 1 Manual adju
static complex double PySampleBuf[SAMP_BUFFER_SIZE]; // buffer for samples returned from Python
static int PySampleCount; // count of samples in buffer
static uint8_t PySampleTxBytes[SAMP_BUFFER_SIZE * 8]; // buffer for Tx sample bytes to send to the hardware
static int PySampleTxCount;
static bool quisk_tx_inhibit;
static int multirx_data_width; // width of graph data to return
@@ -3022,17 +3018,6 @@ static PyObject * add_bscope_samples(PyObject * self, PyObject * args)
return Py_None;
}
static PyObject * get_tx_samples(PyObject * self, PyObject * args)
{
PyObject * by;
if (!PyArg_ParseTuple (args, ""))
return NULL;
by = PyBytes_FromStringAndSize((const char *)PySampleTxBytes, PySampleTxCount);
PySampleTxCount = 0;
return by;
}
static void py_sample_start(void)
{
}
@@ -3055,83 +3040,6 @@ static int py_sample_read(complex double * cSamples)
return n;
}
static int py_sample_tx_save(complex double * cSamples, int nSamples)
{ // These cSamples are 16-bit integers from the microphone
int i;
int16_t rr, ii;
uint8_t rrH, rrL, iiH, iiL;
if (PySampleTxCount + nSamples * 8 >= SAMP_BUFFER_SIZE * 8) {
QuiskPrintf ("py_sample_tx_save: buffer is too full\n");
return 0;
}
for (i = 0; i < nSamples; i++) {
rr = (int16_t)creal(cSamples[i]);
ii = (int16_t)cimag(cSamples[i]);
rrH = 0xFF & (rr >> 8);
rrL = 0xFF & rr;
iiH = 0xFF & (ii >> 8);
iiL = 0xFF & ii;
if (py_sample_tx_endian == 0) { // byte order of samples is little-endian
switch(py_sample_tx_bytes) {
case 2:
PySampleTxBytes[PySampleTxCount++] = rrL;
PySampleTxBytes[PySampleTxCount++] = rrH;
PySampleTxBytes[PySampleTxCount++] = iiL;
PySampleTxBytes[PySampleTxCount++] = iiH;
break;
case 3:
PySampleTxBytes[PySampleTxCount++] = 0;
PySampleTxBytes[PySampleTxCount++] = rrL;
PySampleTxBytes[PySampleTxCount++] = rrH;
PySampleTxBytes[PySampleTxCount++] = 0;
PySampleTxBytes[PySampleTxCount++] = iiL;
PySampleTxBytes[PySampleTxCount++] = iiH;
break;
case 4:
PySampleTxBytes[PySampleTxCount++] = 0;
PySampleTxBytes[PySampleTxCount++] = 0;
PySampleTxBytes[PySampleTxCount++] = rrL;
PySampleTxBytes[PySampleTxCount++] = rrH;
PySampleTxBytes[PySampleTxCount++] = 0;
PySampleTxBytes[PySampleTxCount++] = 0;
PySampleTxBytes[PySampleTxCount++] = iiL;
PySampleTxBytes[PySampleTxCount++] = iiH;
break;
}
}
else { // byte order of samples is big-endian
switch(py_sample_tx_bytes) {
case 2:
PySampleTxBytes[PySampleTxCount++] = rrH;
PySampleTxBytes[PySampleTxCount++] = rrL;
PySampleTxBytes[PySampleTxCount++] = iiH;
PySampleTxBytes[PySampleTxCount++] = iiL;
break;
case 3:
PySampleTxBytes[PySampleTxCount++] = rrH;
PySampleTxBytes[PySampleTxCount++] = rrL;
PySampleTxBytes[PySampleTxCount++] = 0;
PySampleTxBytes[PySampleTxCount++] = iiH;
PySampleTxBytes[PySampleTxCount++] = iiL;
PySampleTxBytes[PySampleTxCount++] = 0;
break;
case 4:
PySampleTxBytes[PySampleTxCount++] = rrH;
PySampleTxBytes[PySampleTxCount++] = rrL;
PySampleTxBytes[PySampleTxCount++] = 0;
PySampleTxBytes[PySampleTxCount++] = 0;
PySampleTxBytes[PySampleTxCount++] = iiH;
PySampleTxBytes[PySampleTxCount++] = iiL;
PySampleTxBytes[PySampleTxCount++] = 0;
PySampleTxBytes[PySampleTxCount++] = 0;
break;
}
}
}
return 0;
}
static PyObject * get_params(PyObject * self, PyObject * args)
{
const char * name;
@@ -3185,22 +3093,18 @@ static PyObject * set_params(PyObject * self, PyObject * args, PyObject * keywds
{ /* Call with keyword arguments ONLY; change local parameters */
static char * kwlist[] = {"quisk_is_vna", "rx_bytes", "rx_endian", "read_error", "clip",
"bscope_bytes", "bscope_endian", "bscope_size", "bandscopeScale", "hermes_pause",
"tx_bytes", "tx_endian",
NULL} ;
int i, nbytes, read_error, clip, bscope_size, hermes_pause;
nbytes = read_error = clip = bscope_size = hermes_pause = -1;
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiiiiiidiii", kwlist,
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiiiiiidii", kwlist,
&quisk_is_vna, &nbytes, &py_sample_rx_endian, &read_error, &clip,
&py_bscope_bytes, &py_bscope_endian, &bscope_size, &bandscopeScale, &hermes_pause,
&py_sample_tx_bytes, &py_sample_tx_endian))
&py_bscope_bytes, &py_bscope_endian, &bscope_size, &bandscopeScale, &hermes_pause))
return NULL;
if (nbytes != -1) {
py_sample_rx_bytes = nbytes;
quisk_sample_source(py_sample_start, py_sample_stop, py_sample_read);
quisk_sample_source4(py_sample_start, py_sample_stop, py_sample_read, NULL);
}
if (py_sample_tx_bytes)
quisk_pt_sample_write = py_sample_tx_save;
if (read_error != -1)
quisk_sound_state.read_error++;
if (clip != -1)
@@ -6186,7 +6090,6 @@ 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."},
{"get_tx_samples", get_tx_samples, METH_VARARGS, "Return Tx samples to send to the hardware."},
{"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."},
+32 -20
View File
@@ -65,15 +65,13 @@ else:
Q3StringTypes = (str, unicode)
class StdOutput(wx.Frame):
def __init__(self, app): # Called from the GUI thread
def __init__(self, app):
self.app = app
self.ctrl = None
self.old_stdout = sys.stdout
self.old_stderr = sys.stderr
self.old_excepthook = sys.excepthook
self.path = os.path.join(app.QuiskFilesDir, "quisk_logfile.txt")
self.gui_thread = threading.get_native_id()
self.text_buffer = ''
try:
size = os.path.getsize(self.path)
except:
@@ -118,9 +116,6 @@ class StdOutput(wx.Frame):
self.fp.write(text)
self.fp.write("\n")
def write(self, text):
if threading.get_native_id() != self.gui_thread:
self.text_buffer += text # When called by non-GUI thread, save text
return
if self.fp:
self.fp.write(text)
if self.old_stdout:
@@ -129,10 +124,6 @@ class StdOutput(wx.Frame):
self.ctrl.write(text)
def flush(self):
pass
def PrintBuffer(self): # Print any saved text from non-GUI thread
if self.text_buffer:
print(self.text_buffer)
self.text_buffer = ''
def ExceptHook(self, typ, value, traceb):
self.Show(True)
self.Raise()
@@ -751,6 +742,7 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
self.vfo = "Main"
self.split_mode = 0
self.split_vfo = 'VFO'
self.lock_mode = 0
h = self.Handlers = {}
h[''] = self.ErrProtocol
h['dump_state'] = self.DumpState
@@ -774,6 +766,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
h['set_split_freq'] = self.SetSplitFreq
h['get_split_vfo'] = self.GetSplitVfo
h['set_split_vfo'] = self.SetSplitVfo
h['get_lock_mode'] = self.GetLockMode
h['set_lock_mode'] = self.SetLockMode
self.MakeDumpState()
def MakeDumpState(self):
dump_state = []
@@ -1018,7 +1012,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
self.ErrParam()
else:
freq = int(freq + 0.5)
self.app.ChangeRxTxFrequency(freq, None)
# Like a real radio, keep the mode. Hamlib caches the mode, so the client would not notice a mode change.
self.app.ChangeRxTxFrequency(freq, None, keep_mode=True)
def GetSplitFreq(self): # The Tx Frequency
self.Reply('TX Frequency', self.app.txFreq + self.app.VFO, 0)
def SetSplitFreq(self): # The Tx Frequency
@@ -1031,7 +1026,7 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
else:
freq = int(freq + 0.5)
# This always controls the Tx frequency
self.app.ChangeRxTxFrequency(None, freq)
self.app.ChangeRxTxFrequency(None, freq, keep_mode=True)
def GetSplitVfo(self):
self.Reply('Split', self.split_mode, 'TX VFO', self.split_vfo, 0)
def SetSplitVfo(self):
@@ -1116,6 +1111,22 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
Lab = label
diff = df
self.app.filterButns.SetLabel(Lab, True)
# The code for the lock_mode and keep_mode was supplied by Karl Heinz - K5KHK. Thanks Karl!
def GetLockMode(self):
# Hamlib netrigctl always reads two lines, so an error reply "RPRT -4" makes it wait for its 10 second timeout.
# Reply like rigctld with the lock value and then "RPRT 0".
if self.extended:
self.Reply('Locked', self.lock_mode, 0)
else:
self.Send("%d\nRPRT 0\n" % self.lock_mode)
def SetLockMode(self):
lock = self.GetParamNumber()
try:
self.lock_mode = int(lock)
except:
self.ErrParam()
else:
self.Reply(0)
def GetVfo(self):
self.Reply('VFO', self.vfo, 0)
def SetVfo(self):
@@ -1511,7 +1522,9 @@ class SoundThread(threading.Thread):
#print ("Quisk thread %12.3f" % ((tm - self.time0) * 1E3))
#self.time0 = tm
if self.samples_from_python:
Hardware.GetRxSamples()
samples = Hardware.GetRxSamples()
if samples:
QS.set_params(rx_samples=samples)
if self.poll_cw:
Hardware.PollCwKey()
if conf.midi_cwkey_device:
@@ -4701,7 +4714,6 @@ class App(wx.App):
msg = QS.GetQuiskPrintf()
if msg:
print(msg, end='')
self.std_out_err.PrintBuffer()
QS.close_key()
if QS.tci_get_params("tci_clients_count") > 0:
QS.tci_set_params(close=1)
@@ -4735,7 +4747,6 @@ class App(wx.App):
msg = QS.GetQuiskPrintf()
if msg:
print(msg, end='')
self.std_out_err.PrintBuffer()
def OnBtnOnOff(self, event):
if event.GetEventObject().GetValue(): # Start samples
try:
@@ -6145,7 +6156,7 @@ class App(wx.App):
self.freqDisplay.Display(self.txFreq + self.VFO)
self.fldigi_new_freq = self.txFreq + self.VFO
return change
def ChangeRxTxFrequency(self, rx_freq=None, tx_freq=None):
def ChangeRxTxFrequency(self, rx_freq=None, tx_freq=None, keep_mode=False):
if not self.split_rxtx and not tx_freq:
tx_freq = rx_freq
if tx_freq:
@@ -6156,7 +6167,7 @@ class App(wx.App):
else: # Change the VFO
vfo = (tx_freq // 5000) * 5000 - 5000
tune = tx_freq - vfo
self.BandFromFreq(tx_freq)
self.BandFromFreq(tx_freq, keep_mode)
self.ChangeHwFrequency(tune, vfo, 'FreqEntry')
if rx_freq and self.split_rxtx: # Frequency must be on-screen
tune = rx_freq - self.VFO
@@ -6346,7 +6357,8 @@ class App(wx.App):
self.pttButton.Enable(False)
else:
self.pttButton.Enable(True)
def BandFromFreq(self, frequency): # Change to a new band based on the frequency
def BandFromFreq(self, frequency, keep_mode=False): # Change to a new band based on the frequency
# If keep_mode is True, do not change to the mode saved for the new band.
if self.screen == self.bandscope_screen:
return
try:
@@ -6368,7 +6380,8 @@ class App(wx.App):
vfo, tune, mode = self.bandState[band]
except KeyError:
vfo, tune, mode = (0, 0, 'LSB')
self.modeButns.SetLabel(mode, True)
if not keep_mode:
self.modeButns.SetLabel(mode, True)
self.ChangeBand(band)
break
def ChangeBand(self, band):
@@ -6903,7 +6916,6 @@ class App(wx.App):
msg = QS.GetQuiskPrintf()
if msg:
print(msg, end='')
self.std_out_err.PrintBuffer()
if self.screen == self.config_screen:
self.screen.OnGraphData() # Send message to draw new data
if self.add_version and Hardware.GetFirmwareVersion() is not None:
+13
View File
@@ -1508,6 +1508,19 @@ remote_radio_ip = ""
# It is only necessary to enter it once on each computer.
remote_radio_password = ""
## remote_radio_mic_jitter_ms Mic buffer msec for remote radio, integer
# This is set on the remote radio, not the control head. The network can hold the control
# head's microphone packets for 100 msec or more and then deliver them in a burst, which is
# common over Wi-Fi. The transmit audio then has gaps. This buffers the microphone audio
# for the given time before transmitting it, so the bursts are smoothed out, at the cost
# of that much more transmit delay. Zero turns the buffer off, which is best on a wired
# network. If needed, try 150 msec. The maximum is 500. A change takes effect when the
# next control head connects.
remote_radio_mic_jitter_ms = 0
#remote_radio_mic_jitter_ms = 100
#remote_radio_mic_jitter_ms = 150
#remote_radio_mic_jitter_ms = 250
## k4_tcp_ip IP address for K4 TCP, text
# This is the Quisk IP address for the TCP server implementing K4 commands.
k4_tcp_ip = ""
+2 -11
View File
@@ -118,24 +118,17 @@ class Hardware:
# length is the number of samples in each block of ADC samples, and equals the FFT size.
QS.set_params(bscope_bytes=int_size, bscope_endian=endian, bscope_size=length)
self.application.bandscope_clock = clock
def InitTxSamples(self, int_size, endian, rate): # Tx sample initialization; you must call this from your hardware __init__().
# int_size is the number of bytes in each I or Q sample: 2, 3, or 4
# endian is the order of bytes in the sample: 0 == little endian; 1 == big endian
# This can be called again to change the format. For example, a different number of bytes for different sample rates.
QS.set_tx_audio(tx_sample_rate=rate) # Available rates are 48, 50, 96, 100, 192 ksps
QS.set_params(tx_bytes=int_size, tx_endian=endian)
#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 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()
pass
def StopSamples(self): # Quisk calls this from the sound thread to stop samples.
def StopSamples(self): # Quisk calls this from the sound thread to stop sending samples.
pass
def GetRxSamples(self): # Quisk calls this frequently from the sound thread. Poll your hardware for samples.
# Return any available samples by calling AddRxSamples() and perhaps AddBscopeSamples() from within this method.
# Call GetTxSamples() from within this method to return any Tx samples that must be sent.
pass
def AddRxSamples(self, samples): # Call this from within GetRxSamples() to record the Rx samples.
# "samples" is int_size of integer I data followed by int_size of integer Q data, repeated.
@@ -149,8 +142,6 @@ class Hardware:
# For Python 2, "samples" must be a byte array or bytes or a string.
# The number of bytes in "samples" must equal the block length times the bytes per sample.
QS.add_bscope_samples(samples)
def GetTxSamples(self): # Call this from within GetRxSamples() to return the Tx samples to send.
return QS.get_tx_samples() # Return a Python bytes object
def GotClip(self): # Call this to indicate that samples were received with the clip (overrange) indicator true.
QS.set_params(clip=1)
def GotReadError(self, print_msg, msg): # Call this to indicate an error in receiving the Rx samples.