3 Commits
Author SHA1 Message Date
James C. Ahlstrom e219a5b975 Add PTT to TX0 2026-09-15 15:54:57 -04:00
James C. Ahlstrom 2a530e5d11 Add TX0 samples 2026-09-15 15:32:53 -04:00
James C. Ahlstrom ceab3bed26 Set up the MicronTRX project by David Fainitski, N7DDC 2026-08-20 15:05:13 -04:00
15 changed files with 566 additions and 197 deletions
-6
View File
@@ -1,9 +1,3 @@
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 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. Quisk can now accept audio samples for Tx from TCI at rates 48000, 24000, 12000 and 8000. Please test.
-12
View File
@@ -1,12 +0,0 @@
# 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.55 #Quisk version 4.2.54
from .quisk import main from .quisk import main
BIN
View File
Binary file not shown.
+8 -101
View File
@@ -53,41 +53,6 @@ static int control_head_data_width; // app.data_width of control head for gr
static int packets_sent; static int packets_sent;
static int packets_recd; 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 // Receive stereo 16-bit pcm radio speaker sound on the control head via UDP
int read_remote_radio_sound_socket(complex double * cSamples) int read_remote_radio_sound_socket(complex double * cSamples)
{ {
@@ -152,9 +117,9 @@ int read_remote_radio_sound_socket(complex double * cSamples)
// Receive stereo 16-bit pcm microphone samples at the remote radio via UDP // Receive stereo 16-bit pcm microphone samples at the remote radio via UDP
int read_remote_mic_sound_socket(complex double * cSamples) int read_remote_mic_sound_socket(complex double * cSamples)
{ {
int i, j, bytes, nInt16, nSamples, nFrames; int i, bytes, nInt16, nSamples;
int16_t buf[UDP_SEND_INT16]; int16_t buf[UDP_SEND_INT16];
double now, due; double samp_r, samp_l;
struct timeval tm_wait; struct timeval tm_wait;
fd_set fds; fd_set fds;
static struct quisk_cHB45Filter HalfBand; static struct quisk_cHB45Filter HalfBand;
@@ -169,8 +134,8 @@ int read_remote_mic_sound_socket(complex double * cSamples)
quisk_filt_cInit(&cFiltInterp3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double)); quisk_filt_cInit(&cFiltInterp3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double));
} }
// read all available packets into the jitter buffer, one per loop // read all available packets, one per loop
now = QuiskTimeSec(); nSamples = 0;
while (1) { while (1) {
tm_wait.tv_sec = 0; tm_wait.tv_sec = 0;
tm_wait.tv_usec = 0; tm_wait.tv_usec = 0;
@@ -187,71 +152,15 @@ int read_remote_mic_sound_socket(complex double * cSamples)
break; break;
} }
if (bytes > 0) { 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; nInt16 = bytes / 2;
for (i = 0; i + 1 < nInt16; i += 2) { for (i = 0; i < nInt16; i += 2) {
if (mic_jb_count >= MIC_JB_SIZE) { // full: drop the oldest frame samp_r = buf[i];
mic_jb_read = (mic_jb_read + 1) % MIC_JB_SIZE; samp_l = buf[i + 1];
mic_jb_count--; cSamples[nSamples++] = (samp_r + I * samp_l) / CLIP16 * CLIP32;
}
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) } // 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_cInterpolate(cSamples, nSamples, &cFiltInterp3, 3);
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand); nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand);
@@ -807,7 +716,6 @@ PyObject * quisk_start_remote_radio_remote_sound(PyObject * self, PyObject * arg
packets_sent = 0; packets_sent = 0;
packets_recd = 0; packets_recd = 0;
mic_jb_reset();
return Py_None; return Py_None;
} }
@@ -834,7 +742,6 @@ PyObject * quisk_stop_remote_radio_remote_sound(PyObject * self, PyObject * args
control_head_graph_socket_started = 0; control_head_graph_socket_started = 0;
remote_radio_graph_socket_started = 0; remote_radio_graph_socket_started = 0;
control_head_data_width = 0; control_head_data_width = 0;
mic_jb_reset();
QuiskPrintf("total packets sent = %i, recd = %i\n", packets_sent, packets_recd); QuiskPrintf("total packets sent = %i, recd = %i\n", packets_sent, packets_recd);
+3 -22
View File
@@ -190,20 +190,10 @@ class Remot: # Remote comtrol base class
return return
try: # Read any data from the socket try: # Read any data from the socket
text = self.remote_ctl_connection.recv(1024) text = self.remote_ctl_connection.recv(1024)
# Thanks to Karl Heinz - K5KHK except:
except (BlockingIOError, InterruptedError, socket.timeout): # No data available now #traceback.print_exc()
return
except OSError: # For example ConnectionResetError: the Control Head is gone
print('Closing Remote Control connection: recv() failed')
self.RemoteCtlClose(False)
return return
else: # We got some characters 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): if not isinstance(text, str):
text = text.decode('utf-8') text = text.decode('utf-8')
self.received += text self.received += text
@@ -385,22 +375,13 @@ class Remot: # Remote comtrol base class
self.cw_next_keydown = None self.cw_next_keydown = None
def StopTransmit(self): def StopTransmit(self):
# PTT code added thanks to Karl Heinz - K5KHK # TODO: Add code for modes other than CW
# 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): while len(self.cw_event_queue):
self.cw_event_queue.popleft() self.cw_event_queue.popleft()
self.cw_next_event_ts = None self.cw_next_event_ts = None
self.cw_next_keydown = None self.cw_next_keydown = None
self.cw_key_down = 0 self.cw_key_down = 0
QS.set_remote_cwkey(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): def ProcessRadioBtn(self, command, cmd_text):
# Large and Small format screens send different button events for radio buttons. # Large and Small format screens send different button events for radio buttons.
+1 -2
View File
@@ -2302,8 +2302,7 @@ class ControlMixin:
if name in ('hot_key_ptt_toggle', 'hot_key_ptt_if_hidden', 'keyupDelay', 'cwTone', 'pulse_audio_verbose_output', 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', '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', '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) setattr(conf, name, x)
application.ImmediateChange(name) application.ImmediateChange(name)
elif name[0:4] in ('lin_', 'win_'): elif name[0:4] in ('lin_', 'win_'):
+1
View File
@@ -0,0 +1 @@
#
+418
View File
@@ -0,0 +1,418 @@
# 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 is_vox = 0; // Is the VOX level exceeded?
static int vox_level = CLIP16; // VOX trigger level as a number 0 to CLIP16 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 timeVOX = 2000; // VOX hang time in milliseconds
static int tx_sample_rate = 48000; // Used for SoapySDR static int tx_sample_rate = 48000; // Used for SoapySDR and Tx samples from Python
static int reverse_tx_sideband; static int reverse_tx_sideband;
static int PsEnable = 0; static int PsEnable = 0;
static int PsCal = 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 ; // hermes handles this itself
else if (quisk_play_state == SOFTWARE_CWKEY) else if (quisk_play_state == SOFTWARE_CWKEY)
serial_key_samples(cSamples, count); serial_key_samples(cSamples, count);
if(quisk_pt_sample_write) { // Used for SoapySDR if(quisk_pt_sample_write && key_down) { // Used for SoapySDR and samples from Python
// Interpolate the mic samples to the Tx sample rate // Interpolate the mic samples to the Tx sample rate
//printf("Tx sample rate %i\n", tx_sample_rate); //printf("Tx sample rate %i\n", tx_sample_rate);
switch (tx_sample_rate) { switch (tx_sample_rate) {
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "quisk" name = "quisk"
version = "4.2.55" version = "4.2.54"
requires-python = ">=3.13" requires-python = ">=3.13"
description = 'QUISK is a Software Defined Radio (SDR) transceiver that can control various radio hardware.' description = 'QUISK is a Software Defined Radio (SDR) transceiver that can control various radio hardware.'
license = "GPL-2.0-or-later" license = "GPL-2.0-or-later"
+100 -3
View File
@@ -115,6 +115,8 @@ int quiskTxHoldState; // hold Tx until the repeater frequency shift is complet
int quisk_is_vna; // zero for normal program, one for the VNA program 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_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_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_bytes;
static int py_bscope_endian; static int py_bscope_endian;
static int quisk_auto_notch; // auto notch control static int quisk_auto_notch; // auto notch control
@@ -159,6 +161,8 @@ 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 complex double PySampleBuf[SAMP_BUFFER_SIZE]; // buffer for samples returned from Python
static int PySampleCount; // count of samples in buffer 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 bool quisk_tx_inhibit;
static int multirx_data_width; // width of graph data to return static int multirx_data_width; // width of graph data to return
@@ -3018,6 +3022,17 @@ static PyObject * add_bscope_samples(PyObject * self, PyObject * args)
return Py_None; 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) static void py_sample_start(void)
{ {
} }
@@ -3040,6 +3055,83 @@ static int py_sample_read(complex double * cSamples)
return n; 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) static PyObject * get_params(PyObject * self, PyObject * args)
{ {
const char * name; const char * name;
@@ -3093,18 +3185,22 @@ static PyObject * set_params(PyObject * self, PyObject * args, PyObject * keywds
{ /* Call with keyword arguments ONLY; change local parameters */ { /* Call with keyword arguments ONLY; change local parameters */
static char * kwlist[] = {"quisk_is_vna", "rx_bytes", "rx_endian", "read_error", "clip", static char * kwlist[] = {"quisk_is_vna", "rx_bytes", "rx_endian", "read_error", "clip",
"bscope_bytes", "bscope_endian", "bscope_size", "bandscopeScale", "hermes_pause", "bscope_bytes", "bscope_endian", "bscope_size", "bandscopeScale", "hermes_pause",
"tx_bytes", "tx_endian",
NULL} ; NULL} ;
int i, nbytes, read_error, clip, bscope_size, hermes_pause; int i, nbytes, read_error, clip, bscope_size, hermes_pause;
nbytes = read_error = clip = bscope_size = hermes_pause = -1; nbytes = read_error = clip = bscope_size = hermes_pause = -1;
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiiiiiidii", kwlist, if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiiiiiidiii", kwlist,
&quisk_is_vna, &nbytes, &py_sample_rx_endian, &read_error, &clip, &quisk_is_vna, &nbytes, &py_sample_rx_endian, &read_error, &clip,
&py_bscope_bytes, &py_bscope_endian, &bscope_size, &bandscopeScale, &hermes_pause)) &py_bscope_bytes, &py_bscope_endian, &bscope_size, &bandscopeScale, &hermes_pause,
&py_sample_tx_bytes, &py_sample_tx_endian))
return NULL; return NULL;
if (nbytes != -1) { if (nbytes != -1) {
py_sample_rx_bytes = nbytes; py_sample_rx_bytes = nbytes;
quisk_sample_source4(py_sample_start, py_sample_stop, py_sample_read, NULL); quisk_sample_source(py_sample_start, py_sample_stop, py_sample_read);
} }
if (py_sample_tx_bytes)
quisk_pt_sample_write = py_sample_tx_save;
if (read_error != -1) if (read_error != -1)
quisk_sound_state.read_error++; quisk_sound_state.read_error++;
if (clip != -1) if (clip != -1)
@@ -6090,6 +6186,7 @@ static PyMethodDef QuiskMethods[] = {
{"get_filter_rate", get_filter_rate, METH_VARARGS, "Return the sample rate used for the filters."}, {"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_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_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."}, {"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_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."}, {"measure_audio", measure_audio, METH_VARARGS, "Set the method, return the measured audio voltage."},
+19 -31
View File
@@ -65,13 +65,15 @@ else:
Q3StringTypes = (str, unicode) Q3StringTypes = (str, unicode)
class StdOutput(wx.Frame): class StdOutput(wx.Frame):
def __init__(self, app): def __init__(self, app): # Called from the GUI thread
self.app = app self.app = app
self.ctrl = None self.ctrl = None
self.old_stdout = sys.stdout self.old_stdout = sys.stdout
self.old_stderr = sys.stderr self.old_stderr = sys.stderr
self.old_excepthook = sys.excepthook self.old_excepthook = sys.excepthook
self.path = os.path.join(app.QuiskFilesDir, "quisk_logfile.txt") self.path = os.path.join(app.QuiskFilesDir, "quisk_logfile.txt")
self.gui_thread = threading.get_native_id()
self.text_buffer = ''
try: try:
size = os.path.getsize(self.path) size = os.path.getsize(self.path)
except: except:
@@ -116,6 +118,9 @@ class StdOutput(wx.Frame):
self.fp.write(text) self.fp.write(text)
self.fp.write("\n") self.fp.write("\n")
def write(self, text): 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: if self.fp:
self.fp.write(text) self.fp.write(text)
if self.old_stdout: if self.old_stdout:
@@ -124,6 +129,10 @@ class StdOutput(wx.Frame):
self.ctrl.write(text) self.ctrl.write(text)
def flush(self): def flush(self):
pass 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): def ExceptHook(self, typ, value, traceb):
self.Show(True) self.Show(True)
self.Raise() self.Raise()
@@ -742,7 +751,6 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
self.vfo = "Main" self.vfo = "Main"
self.split_mode = 0 self.split_mode = 0
self.split_vfo = 'VFO' self.split_vfo = 'VFO'
self.lock_mode = 0
h = self.Handlers = {} h = self.Handlers = {}
h[''] = self.ErrProtocol h[''] = self.ErrProtocol
h['dump_state'] = self.DumpState h['dump_state'] = self.DumpState
@@ -766,8 +774,6 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
h['set_split_freq'] = self.SetSplitFreq h['set_split_freq'] = self.SetSplitFreq
h['get_split_vfo'] = self.GetSplitVfo h['get_split_vfo'] = self.GetSplitVfo
h['set_split_vfo'] = self.SetSplitVfo h['set_split_vfo'] = self.SetSplitVfo
h['get_lock_mode'] = self.GetLockMode
h['set_lock_mode'] = self.SetLockMode
self.MakeDumpState() self.MakeDumpState()
def MakeDumpState(self): def MakeDumpState(self):
dump_state = [] dump_state = []
@@ -1012,8 +1018,7 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
self.ErrParam() self.ErrParam()
else: else:
freq = int(freq + 0.5) freq = int(freq + 0.5)
# 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)
self.app.ChangeRxTxFrequency(freq, None, keep_mode=True)
def GetSplitFreq(self): # The Tx Frequency def GetSplitFreq(self): # The Tx Frequency
self.Reply('TX Frequency', self.app.txFreq + self.app.VFO, 0) self.Reply('TX Frequency', self.app.txFreq + self.app.VFO, 0)
def SetSplitFreq(self): # The Tx Frequency def SetSplitFreq(self): # The Tx Frequency
@@ -1026,7 +1031,7 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
else: else:
freq = int(freq + 0.5) freq = int(freq + 0.5)
# This always controls the Tx frequency # This always controls the Tx frequency
self.app.ChangeRxTxFrequency(None, freq, keep_mode=True) self.app.ChangeRxTxFrequency(None, freq)
def GetSplitVfo(self): def GetSplitVfo(self):
self.Reply('Split', self.split_mode, 'TX VFO', self.split_vfo, 0) self.Reply('Split', self.split_mode, 'TX VFO', self.split_vfo, 0)
def SetSplitVfo(self): def SetSplitVfo(self):
@@ -1111,22 +1116,6 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
Lab = label Lab = label
diff = df diff = df
self.app.filterButns.SetLabel(Lab, True) 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): def GetVfo(self):
self.Reply('VFO', self.vfo, 0) self.Reply('VFO', self.vfo, 0)
def SetVfo(self): def SetVfo(self):
@@ -1522,9 +1511,7 @@ class SoundThread(threading.Thread):
#print ("Quisk thread %12.3f" % ((tm - self.time0) * 1E3)) #print ("Quisk thread %12.3f" % ((tm - self.time0) * 1E3))
#self.time0 = tm #self.time0 = tm
if self.samples_from_python: if self.samples_from_python:
samples = Hardware.GetRxSamples() Hardware.GetRxSamples()
if samples:
QS.set_params(rx_samples=samples)
if self.poll_cw: if self.poll_cw:
Hardware.PollCwKey() Hardware.PollCwKey()
if conf.midi_cwkey_device: if conf.midi_cwkey_device:
@@ -4714,6 +4701,7 @@ class App(wx.App):
msg = QS.GetQuiskPrintf() msg = QS.GetQuiskPrintf()
if msg: if msg:
print(msg, end='') print(msg, end='')
self.std_out_err.PrintBuffer()
QS.close_key() QS.close_key()
if QS.tci_get_params("tci_clients_count") > 0: if QS.tci_get_params("tci_clients_count") > 0:
QS.tci_set_params(close=1) QS.tci_set_params(close=1)
@@ -4747,6 +4735,7 @@ class App(wx.App):
msg = QS.GetQuiskPrintf() msg = QS.GetQuiskPrintf()
if msg: if msg:
print(msg, end='') print(msg, end='')
self.std_out_err.PrintBuffer()
def OnBtnOnOff(self, event): def OnBtnOnOff(self, event):
if event.GetEventObject().GetValue(): # Start samples if event.GetEventObject().GetValue(): # Start samples
try: try:
@@ -6156,7 +6145,7 @@ class App(wx.App):
self.freqDisplay.Display(self.txFreq + self.VFO) self.freqDisplay.Display(self.txFreq + self.VFO)
self.fldigi_new_freq = self.txFreq + self.VFO self.fldigi_new_freq = self.txFreq + self.VFO
return change return change
def ChangeRxTxFrequency(self, rx_freq=None, tx_freq=None, keep_mode=False): def ChangeRxTxFrequency(self, rx_freq=None, tx_freq=None):
if not self.split_rxtx and not tx_freq: if not self.split_rxtx and not tx_freq:
tx_freq = rx_freq tx_freq = rx_freq
if tx_freq: if tx_freq:
@@ -6167,7 +6156,7 @@ class App(wx.App):
else: # Change the VFO else: # Change the VFO
vfo = (tx_freq // 5000) * 5000 - 5000 vfo = (tx_freq // 5000) * 5000 - 5000
tune = tx_freq - vfo tune = tx_freq - vfo
self.BandFromFreq(tx_freq, keep_mode) self.BandFromFreq(tx_freq)
self.ChangeHwFrequency(tune, vfo, 'FreqEntry') self.ChangeHwFrequency(tune, vfo, 'FreqEntry')
if rx_freq and self.split_rxtx: # Frequency must be on-screen if rx_freq and self.split_rxtx: # Frequency must be on-screen
tune = rx_freq - self.VFO tune = rx_freq - self.VFO
@@ -6357,8 +6346,7 @@ class App(wx.App):
self.pttButton.Enable(False) self.pttButton.Enable(False)
else: else:
self.pttButton.Enable(True) self.pttButton.Enable(True)
def BandFromFreq(self, frequency, keep_mode=False): # Change to a new band based on the frequency def BandFromFreq(self, frequency): # 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: if self.screen == self.bandscope_screen:
return return
try: try:
@@ -6380,7 +6368,6 @@ class App(wx.App):
vfo, tune, mode = self.bandState[band] vfo, tune, mode = self.bandState[band]
except KeyError: except KeyError:
vfo, tune, mode = (0, 0, 'LSB') vfo, tune, mode = (0, 0, 'LSB')
if not keep_mode:
self.modeButns.SetLabel(mode, True) self.modeButns.SetLabel(mode, True)
self.ChangeBand(band) self.ChangeBand(band)
break break
@@ -6916,6 +6903,7 @@ class App(wx.App):
msg = QS.GetQuiskPrintf() msg = QS.GetQuiskPrintf()
if msg: if msg:
print(msg, end='') print(msg, end='')
self.std_out_err.PrintBuffer()
if self.screen == self.config_screen: if self.screen == self.config_screen:
self.screen.OnGraphData() # Send message to draw new data self.screen.OnGraphData() # Send message to draw new data
if self.add_version and Hardware.GetFirmwareVersion() is not None: if self.add_version and Hardware.GetFirmwareVersion() is not None:
-13
View File
@@ -1508,19 +1508,6 @@ remote_radio_ip = ""
# It is only necessary to enter it once on each computer. # It is only necessary to enter it once on each computer.
remote_radio_password = "" 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 ## k4_tcp_ip IP address for K4 TCP, text
# This is the Quisk IP address for the TCP server implementing K4 commands. # This is the Quisk IP address for the TCP server implementing K4 commands.
k4_tcp_ip = "" k4_tcp_ip = ""
+11 -2
View File
@@ -118,17 +118,24 @@ class Hardware:
# length is the number of samples in each block of ADC samples, and equals the FFT size. # 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) QS.set_params(bscope_bytes=int_size, bscope_endian=endian, bscope_size=length)
self.application.bandscope_clock = clock 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. #def PollCwKey(self): # Optional. Called frequently by the sound thread to check the CW key status.
# pass # Do not define if not needed. # pass # Do not define if not needed.
#def PollGuiControl(self): # Optional. Called frequently by the GUI thread to change GUI settings (PTT etc.) #def PollGuiControl(self): # Optional. Called frequently by the GUI thread to change GUI settings (PTT etc.)
# pass # Do not define if not needed. # pass # Do not define if not needed.
def StartSamples(self): # Quisk calls this from the sound thread to start sending samples. def StartSamples(self): # Quisk calls this from the sound thread to start samples.
# If you return a string, it replaces the string returned from hardware open() # If you return a string, it replaces the string returned from hardware open()
pass pass
def StopSamples(self): # Quisk calls this from the sound thread to stop sending samples. def StopSamples(self): # Quisk calls this from the sound thread to stop samples.
pass pass
def GetRxSamples(self): # Quisk calls this frequently from the sound thread. Poll your hardware for samples. 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. # 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 pass
def AddRxSamples(self, samples): # Call this from within GetRxSamples() to record the Rx samples. 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. # "samples" is int_size of integer I data followed by int_size of integer Q data, repeated.
@@ -142,6 +149,8 @@ class Hardware:
# For Python 2, "samples" must be a byte array or bytes or a string. # 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. # The number of bytes in "samples" must equal the block length times the bytes per sample.
QS.add_bscope_samples(samples) 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. def GotClip(self): # Call this to indicate that samples were received with the clip (overrange) indicator true.
QS.set_params(clip=1) QS.set_params(clip=1)
def GotReadError(self, print_msg, msg): # Call this to indicate an error in receiving the Rx samples. def GotReadError(self, print_msg, msg): # Call this to indicate an error in receiving the Rx samples.