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
22 changed files with 590 additions and 784 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.
+2 -2
View File
@@ -128,10 +128,10 @@ static PyObject * open_rx_udp(const char * ip, int port)
shutdown(rx_udp_socket, QUISK_SHUT_BOTH); shutdown(rx_udp_socket, QUISK_SHUT_BOTH);
close(rx_udp_socket); close(rx_udp_socket);
rx_udp_socket = INVALID_SOCKET; rx_udp_socket = INVALID_SOCKET;
snprintf(buf, sizeof(buf), "Failed to connect to UDP %s port %u", ip, port); sprintf(buf, "Failed to connect to UDP %s port %u", ip, port);
} }
else { else {
snprintf(buf, sizeof(buf), "Capture from UDP %s port %u", ip, port); sprintf(buf, "Capture from UDP %s port %u", ip, port);
#if DEBUG_IO #if DEBUG_IO
if (getsockopt(rx_udp_socket, SOL_SOCKET, SO_RCVBUF, (char *)&intbuf, &bufsize) == 0) if (getsockopt(rx_udp_socket, SOL_SOCKET, SO_RCVBUF, (char *)&intbuf, &bufsize) == 0)
{ {
+12 -2
View File
@@ -727,6 +727,17 @@ class Configuration:
count -= 1 count -= 1
return count return count
class xxConfigHelp(wx.html.HtmlWindow): # The "Help with Radios" first-level page
"""Create the help screen for the configuration tabs."""
def __init__(self, parent):
wx.html.HtmlWindow.__init__(self, parent, -1, size=(win_width, 100))
if "gtk2" in wx.PlatformInfo:
self.SetStandardFonts()
self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22])
self.SetBackgroundColour(parent.bg_color)
# read in text from file help_conf.html in the directory of this module
self.LoadFile('help_conf.html')
class QPowerMeterCalibration(wx.Frame): class QPowerMeterCalibration(wx.Frame):
"""Create a window to enter the power output and corresponding ADC value AIN1/2""" """Create a window to enter the power output and corresponding ADC value AIN1/2"""
def __init__(self, parent, local_names): def __init__(self, parent, local_names):
@@ -2291,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
@@ -307,7 +307,6 @@ These are part of your operating system. You only install them once unless it ch
</p> </p>
<br> <br>
<pre> <pre>
sudo apt-get install gcc
sudo apt-get install libfftw3-dev sudo apt-get install libfftw3-dev
sudo apt-get install libasound2-dev sudo apt-get install libasound2-dev
sudo apt-get install portaudio19-dev sudo apt-get install portaudio19-dev
+1 -1
View File
@@ -56,7 +56,7 @@ $(OBJDIR):
@mkdir -p $(OBJDIR) @mkdir -p $(OBJDIR)
# Link the shared library object # Link the shared library object
$(TARGET): $(OBJS) ac2yd/remote.o $(TARGET): $(OBJS)
$(CC) -o $@ $(OBJS) ac2yd/remote.o $(LDFLAGS) $(CC) -o $@ $(OBJS) ac2yd/remote.o $(LDFLAGS)
# Define the location for dependency files # Define the location for dependency files
+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"
+103 -12
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)
@@ -4830,7 +4926,7 @@ static PyObject * set_multirx_mode(PyObject * self, PyObject * args)
if (!PyArg_ParseTuple (args, "ii", &index, &mode)) if (!PyArg_ParseTuple (args, "ii", &index, &mode))
return NULL; return NULL;
if (index >= 0 && index < QUISK_MAX_SUB_RECEIVERS) if (index < QUISK_MAX_SUB_RECEIVERS)
multirx_mode[index] = mode; multirx_mode[index] = mode;
Py_INCREF (Py_None); Py_INCREF (Py_None);
return Py_None; return Py_None;
@@ -4842,7 +4938,7 @@ static PyObject * set_multirx_freq(PyObject * self, PyObject * args)
if (!PyArg_ParseTuple (args, "ii", &index, &freq)) if (!PyArg_ParseTuple (args, "ii", &index, &freq))
return NULL; return NULL;
if (index >= 0 && index < QUISK_MAX_SUB_RECEIVERS) if (index < QUISK_MAX_SUB_RECEIVERS)
multirx_freq[index] = freq; multirx_freq[index] = freq;
Py_INCREF (Py_None); Py_INCREF (Py_None);
return Py_None; return Py_None;
@@ -4860,7 +4956,7 @@ static PyObject * set_multirx_play_channel(PyObject * self, PyObject * args)
{ {
if (!PyArg_ParseTuple (args, "i", &multirx_play_channel)) if (!PyArg_ParseTuple (args, "i", &multirx_play_channel))
return NULL; return NULL;
if (multirx_play_channel < 0 || multirx_play_channel >= QUISK_MAX_SUB_RECEIVERS) if (multirx_play_channel >= QUISK_MAX_SUB_RECEIVERS)
multirx_play_channel = -1; multirx_play_channel = -1;
Py_INCREF (Py_None); Py_INCREF (Py_None);
return Py_None; return Py_None;
@@ -5638,10 +5734,6 @@ static void measure_freq(complex double * cSamples, int nSamples, int srate)
ipeak = i; ipeak = i;
} }
} }
if (ipeak < 1)
ipeak = 1;
else if (ipeak > fft_size - 2)
ipeak = fft_size - 2;
c3 = 1.36 * (fft_average[ipeak+1] - fft_average[ipeak - 1]) / (fft_average[ipeak-1] + fft_average[ipeak] + fft_average[ipeak+1]); c3 = 1.36 * (fft_average[ipeak+1] - fft_average[ipeak - 1]) / (fft_average[ipeak-1] + fft_average[ipeak] + fft_average[ipeak+1]);
freq = srate * (2 * (ipeak + c3) - fft_size) / 2 / fft_size; freq = srate * (2 * (ipeak + c3) - fft_size) / 2 / fft_size;
freq += rx_tune_freq; freq += rx_tune_freq;
@@ -6010,8 +6102,6 @@ static PyObject * record_app(PyObject * self, PyObject * args)
} }
// Initialize plan for multirx FFT // Initialize plan for multirx FFT
multirx_fft_width = multirx_data_width * MULTIRX_FFT_MULT; // Use larger FFT than graph size multirx_fft_width = multirx_data_width * MULTIRX_FFT_MULT; // Use larger FFT than graph size
if (multirx_fft_next_samples)
free(multirx_fft_next_samples);
multirx_fft_next_samples = (fftw_complex *)malloc(multirx_fft_width * sizeof(fftw_complex)); multirx_fft_next_samples = (fftw_complex *)malloc(multirx_fft_width * sizeof(fftw_complex));
multirx_fft_next_plan = quisk_create_or_cache_fftw_plan_dft_1d(multirx_fft_width, multirx_fft_next_samples, multirx_fft_next_samples, FFTW_FORWARD, FFTW_MEASURE); multirx_fft_next_plan = quisk_create_or_cache_fftw_plan_dft_1d(multirx_fft_width, multirx_fft_next_samples, multirx_fft_next_samples, FFTW_FORWARD, FFTW_MEASURE);
if (current_graph) if (current_graph)
@@ -6096,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."},
+23 -36
View File
@@ -39,7 +39,6 @@ from filters import Filters
import dxcluster import dxcluster
import configure import configure
import quisk_conf_defaults as conf import quisk_conf_defaults as conf
import quisk_utils
import quisk_wdsp import quisk_wdsp
for name in configure.name2format: # Add defaults. These may be overwritten by a user config file. for name in configure.name2format: # Add defaults. These may be overwritten by a user config file.
@@ -66,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:
@@ -117,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:
@@ -125,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()
@@ -743,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
@@ -767,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 = []
@@ -1013,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
@@ -1027,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):
@@ -1112,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):
@@ -1523,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:
@@ -2585,7 +2571,7 @@ class GraphScreen(wx.Window):
else: else:
self.mouse_is_rx = False self.mouse_is_rx = False
if self.mouse_is_rx: if self.mouse_is_rx:
freq = application.rxFreq + self.VFO + quisk_utils.wheel_step(event.GetWheelRotation(), event.GetWheelDelta(), wm) freq = application.rxFreq + self.VFO + wm * event.GetWheelRotation() // event.GetWheelDelta()
if conf.freq_spacing: if conf.freq_spacing:
freq = self.FreqRound(freq, 0) freq = self.FreqRound(freq, 0)
elif freq >= 0: elif freq >= 0:
@@ -2595,7 +2581,7 @@ class GraphScreen(wx.Window):
tune = freq - self.VFO tune = freq - self.VFO
self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseWheel', event=event, rx_freq=tune) self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseWheel', event=event, rx_freq=tune)
else: else:
freq = self.txFreq + self.VFO + quisk_utils.wheel_step(event.GetWheelRotation(), event.GetWheelDelta(), wm) freq = self.txFreq + self.VFO + wm * event.GetWheelRotation() // event.GetWheelDelta()
if conf.freq_spacing: if conf.freq_spacing:
freq = self.FreqRound(freq, 0) freq = self.FreqRound(freq, 0)
elif freq >= 0: elif freq >= 0:
@@ -3663,8 +3649,8 @@ class HelpScreen(wx.html.HtmlWindow):
if "gtk2" in wx.PlatformInfo: if "gtk2" in wx.PlatformInfo:
self.SetStandardFonts() self.SetStandardFonts()
self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22]) self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22])
if not self.LoadFile('help.html'): # read in text from file help.html in the directory of this module
self.LoadFile('/usr/share/doc/quisk/help.html') self.LoadFile('help.html')
def OnGraphData(self, data): def OnGraphData(self, data):
pass pass
def ChangeYscale(self, y_scale): def ChangeYscale(self, y_scale):
@@ -4715,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)
@@ -4748,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:
@@ -6157,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:
@@ -6168,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
@@ -6358,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:
@@ -6381,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
@@ -6917,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:
+1 -30
View File
@@ -172,22 +172,6 @@ tx_level = {}
# This is the maximum value of the slider. # This is the maximum value of the slider.
digital_tx_level = 100 digital_tx_level = 100
################ Receivers SuperPuper SDR, by UT3MK and US5NCJ
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'ut3mkpkg/quisk_hardware.py'
## widgets_file_name Widget file path, rfile
# This optional file adds additional controls for the radio.
#widgets_file_name = ''
## sdr1000_serial_port Serial port, text
# This is serial Port for the synthesizer, usually /dev/ttyUSBn on Linux or COMn on Win
sdr1000_serial_port = "/dev/ttyUSB0"
## sdr1000_serial_rate Serial baud rate, integer
# This is baud rate for the synthesizer.
sdr1000_serial_rate = 57600
################ Receivers HiQSDR, The original N2ADR and HiQSDR radios. ################ Receivers HiQSDR, The original N2ADR and HiQSDR radios.
@@ -441,7 +425,7 @@ hermes_PTT_hang_time = 4
# This option only applies to the Hermes Lite 2. Set this to None if you don't have a tuner. # This option only applies to the Hermes Lite 2. Set this to None if you don't have a tuner.
# Set this to "Tune" to control the Icom AH-4 compatible ATU attached to the Hermes Lite 2 end plate. # Set this to "Tune" to control the Icom AH-4 compatible ATU attached to the Hermes Lite 2 end plate.
# Note that this option only applies to the HL2 end plate, not the IO board. # Note that this option only applies to the HL2 end plate, not the IO board.
# A tuner attached to the IO board is controlled from a button on the last line of the screen. # A tuner attached to the IO board is controlled from a button on the last line of the screen.
# Then when the Spot button is pressed with a positive power, a tune request is sent to the ATU. # Then when the Spot button is pressed with a positive power, a tune request is sent to the ATU.
# If the Spot button is pressed with a zero power level, the tuner is set to bypass mode. # If the Spot button is pressed with a zero power level, the tuner is set to bypass mode.
# Changes are immediate (no need to restart). # Changes are immediate (no need to restart).
@@ -1524,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.
-23
View File
@@ -1,29 +1,6 @@
from __future__ import print_function from __future__ import print_function
from __future__ import division from __future__ import division
def wheel_step(rotation, delta, step):
"""Convert signed wheel rotation into configured frequency steps.
A measured downward event reported rotation=-180 with delta=120. Scaling
that by a 50 Hz step gives -75 Hz; flooring the resulting absolute frequency
made downward tuning jump -100 Hz while upward tuning moved only +50 Hz.
Round the wheel units symmetrically first: +/-180 now gives +/-50 Hz, while
+/-240 still gives +/-100 Hz rather than being collapsed to one step.
"""
if delta == 0:
return 0
rotation = int(rotation)
delta = int(delta)
step = int(step)
direction = -1 if rotation * delta < 0 else 1
whole_steps, remainder = divmod(abs(rotation), abs(delta))
if remainder * 2 > abs(delta):
whole_steps += 1
return direction * whole_steps * step
class SplineInterpolator: # From Numerical Recipes in C class SplineInterpolator: # From Numerical Recipes in C
"""Interpolate a table of [x, y] values.""" """Interpolate a table of [x, y] values."""
def __init__(self, table, Xmin=None, Xmax=None): def __init__(self, table, Xmin=None, Xmax=None):
+3 -4
View File
@@ -26,7 +26,6 @@ import threading, webbrowser
import _quisk as QS import _quisk as QS
from quisk_widgets import * from quisk_widgets import *
import configure import configure
import quisk_utils
DEBUG = 0 DEBUG = 0
@@ -631,7 +630,7 @@ class GraphScreen(wx.Window):
mouse_x, mouse_y = self.GetMousePosition(event) mouse_x, mouse_y = self.GetMousePosition(event)
self.SetTxFreq(index=mouse_x - self.originX) self.SetTxFreq(index=mouse_x - self.originX)
def OnWheel(self, event): def OnWheel(self, event):
tune = self.display.tune_tx + quisk_utils.wheel_step(event.GetWheelRotation(), event.GetWheelDelta(), 1) tune = self.display.tune_tx + event.GetWheelRotation() // event.GetWheelDelta()
self.SetTxFreq(index=tune) self.SetTxFreq(index=tune)
class HelpScreen(wx.html.HtmlWindow): class HelpScreen(wx.html.HtmlWindow):
@@ -641,8 +640,8 @@ class HelpScreen(wx.html.HtmlWindow):
if "gtk2" in wx.PlatformInfo: if "gtk2" in wx.PlatformInfo:
self.SetStandardFonts() self.SetStandardFonts()
self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22]) self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22])
if not self.LoadFile('help_vna.html'): # read in text from file help.html in the directory of this module
self.LoadFile('/usr/share/doc/quisk/help_vna.html') self.LoadFile('help_vna.html')
def OnLinkClicked(self, link): def OnLinkClicked(self, link):
webbrowser.open(link.GetHref(), new=2) webbrowser.open(link.GetHref(), new=2)
-2
View File
@@ -1,2 +0,0 @@
This directory has files for the SDR-1000 clone synthesizer by
UT3MK and US5NCJ
-523
View File
@@ -1,523 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import serial, time, logging
import re
import _quisk as QS
import quisk_conf_defaults as conf
import sys, wx, traceback
# ── ПОСЛЕДОВАТЕЛЬНЫЙ ПОРТ ДЛЯ CAT ───────────────────────────────────
sdr1000_serial_port = "/dev/ttyUSB0" # Windows: "COM3" / Linux: "/dev/ttyUSB0"
sdr1000_serial_rate = 57600 # PowerSDR CAT: до 115200
# ── ЛОГ ────────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-5s %(message)s",
datefmt="%H:%M:%S",
)
log = logging.getLogger("SDR1000")
Mode2Index = {'CWL':0, 'CWU':1, 'LSB':2, 'USB':3, 'AM':4,
'FM':5, 'EXT':6, 'DGT-U':7, 'DGT-L':8, 'DGT-IQ':9,
'IMD':10, 'FDV-U':11, 'FDV-L':12, 'DGT-FM':13}
# ── КЛАСС УПРАВЛЕНИЯ ЖЕЛЕЗОМ ────────────────────────────────────────
from quisk_hardware_model import Hardware as BaseHardware
class Hardware(BaseHardware):
"""
Quisk Hardware-класс для FlexRadio SDR-1000.
Использует CAT-команды протокола PowerSDR (ZZ + Kenwood).
"""
# Буфер для приема данных с com-порта
serialbuf = '';
# Буфер для отправки команд на com-порт
cmdbuf = '';
# Буфер отправки IF
statusbuf ='';
statussending = False;
# Сопоставление размера шагов
STEP_MAP = {
'1': '0000', '10': '0001', '50': '1000', '100': '0010',
'250': '1001', '500': '1010', '1k': '0011', '5k': '1011',
'9k': '1100', '10k': '0100', '100k':'0101', '1M': '0110',
'10M': '0111',
}
STEP_VAL = {
'1': 1,
'10': 10,
'50': 50,
'100': 100,
'250': 250,
'500': 500,
'1k': 1000,
'5k': 5000,
'9k': 9000,
'10k': 10000,
'100k': 100000,
'1M': 1000000,
'10M': 10000000,
}
# STEP_ENA = ['1', '10', '50', '100', '250', '500', '1k', '5k', '10k', '100k' ]
STEP_ENA = [ '10', '50', '100', '250', '500', '1k', '5k', '10k', '100k' ]
BAND_ENA = ['80', '40', '30', '20', '17', '15', '12', '10']
watchdog = 0
watchdog_limit = 100
# Сопоставление режимов Quisk → код MD (PowerSDR/Kenwood)
# TODO
MODE_MAP = {
'LSB': '1', 'USB': '2', 'CWU': '3', 'FM': '4',
'AM': '5', 'DGT-L': '6', 'CWL': '7',
# Quisk-специфичные → стандартные
'DGT-U':'2', 'FMN': '4', 'AMN': '5',
'DGT-FM': '4', 'FDV-U':'2', 'FDV-L':'1',
'FSK' : '9', 'FSKR' : '2',
}
def __init__(self, app, conf):
BaseHardware.__init__(self, app, conf)
self.ser = None
self.current_freq = 0
self.current_vfo = 0
self.current_mode = 'USB'
self.current_band = '80'
self.current_step = '50'
self.rf_gain_labels = ('RF -20', 'RF -10', 'RF 0', 'RF +10' )
self.current_pre = 0
self.current_ritfreq = 0
self.rit_active = False
self.ptt_active = False
# ── Открытие/закрытие ────────────────────────────────────────────
def open(self):
"""Вызывается один раз при старте Quisk."""
try:
self.ser = serial.Serial(
sdr1000_serial_port,
sdr1000_serial_rate,
timeout=0, xonxoff=True,
)
log.debug("SDR-1000: COM-порт открыт %s @ %d",
sdr1000_serial_port, sdr1000_serial_rate)
except serial.SerialException as e:
log.error("SDR-1000: не удалось открыть порт: %s", e)
self.ser = None
return "SDR-1000: ошибка порта %s" % sdr1000_serial_port
msg = "SDR-1000 подключен (%s, %s)" % (
sdr1000_serial_port, "PowerSDR")
return msg
def close(self):
"""Вызывается один раз при завершении Quisk."""
if self.ptt_active:
self.ser.cmdbuf += 'RX;' # снять передачу
if self.ser and self.ser.is_open:
self.cmdbuf = ''
self.ser.flush
self.ser.close()
log.debug("SDR-1000: порт закрыт")
# ── Низкоуровневый CAT-обмен ────────────────────────────────────
def _cat(self):
"""
Вызываться будет периодически для чтения порта
"""
if not self.ser or not self.ser.is_open:
return ""
try:
#self.ser.write(frame)
if self.ser.in_waiting > 0:
sdata = self.ser.read(size=self.ser.in_waiting)
self.serialbuf += sdata.decode('ascii')
else:
self.watchdog += 1
if self.watchdog > self.watchdog_limit:
log.debug("Watchdog! Resetting arduino")
self.ser.dtr = 0
time.sleep(0.1)
self.ser.dtr = 1
self.ser.flush
self.ser.close()
time.sleep(0.1)
try:
self.ser = serial.Serial(
sdr1000_serial_port,
sdr1000_serial_rate,
timeout=0, xonxoff=True,
)
log.debug("SDR-1000: COM-порт открыт %s @ %d",
sdr1000_serial_port, sdr1000_serial_rate)
except serial.SerialException as e:
log.error("SDR-1000: не удалось открыть порт: %s", e)
self.ser = None
return "SDR-1000: ошибка порта %s" % sdr1000_serial_port
self.ser.flushInput()
self.ser.flushOutput()
self.watchdog = 0
ans = ''
while ';' in self.serialbuf:
cmds = self.serialbuf.partition(';')
cmd = cmds[0]
self.serialbuf = cmds[-1]
# log.info("Splitted to %s and %s", cmd, self.serialbuf)
if len(cmd) > 0:
answer = ''
cmddone = 0
match cmd:
case 'IF':
# Запрос полного статуса
# Возврат в statusbuf
self.statusbuf = self.ReturnInfo()
log.debug("Status refresh, buf: %s", self.serialbuf)
cmddone = 1
case 'ZZSM0':
# Запрос показаний S-метра
ans = self.ReturnSmeterA()
log.debug("S-meter refresh, buf: %s", ans)
cmddone = 1
case 'PS1':
# Включение
ans = 'PS1;'
log.debug("PowerON, buf: %s", ans)
cmddone = 1
case 'ZZPA':
# Запрос показаний PRE ! TODO смена режима
ans = self.ReturnPre()
log.debug("PRE answer, buf: %s", ans)
cmddone = 1
case 'UP':
# Запрос на увеличение частоты
self.application.ChangeHwFrequency(9000,
self.current_vfo+self.STEP_VAL[self.current_step])
cmddone = 1
case 'DN':
# Запрос на уменьшение частоты
self.application.ChangeHwFrequency(9000,
self.current_vfo-self.STEP_VAL[self.current_step])
cmddone = 1
case 'ZZSU':
# Запрос на увеличение шага
self.StepInc()
cmddone = 1
case 'ZZSD':
# Запрос на уменьшение шага
self.StepDec()
cmddone = 1
case 'BU':
# Запрос смены диапазона
self.BandUp()
cmddone = 1
case 'BD':
# Запрос смены диапазона
self.BandDn()
cmddone = 1
case 'TX':
# Transmit
QS.set_PTT(1)
self.ptt_active = True
log.info("PTT ON")
cmddone = 1
case 'RX':
# Recieve
QS.set_PTT(0)
self.ptt_active = False
log.info("PTT OFF")
cmddone = 1
case 'ZZQR':
# Запрос восстановление QuickSave
cmddone = 1
if len(cmd) > 4 and not cmddone:
log.debug("Try to match cmd: %s, buf: %s", cmd[0:4], self.serialbuf)
match cmd[0:4]:
case 'ZZPA':
# Запрос изменения PRE
self.SetPre(cmd)
ans = self.ReturnPre()
log.debug("PRE set, buf: %s", ans)
cmddone = 1
case 'ZZMD':
# Запрос изменения режима
self.SetMode(cmd)
log.debug("Mode set, buf: %s", cmd)
cmddone = 1
case 'FA00':
# Запрос изменения частоты
self.SetFreq(cmd)
log.debug("Freq set, buf: %s", cmd)
cmddone = 1
if not cmddone:
log.info("unknown cmd: %s, buf: %s", cmd, self.serialbuf)
ans = '?;'
if len(ans) > 0:
log.debug("Sending answer, buf: %s", ans)
self.ser.write(ans.encode('ascii'))
if len(self.serialbuf) == 0:
if len(self.cmdbuf) > 0:
cmds = self.cmdbuf.partition(';')
cmd = cmds[0]
self.cmdbuf = cmds[-1]
log.debug("Sending cmd %s, buf: %s", cmd, self.cmdbuf)
self.ser.write(cmd.encode('ascii'))
if (len(self.statusbuf) > 0) and not self.statussending:
self.statussending = True
log.debug("Sending status, buf: %s", self.statusbuf)
self.ser.write(self.statusbuf.encode('ascii'))
self.statusbuf = ''
self.statussending = False
# End of CAT
except Exception as e:
log.error("SDR-1000 CAT error: %s", e)
return ""
@staticmethod
def _fmt_freq(hz):
"""Частота → 11 цифр: 14200000 → '00014200000'."""
return "%011d" % int(hz)
# ── Quisk API: частота ──────────────────────────────────────────
def ChangeFrequency(self, tune, vfo, source='', band='', event=None):
"""Quisk вызывает при смене частоты. Возвращает (tune, vfo)."""
if tune and tune != self.current_freq:
self.current_freq = tune
self.current_vfo = tune-9000
if re.search('FA\\d{11};', self.cmdbuf,0):
log.debug("--> RE MATCH")
re.sub('FA\\d{11};', "FA" + self._fmt_freq(tune)+";", self.cmdbuf, 1)
# self.cmdbuf += ("FA" + self._fmt_freq(tune)+";")
else:
self.cmdbuf += ("FA" + self._fmt_freq(tune)+";")
log.debug("VFO A → %d Hz (%.6f MHz)", self.current_vfo, self.current_freq / 1e6)
return self.current_freq, self.current_vfo
def ReturnInfo(self):
ritstr = self.application.ritFreq
if ritstr >= 0:
ritsign = "+"
else:
ritsign = ""
answer = "IF%011d%4s%s%05d%01d0000%01d%1s0000000;" % (
self.current_freq, self.STEP_MAP[self.current_step],
ritsign,ritstr,self.application.ritButton.GetValue(),
self.ptt_active,self.MODE_MAP[self.current_mode])
# log.info(answer)
return answer
def ReturnSmeterA(self):
i = round((self.application.smeter_db + 67) * 2)
if i < 0:
i = 0
elif i > 260:
i = 260
answer = "ZZSM0%03d;" % ( i )
# log.info(answer)
return answer
def ReturnPre(self):
answer = "ZZPA%01d;" % ( self.current_pre )
return answer
def SetPre(self, precmd):
if len(precmd) == 5:
match precmd:
case 'ZZPA0': i = 0
case 'ZZPA1': i = 1
case 'ZZPA2': i = 2
case 'ZZPA3': i = 3
case _: i = 0
self.current_pre = i
def SetFreq(self, freqcmd):
if len(freqcmd) == 13:
newfreq = int(freqcmd[2:])
log.info("Request to change freq: %d", newfreq)
self.current_vfo=newfreq-9000
self.application.ChangeHwFrequency(9000, self.current_vfo)
def OnButtonRfGain(self, event):
btn = event.GetEventObject()
n = btn.index
self.current_pre = n
self.cmdbuf += ("ZZPA%1d;" % self.current_pre)
def SetMode(self, modecmd):
if len(modecmd) == 6:
log.info("Request to change mode: %s", modecmd[4:])
match modecmd[4:]:
# 00 = LSB => LSB
# 01 = USB => USB
# 02 = DSB => USB
# 03 = CWL => CW
# 04 = CWU => CW
# 05 = FMN => FMN
# 06 = AM => AM
# 07 = DIGU => FSKR
# 08 = SPEC => AMN
# 09 = DIGL => FSK
# 10 = SAM => USB
# 11 = DRM => USB
case '00': newmode = 'LSB'
case '01': newmode = 'USB'
case '02': newmode = 'USB'
case '03': newmode = 'CWL'
case '04': newmode = 'CWU'
case '05': newmode = 'FM'
case '06': newmode = 'AM'
case '07': newmode = 'DGT-U'
case '08': newmode = 'AM'
case '09': newmode = 'DGT-L'
case '10': newmode = 'USB'
case '11': newmode = 'USB'
case _: newmode = 'USB'
self.current_mode = newmode
self.application.mode = self.current_mode
self.application.MakeFilterButtons(self.application.Mode2Filters(self.current_mode))
QS.set_rx_mode(Mode2Index.get(self.current_mode, 3))
if self.current_mode == 'CWL':
self.application.SetRit(conf.cwTone)
elif self.current_mode == 'CWU':
self.application.SetRit(-conf.cwTone)
else:
self.application.SetRit(0)
if self.current_mode in ('CWL', 'CWU'):
self.application.SetFilterByMode('CW')
elif self.current_mode in ('LSB', 'USB'):
self.application.SetFilterByMode('SSB')
elif self.current_mode == 'AM':
self.application.SetFilterByMode('AM')
elif self.current_mode[0:2] == 'FM':
self.application.SetFilterByMode('FM')
elif self.current_mode[0:4] == 'DGT-':
self.application.SetFilterByMode('DGT')
self.application.sliderSquelch.DeleteSliderWindow()
if self.current_mode == 'FM':
self.application.sliderSquelch.SetSlider(self.application.levelSquelch)
else:
self.application.sliderSquelch.SetSlider(self.application.levelSquelchSSB)
self.application.OnBtnSquelch()
if self.current_mode not in ('FDV-L', 'FDV-U'):
self.application.graph.SetDisplayMsg()
self.application.waterfall.SetDisplayMsg()
self.application.SetTxAudio()
def StepInc(self):
i = self.STEP_ENA.index(self.current_step)
log.debug("Found index of %s, idx: %d", self.current_step, i)
if i < (len(self.STEP_ENA)-1):
self.current_step = self.STEP_ENA[i + 1]
log.debug("Step changed to %s, idx: %d", self.current_step, i+1)
else:
log.debug("Step not changed, remain %s", self.current_step)
def StepDec(self):
i = self.STEP_ENA.index(self.current_step)
log.info("Found index of %s, idx: %d", self.current_step, i)
if i > 0:
self.current_step = self.STEP_ENA[i - 1]
log.debug("Step changed to %s, idx: %d", self.current_step, i-1)
else:
log.debug("Step not changed, remain %s", self.current_step)
def BandUp(self):
i = self.BAND_ENA.index(self.current_band)
log.debug("Found index of %s, idx: %d", self.current_band, i)
if i < (len(self.BAND_ENA)-1):
self.current_band = self.BAND_ENA[i + 1]
else:
self.current_band = self.BAND_ENA[0]
log.info("Band changed to %s, idx: %d", self.current_band, i)
vfo, tune, mode = self.application.bandState[self.current_band]
self.application.ChangeHwFrequency(9000, vfo+tune)
def BandDn(self):
i = self.BAND_ENA.index(self.current_band)
log.debug("Found index of %s, idx: %d", self.current_band, i)
if i > 0:
self.current_band = self.BAND_ENA[i - 1]
else:
self.current_band = self.BAND_ENA[len(self.BAND_ENA)-1]
log.info("Band changed to %s, idx: %d", self.current_band, i)
vfo, tune, mode = self.application.bandState[self.current_band]
self.application.ChangeHwFrequency(9000, vfo+tune)
def ReturnFrequency(self):
"""Опрос ~10 Гц. Возвращает (tune, vfo) или (None, None)."""
# SDR-1000 не сообщает частоту сам, поэтому возвращаем None
return None, None
# ── Quisk API: режим ─────────────────────────────────────────────
def ChangeMode(self, mode):
"""Quisk вызывает при смене режима. mode: 'USB', 'CW', и т.д."""
# Нормализуем DGT-* → USB
# if mode.startswith('DGT'):
# mode = 'USB'
code = self.MODE_MAP.get(mode)
if not code:
log.warning("Неизвестный режим Quisk: %s → по умолчанию USB", mode)
code = '2'
self.cmdbuf += ("MD" + code+';')
self.current_mode = mode
log.debug("Режим → %s (MD%s)", mode, code)
# ── Quisk API: диапазон ─────────────────────────────────────────
def ChangeBand(self, band):
"""Quisk вызывает при смене диапазона. band: '160', '80', …"""
BaseHardware.ChangeBand(self, band)
log.info("Диапазон → %s м", band)
# ── Quisk API: PTT ──────────────────────────────────────────────
def OnButtonPTT(self, event=None):
"""Quisk вызывает при нажатии/отпускании PTT."""
if event:
self.ptt_active = bool(event.GetEventObject().GetValue())
#self._cat("ZZTX%d" % (1 if self.ptt_active else 0))
log.debug("PTT %s", "ON" if self.ptt_active else "OFF")
# ── Quisk API: HeartBeat (~10 Гц) ───────────────────────────────
def HeartBeat(self):
"""Периодический опрос — можно использовать для синхронизации."""
self._cat()
def FastHeartBeat(self): # Called frequently by the GUI thread
pass
# ── Quisk API: TUNE ─────────────────────────────────────────────
def OnBtnFDX(self, is_fdx):
"""Кнопка FDX (полный дуплекс)."""
log.debug("FDX → %s", is_fdx)