13 Commits
Author SHA1 Message Date
James C. Ahlstrom c2085a8160 Remove wheel tester. 2026-10-27 17:05:25 -04:00
James C. Ahlstrom 2b6b6fa9ea Merge branch 'master' of github.com:jimahlstrom/quisk
Merge mouse wheel logic.
2026-10-27 16:58:05 -04:00
James C. Ahlstrom 36210d4e4c Request by Jaroslav, OK2JRQ: alternate docs directory. 2026-10-27 16:56:09 -04:00
jimahlstrom eb9434c52a Merge pull request #4 from prof7bit/fix-wheel-steps
Fix symmetric mouse wheel tuning steps. Thanks to Bernd, DD4UG.
2026-10-27 16:49:51 -04:00
Bernd db928b6fcb Fix symmetric mouse wheel tuning steps 2026-10-24 13:45:22 +02:00
jimahlstrom a90f8b80bd Merge pull request #3 from argusSecurityBot/fix-bounds-checks-and-leak
Fix out-of-bounds accesses and a leak in quisk.c and afedrinet_io.c
2026-10-22 16:13:49 -04:00
argusSecurityBot faaeda54e5 Fix out-of-bounds accesses and a leak in quisk.c and afedrinet_io.c
- afedrinet_io.c open_rx_udp: use snprintf so a long ip string cannot
  overflow the 128-byte stack buffer.
- quisk.c set_multirx_mode, set_multirx_freq: reject a negative index.
- quisk.c set_multirx_play_channel: treat a negative channel like an
  out-of-range one.
- quisk.c measure_freq: keep ipeak inside [1, fft_size - 2] so that
  fft_average[ipeak - 1] and fft_average[ipeak + 1] stay in bounds when
  no peak is found.
- quisk.c record_app: free the previous multirx_fft_next_samples buffer
  before allocating a new one, like fft_window and current_graph.
2026-10-20 16:46:24 +00:00
James C. Ahlstrom c2304b23ee Minor correction to UT3MK radio 2026-10-19 13:43:22 -04:00
jimahlstrom 09154658ae Merge pull request #2 from zxalexis/master
Added UT3MK SDR radios
2026-10-19 13:22:35 -04:00
James C. Ahlstrom 20bd313041 Correction to makefile 2026-10-19 13:00:09 -04:00
zx d39722b170 Added UT3MK SDR radios 2026-10-18 22:02:37 +03:00
James C. Ahlstrom c97ffa155e Quisk version 4.2.55 2026-10-12 13:37:47 -04:00
jimahlstrom 745882a506 Add README for Quisk Software Defined Radio
Added introductory information about Quisk software defined radio, including its features and support links.
2026-09-27 16:19:11 -04:00
18 changed files with 774 additions and 45 deletions
+6
View File
@@ -1,3 +1,9 @@
Quisk Version 4.2.55 October 2026
==================================
Karl Heinz, K5KHK, provided a patche to fix the AC2YD remote control head. He provided another patch
to fix Hamlib control when using WSJT-X improved. He added a mic jitter buffer to the remote control
head so it works over WiFi. Thanks Karl!
Quisk Version 4.2.54 August 2026 Quisk 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
@@ -0,0 +1,12 @@
# Quisk Software Defined Radio
Quisk is a software defined radio transceiver that runs on a PC or on a single board computer such as a Raspberry Pi.
It is written in Python and C.
It works with various kinds of radio hardware such as SoftRock, Hermes Lite 2,
the SDR-IQ by RfSpace, and with network UDP samples.
Quisk is easily extensible by writing your own hardware file in Python.
I wrote Quisk back in 2008, and I have been maintaining it ever since.
For questions and support, please visit the group https://groups.io/g/n2adr-sdr.
For more information see https://james.ahlstrom.name/quisk.
+1 -1
View File
@@ -1,2 +1,2 @@
#Quisk version 4.2.54 #Quisk version 4.2.55
from .quisk import main from .quisk import main
+101 -8
View File
@@ -53,6 +53,41 @@ static int control_head_data_width; // app.data_width of control head for gr
static int packets_sent; static int packets_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)
{ {
@@ -117,9 +152,9 @@ int read_remote_radio_sound_socket(complex double * cSamples)
// Receive stereo 16-bit pcm microphone samples at the remote radio via UDP // 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, bytes, nInt16, nSamples; int i, j, bytes, nInt16, nSamples, nFrames;
int16_t buf[UDP_SEND_INT16]; int16_t buf[UDP_SEND_INT16];
double samp_r, samp_l; double now, due;
struct timeval tm_wait; struct timeval tm_wait;
fd_set fds; fd_set fds;
static struct quisk_cHB45Filter HalfBand; static struct quisk_cHB45Filter HalfBand;
@@ -134,8 +169,8 @@ int read_remote_mic_sound_socket(complex double * cSamples)
quisk_filt_cInit(&cFiltInterp3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double)); quisk_filt_cInit(&cFiltInterp3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double));
} }
// read all available packets, one per loop // read all available packets into the jitter buffer, one per loop
nSamples = 0; now = QuiskTimeSec();
while (1) { while (1) {
tm_wait.tv_sec = 0; tm_wait.tv_sec = 0;
tm_wait.tv_usec = 0; tm_wait.tv_usec = 0;
@@ -152,15 +187,71 @@ 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 < nInt16; i += 2) { for (i = 0; i + 1 < nInt16; i += 2) {
samp_r = buf[i]; if (mic_jb_count >= MIC_JB_SIZE) { // full: drop the oldest frame
samp_l = buf[i + 1]; mic_jb_read = (mic_jb_read + 1) % MIC_JB_SIZE;
cSamples[nSamples++] = (samp_r + I * samp_l) / CLIP16 * CLIP32; mic_jb_count--;
}
j = (mic_jb_read + mic_jb_count) % MIC_JB_SIZE * 2;
mic_jb[j] = buf[i];
mic_jb[j + 1] = buf[i + 1];
mic_jb_count++;
} }
} }
} // while(1) } // 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);
@@ -716,6 +807,7 @@ 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;
} }
@@ -742,6 +834,7 @@ 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);
+22 -3
View File
@@ -190,10 +190,20 @@ 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)
except: # Thanks to Karl Heinz - K5KHK
#traceback.print_exc() except (BlockingIOError, InterruptedError, socket.timeout): # No data available now
return
except OSError: # For example ConnectionResetError: the Control Head is gone
print('Closing Remote Control connection: recv() failed')
self.RemoteCtlClose(False)
return 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
@@ -375,13 +385,22 @@ class Remot: # Remote comtrol base class
self.cw_next_keydown = None self.cw_next_keydown = None
def StopTransmit(self): def StopTransmit(self):
# TODO: Add code for modes other than CW # PTT code added thanks to Karl Heinz - K5KHK
# Called whenever the remote session ends (RemoteCtlClose). Release everything the
# Control Head may have keyed: the CW key and, for voice and digital modes, PTT.
# Without the PTT release, a Control Head that disappears while transmitting (a crash,
# a lost network, a dead battery) leaves the transmitter keyed until someone unkeys it
# at the radio.
while len(self.cw_event_queue): 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;
sprintf(buf, "Failed to connect to UDP %s port %u", ip, port); snprintf(buf, sizeof(buf), "Failed to connect to UDP %s port %u", ip, port);
} }
else { else {
sprintf(buf, "Capture from UDP %s port %u", ip, port); snprintf(buf, sizeof(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)
{ {
+2 -12
View File
@@ -727,17 +727,6 @@ 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):
@@ -2302,7 +2291,8 @@ 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,6 +307,7 @@ 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) $(TARGET): $(OBJS) ac2yd/remote.o
$(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
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "quisk" name = "quisk"
version = "4.2.54" version = "4.2.55"
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"
+9 -3
View File
@@ -4830,7 +4830,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 < QUISK_MAX_SUB_RECEIVERS) if (index >= 0 && 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 +4842,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 < QUISK_MAX_SUB_RECEIVERS) if (index >= 0 && 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 +4860,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 >= QUISK_MAX_SUB_RECEIVERS) if (multirx_play_channel < 0 || 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,6 +5638,10 @@ 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;
@@ -6006,6 +6010,8 @@ 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)
+33 -10
View File
@@ -39,6 +39,7 @@ 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.
@@ -742,6 +743,7 @@ 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
@@ -765,6 +767,8 @@ 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 = []
@@ -1009,7 +1013,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
self.ErrParam() self.ErrParam()
else: else:
freq = int(freq + 0.5) freq = int(freq + 0.5)
self.app.ChangeRxTxFrequency(freq, None) # Like a real radio, keep the mode. Hamlib caches the mode, so the client would not notice a mode change.
self.app.ChangeRxTxFrequency(freq, None, keep_mode=True)
def GetSplitFreq(self): # The Tx Frequency 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
@@ -1022,7 +1027,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) self.app.ChangeRxTxFrequency(None, freq, keep_mode=True)
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):
@@ -1107,6 +1112,22 @@ 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):
@@ -2564,7 +2585,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 + wm * event.GetWheelRotation() // event.GetWheelDelta() freq = application.rxFreq + self.VFO + quisk_utils.wheel_step(event.GetWheelRotation(), event.GetWheelDelta(), wm)
if conf.freq_spacing: if conf.freq_spacing:
freq = self.FreqRound(freq, 0) freq = self.FreqRound(freq, 0)
elif freq >= 0: elif freq >= 0:
@@ -2574,7 +2595,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 + wm * event.GetWheelRotation() // event.GetWheelDelta() freq = self.txFreq + self.VFO + quisk_utils.wheel_step(event.GetWheelRotation(), event.GetWheelDelta(), wm)
if conf.freq_spacing: if conf.freq_spacing:
freq = self.FreqRound(freq, 0) freq = self.FreqRound(freq, 0)
elif freq >= 0: elif freq >= 0:
@@ -3642,8 +3663,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])
# read in text from file help.html in the directory of this module if not self.LoadFile('help.html'):
self.LoadFile('help.html') self.LoadFile('/usr/share/doc/quisk/help.html')
def OnGraphData(self, data): def OnGraphData(self, data):
pass pass
def ChangeYscale(self, y_scale): def ChangeYscale(self, y_scale):
@@ -6136,7 +6157,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): def ChangeRxTxFrequency(self, rx_freq=None, tx_freq=None, keep_mode=False):
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:
@@ -6147,7 +6168,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) self.BandFromFreq(tx_freq, keep_mode)
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
@@ -6337,7 +6358,8 @@ 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): # Change to a new band based on the frequency def BandFromFreq(self, frequency, keep_mode=False): # Change to a new band based on the frequency
# If keep_mode is True, do not change to the mode saved for the new band.
if self.screen == self.bandscope_screen: if self.screen == self.bandscope_screen:
return return
try: try:
@@ -6359,7 +6381,8 @@ 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')
self.modeButns.SetLabel(mode, True) if not keep_mode:
self.modeButns.SetLabel(mode, True)
self.ChangeBand(band) self.ChangeBand(band)
break break
def ChangeBand(self, band): def ChangeBand(self, band):
+30 -1
View File
@@ -172,6 +172,22 @@ 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.
@@ -425,7 +441,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).
@@ -1508,6 +1524,19 @@ 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 = ""
+23
View File
@@ -1,6 +1,29 @@
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):
+4 -3
View File
@@ -26,6 +26,7 @@ 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
@@ -630,7 +631,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 + event.GetWheelRotation() // event.GetWheelDelta() tune = self.display.tune_tx + quisk_utils.wheel_step(event.GetWheelRotation(), event.GetWheelDelta(), 1)
self.SetTxFreq(index=tune) self.SetTxFreq(index=tune)
class HelpScreen(wx.html.HtmlWindow): class HelpScreen(wx.html.HtmlWindow):
@@ -640,8 +641,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])
# read in text from file help.html in the directory of this module if not self.LoadFile('help_vna.html'):
self.LoadFile('help_vna.html') self.LoadFile('/usr/share/doc/quisk/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
@@ -0,0 +1,2 @@
This directory has files for the SDR-1000 clone synthesizer by
UT3MK and US5NCJ
+1
View File
@@ -0,0 +1 @@
#
+523
View File
@@ -0,0 +1,523 @@
# -*- 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)