16 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
James C. Ahlstrom 674bbad1ab fix non-ascii char in setup.py 2026-08-20 14:08:21 -04:00
James C. Ahlstrom 67c6fbcad9 Quisk version 4.2.54 2026-08-04 12:01:45 -04:00
James C. Ahlstrom 84e194c644 Quisk version 4.2.53 2026-06-30 17:06:52 -04:00
52 changed files with 2316 additions and 251 deletions
+24
View File
@@ -1,3 +1,27 @@
Quisk Version 4.2.55 October 2026
==================================
Karl Heinz, K5KHK, provided a patche to fix the AC2YD remote control head. He provided another patch
to fix Hamlib control when using WSJT-X improved. He added a mic jitter buffer to the remote control
head so it works over WiFi. Thanks Karl!
Quisk Version 4.2.54 August 2026
=================================
Quisk can now accept audio samples for Tx from TCI at rates 48000, 24000, 12000 and 8000. Please test.
Quisk now includes program icons in the directory "quisk/icons". The Windows icon is quisk/icons/quisk.ico.
On Windows, right-click the desktop icon and change the icon to <path-to-quisk>/quisk/icons/quisk.ico.
The Linux icon is quisk/icons/quisk.svg. On Linux, change the quisk.desktop file to
Icon=<path-to-quisk>/quisk/icons/quisk.svg.
Quisk Version 4.2.53 June 2026
===============================
I added TCI support for Software Defined Connectors CW skimmer. Please test.
This version of Quisk now works on Python 3.14 and higher. I changed to a new build system because
the old one has been deprecated by Python. They no longer support running setup.py.
The Linux version has a new makefile. The Windows version is installed by pip as usual.
Please test.
Quisk Version 4.2.52 May 2026
==============================
This is a fix for various TCI bugs. Please test with WSJT-X and let me know if all problems are solved.
+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.52
#Quisk version 4.2.55
from .quisk import main
+102 -8
View File
@@ -9,6 +9,7 @@
#include <Python.h>
#include <complex.h>
#include <math.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include <errno.h>
@@ -52,6 +53,41 @@ static int control_head_data_width; // app.data_width of control head for gr
static int packets_sent;
static int packets_recd;
///// The jitter buffer is thanks to Karl Heinz - K5KHK
// Jitter buffer for the microphone samples received from the control head. The network can hold
// packets for 100 msec or more (Wi-Fi power saving and scanning) and then deliver them in a burst,
// but the Hermes Tx buffer only has 50 msec of slack. Without a buffer here, each late burst is an
// underflow (a gap on the air) followed by an overflow (discarded samples). So buffer mic_jb_target
// frames before playing, and then release them at a steady MIC_JB_RATE.
// The buffer adds its delay to the transmit audio, so it is off unless the configuration item
// remote_radio_mic_jitter_ms is set. When it is off, samples are passed on as they arrive.
#define MIC_JB_RATE 8000 // frames per second sent by the control head
#define MIC_JB_SIZE 8000 // ring buffer size in stereo frames
#define MIC_JB_MAX_MS 500 // largest configurable delay; MIC_JB_SIZE must hold this plus MIC_JB_EXTRA
#define MIC_JB_EXTRA 2000 // trim back to mic_jb_target above mic_jb_target + this (clock drift): 250 msec
#define MIC_JB_IDLE 0.5 // seconds without packets that end a transmission
static int16_t mic_jb[MIC_JB_SIZE * 2];
static int mic_jb_target; // frames to buffer before playing, from remote_radio_mic_jitter_ms; zero for off
static int mic_jb_read, mic_jb_count, mic_jb_playing, mic_jb_starved;
static int mic_jb_underruns, mic_jb_overruns;
static double mic_jb_time, mic_jb_frac, mic_jb_last_packet;
static void mic_jb_reset(void)
{
int msec;
msec = QuiskGetConfigInt("remote_radio_mic_jitter_ms", 0);
if (msec < 0)
msec = 0;
else if (msec > MIC_JB_MAX_MS)
msec = MIC_JB_MAX_MS;
mic_jb_target = msec * MIC_JB_RATE / 1000;
mic_jb_read = mic_jb_count = mic_jb_playing = mic_jb_starved = 0;
mic_jb_underruns = mic_jb_overruns = 0;
mic_jb_frac = 0;
}
// Receive stereo 16-bit pcm radio speaker sound on the control head via UDP
int read_remote_radio_sound_socket(complex double * cSamples)
{
@@ -116,9 +152,9 @@ int read_remote_radio_sound_socket(complex double * cSamples)
// Receive stereo 16-bit pcm microphone samples at the remote radio via UDP
int read_remote_mic_sound_socket(complex double * cSamples)
{
int i, bytes, nInt16, nSamples;
int i, j, bytes, nInt16, nSamples, nFrames;
int16_t buf[UDP_SEND_INT16];
double samp_r, samp_l;
double now, due;
struct timeval tm_wait;
fd_set fds;
static struct quisk_cHB45Filter HalfBand;
@@ -133,8 +169,8 @@ int read_remote_mic_sound_socket(complex double * cSamples)
quisk_filt_cInit(&cFiltInterp3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double));
}
// read all available packets, one per loop
nSamples = 0;
// read all available packets into the jitter buffer, one per loop
now = QuiskTimeSec();
while (1) {
tm_wait.tv_sec = 0;
tm_wait.tv_usec = 0;
@@ -151,15 +187,71 @@ int read_remote_mic_sound_socket(complex double * cSamples)
break;
}
if (bytes > 0) {
if (mic_jb_starved) { // the buffer ran dry; count it unless the transmission had ended
mic_jb_starved = 0;
if (now - mic_jb_last_packet < MIC_JB_IDLE)
mic_jb_underruns++;
}
mic_jb_last_packet = now;
nInt16 = bytes / 2;
for (i = 0; i < nInt16; i += 2) {
samp_r = buf[i];
samp_l = buf[i + 1];
cSamples[nSamples++] = (samp_r + I * samp_l) / CLIP16 * CLIP32;
for (i = 0; i + 1 < nInt16; i += 2) {
if (mic_jb_count >= MIC_JB_SIZE) { // full: drop the oldest frame
mic_jb_read = (mic_jb_read + 1) % MIC_JB_SIZE;
mic_jb_count--;
}
j = (mic_jb_read + mic_jb_count) % MIC_JB_SIZE * 2;
mic_jb[j] = buf[i];
mic_jb[j + 1] = buf[i + 1];
mic_jb_count++;
}
}
} // while(1)
// release the frames that are due since the last call
nSamples = 0;
if (mic_jb_target == 0) { // the jitter buffer is off: pass on everything received
nFrames = mic_jb_count;
}
else if ( ! mic_jb_playing) {
nFrames = 0;
if (mic_jb_count >= mic_jb_target) {
mic_jb_playing = 1;
mic_jb_time = now;
mic_jb_frac = 0;
}
}
else {
due = (now - mic_jb_time) * MIC_JB_RATE + mic_jb_frac;
if (due < 0) // the clock was set back
due = 0;
else if (due > MIC_JB_RATE / 2) // limit to 0.5 sec, well within SAMP_BUFFER_SIZE after interpolation
due = MIC_JB_RATE / 2;
mic_jb_time = now;
nFrames = (int)due;
mic_jb_frac = due - nFrames;
if (nFrames > mic_jb_count) { // ran dry: play what is left, then buffer up again
nFrames = mic_jb_count;
mic_jb_playing = 0;
mic_jb_starved = 1;
}
}
for (i = 0; i < nFrames; i++) {
j = mic_jb_read * 2;
cSamples[nSamples++] = ((double)mic_jb[j] + I * (double)mic_jb[j + 1]) / CLIP16 * CLIP32;
mic_jb_read = (mic_jb_read + 1) % MIC_JB_SIZE;
}
mic_jb_count -= nFrames;
if (mic_jb_target && mic_jb_count > mic_jb_target + MIC_JB_EXTRA) { // the control head's clock is faster than ours
i = mic_jb_count - mic_jb_target;
mic_jb_read = (mic_jb_read + i) % MIC_JB_SIZE;
mic_jb_count -= i;
mic_jb_overruns++;
}
if ((mic_jb_underruns || mic_jb_overruns) && now - mic_jb_last_packet > MIC_JB_IDLE) {
QuiskPrintf("Remote mic jitter buffer: %d underruns, %d overruns\n", mic_jb_underruns, mic_jb_overruns);
mic_jb_underruns = mic_jb_overruns = 0;
}
nSamples = quisk_cInterpolate(cSamples, nSamples, &cFiltInterp3, 3);
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand);
@@ -715,6 +807,7 @@ PyObject * quisk_start_remote_radio_remote_sound(PyObject * self, PyObject * arg
packets_sent = 0;
packets_recd = 0;
mic_jb_reset();
return Py_None;
}
@@ -741,6 +834,7 @@ PyObject * quisk_stop_remote_radio_remote_sound(PyObject * self, PyObject * args
control_head_graph_socket_started = 0;
remote_radio_graph_socket_started = 0;
control_head_data_width = 0;
mic_jb_reset();
QuiskPrintf("total packets sent = %i, recd = %i\n", packets_sent, packets_recd);
+22 -3
View File
@@ -190,10 +190,20 @@ class Remot: # Remote comtrol base class
return
try: # Read any data from the socket
text = self.remote_ctl_connection.recv(1024)
except:
#traceback.print_exc()
# Thanks to Karl Heinz - K5KHK
except (BlockingIOError, InterruptedError, socket.timeout): # No data available now
return
except OSError: # For example ConnectionResetError: the Control Head is gone
print('Closing Remote Control connection: recv() failed')
self.RemoteCtlClose(False)
return
else: # We got some characters
if not text: # recv() returns nothing only when the Control Head has closed the connection
# Close now rather than waiting for the heartbeat timeout: a Control Head that
# crashed or lost its network may have left the transmitter keyed.
print('Closing Remote Control connection: closed by Control Head')
self.RemoteCtlClose(False)
return
if not isinstance(text, str):
text = text.decode('utf-8')
self.received += text
@@ -375,13 +385,22 @@ class Remot: # Remote comtrol base class
self.cw_next_keydown = None
def StopTransmit(self):
# TODO: Add code for modes other than CW
# PTT code added thanks to Karl Heinz - K5KHK
# Called whenever the remote session ends (RemoteCtlClose). Release everything the
# Control Head may have keyed: the CW key and, for voice and digital modes, PTT.
# Without the PTT release, a Control Head that disappears while transmitting (a crash,
# a lost network, a dead battery) leaves the transmitter keyed until someone unkeys it
# at the radio.
while len(self.cw_event_queue):
self.cw_event_queue.popleft()
self.cw_next_event_ts = None
self.cw_next_keydown = None
self.cw_key_down = 0
QS.set_remote_cwkey(0)
ptt = getattr(self.app, 'pttButton', None)
if ptt and ptt.GetValue():
print('Remote Control connection closed while transmitting: releasing PTT')
ptt.SetValue(0, True) # Same as releasing PTT at the radio; runs OnButtonPTT
def ProcessRadioBtn(self, command, cmd_text):
# Large and Small format screens send different button events for radio buttons.
+6 -5
View File
@@ -3,6 +3,7 @@
#ifdef MS_WINDOWS
#include <Winsock2.h>
#include <windows.h>
#include <string.h>
#else
#include <sys/types.h>
#include <sys/stat.h>
@@ -105,7 +106,7 @@ static PyObject * open_rx_udp(const char * ip, int port)
wVersionRequested = MAKEWORD(2, 2);
if (WSAStartup(wVersionRequested, &wsaData) != 0) {
sprintf(buf, "Failed to initialize Winsock (WSAStartup)");
return PyString_FromString(buf);
return PyUnicode_FromString(buf);
}
#endif
// quisk_using_udp = 1;
@@ -127,10 +128,10 @@ static PyObject * open_rx_udp(const char * ip, int port)
shutdown(rx_udp_socket, QUISK_SHUT_BOTH);
close(rx_udp_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 {
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 (getsockopt(rx_udp_socket, SOL_SOCKET, SO_RCVBUF, (char *)&intbuf, &bufsize) == 0)
{
@@ -145,7 +146,7 @@ static PyObject * open_rx_udp(const char * ip, int port)
else {
sprintf(buf, "Failed to open socket");
}
return PyString_FromString(buf);
return PyUnicode_FromString(buf);
}
static PyObject * close_rx_udp(PyObject * self, PyObject * args)
@@ -343,7 +344,7 @@ static PyObject * open_samples(PyObject * self, PyObject * args)
quisk_sample_source(NULL, NULL, &afedri_read_rx_udp);
//////////////
return open_rx_udp(ip, port); // AFEDRI specific
// return PyString_FromString(buf); // return a string message
// return PyUnicode_FromString(buf); // return a string message
}
// Miscellaneous functions needed by the SDR-IQ; called from the GUI thread as
+21 -4
View File
@@ -1,7 +1,24 @@
MODULE_NAME := afedrinet_io
OBJDIR := ../build/local
afedrinet2:
python2 setup.py build_ext --force --inplace
CFLAGS := $(shell python3 ../makefil.py --cflags)
LDFLAGS := $(shell python3 ../makefil.py --ldflags)
EXTENSION_SUFFIX := $(shell python3 ../makefil.py --suffix)
TARGET := $(MODULE_NAME)$(EXTENSION_SUFFIX)
afedrinet3:
# Default target: Link the shared library object
$(TARGET): afedrinet_io.o $(OBJDIR)/import_quisk_api.o $(OBJDIR)/is_key_down.o
$(CC) -o $@ afedrinet_io.o $(LDFLAGS) $(OBJDIR)/import_quisk_api.o $(OBJDIR)/is_key_down.o
# Compile the C source files into object files
afedrinet_io.o: afedrinet_io.c ../quisk.h
$(CC) -o $@ -c $< $(CFLAGS) -I..
.PHONY: clean
clean:
-rm -f *.o
-rm -f $(MODULE_NAME)*.so
.PHONY: afedrinet-old
afedrinet-old:
python3 setup.py build_ext --force --inplace
+2 -12
View File
@@ -727,17 +727,6 @@ class Configuration:
count -= 1
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):
"""Create a window to enter the power output and corresponding ADC value AIN1/2"""
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',
'start_cw_delay', 'start_ssb_delay', 'maximum_tx_secs', 'quisk_serial_cts', 'quisk_serial_dsr',
'hot_key_ptt1', 'hot_key_ptt2', 'midi_ptt_toggle', 'TxRxSilenceMsec',
'fixed_tune_offset', 'calibrate_temp_20', 'calibrate_temp_40', 'calibrate_current_0', 'calibrate_current_1'):
'fixed_tune_offset', 'calibrate_temp_20', 'calibrate_temp_40', 'calibrate_current_0', 'calibrate_current_1',
'remote_radio_mic_jitter_ms'):
setattr(conf, name, x)
application.ImmediateChange(name)
elif name[0:4] in ('lin_', 'win_'):
+8 -4
View File
@@ -84,7 +84,7 @@ h3 {font-size:1.6rem;}
<div class="contents">
<h1>
<a id="Welcome"></a>
Welcome to QUISK (September 2025)
Welcome to QUISK (June 2026)
</h1>
<p>
@@ -197,7 +197,7 @@ Quisk is free open source software written in Python and C. It is hosted on the
<br>
<p>
If you have Python 3 installed on your computer, just continue to use that version.
<b>On Windows Quisk requires Python 3.8, 3.9, 3.10, 3.11, 3.12 or 3.13.</b>
On Windows Quisk requires Python 3.8 or higher.
If you want to install a more recent version of Python, uninstall the old version first.
Running multiple versions of Python is possible but can be confusing and is not necessary.
<b>A Quisk installation is needed for each version of Python you have. If you upgrade to a newer Python
@@ -208,8 +208,7 @@ you must re-install Quisk. But your Quisk settings will be preserved.</b>
<p>
Windows does not include Python, so you must first install a recent version of 64-bit Python 3.
If you already have 64-bit Python 3 installed, just keep using it.
<b>On Windows Quisk requires Python 3.8, 3.9, 3.10, 3.11, 3.12 or 3.13.</b>
There may be newer versions of Python, but until support is added, use the versions listed above.
On Windows Quisk requires Python 3.8 or higher.
Python is available from <a href="http://www.python.org/">http://www.python.org</a>.
There is an option to <b>Add Python to PATH</b>. You MUST select
this option so that you can start python by just typing "python" instead of the whole path
@@ -256,6 +255,8 @@ and then name the shortcut, perhaps "Quisk".
If you check the properties you will see that Windows expanded the location and start directory to the full path.
If you are curious, Quisk and its Python source files are installed in "<b>your_install_directory</b>\Lib\site-packages\quisk".
If you change to that directory you can run Quisk with "pythonw quisk.py".
Quisk has its icon file in quisk\icons. Right-click your desktop shortcut and change the Icon to
"<b>your_install_directory</b>\Lib\site-packages\quisk\icons\quisk.ico".
</p>
<br>
<p>
@@ -306,6 +307,7 @@ These are part of your operating system. You only install them once unless it ch
</p>
<br>
<pre>
sudo apt-get install gcc
sudo apt-get install libfftw3-dev
sudo apt-get install libasound2-dev
sudo apt-get install portaudio19-dev
@@ -383,12 +385,14 @@ You only clone github for the initial installation. To update Quisk, use pull fo
<pre>
cd ~/quisk
git pull
make clean
make
</pre>
<br>
<p>
You run Quisk by running the file quisk.py with python3. For example, "python3 /home/jim/quisk/quisk.py".
You can create a desktop icon to do that, but the method depends on your version of Linux.
The quisk icon is the file quisk/icons/quisk.svg.
<b>Never run Quisk with root privelege by using sudo. Fix the permission problem instead.</b>
To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios.
Add a radio from the list of hardware radios available. Or add the best match.
+1
View File
@@ -1,5 +1,6 @@
#include <Python.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <complex.h>
#include "quisk.h"
+1
View File
@@ -1,6 +1,7 @@
#include <Python.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include <complex.h> // Use native C99 complex type for fftw3
#include "quisk.h"
#include "filter.h"
+4 -4
View File
@@ -414,7 +414,7 @@ double quiskFilt111D4Coefs[196] = { 0.000022860383861326, 0.000070265173417164,
0.000878636067621920, 0.000694724116305488, 0.000487289901409396, 0.000301317969124407, 0.000160689233204503,
0.000070265173417164, 0.000022860383861326} ;
// Sample 53333 Hz, pass 20000, stop 24000, ripple 0.1 dB, atten 100 dB. For SDR-IQ. Stop 0.45000.
// Sample 53333 Hz, pass 20000, stop 24000, ripple 0.1 dB, atten 100 dB. For SDR-IQ. Pass 0.375. Stop 0.45000.
double quiskFilt53D1Coefs[55] = { 0.000318858391362187, 0.001662116369387873, -0.000065237353639775, -0.000854885424783294,
0.001693126335254083, -0.002034310867663472, 0.001354648789538254, 0.000509693525155950, -0.003079059747940221, 0.005242349925004462,
-0.005626507739484119, 0.003235398612833476, 0.001883583294529978, -0.008273775245721798, 0.013282431836060808, -0.013924391581141530,
@@ -426,7 +426,7 @@ double quiskFilt53D1Coefs[55] = { 0.000318858391362187, 0.001662116369387873, -0
-0.003079059747940221, 0.000509693525155950, 0.001354648789538254, -0.002034310867663472, 0.001693126335254083, -0.000854885424783294,
-0.000065237353639775, 0.001662116369387873, 0.000318858391362187 } ;
// Sample 53333 Hz, pass 10000, stop 12000, ripple 0.5 dB, atten 100 dB. For SDR-IQ. Stop 0.22500.
// Sample 53333 Hz, pass 10000, stop 12000, ripple 0.5 dB, atten 100 dB. For SDR-IQ. Pass 0.1875. Stop 0.22500.
double quiskFilt53D2Coefs[93] = { 0.000107966318729964, 0.000421475598772498, 0.000731343669484184, 0.000328611865414609,
-0.001457272471508304, -0.004143803290279123, -0.005736802939367581, -0.004303861070721504, -0.000415676176666607,
0.002561521351129964, 0.001705074918770091, -0.001794491466202784, -0.003398011312785532, -0.000595401366072839,
@@ -603,7 +603,7 @@ double quiskAudio48p6Coefs[71] = { 0.000324677779792651, 0.001038192405843851, 0
0.001132278163758895, 0.002811674671240099, 0.002938055116822665, 0.002082337464742640, 0.001038192405843851,
0.000324677779792651} ;
// Sample 96 kHz, pass 6, stop 40, ripple 0.1 dB, atten 100 dB. Stop 0.41667.
// Sample 96 kHz, pass 6, stop 40, ripple 0.1 dB, atten 100 dB. Pass 0.0625. Stop 0.41667.
double quiskAudio96Coefs[11] = { -0.004756607210954994, -0.022893760473075838, -0.023609616122334920,
0.078161168368381481, 0.277880235815005590, 0.388489826271597950, 0.277880235815005590,
0.078161168368381481, -0.023609616122334920, -0.022893760473075838, -0.004756607210954994} ;
@@ -663,7 +663,7 @@ double quiskAudioFmHpCoefs[309] = { 0.004847574947800705, -0.001211970386842814,
-0.000365311474947399, -0.000434052387522716, -0.000504808183463150, -0.000579117971409598, -0.000658731927598885, -0.000745515693376396,
-0.000841696354699768, -0.000949749961336003, -0.001072277374239886, -0.001211970386842814, 0.004847574947800705} ;
// Sample 24 kHz, pass 3, stop 4, ripple 0.1 dB, atten 100 dB. Stop 0.166667.
// Sample 24 kHz, pass 3, stop 4, ripple 0.1 dB, atten 100 dB. Pass 0.125. Stop 0.166667.
double quiskAudio24p3Coefs[100] = { 0.000040101217607380, 0.000144142945632828, 0.000313272623844654, 0.000472482653992188,
0.000478145332060253, 0.000196098255702949, -0.000370577202968848, -0.000996638165520275, -0.001297816111521122,
-0.000964536466628102, -0.000042557313824444, 0.000962997869327471, 0.001323534221298773, 0.000594056697947640,
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

+221
View File
@@ -0,0 +1,221 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 512 512"
version="1.1"
id="svg5"
sodipodi:docname="quisk.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="1"
inkscape:cx="36.5"
inkscape:cy="265"
inkscape:window-width="1854"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#c0c276;fill-opacity:1;stroke:#7180ff;stroke-width:48.671;stroke-dasharray:none;stroke-opacity:1"
id="rect556"
width="341.84088"
height="349.28082"
x="62.823105"
y="39.435188"
ry="63.709782" />
<path
style="fill:none;stroke:#7180ff;stroke-width:43.2582;stroke-dasharray:none;stroke-opacity:1"
id="path3352"
d="m 193.20147,396.22476 c 6.34642,4.65885 12.2534,9.94281 18.02477,15.29408 7.66447,7.60025 15.04758,15.4729 22.82647,22.96045 6.77582,6.4671 13.60289,12.87487 20.7907,18.88349 12.48185,9.03783 26.6187,15.49278 41.24908,20.07891 13.25326,2.83808 26.93926,1.98796 40.38664,1.44647 9.20443,-0.51765 18.4186,-0.74315 27.63591,-0.83235 5.72468,-0.0231 11.44941,-0.0285 17.17409,-0.009 6.5518,0.45377 13.10409,0.8185 19.62685,1.59571 0,0 -12.83677,-9.54278 -12.83677,-9.54278 v 0 c -6.57664,-0.66042 -13.17806,-1.05429 -19.77266,-1.4853 -5.6652,0.0193 -11.33079,0.0283 -16.99467,0.16838 -9.18212,0.18217 -18.37151,0.31632 -27.53553,0.96497 -13.36116,0.68668 -27.00081,1.48715 -40.0815,-1.91133 -5.00213,-1.62118 -9.82704,-3.39549 -14.63078,-5.53945 -1.63493,-0.7297 -6.26911,-3.36104 -4.89255,-2.21625 2.66168,2.21356 5.85064,3.70365 8.82036,5.48275 1.18824,0.71182 -2.31609,-1.51997 -3.47415,-2.27995 -7.27169,-6.00427 -14.22221,-12.36521 -21.05126,-18.86768 -7.76318,-7.43952 -15.14957,-15.24359 -22.67391,-22.91764 -5.64713,-5.35952 -11.55857,-10.4455 -17.53983,-15.42662 0,0 -15.05126,-5.84659 -15.05126,-5.84659 z" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:17.3033;stroke-dasharray:none;stroke-opacity:1"
d="m 91.391053,213.41065 14.719477,-17.25897 11.13289,28.30763 12.1198,-17.78253 21.1946,-4.54944 18.44448,16.54887 12.64335,-45.07916 6.78807,-41.91379 11.53004,-41.685155 12.62529,26.532375 11.28934,37.31624 2.38907,28.97559 7.74488,35.33638 30.5342,-4.30873 22.19957,-13.63027 4.61563,-22.8555 13.28124,-96.170083 17.96907,92.980663 3.0811,45.22358 24.94369,-9.63446 10.832,-20.40026 14.95416,42.66002"
id="path15623"
sodipodi:nodetypes="cccccccccccccccccccccc" />
<rect
style="fill:#3e4fa1;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="rect16404"
width="17.303272"
height="117.87855"
x="86.571724"
y="246.9482"
inkscape:tile-cx="107.539"
inkscape:tile-cy="302.5"
inkscape:tile-w="16"
inkscape:tile-h="109"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<rect
style="fill:#3e4fa1;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18731"
width="17.303272"
height="117.87855"
x="103.87501"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<rect
style="fill:#3e4fa1;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18733"
width="17.303272"
height="117.87855"
x="121.17827"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<rect
style="fill:#3e4fa1;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18735"
width="17.303272"
height="117.87855"
x="138.48155"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<rect
style="fill:#3e4fa1;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18737"
width="17.303272"
height="117.87855"
x="155.78482"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<rect
style="fill:#dc34c6;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18739"
width="17.303272"
height="117.87855"
x="173.08809"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<rect
style="fill:#fffa41;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18741"
width="17.303272"
height="117.87855"
x="190.39134"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<rect
style="fill:#d20ac7;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18743"
width="17.303272"
height="117.87855"
x="207.69463"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<rect
style="fill:#3e4fa1;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18745"
width="17.303272"
height="117.87855"
x="224.99789"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<use
x="0"
y="0"
inkscape:tiled-clone-of="#rect16404"
xlink:href="#rect16404"
transform="translate(155.72944)"
id="use18747"
style="fill:#3e4fa1;fill-opacity:1" />
<rect
style="fill:#3e4fa1;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18749"
width="17.303272"
height="117.87855"
x="259.60446"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<rect
style="fill:#b931b2;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18751"
width="17.303272"
height="117.87855"
x="276.90775"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<rect
style="fill:#e7ec2e;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18753"
width="17.303272"
height="117.87855"
x="294.21097"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<rect
style="fill:#a735a6;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18755"
width="17.303272"
height="117.87855"
x="311.51425"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<rect
style="fill:#3e4fa1;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18757"
width="17.303272"
height="117.87855"
x="328.81754"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<rect
style="fill:#3e4fa1;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18759"
width="17.303272"
height="117.87855"
x="346.12079"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<rect
style="fill:#3e4fa1;fill-opacity:1;stroke:none;stroke-width:34.8847;stroke-dasharray:none;stroke-opacity:1"
id="use18761"
width="17.303272"
height="117.87855"
x="363.42407"
y="246.9482"
inkscape:tile-x0="99.539001"
inkscape:tile-y0="248" />
<path
id="path12957"
style="fill:none;fill-opacity:1;stroke:#7180ff;stroke-width:48.671;stroke-dasharray:none;stroke-opacity:1"
d="m 404.54932,300.57496 v 23.76192 c 0,35.29519 -28.4146,63.70978 -63.70977,63.70978 H 126.41823 c -35.295185,0 -63.709785,-28.41459 -63.709785,-63.70978 v -23.19942" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

+2
View File
@@ -32,6 +32,8 @@ with this:
*/
#include <Python.h>
#include <string.h>
#include <stdio.h>
void ** Quisk_API; // array of pointers to functions and variables from module _quisk
struct sound_conf * pt_quisk_sound_state; // pointer to quisk_sound_state
+1
View File
@@ -1,5 +1,6 @@
#include <Python.h> // used by quisk.h
#include <complex.h> // Used by quisk.h
#include <string.h>
#include "quisk.h"
// This module provides methods to access the state of the key.
+70
View File
@@ -0,0 +1,70 @@
# This provides helper functions for our makefile.
# Changes for MacOS support thanks to Mario, DL3LSM.
# Changes for building from macports provided by Eric, KM4DSJ
# Updated code for a Mac build contributed by Christoph, DL1YCF, December 2020.
# Converted from setup.py to makefile by Jim Ahlstrom, June 2026
import sys, os, subprocess, sysconfig
if len(sys.argv) < 2:
sys.exit(2)
job = sys.argv[1]
def flags ():
# Fetch compiler flags and link flags from Python
result = subprocess.run(["python3-config", "--cflags"], capture_output=True, text=True, check=True)
cflags = result.stdout.strip()
cflags += " -fPIC"
result = subprocess.run(["python3-config", "--ldflags"], capture_output=True, text=True, check=True)
ldflags = result.stdout.strip()
ldflags += " -shared -lm -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-z,relro -g -fwrapv -O2 -lfftw3"
if sys.platform == "darwin": # Build for Macintosh
cflags += " -DQUISK_HAVE_PORTAUDIO "
ldflags += " -lportaudio "
if os.path.isdir('/opt/local/include'): # MacPorts
base_dir = '/opt/local'
elif os.path.isdir('/usr/local/include'): # HomeBrew on macOS Intel
base_dir = '/usr/local'
elif os.path.isdir('/opt/homebrew/include'): # HomeBrew on Apple Silicon
base_dir = '/opt/homebrew'
else: # Regular build?
base_dir = '/usr'
if os.path.isfile(base_dir + "/include/pulse/pulseaudio.h"):
cflags += " -DQUISK_HAVE_PULSEAUDIO "
ldflags += " -lpulse "
cflags += " -I. -I%s/include " % base_dir
ldflags += " -L. -L%s/lib " % base_dir
elif "freebsd" in sys.platform: #Build for FreeBSD
cflags += " -DQUISK_HAVE_PULSEAUDIO -I. -I/usr/local/include"
ldflags += " -lpulse -L. -L/usr/local/lib"
else: # Linux
cflags += " -DQUISK_HAVE_ALSA -DQUISK_HAVE_PULSEAUDIO"
ldflags += " -lasound -lpulse"
if os.path.isfile("/usr/include/portaudio.h"):
cflags += " -DQUISK_HAVE_PORTAUDIO"
ldflags += " -lportaudio"
return cflags, ldflags
if job == "--soapy":
# See if soapy is available
if os.path.isdir("/usr/include/SoapySDR") or os.path.isdir("/usr/local/include/SoapySDR"):
print('1')
else:
print('0')
elif job == "--suffix":
# Determine the platform-specific extension suffix (e.g., .cpython-311-x86_64-linux-gnu.so)
print(sysconfig.get_config_var('EXT_SUFFIX'))
elif job == "--cflags":
cc, ld = flags()
print(cc)
elif job == "--ldflags":
cc, ld = flags()
print(ld)
else:
sys.exit(1)
+91 -16
View File
@@ -1,18 +1,93 @@
.PHONY: quisk
# This makefile replaces the old Python "setup.py build_ext" which has been deprecated.
# See "quisk-old" below to make Quisk using the old setup.py.
quisk:
MODULE_NAME := _quisk
OBJDIR := build/local
SUBDIRS := afedrinet
ifeq ($(shell python3 makefil.py --soapy), 1)
SUBDIRS := $(SUBDIRS) soapypkg
endif
SRCS := \
base64.c \
extdemod.c \
filter.c \
freedv.c \
handshake.c \
is_key_down.c \
microphone.c \
quisk.c \
quisk_wdsp.c \
sha1.c \
sound.c\
sound_alsa.c \
sound_directx.c \
sound_portaudio.c \
sound_pulseaudio.c \
sound_wasapi.c \
tci.c \
utf8.c \
utility.c \
ws.c
CFLAGS := $(shell python3 makefil.py --cflags)
LDFLAGS := $(shell python3 makefil.py --ldflags)
EXTENSION_SUFFIX := $(shell python3 makefil.py --suffix)
TARGET := $(MODULE_NAME)$(EXTENSION_SUFFIX)
#$(info Info CFLAGS $(CFLAGS))
#$(info Info LDFLAGS $(LDFLAGS))
#$(info Info TARGET $(TARGET))
#$(info Info SUBDIRS $(SUBDIRS))
OBJS := $(patsubst %.c, $(OBJDIR)/%.o, $(SRCS))
.PHONY: all clean test
.PHONY: $(SUBDIRS)
# Default target
all: $(OBJDIR) $(OBJDIR)/import_quisk_api.o ac2yd/remote.o $(TARGET) $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $@
$(OBJDIR):
@mkdir -p $(OBJDIR)
# Link the shared library object
$(TARGET): $(OBJS) ac2yd/remote.o
$(CC) -o $@ $(OBJS) ac2yd/remote.o $(LDFLAGS)
# Define the location for dependency files
DEPS = $(patsubst %.o,$(OBJDIR)/%.d,$(notdir $(OBJS)))
DEPFLAGS = -MMD -MP -MF $(OBJDIR)/$(notdir $*).d
-include $(DEPS)
# Compile the C source files into object files
ac2yd/remote.o: ac2yd/remote.c quisk.h filter.h
$(CC) -o $@ -c $< $(CFLAGS) $(DEPFLAGS)
$(OBJDIR)/import_quisk_api.o: import_quisk_api.c
$(CC) -o $@ -c $< $(CFLAGS) $(DEPFLAGS)
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) -o $@ -c $< $(CFLAGS) $(DEPFLAGS)
clean:
-rm -f $(OBJDIR)/*.o $(OBJDIR)/*.d
-rm -f ac2yd/*.o
-rm -f $(MODULE_NAME)*.so
for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir clean; \
done
# Run a test script to verify that the module can be imported
test: $(TARGET)
@python3 -c "import $(MODULE_NAME); print('Successfully imported:', $(MODULE_NAME).__file__)"
.PHONY: quisk-old
quisk-old:
python3 setup.py build_ext --force --inplace
@echo
quisk3:
python3 setup.py build_ext --force --inplace
@echo
soapy3:
(cd soapypkg; make soapy3)
afedrinet3:
(cd afedrinet; make afedrinet3)
perseus3:
(cd perseuspkg; make perseus3)
+1
View File
@@ -3,6 +3,7 @@
#include <math.h>
#include <sys/time.h>
#include <complex.h>
#include <string.h>
#include <fftw3.h>
#include "quisk.h"
#include <sys/types.h>
+21 -3
View File
@@ -1,7 +1,25 @@
MODULE_NAME := perseus
OBJDIR := ../build/local
perseus2:
python2 setup.py build_ext --force --inplace
CFLAGS := $(shell python3 ../makefil.py --cflags)
LDFLAGS := $(shell python3 ../makefil.py --ldflags)
EXTENSION_SUFFIX := $(shell python3 ../makefil.py --suffix)
TARGET := $(MODULE_NAME)$(EXTENSION_SUFFIX)
perseus3:
# Default target: Link the shared library object
$(TARGET): perseus.o $(OBJDIR)/import_quisk_api.o
$(CC) -o $@ perseus.o $(LDFLAGS) $(OBJDIR)/import_quisk_api.o
# Compile the C source files into object files
perseus.o: perseus.c ../quisk.h ../filter.h
$(CC) -o $@ -c $< $(CFLAGS) -I..
.PHONY: clean
clean:
-rm -f *.o
-rm -f $(TARGET)
.PHONY: perseus-old
perseus-old:
python3 setup.py build_ext --force --inplace
+2 -2
View File
@@ -297,11 +297,11 @@ static PyObject * open_device(PyObject * self, PyObject * args)
main_cleanup:
return PyString_FromString("ERROR");
return PyUnicode_FromString("ERROR");
exit_success:
return PyString_FromString(buf128);
return PyUnicode_FromString(buf128);
}
+47
View File
@@ -0,0 +1,47 @@
[build-system]
requires = ["setuptools>=61.0.0"]
build-backend = "setuptools.build_meta"
[project]
name = "quisk"
version = "4.2.55"
requires-python = ">=3.13"
description = 'QUISK is a Software Defined Radio (SDR) transceiver that can control various radio hardware.'
license = "GPL-2.0-or-later"
license-files = ["license.txt"]
authors = [
{name = "James C. Ahlstrom", email="jahlstr@gmail.com"}
]
keywords = ['quisk', 'sdr', 'software defined radio']
classifiers = [
'Development Status :: 6 - Mature',
'Environment :: X11 Applications',
'Environment :: Win32 (MS Windows)',
'Intended Audience :: End Users/Desktop',
'Natural Language :: English',
'Operating System :: POSIX :: Linux',
'Operating System :: Microsoft :: Windows',
'Programming Language :: Python :: 3',
'Programming Language :: C',
'Topic :: Communications :: Ham Radio',
]
dynamic = ["readme"]
[project.gui-scripts]
quisk = "quisk.quisk:main"
[project.urls]
Homepage = "https://james.ahlstrom.name/quisk/index.html"
Documentation = "https://james.ahlstrom.name/quisk/docs.html"
Repository = "https://github.com/jimahlstrom/quisk"
Changelog = "https://james.ahlstrom.name/quisk/CHANGELOG.txt"
[tool.setuptools.package-data]
"quisk.icons" = ["*"]
[tool.setuptools]
package-dir = {"quisk" = "."}
packages = ["quisk", "quisk.n2adr", "quisk.softrock", "quisk.freedvpkg",
"quisk.hermes", "quisk.hiqsdr", "quisk.afedrinet", "quisk.soapypkg",
"quisk.sdrmicronpkg", "quisk.perseuspkg", "quisk.ac2yd", "quisk.multuspkg"]
+28 -25
View File
@@ -3,6 +3,7 @@
#include <stdlib.h>
#include <math.h>
#include <complex.h> // Use native C99 complex type for fftw3
#include <string.h>
#include <fftw3.h>
#include <sys/types.h>
#include <stdbool.h>
@@ -4027,7 +4028,7 @@ static PyObject * open_rx_udp(PyObject * self, PyObject * args)
wVersionRequested = MAKEWORD(2, 2);
if (WSAStartup(wVersionRequested, &wsaData) != 0) {
sprintf(buf, "Failed to initialize Winsock (WSAStartup)");
return PyString_FromString(buf);
return PyUnicode_FromString(buf);
}
else {
cleanupWSA = 1;
@@ -4076,7 +4077,7 @@ static PyObject * open_rx_udp(PyObject * self, PyObject * args)
else {
sprintf(buf, "Failed to open socket");
}
return PyString_FromString(buf);
return PyUnicode_FromString(buf);
}
static PyObject * open_sound(PyObject * self, PyObject * args)
@@ -4290,7 +4291,7 @@ static PyObject * mixer_set(PyObject * self, PyObject * args)
return NULL;
quisk_alsa_mixer_set(card_name, numid, value, err_msg, QUISK_SC_SIZE);
return PyString_FromString(err_msg);
return PyUnicode_FromString(err_msg);
}
static PyObject * pc_to_hermes(PyObject * self, PyObject * args)
@@ -4493,12 +4494,12 @@ static PyObject * ip_interfaces(PyObject * self, PyObject * args)
baddr |= ~netmask;
tup = PyTuple_New(4);
if (name == NULL)
PyTuple_SetItem(tup, 0, PyString_FromString("unnamed"));
PyTuple_SetItem(tup, 0, PyUnicode_FromString("unnamed"));
else
PyTuple_SetItem(tup, 0, PyString_FromString(name));
PyTuple_SetItem(tup, 1, PyString_FromString(Win_NtoA(ipAddr)));
PyTuple_SetItem(tup, 2, PyString_FromString(Win_NtoA(netmask)));
PyTuple_SetItem(tup, 3, PyString_FromString(Win_NtoA(baddr)));
PyTuple_SetItem(tup, 0, PyUnicode_FromString(name));
PyTuple_SetItem(tup, 1, PyUnicode_FromString(Win_NtoA(ipAddr)));
PyTuple_SetItem(tup, 2, PyUnicode_FromString(Win_NtoA(netmask)));
PyTuple_SetItem(tup, 3, PyUnicode_FromString(Win_NtoA(baddr)));
PyList_Append(pylist, tup);
Py_DECREF(tup);
}
@@ -4517,10 +4518,10 @@ static PyObject * ip_interfaces(PyObject * self, PyObject * args)
while(p) {
if ((p->ifa_addr) && p->ifa_addr->sa_family == AF_INET) {
tup = PyTuple_New(4);
PyTuple_SetItem(tup, 0, PyString_FromString(p->ifa_name));
PyTuple_SetItem(tup, 1, PyString_FromString(Lin_NtoA(p->ifa_addr)));
PyTuple_SetItem(tup, 2, PyString_FromString(Lin_NtoA(p->ifa_netmask)));
PyTuple_SetItem(tup, 3, PyString_FromString(Lin_NtoA(p->ifa_broadaddr)));
PyTuple_SetItem(tup, 0, PyUnicode_FromString(p->ifa_name));
PyTuple_SetItem(tup, 1, PyUnicode_FromString(Lin_NtoA(p->ifa_addr)));
PyTuple_SetItem(tup, 2, PyUnicode_FromString(Lin_NtoA(p->ifa_netmask)));
PyTuple_SetItem(tup, 3, PyUnicode_FromString(Lin_NtoA(p->ifa_broadaddr)));
PyList_Append(pylist, tup);
Py_DECREF(tup);
}
@@ -4829,7 +4830,7 @@ static PyObject * set_multirx_mode(PyObject * self, PyObject * args)
if (!PyArg_ParseTuple (args, "ii", &index, &mode))
return NULL;
if (index < QUISK_MAX_SUB_RECEIVERS)
if (index >= 0 && index < QUISK_MAX_SUB_RECEIVERS)
multirx_mode[index] = mode;
Py_INCREF (Py_None);
return Py_None;
@@ -4841,7 +4842,7 @@ static PyObject * set_multirx_freq(PyObject * self, PyObject * args)
if (!PyArg_ParseTuple (args, "ii", &index, &freq))
return NULL;
if (index < QUISK_MAX_SUB_RECEIVERS)
if (index >= 0 && index < QUISK_MAX_SUB_RECEIVERS)
multirx_freq[index] = freq;
Py_INCREF (Py_None);
return Py_None;
@@ -4859,7 +4860,7 @@ static PyObject * set_multirx_play_channel(PyObject * self, PyObject * args)
{
if (!PyArg_ParseTuple (args, "i", &multirx_play_channel))
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;
Py_INCREF (Py_None);
return Py_None;
@@ -5637,6 +5638,10 @@ static void measure_freq(complex double * cSamples, int nSamples, int srate)
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]);
freq = srate * (2 * (ipeak + c3) - fft_size) / 2 / fft_size;
freq += rx_tune_freq;
@@ -5660,7 +5665,7 @@ static PyObject * Xdft(PyObject * pyseq, int inverse, int window)
static fftw_complex * samples; // complex data for fft
static fftw_plan planF, planB; // fft plan for fftW
static double * fft_window; // window function
Py_complex pycx; // Python C complex value
complex double pycx;
if (PySequence_Check(pyseq) != 1) {
PyErr_SetString (QuiskError, "DFT input data is not a sequence");
@@ -5695,22 +5700,20 @@ static PyObject * Xdft(PyObject * pyseq, int inverse, int window)
for (i = 0; i < size; i++) {
obj = PySequence_GetItem(pyseq, j);
if (PyComplex_Check(obj)) {
pycx = PyComplex_AsCComplex(obj);
pycx = PyComplex_RealAsDouble(obj) + I * PyComplex_ImagAsDouble(obj);
}
else if (PyFloat_Check(obj)) {
pycx.real = PyFloat_AsDouble(obj);
pycx.imag = 0;
pycx = PyFloat_AsDouble(obj);
}
else if (PyInt_Check(obj)) {
pycx.real = PyInt_AsLong(obj);
pycx.imag = 0;
pycx = PyInt_AsLong(obj);
}
else {
Py_XDECREF(obj);
PyErr_SetString (QuiskError, "DFT input data is not a complex/float/int number");
return NULL;
}
samples[i] = pycx.real + I * pycx.imag;
samples[i] = pycx;
if (++j >= size)
j = 0;
Py_XDECREF(obj);
@@ -5736,9 +5739,7 @@ static PyObject * Xdft(PyObject * pyseq, int inverse, int window)
pyseq = PyList_New(fft_size);
j = (size - 1) / 2; // zero frequency in input
for (i = 0; i < fft_size; i++) {
pycx.real = creal(samples[i]);
pycx.imag = cimag(samples[i]);
PyList_SetItem(pyseq, j, PyComplex_FromCComplex(pycx));
PyList_SetItem(pyseq, j, PyComplex_FromDoubles(creal(samples[i]), cimag(samples[i])));
if (++j >= size)
j = 0;
}
@@ -6009,6 +6010,8 @@ static PyObject * record_app(PyObject * self, PyObject * args)
}
// Initialize plan for multirx FFT
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_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)
+3
View File
@@ -29,6 +29,8 @@
#define QUISK_CWKEY_DOWN (quisk_hardware_cwkey || quisk_serial_key_down || quisk_midi_cwkey || quisk_remote_cwkey)
#include <stdio.h>
#include <stdlib.h>
#ifdef MS_WINDOWS
#define QUISK_SHUT_RD SD_RECEIVE
#define QUISK_SHUT_BOTH SD_BOTH
@@ -264,6 +266,7 @@ extern void send_graph_data(double * fft_avg, int fft_size, double zoom, double
PyObject * quisk_tci_set_params(PyObject * self, PyObject * args, PyObject * keywds);
PyObject * quisk_tci_get_params(PyObject * self, PyObject * args);
void tci_send_audio(complex double * cSamples, int nSamples);
void tci_send_iq(complex double * cSamples, int nSamples);
int tci_get_mic(complex double * cSamples, int mic_count);
extern uint64_t tci_tx_audio_client; // This is ws_cli_conn_t defined in ws.h.
+90 -31
View File
@@ -39,6 +39,7 @@ from filters import Filters
import dxcluster
import configure
import quisk_conf_defaults as conf
import quisk_utils
import quisk_wdsp
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.split_mode = 0
self.split_vfo = 'VFO'
self.lock_mode = 0
h = self.Handlers = {}
h[''] = self.ErrProtocol
h['dump_state'] = self.DumpState
@@ -765,6 +767,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
h['set_split_freq'] = self.SetSplitFreq
h['get_split_vfo'] = self.GetSplitVfo
h['set_split_vfo'] = self.SetSplitVfo
h['get_lock_mode'] = self.GetLockMode
h['set_lock_mode'] = self.SetLockMode
self.MakeDumpState()
def MakeDumpState(self):
dump_state = []
@@ -1009,7 +1013,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
self.ErrParam()
else:
freq = int(freq + 0.5)
self.app.ChangeRxTxFrequency(freq, None)
# Like a real radio, keep the mode. Hamlib caches the mode, so the client would not notice a mode change.
self.app.ChangeRxTxFrequency(freq, None, keep_mode=True)
def GetSplitFreq(self): # The Tx Frequency
self.Reply('TX Frequency', self.app.txFreq + self.app.VFO, 0)
def SetSplitFreq(self): # The Tx Frequency
@@ -1022,7 +1027,7 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
else:
freq = int(freq + 0.5)
# This always controls the Tx frequency
self.app.ChangeRxTxFrequency(None, freq)
self.app.ChangeRxTxFrequency(None, freq, keep_mode=True)
def GetSplitVfo(self):
self.Reply('Split', self.split_mode, 'TX VFO', self.split_vfo, 0)
def SetSplitVfo(self):
@@ -1107,6 +1112,22 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
Lab = label
diff = df
self.app.filterButns.SetLabel(Lab, True)
# The code for the lock_mode and keep_mode was supplied by Karl Heinz - K5KHK. Thanks Karl!
def GetLockMode(self):
# Hamlib netrigctl always reads two lines, so an error reply "RPRT -4" makes it wait for its 10 second timeout.
# Reply like rigctld with the lock value and then "RPRT 0".
if self.extended:
self.Reply('Locked', self.lock_mode, 0)
else:
self.Send("%d\nRPRT 0\n" % self.lock_mode)
def SetLockMode(self):
lock = self.GetParamNumber()
try:
self.lock_mode = int(lock)
except:
self.ErrParam()
else:
self.Reply(0)
def GetVfo(self):
self.Reply('VFO', self.vfo, 0)
def SetVfo(self):
@@ -2564,7 +2585,7 @@ class GraphScreen(wx.Window):
else:
self.mouse_is_rx = False
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:
freq = self.FreqRound(freq, 0)
elif freq >= 0:
@@ -2574,7 +2595,7 @@ class GraphScreen(wx.Window):
tune = freq - self.VFO
self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseWheel', event=event, rx_freq=tune)
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:
freq = self.FreqRound(freq, 0)
elif freq >= 0:
@@ -3642,8 +3663,8 @@ class HelpScreen(wx.html.HtmlWindow):
if "gtk2" in wx.PlatformInfo:
self.SetStandardFonts()
self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22])
# read in text from file help.html in the directory of this module
self.LoadFile('help.html')
if not self.LoadFile('help.html'):
self.LoadFile('/usr/share/doc/quisk/help.html')
def OnGraphData(self, data):
pass
def ChangeYscale(self, y_scale):
@@ -4030,7 +4051,9 @@ class App(wx.App):
self.file_name_play_cq = ''
self.midiControls = {} # Control object and associated function for Midi control name
self.tci_vfo = 0
self.tci_started = False
self.tci_dds = 0
self.tci_modulation = ''
self.tci_split_enable = 0
# get the screen size - thanks to Lucian Langa
x, y, self.screen_width, self.screen_height = wx.Display().GetGeometry() # Using display index 0
self.Bind(wx.EVT_IDLE, self.OnIdle)
@@ -4361,12 +4384,12 @@ class App(wx.App):
value = getattr(conf, "pulse_audio_verbose_output")
QS.tci_set_params(verbose=value)
QS.tci_set_params(start=1);
self.tci_started = True
QS.tci_set_params(tci_dds=self.VFO)
QS.tci_set_params(tci_if=self.txFreq)
self.tci_vfo = self.txFreq + self.VFO
self.tci_dds = self.VFO
QS.tci_set_params(tci_dds=self.tci_dds)
self.tci_vfo = self.rxFreq + self.VFO
QS.tci_set_params(tci_vfo=self.tci_vfo)
QS.tci_set_params(tci_modulation=self.mode)
self.tci_modulation = self.mode
QS.tci_set_params(tci_modulation=self.tci_modulation)
QS.tci_set_params(tci_trx=0)
QS.tci_set_params(tci_split_enable=0)
return True
@@ -4693,6 +4716,8 @@ class App(wx.App):
if msg:
print(msg, end='')
QS.close_key()
if QS.tci_get_params("tci_clients_count") > 0:
QS.tci_set_params(close=1)
QS.set_file_name(record_button=0) # Turn off file recording
time.sleep(0.1)
if self.sound_thread:
@@ -5782,7 +5807,6 @@ class App(wx.App):
QS.set_sidetone(self.sidetone_volume, self.sidetone_0to1, self.ritFreq, conf.keyupDelay)
def OnBtnSplit(self, event): # Called when the Split check button is pressed
self.split_rxtx = self.splitButton.GetValue()
QS.tci_set_params(tci_split_enable=self.split_rxtx)
if self.split_rxtx:
if self.split_offset == 0:
if self.mode in ("CWL", "CWU"):
@@ -5796,7 +5820,7 @@ class App(wx.App):
QS.set_split_rxtx(0)
self.split_offset = self.txFreq - self.rxFreq
self.txFreq = self.rxFreq
self.ChangeHwFrequency(self.txFreq, self.VFO, 'OnSplit', event=event)
self.ChangeHwFrequency(self.txFreq, self.VFO, 'OnSplit', event=event, rx_freq=self.rxFreq)
self.screen.SetTxFreq(self.txFreq, self.rxFreq)
def OnMenuSplitPlay1(self, event):
self.split_rxtx_play = 1
@@ -6133,7 +6157,7 @@ class App(wx.App):
self.freqDisplay.Display(self.txFreq + self.VFO)
self.fldigi_new_freq = self.txFreq + self.VFO
return change
def ChangeRxTxFrequency(self, rx_freq=None, tx_freq=None):
def ChangeRxTxFrequency(self, rx_freq=None, tx_freq=None, keep_mode=False):
if not self.split_rxtx and not tx_freq:
tx_freq = rx_freq
if tx_freq:
@@ -6144,7 +6168,7 @@ class App(wx.App):
else: # Change the VFO
vfo = (tx_freq // 5000) * 5000 - 5000
tune = tx_freq - vfo
self.BandFromFreq(tx_freq)
self.BandFromFreq(tx_freq, keep_mode)
self.ChangeHwFrequency(tune, vfo, 'FreqEntry')
if rx_freq and self.split_rxtx: # Frequency must be on-screen
tune = rx_freq - self.VFO
@@ -6168,7 +6192,6 @@ class App(wx.App):
self.ChangeRxTxFrequency(self.rxFreq + delta + self.VFO, self.txFreq + delta + self.VFO)
Hardware.ChangeMode(mode)
self.mode = mode
QS.tci_set_params(tci_modulation=mode)
self.MakeFilterButtons(self.Mode2Filters(mode))
QS.set_rx_mode(Mode2Index.get(mode, 3))
if mode == 'CWL':
@@ -6335,7 +6358,8 @@ class App(wx.App):
self.pttButton.Enable(False)
else:
self.pttButton.Enable(True)
def BandFromFreq(self, frequency): # Change to a new band based on the frequency
def BandFromFreq(self, frequency, keep_mode=False): # Change to a new band based on the frequency
# If keep_mode is True, do not change to the mode saved for the new band.
if self.screen == self.bandscope_screen:
return
try:
@@ -6357,7 +6381,8 @@ class App(wx.App):
vfo, tune, mode = self.bandState[band]
except KeyError:
vfo, tune, mode = (0, 0, 'LSB')
self.modeButns.SetLabel(mode, True)
if not keep_mode:
self.modeButns.SetLabel(mode, True)
self.ChangeBand(band)
break
def ChangeBand(self, band):
@@ -6829,22 +6854,56 @@ class App(wx.App):
if self.timer - self.clip_time0 > 1.0:
self.clip_time0 = 0
self.freqDisplay.Clip(0)
if QS.tci_get_params("tci_clients_count") > 0:
# These are change requests from TCI clients. All must be echoed with the new value from Quisk.
# modulation
mode = QS.tci_get_params("tci_modulation")
if mode is not None: # TCI set new modulation
self.modeButns.SetLabel(mode, True)
self.tci_modulation = self.mode
QS.tci_set_params(tci_modulation=self.tci_modulation)
# split
split = QS.tci_get_params("tci_split_enable")
if split is not None: # TCI set split_enable
self.splitButton.SetValue(split, True)
self.tci_split_enable = self.split_rxtx
QS.tci_set_params(tci_split_enable=self.tci_split_enable)
# tci dds == self.VFO
tci_dds = QS.tci_get_params("tci_dds")
if tci_dds is not None:
self.ChangeHwFrequency(0, tci_dds, 'TCIrequest')
self.tci_dds = self.VFO
QS.tci_set_params(tci_dds=self.tci_dds)
# tci_vfo == self.VFO + self.rxFreq
tci_vfo = QS.tci_get_params("tci_vfo")
if tci_vfo is not None:
self.ChangeRxTxFrequency(tci_vfo, None)
self.tci_vfo = self.VFO + self.rxFreq
QS.tci_set_params(tci_vfo=self.tci_vfo)
# tci_trx handled elsewhere
if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks:
self.heart_time0 = self.timer
Hardware.HeartBeat()
if self.tci_started:
if self.tci_vfo != self.txFreq + self.VFO: # limit the speed of frequency updates
self.tci_vfo = self.txFreq + self.VFO
if QS.tci_get_params("tci_clients_count") > 0: # limit the speed of frequency updates
# these are changes made in Quisk that must be broadcast to all clients.
# modulation
if self.tci_modulation != self.mode:
self.tci_modulation = self.mode
QS.tci_set_params(tci_modulation=self.tci_modulation)
# split
if self.tci_split_enable != self.split_rxtx:
self.tci_split_enable = self.split_rxtx
QS.tci_set_params(tci_split_enable=self.tci_split_enable)
# tci dds == self.VFO
if self.tci_dds != self.VFO:
self.tci_dds = self.VFO
QS.tci_set_params(tci_dds=self.tci_dds)
# tci_vfo == self.VFO + self.rxFreq
tci_vfo = self.VFO + self.rxFreq
if self.tci_vfo != tci_vfo:
self.tci_vfo = tci_vfo
QS.tci_set_params(tci_vfo=self.tci_vfo)
freq = QS.tci_get_params("tci_vfo")
if freq is not None: # TCI frequency
self.ChangeRxTxFrequency(freq, None)
mode = QS.tci_get_params("tci_modulation")
if mode is not None: # TCI mode
self.modeButns.SetLabel(mode, True)
split = QS.tci_get_params("tci_split_enable")
if split is not None: # TCI split_enable
self.splitButton.SetValue(split, True)
# tci_trx handled elsewhere
if self.is_HermesLite2:
self.tx_inhibit = QS.get_params('quisk_tx_inhibit')
else:
+30 -1
View File
@@ -172,6 +172,22 @@ tx_level = {}
# This is the maximum value of the slider.
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.
@@ -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.
# 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.
# 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.
# 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).
@@ -1508,6 +1524,19 @@ remote_radio_ip = ""
# It is only necessary to enter it once on each computer.
remote_radio_password = ""
## remote_radio_mic_jitter_ms Mic buffer msec for remote radio, integer
# This is set on the remote radio, not the control head. The network can hold the control
# head's microphone packets for 100 msec or more and then deliver them in a burst, which is
# common over Wi-Fi. The transmit audio then has gaps. This buffers the microphone audio
# for the given time before transmitting it, so the bursts are smoothed out, at the cost
# of that much more transmit delay. Zero turns the buffer off, which is best on a wired
# network. If needed, try 150 msec. The maximum is 500. A change takes effect when the
# next control head connects.
remote_radio_mic_jitter_ms = 0
#remote_radio_mic_jitter_ms = 100
#remote_radio_mic_jitter_ms = 150
#remote_radio_mic_jitter_ms = 250
## k4_tcp_ip IP address for K4 TCP, text
# This is the Quisk IP address for the TCP server implementing K4 commands.
k4_tcp_ip = ""
+23
View File
@@ -1,6 +1,29 @@
from __future__ import print_function
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
"""Interpolate a table of [x, y] values."""
def __init__(self, table, Xmin=None, Xmax=None):
+4 -3
View File
@@ -26,6 +26,7 @@ import threading, webbrowser
import _quisk as QS
from quisk_widgets import *
import configure
import quisk_utils
DEBUG = 0
@@ -630,7 +631,7 @@ class GraphScreen(wx.Window):
mouse_x, mouse_y = self.GetMousePosition(event)
self.SetTxFreq(index=mouse_x - self.originX)
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)
class HelpScreen(wx.html.HtmlWindow):
@@ -640,8 +641,8 @@ class HelpScreen(wx.html.HtmlWindow):
if "gtk2" in wx.PlatformInfo:
self.SetStandardFonts()
self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22])
# read in text from file help.html in the directory of this module
self.LoadFile('help_vna.html')
if not self.LoadFile('help_vna.html'):
self.LoadFile('/usr/share/doc/quisk/help_vna.html')
def OnLinkClicked(self, link):
webbrowser.open(link.GetHref(), new=2)
+2
View File
@@ -1,7 +1,9 @@
// This module provides C access to the WDSP SDR library.
#include <Python.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <complex.h>
#define CLIP32 2147483647
+1
View File
@@ -0,0 +1 @@
#
+569
View File
@@ -0,0 +1,569 @@
# SDR-TRX Quisk Configuration File
# This file is to integrate the function of control of the SDR-TRX wint Quisk and with WSJT-X.
# This is for control of the SDR-TRX with using the UDP port port 12346. WSJT-X interfaces with this
# locally using UDP on port 2237. Capital letters begin commands to the SDR-TRX from Quisk,
# and lowercase letters are for commands coming from WSJT-X (through this Quisk interface).
# This is for usinng the SDR-TRX without a 3.5 mm audio card to the soundcard of the computer,
# and the WSJT-X software using UDP, and the data coming over UDP part 12345 too.
# It requires the libldl/ directory from weakmon to be in the directory you run this from.
# It also requires the WSJTXClass.py file from wsjtx_transceiver_interface to be in the lib directory
# and the version of that class. I have supplied a pruned down versions of ft8.py and ft4.45, because we don't need much
# other than FT8Send and FT4Send from those files. You need to edit transceiver_config.yml to have your callsign and grid, etc.
import socket
import time
import yaml
import struct
import datetime
import sys
import os
from ft8 import FT8Send
from ft4 import FT4Send
from quisk_hardware_model import Hardware as BaseHardware
import struct
import collections
import lib.WSJTXClass as WSJTXClass # To use encoders
sys.path.append(os.path.expandvars('$WEAKMON'))
DEBUG_ON = True
#
# Define the name of the hardware and the items on the hardware screen (see quisk_conf_defaults.py):
#
################ Receivers SdrTrx, The SdrTrx radio by Rob Frohne, KL7NA
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'sdrtrxpkg/quisk_hardware.py'
## widgets_file_name Widget file path, rfile
# This optional file adds additional controls for the radio.
#widgets_file_name = ''
## si570_xtal_freq Si570 crystal frequency, integer
# This is the Si570 startup frequency in Hz. 114.285MHz is the typical
# value from the data sheet; you can use 'usbsoftrock calibrate' to find
# the value for your device.
si570_xtal_freq = 114285000
## rx_max_amplitude_correct Max ampl correct, number
# If you get your I/Q samples from a sound card, you will need to correct the
# amplitude and phase for inaccuracies in the analog hardware. The correction is
# entered using the controls from the "Rx Phase" button on the config screen.
# You must enter a positive number. This controls the range of the control.
rx_max_amplitude_correct = 0.2
## rx_max_phase_correct Max phase correct, number
# If you get your I/Q samples from a sound card, you will need to correct the
# amplitude and phase for inaccuracies in the analog hardware. The correction is
# entered using the controls from the "Rx Phase" button on the config screen.
# You must enter a positive number. This controls the range of the control in degrees.
rx_max_phase_correct = 10.0
# SDR-TRX Hardware Control Class
#
class Hardware(BaseHardware):
# These are "static variable substitutes" since python doesn't have them.
# They are shared by all instances of the class and don't get redefinede each time a method is called.
# Move these into the Hardware class.
# Uncomment these lines if you wish to use Pulseaudio
name_of_sound_play = "pulse"
# Radio's Frequency limits.
radio_lower = 3500000
radio_upper = 30000000
PACKET_SIZE = 1444 # 4 bytes for packet number + 360 * 4 bytes for int32_t pairs
# Using 16 bit samples, so 2 bytes per sample.
# Set the number of Hz the signal is tuned to above the center frequency to avoid 1/f noise.
vfo_Center_Offset = 10000
tx_ready_wsjtx = False
tx_ready_wsjtx_sent = False
tx_now = False
# This is the open code for the WSJT server.
ft8_encoder = FT8Send()
ft4_encoder = FT4Send()
class Packet:
def __init__(self, data):
self.number = struct.unpack('<I', data[:4])[0]
self.pairs = [struct.unpack('<hh', data[i:i+4]) for i in range(4, len(data), 4)]
class PacketQueue:
def __init__(self):
self.queue = collections.deque()
def add_packet(self, packet):
self.queue.append(packet)
self.queue = collections.deque(sorted(self.queue, key=lambda p: p.number))
def check_missing_packets(self):
for i in range(1, len(self.queue)):
if self.queue[i].number != self.queue[i-1].number + 1:
avg_pairs = [((self.queue[i-1].pairs[j][0] + self.queue[i].pairs[j][0]) // 2,
(self.queue[i-1].pairs[j][1] + self.queue[i].pairs[j][1]) // 2)
for j in range(len(self.queue[i-1].pairs))]
self.queue.insert(i, Packet(struct.pack('<I', self.queue[i-1].number + 1) +
b''.join(struct.pack('<ii', pair[0], pair[1]) for pair in avg_pairs)))
def get_packets(self):
packets = list(self.queue)
self.queue.clear()
return packets
def open(self):
# Connection for WSJT-X and data
self.PICO_UDP_IP = None # Will be set to the IP of the machine sending data
self.COMMAND_UDP_PORT = 12346 # This is the port the Pico listens on for UDP commands coming from quisk.
self.DATA_UDP_PORT = 12345 # This is the port the quisk listens on for UDP IQ data coming from the Pico W.
self.BROADCAST_PORT = 12347 # This is the port the Pico gets our IP address from.-
self.broadcast_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.broadcast_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
self.broadcast_message = "Quisk"
self.broadcast_sock.setblocking(False)
self.isConnected = False
self.command_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.command_sock.setblocking(False)
self.data_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.data_sock.bind(('', self.DATA_UDP_PORT))
print("Data socket bound to IP:", self.data_sock.getsockname()[0])
self.data_sock.setblocking(False)
self.establish_connection()
self.InitSamples(2, 0) # Two bytes per sample, little endian.
self.queue = self.PacketQueue()
time.sleep(2)
# Poll for version. Should probably confirm the response on this.
version = str(self.get_parameter("VER")) # The way the firmware is now, this sets it up to use the UART or UDP.
# UART is used if get_parameter uses the UART, and UDP is used if it uses the UDP.
print(version)
# Return an informative message for the config screen
t = str(version) + ". Capture from sound card %s." % self.conf.name_of_sound_capt
configs_file = open('transceiver_config.yml', 'r')
configs = yaml.load(configs_file, Loader=yaml.BaseLoader)
# Global variables
self.callsign = configs['callsign']
self.grid = configs['grid']
self.current_msg = ''
self.rx_callsign = ''
self.mode = 'FT8'
self.tx_freq = 1200
# Connection for WSJT-X
WSJTX_UDP_IP = "127.0.0.1" # Assuming you are running WSJT-X on the same computer as Quisk...
WSJTX_UDP_PORT = 2237
self.wsjtx_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.wsjtx_sock.bind((WSJTX_UDP_IP, WSJTX_UDP_PORT))
self.wsjtx_sock.setblocking(False)
self.StartSamples()
return t
def close(self):
# Called once to close the Hardware
self.get_parameter("RX") # Make sure we are in RX mode.
time.sleep(1) # Wait for RX to be set.
self.StopSamples()
self.command_sock.close()
self.wsjtx_sock.close()
self.broadcast_sock.close()
def ChangeFrequency(self, tune, vfo, source='', band='', event=None):
# Called whenever quisk requests a frequency change.
# This sends the FREQ command dto set the centre frequency of the radio,
# and will also move the 'tune' frequency (the section within the RX passband
# which is to be demodulated) if it falls outside the passband (+/- sample_rate/2).
print("Setting VFO to %d, with tune to %d." % (vfo, tune))
print("Setting VFO to %d." % vfo)
if vfo < self.radio_lower:
vfo = self.radio_lower
print("Outside range! Setting to %d" % self.radio_lower)
if vfo > self.radio_upper:
vfo = self.radio_upper
print("Outside range! Setting to %d" % self.radio_upper)
# print("sample_rate =", sample_rate) # I believe sample_rate comes from quisk.
# If the tune frequency is outside the RX bandwidth, set it to somewhere within that bandwidth.
if tune > (vfo + sample_rate / 2) or tune < (vfo - sample_rate / 2):
tune = vfo + self.vfo_Center_Offset
print("Bringing tune frequency back into the RX bandwidth.")
# success = self.set_parameter("FREQ",str(vfo))
self.set_parameter("FREQ", str(vfo))
self.set_parameter("TX_FREQ", str(tune))
return tune, vfo
def OnButtonPTT(self, event):
btn = event.GetEventObject()
if btn.GetValue():
self.get_parameter("TX")
print("TX")
else:
self.get_parameter("RX")
print("RX")
return BaseHardware.OnButtonPTT(self, event)
def establish_connection(self):
while not self.isConnected:
print("isConnected is: ", self.isConnected)
# Check if a connection is established
# Wait for data to arrive and set PICO_UDP_IP to the sender's IP
while self.PICO_UDP_IP is None: # This needs to be fixed. It continues to send broadcast packets.
try:
print("Sending broadcast message...")
self.broadcast_sock.sendto(self.broadcast_message.encode(), ('<broadcast>', self.BROADCAST_PORT))
time.sleep(1) # Wait for 1 second before sending the next broadcast message
except Exception as e:
print("Broadcast message did not work: {0}".format(e))
break
try:
data, addr = self.data_sock.recvfrom(self.PACKET_SIZE) # Adjust the buffer size as needed
self.PICO_UDP_IP = addr[0]
print("PICO_UDP_IP set to:", self.PICO_UDP_IP)
# if data == self.broadcast_message:
self.isConnected = True
except socket.error:
if self.PICO_UDP_IP is not None:
print("Connection established with Pico W at IP:", self.PICO_UDP_IP, "but exception occurred.")
break # Exit the loop if self.PICO_UDP_IP is not None
pass # No data available yet
def GetRxSamples(self):
# Initialize packet counter and time marker
# This routine is made for FT8 and FT4, where we need to send a packet every 15 seconds.
# The Pico W sending the data has a sample rate of 48046 samples per second > 48 ks/s.
# We made the packets so they carry 180 samples per packet. That gives 4000 packets
# per 15 seconds. We discard any packets after that four times a minute at 59, 14, 29,
# and 44 seconds. This should make four ticks per minute, and we should be able to ignore
# # them if we are doing FT8 or FT4. Something else might be more optimum for other modes.
packet_counter = 0
next_time_marker = 0
send_packets = False
while True:
# Check if we've reached the next time marker
current_time = time.time()
current_seconds = current_time % 60
# Define the time markers
if 59 <= current_seconds < 60:
next_time_marker = current_time + 1 - current_seconds
send_packets = True
elif 14 <= current_seconds < 15:
next_time_marker = current_time + 1 - (current_seconds - 14)
send_packets = True
elif 29 <= current_seconds < 30:
next_time_marker = current_time + 1 - (current_seconds - 29)
send_packets = True
elif 44 <= current_seconds < 45:
next_time_marker = current_time + 1 - (current_seconds - 44)
send_packets = True
# If we've reached the next time marker, reset the packet counter
if current_time >= next_time_marker and send_packets:
packet_counter = 0
send_packets = False
try:
data, addr = self.data_sock.recvfrom(self.PACKET_SIZE)
self.no_data_repeat = 0
except socket.error:
# Send a broadcast message to the Pico W to get the IP address in case it has reset.
self.broadcast_sock.sendto(self.broadcast_message.encode(), ('<broadcast>', self.BROADCAST_PORT))
break # No more packets available
packet = self.Packet(data)
self.queue.add_packet(packet)
# Check for missing packets and fill them in
self.queue.check_missing_packets()
# Get the packets and pass them to the processing thread
packets = self.queue.get_packets()
for packet in packets:
data = b''.join(struct.pack('<hh', pair[0], pair[1]) for pair in packet.pairs)
# if len(data) % 4 != 0: # Each I/Q pair is now 4 bytes (2 * 2 bytes)
# self.GotReadError(self, DEBUG_ON, "Error: Mismatched number of I and Q samples")
# continue
# If we've already sent 4000 packets since the last time marker, discard the rest
if packet_counter >= 4000:
continue
self.AddRxSamples(data)
packet_counter += 1
#
# UDP comms functions, to communicate with the Raspberry Pi Pico W board used in the SDR-TRX.
#
def get_parameter(self, string):
string = string + "\n"
self.command_sock.sendto(string.encode(), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
return self.get_argument()
def set_parameter(self, string, arg):
string = string + "," + arg + "\r" + "\n"
self.command_sock.sendto(string.encode(), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
print('arg is: ', arg)
temp_arg = self.get_argument()
print('temp_arg is: ', temp_arg)
return True
def get_argument(self):
self.command_sock.setblocking(True)
try:
data1 = self.command_sock.recv(1024)
print('Received on line 178: ', data1)
except Exception as e:
print("Exception occurred: {0}".format(e))
return -1
self.command_sock.setblocking(False)
if not data1.startswith(b'OK'):
print('Received on line 185: ', data1)
# Maybe we didn't catch an OK line?
else:
while True:
try:
data1 = self.command_sock.recv(1024)
print('Received on line 190: ', data1)
break
except BlockingIOError:
pass
# Check to see if we have a comma in the string. If not, there is no argument.
if data1.find(b',') == -1:
return -1
data1 = data1.split(b',')[1].rstrip(b'\r\n')
print("data1 =")
print(data1)
# Check for the OK string. Should we wait for the OK string?
try:
data2 = self.command_sock.recv(1024)
if data2.startswith(b'OK'):
return data2
except BlockingIOError:
return -1
# We might want to integrate the UDP reads below into the stuff above.
# The code below is to interface WSJT-X with SDR-TRX.
def encode_ft8(self, msg):
try:
a77 = self.ft8_encoder.pack(msg, 1)
symbols = self.ft8_encoder.make_symbols(a77)
except Exception as e:
print("FT8 encoder error, check message!")
print(f"Exception type: {type(e).__name__}")
print(f"Exception message: {str(e)}")
symbols = None
time.sleep(3)
return symbols
def encode_ft4(self, msg):
try:
a77 = self.ft4_encoder.pack(msg, 1)
symbols = self.ft4_encoder.make_symbols(a77)
except:
print("FT4 encoder error, check message!")
symbols = None
time.sleep(3)
return symbols
def load_symbols(self, symbols):
print("Load symbols into transmitter..")
self.command_sock.sendto(b'm', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
count = 0
for symbol in symbols:
self.command_sock.sendto(struct.pack('>B', symbol), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
count += 1
# Wait to avoid Arduino serial buffer overflow. This may not be needed with the Pico.
if count % 50 == 0:
time.sleep(0.05)
self.command_sock.sendto(b'\0', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
resp = self.command_sock.recv(1)
if resp == b'm':
print("Load OK")
else:
print(resp)
def change_freq(self, new_freq):
#global self.tx_freq
self.command_sock.setblocking(True)
print("Change TX frequency to:", new_freq)
self.command_sock.sendto(b'o', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
self.command_sock.sendto(struct.pack('<H', new_freq), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
time.sleep(0.05)
resp = self.command_sock.recv(1)
print("resp after changing offset is: ", resp)
self.command_sock.setblocking(False)
if resp == b'o':
print("New freq OK")
self.tx_freq = new_freq
def set_mode(self, new_mode): # New more robust protocol.
#global mode
if new_mode == 'FT8':
self.command_sock.sendto(b'e', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
time.sleep(.05)
resp = self.command_sock.read(1)
print("resp = ", resp)
if resp == b'e':
self.mode = new_mode
print("Switched to: {0}".format(new_mode))
self.current_msg = ''
return True
elif new_mode == 'FT4':
self.command_sock.sendto(b'f', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
time.sleep(0.05)
resp = self.command_sock.recv(1)
print("resp = ", resp)
if resp == b'f':
self.mode = new_mode
self.current_msg = ''
print("Switched to: {0}".format(new_mode))
return True
else:
return False
def new_msg(self, msg):
#global self.current_msg
#global self.mode
if msg != self.current_msg:
print("Message: {0}".format(msg))
if 'FT8' in self.mode:
symbols = self.encode_ft8(msg)
else:
symbols = self.encode_ft4(msg)
if symbols.any():
# symbols = [kk for kk in range(79)]
self.load_symbols(symbols)
self.current_msg = msg
else:
return
else:
time.sleep(0.005) # Do we need this?
def transmit(self):
if False: # not current_msg:
print("No previous message!")
time.sleep(1)
else:
print("TX!")
self.command_sock.sendto(b't', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
# self.tx_now = False
def check_time_window(self, utc_time):
time_window = 15 if 'FT8' in self.mode else 7
print("Time window: ", time_window) # Is correct for FT8 and FT4.
rm = utc_time.second % time_window
if rm > 1 and rm < time_window - 1: # Within one second of exact time for FT8.
return False
else:
return True
# See if HeartBeat will work for this:
def HeartBeat(self): # Should be called at 10 Hz from the main.
# Check transmitter is initialized
#raise Exception("In HeartBeat")
# print("\n\nWait for transmitter ready...")
if self.tx_ready_wsjtx == False:
if self.tx_ready_wsjtx_sent == False:
self.command_sock.sendto(b'r', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
time.sleep(0.05)
self.tx_ready_wsjtx_sent = True
while True:
try:
x = self.command_sock.recv(1)
if x == b'r':
print("Transmitter ready!")
self.tx_ready_wsjtx = True
break
except BlockingIOError:
pass
if self.tx_ready_wsjtx == True:
try:
fileContent = self.wsjtx_sock.recv(1024)
except Exception as e:
if e == BlockingIOError: # This is our way of pollig the socket.
# Maybe use select to stop the errors printing after we quit.
return BaseHardware.HeartBeat(self)
else:
# print("Error in HeartBeat")
# print(f"Exception type: {type(e).__name__}")
# print(f"Exception message: {str(e)}")
pass
try:
NewPacket = WSJTXClass.WSJTX_Packet(fileContent, 0)
NewPacket.Decode()
if NewPacket.PacketType == 1:
# print('New Packet is type 1')
StatusPacket = WSJTXClass.WSJTX_Status(fileContent, NewPacket.index)
StatusPacket.Decode()
# Check TX frequency and update transceiver
new_freq = StatusPacket.TxDF
new_mode = StatusPacket.TxMode.strip()
if new_freq != self.tx_freq:
# time.sleep(0.03)
print('Changing frequency from {0} to {1}'.format(self.tx_freq, new_freq))
try:
self.change_freq(new_freq)
except Exception as e:
print(f"Exception occurred: {e}")
print ('It is now ', self.tx_freq)
# print('new_mode', new_mode, ' self.mode is: ', self.mode)
if new_mode != self.mode:
print("Mode before: {0}".format(self.mode))
self.set_mode(new_mode)
print("New mode after: {0}".format(self.mode))
# Check if TX is enabled
if StatusPacket.Transmitting == 1:
# Check time, avoid transmitting out of the time slot
current_time = datetime.datetime.now()
utc_time = current_time.astimezone(datetime.timezone.utc)
self.tx_now = self.check_time_window(utc_time)
if self.tx_now:
self.command_sock.sendto(b'p', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
message = StatusPacket.TxMessage
message = message.replace('<', '')
message = message.replace('>', '')
print('Message is: ', message)
self.new_msg(message.strip())
print("Encoded message is: ", self.current_msg)
# print("tx_now is: ", self.tx_now)
if self.tx_now:
print('Transmitting')
self.transmit()
print("Time: {0}:{1}:{2}".format(utc_time.hour, utc_time.minute, utc_time.second))
else:
pass
# print('Not Transmitting')
return BaseHardware.HeartBeat(self)
except Exception as e:
# print("Error in HeartBeat")
# print(f"Exception type: {type(e).__name__}")
# print(f"Exception message: {str(e)}")
# traceback.print_exc()
return BaseHardware.HeartBeat(self)
+3 -9
View File
@@ -4,14 +4,8 @@ import os
# This file is used to build the Linux and Mac versions of Quisk. Windows builds are not included.
# You must define the version here. A title string including
# the version will be written to __init__.py and read by quisk.py.
Version = '4.2.52'
fp = open("__init__.py", "w") # write title string
fp.write("#Quisk version %s\n" % Version)
fp.write("from .quisk import main\n")
fp = open('__init__.py') # Read in the version
Version = fp.readline().strip()[15:]
fp.close()
sources = ['quisk.c', 'sound.c', 'is_key_down.c', 'microphone.c', 'utility.c',
@@ -88,7 +82,7 @@ N1MM+ and software that uses Hamlib.
'quisk.hermes', 'quisk.hiqsdr', 'quisk.afedrinet', 'quisk.soapypkg',
'quisk.sdrmicronpkg', 'quisk.perseuspkg', 'quisk.ac2yd', 'quisk.multuspkg'],
package_dir = {'quisk' : '.'},
package_data = {'' : ['*.txt', '*.html', '*.so', '*.dll']},
package_data = {'' : ['*.txt', '*.html', '*.so', '*.dll'], 'quisk' : ['icons/*']},
entry_points = {'gui_scripts' : ['quisk = quisk.quisk:main', 'quisk_vna = quisk.quisk_vna:main']},
ext_modules = Modules,
provides = ['quisk'],
+21 -4
View File
@@ -1,7 +1,24 @@
MODULE_NAME := soapy
OBJDIR := ../build/local
soapy2:
python2 setup.py build_ext --force --inplace
CFLAGS := $(shell python3 ../makefil.py --cflags)
LDFLAGS := $(shell python3 ../makefil.py --ldflags)
EXTENSION_SUFFIX := $(shell python3 ../makefil.py --suffix)
TARGET := $(MODULE_NAME)$(EXTENSION_SUFFIX)
soapy3:
# Default target: Link the shared library object
$(TARGET): soapy.o $(OBJDIR)/import_quisk_api.o
$(CC) -o $@ soapy.o $(LDFLAGS) -lSoapySDR $(OBJDIR)/import_quisk_api.o
# Compile the C source files into object files
soapy.o: soapy.c ../quisk.h ../filter.h
$(CC) -o $@ -c $< $(CFLAGS) -I..
.PHONY: clean
clean:
-rm -f *.o
-rm -f $(MODULE_NAME)*.so
.PHONY: soapy-old
soapy-old:
python3 setup.py build_ext --force --inplace
+8 -7
View File
@@ -1,5 +1,6 @@
#include <Python.h>
#include <complex.h>
#include <string.h>
#include <SoapySDR/Device.h>
#include <SoapySDR/Formats.h>
#include <SoapySDR/Version.h>
@@ -223,7 +224,7 @@ static PyObject * open_device(PyObject * self, PyObject * args)
else {
snprintf(buf128, 128, "SoapySDRDevice_make fail: %s", SoapySDRDevice_lastError());
}
return PyString_FromString(buf128);
return PyUnicode_FromString(buf128);
}
static void get_direc_len(const char * name, int * direction, int * length)
@@ -254,7 +255,7 @@ static PyObject * get_device_list(PyObject * self, PyObject * args) // Called fr
for (i = 0; i < length; i++) {
dict = PyDict_New();
for (j = 0; j < results[i].size; j++)
PyDict_SetItemString(dict, results[i].keys[j], PyString_FromString(results[i].vals[j]));
PyDict_SetItemString(dict, results[i].keys[j], PyUnicode_FromString(results[i].vals[j]));
PyList_Append(devices, dict);
Py_DECREF(dict);
}
@@ -329,7 +330,7 @@ static PyObject * set_parameter(PyObject * self, PyObject * args) // Called from
}
}
if (msg200[0])
return PyString_FromString(msg200);
return PyUnicode_FromString(msg200);
Py_INCREF (Py_None);
return Py_None;
}
@@ -377,7 +378,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from
pylist = PyList_New(0);
names = SoapySDRDevice_listAntennas(sdev, direction, 0, &len_list);
for (i = 0; i < len_list; i++) {
pyobj = PyString_FromString(names[i]);
pyobj = PyUnicode_FromString(names[i]);
PyList_Append(pylist, pyobj);
Py_DECREF(pyobj);
}
@@ -440,7 +441,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from
pylist = PyList_New(0);
names = SoapySDRDevice_listGains(sdev, direction, 0, &len_list);
for (i = 0; i < len_list; i++) {
pyobj = PyString_FromString(names[i]);
pyobj = PyUnicode_FromString(names[i]);
PyList_Append(pylist, pyobj);
Py_DECREF(pyobj);
}
@@ -450,7 +451,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from
else if ( ! strncmp(name, "soapy_listGainsValues", length)) {
pylist = PyList_New(0);
pylst2 = PyList_New(0); // First element is the total gain
pyobj = PyString_FromString("total");
pyobj = PyUnicode_FromString("total");
PyList_Append(pylst2, pyobj);
Py_DECREF(pyobj);
range = SoapySDRDevice_getGainRange(sdev, direction, 0);
@@ -460,7 +461,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from
names = SoapySDRDevice_listGains(sdev, direction, 0, &len_list);
for (i = 0; i < len_list; i++) {
pylst2 = PyList_New(0);
pyobj = PyString_FromString(names[i]);
pyobj = PyUnicode_FromString(names[i]);
PyList_Append(pylst2, pyobj);
Py_DECREF(pyobj);
range = SoapySDRDevice_getGainElementRange(sdev, direction, 0, names[i]);
+3
View File
@@ -4,6 +4,7 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <complex.h>
#include <string.h>
#include <math.h>
#include <sys/time.h>
#include <time.h>
@@ -986,6 +987,8 @@ int quisk_read_sound(void) // Called from sound thread
// Perhaps replace the samples with samples from a file
if (quisk_record_state == FILE_PLAY_SAMPLES)
quisk_play_samples(cSamples, nSamples);
// Send radio IQ samples to TCI
tci_send_iq(cSamples, nSamples);
#if ! DEBUG_MIC
nSamples = quisk_process_samples(cSamples, nSamples);
#endif
+5 -4
View File
@@ -2,6 +2,7 @@
* This modue provides sound access for QUISK using the ALSA
* library for Linux.
*/
#include <string.h>
#ifdef QUISK_HAVE_ALSA
#include <Python.h>
#include <complex.h>
@@ -454,7 +455,7 @@ static int device_list(PyObject * py, snd_pcm_stream_t stream, char * name)
}
snprintf(buf100, 100, "%s %s (hw:%d,%d)", card_text, pcm_text, card, dev);
if (py) { // add to list of devices
PyList_Append(py, PyString_FromString(buf100));
PyList_Append(py, PyUnicode_FromString(buf100));
}
if (name) { // return the "hw:" name
if (strstr(buf100, name)) {
@@ -1129,10 +1130,10 @@ static void midi_in_devices(PyObject * pylist, int just_names)
}
else {
pytup = PyTuple_New(2);
PyList_Append(pylist, pytup);
PyTuple_SET_ITEM(pytup, 0, PyUnicode_DecodeUTF8(friendly_name, strlen(friendly_name), "replace"));
PyTuple_SetItem(pytup, 0, PyUnicode_DecodeUTF8(friendly_name, strlen(friendly_name), "replace"));
snprintf(card_name, 32, "hw:%d,%d,%d", card, device, sub);
PyTuple_SET_ITEM(pytup, 1, PyUnicode_DecodeUTF8(card_name, strlen(card_name), "replace"));
PyTuple_SetItem(pytup, 1, PyUnicode_DecodeUTF8(card_name, strlen(card_name), "replace"));
PyList_Append(pylist, pytup);
}
}
}
+3 -8
View File
@@ -1,3 +1,5 @@
#include <string.h>
#ifdef QUISK_HAVE_DIRECTX
#include <Python.h>
@@ -55,14 +57,7 @@ static int match_name(LPCTSTR lpszDesc, const char * name)
Py_DECREF(py_unicode);
return 0;
}
#if PY_MAJOR_VERSION >= 3
if (PyUnicode_READY(py_unicode) == 0)
length = PyUnicode_GET_LENGTH(py_unicode);
else
length = 0;
#else
length = PyUnicode_GET_SIZE(py_unicode);
#endif
length = PyUnicode_GetLength(py_unicode);
if (length <= 0) {
Py_DECREF(py_unicode);
Py_DECREF(py_substring);
+3 -1
View File
@@ -1,6 +1,8 @@
/*
* This module provides sound access for QUISK using the portaudio library.
*/
#include <string.h>
#ifdef QUISK_HAVE_PORTAUDIO
#include <Python.h>
@@ -475,7 +477,7 @@ static int device_list(PyObject * py, int input)
// found one
if (py) {
snprintf(buf100, 100, "%s", info->name);
PyList_Append(py, PyString_FromString(buf100));
PyList_Append(py, PyUnicode_FromString(buf100));
}
}
}
+8 -6
View File
@@ -34,6 +34,8 @@
* 2020 Apr by N2ADR Changes in printf's
*/
#include <string.h>
#ifdef QUISK_HAVE_PULSEAUDIO
#include <Python.h>
@@ -1030,20 +1032,20 @@ static void source_sink(const char * name, const char * descr, pa_proplist * pro
PyObject * pytup;
pytup = PyTuple_New(3);
PyList_Append(pylist, pytup);
PyTuple_SET_ITEM(pytup, 0, PyString_FromString(name));
PyTuple_SET_ITEM(pytup, 1, PyString_FromString(descr));
PyTuple_SetItem(pytup, 0, PyUnicode_FromString(name));
PyTuple_SetItem(pytup, 1, PyUnicode_FromString(descr));
value = pa_proplist_gets(props, "device.api");
if (value && ! strcmp(value, "alsa")) {
snprintf(buf300, 300, "%s %s (hw:%s,%s)", pa_proplist_gets(props, "alsa.card_name"), pa_proplist_gets(props, "alsa.name"),
pa_proplist_gets(props, "alsa.card"), pa_proplist_gets(props, "alsa.device"));
PyTuple_SET_ITEM(pytup, 2, PyString_FromString(buf300));
PyTuple_SetItem(pytup, 2, PyUnicode_FromString(buf300));
}
else {
PyTuple_SET_ITEM(pytup, 2, PyString_FromString(""));
PyTuple_SetItem(pytup, 2, PyUnicode_FromString(""));
}
PyList_Append(pylist, pytup);
}
@@ -1074,7 +1076,7 @@ static void index_callback(pa_context *c, uint32_t idx, void *userdata) {
//printf("%u\n", idx);
}
PyObject * quisk_pulseaudio_sound_devices(PyObject * self, PyObject * args)
PyObject * quisk_pulseaudio_sound_devices(PyObject * self, PyObject * args) // Called from the GUI thread
{ // Return a list of PulseAudio device names [pycapt, pyplay]
PyObject * pylist, * pycapt, * pyplay;
pa_mainloop *pa_names_ml;
+9 -6
View File
@@ -7,6 +7,9 @@
* Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!!
*/
#include <string.h>
#ifdef QUISK_HAVE_WASAPI
#define UNICODE
@@ -1125,10 +1128,10 @@ PyObject * quisk_wasapi_sound_devices(PyObject * self, PyObject * args) // Call
EXIT_ON_ERROR(hr)
// data items are (name, id, is_raw)
pytup = PyTuple_New(3);
PyTuple_SetItem(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1));
PyTuple_SetItem(pytup, 1, PyUnicode_FromWideChar(pwszID, -1));
PyTuple_SetItem(pytup, 2, PyInt_FromLong(1));
PyList_Append(pyplay, pytup);
PyTuple_SET_ITEM(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1));
PyTuple_SET_ITEM(pytup, 1, PyUnicode_FromWideChar(pwszID, -1));
PyTuple_SET_ITEM(pytup, 2, PyInt_FromLong(1));
CoTaskMemFree(pwszID);
pwszID = NULL;
PropVariantClear(&varName);
@@ -1155,10 +1158,10 @@ PyObject * quisk_wasapi_sound_devices(PyObject * self, PyObject * args) // Call
EXIT_ON_ERROR(hr)
// data items are (name, id, is_raw)
pytup = PyTuple_New(3);
PyTuple_SetItem(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1));
PyTuple_SetItem(pytup, 1, PyUnicode_FromWideChar(pwszID, -1));
PyTuple_SetItem(pytup, 2, PyInt_FromLong(1));
PyList_Append(pycapt, pytup);
PyTuple_SET_ITEM(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1));
PyTuple_SET_ITEM(pytup, 1, PyUnicode_FromWideChar(pwszID, -1));
PyTuple_SET_ITEM(pytup, 2, PyInt_FromLong(1));
CoTaskMemFree(pwszID);
pwszID = NULL;
PropVariantClear(&varName);
+296 -51
View File
@@ -2,36 +2,37 @@
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include "ws.h"
#include <pthread.h>
#include <complex.h>
#include <time.h>
#include "quisk.h"
#include "filter.h"
#define TCI_STREAM_DATA_BYTES 16384
#define TCI_RX_BUF_SIZE 1024
#define TCI_COMMAND_SIZE 32
#define TCI_COMMAND_SIZE 64
#define BAD_TCI_IF 999999999
static int verbose; // non-zero for verbose log messages
static int tci_port;
// Implement the ExpertSDR2 Version 1.4 protocol:
// The only sample rate is 48000. The Tx audio parameters are copied from the Rx audio stream.
// The Tx audio parameters are copied from the Rx audio stream.
// The only sample type is FLOAT32 type 3.
// WSJT-X RX_AUDIO_STREAM must be two channels and Stream.length is the number of floats (not samples).
// WSJT-X TX_AUDIO_STREAM always returns two channels and Stream.length is the number of floats (not samples).
// WSJT-X TX_AUDIO_STREAM is twice as long, but the data is in the first half and garbage is in the second half.
// WSJT-X TX_CHRONO Stream.length is the number of floats, and the returned TX_AUDIO_STREAM length is equal to it.
// It is unclear whether Stream.length is the number of floats, or half that for two channels. And version 1.4 lacks Stream.channels.
// The protocol name for TCI version 1.4 is "protocol:ESDR,1.4;" and the port is 40001.
// The protocol name for TCI version 2.0 is "protocol:ExpertSDR3,2.0;" and the port is 50001.
// The TCI modulations are: am, sam, dsb, lsb, usb, cw, nfm, wfm, spec, digl, digu, drm.
// These are TCI parameters:
//static int64_t quisk_dds;
//static int quisk_if;
static long long quisk_dds;
static long long client_dds=-1;
static long long quisk_vfo;
static long long client_vfo=-1;
static int quisk_if;
static int client_if=BAD_TCI_IF;
static int quisk_trx;
static int client_trx=-1;
static int quisk_split_enable;
@@ -79,6 +80,16 @@ struct ClientData { // data for each client
int audio_stream_channels;
int audio_stream_samples;
int audio_stream_bytes_per_sample;
// Rx stream decimation filters
struct quisk_cHB45Filter HalfBand1;
struct quisk_cFilter filtDecim3;
struct quisk_cFilter filtDecim48to24;
// Tx stream interpolation filters
struct quisk_cHB45Filter HalfBandTx1;
struct quisk_cHB45Filter HalfBandTx2;
struct quisk_cFilter filtTxInterp3;
int send_IQ_stream;
int IQ_stream_samplerate;
} ;
struct _Stream {
@@ -132,11 +143,14 @@ struct _StreamTypes StreamTypes[NUM_STREAM_TYPES] = {
{"lineout ", 0}
} ;
static void PrintStream(const char * msg, size_t size)
static int interpolate_tx_audio_stream(complex double * cSamples, int nSamples, struct ClientData * ctx);
static int decimate_audio_stream(complex double * cSamples, int nSamples, struct ClientData * ctx);
static void PrintStream(const char * msg, size_t size, int always)
{
struct _Stream * pt = (struct _Stream *)msg;
if (time(NULL) - StreamTypes[pt->type].when > 5) {
if (always || time(NULL) - StreamTypes[pt->type].when > 5) {
StreamTypes[pt->type].when = time(NULL);
if (pt->type == TX_AUDIO_STREAM)
QuiskPrintf("TCI Receiving %slength %5d:", StreamTypes[pt->type].name, (int)size);
@@ -164,7 +178,7 @@ static int sendframe_txt_bcast(uint16_t port, const char * msg)
static size_t sendframe_bin(ws_cli_conn_t client, const char * msg, size_t size)
{
if (verbose)
PrintStream(msg, size);
PrintStream(msg, size, 0);
return ws_sendframe_bin(client, msg, size);
}
@@ -177,8 +191,12 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx)
if (verbose)
QuiskPrintf("TCI Received text %s\n", ctx->msg_buf);
for (i = 0; ctx->msg_buf[i]; i++)
//QuiskPrintf("TCI Received text %s %20.6lf\n", ctx->msg_buf, QuiskTimeSec());
for (i = 0; ctx->msg_buf[i]; i++) {
char_buf[i] = ctx->msg_buf[i]; // make a copy of the command
ctx->msg_buf[i] = tolower(ctx->msg_buf[i]);
}
char_buf[i] = 0;
command = strtok_r(ctx->msg_buf, ":", &saveptr);
if (command == NULL)
return 0;
@@ -193,10 +211,20 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx)
for (i = 0; i < tci_clients_count; i++) {
if (tci_clients_list[i] == client) {
ctx->send_Rx_audio_stream = 1;
// Rx stream filters
memset(&ctx->HalfBand1,
0, sizeof(struct quisk_cHB45Filter));
quisk_filt_cInit(&ctx->filtDecim3,
quiskFilt144D3Coefs, sizeof(quiskFilt144D3Coefs)/sizeof(double));
quisk_filt_cInit(&ctx->filtDecim48to24,
quiskFilt48dec24Coefs, sizeof(quiskFilt48dec24Coefs)/sizeof(double));
break;
}
}
pthread_mutex_unlock(&clients_list_mutex);
sendframe_txt(client, char_buf);
return 0;
}
else if (strcmp(command, "audio_stop") == 0) {
pthread_mutex_lock(&clients_list_mutex);
@@ -207,19 +235,24 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx)
}
}
pthread_mutex_unlock(&clients_list_mutex);
sendframe_txt(client, char_buf);
return 0;
}
else if (strcmp(command, "audio_stream_sample_type") == 0) {
if (strcmp(arg1, "float32") == 0) {
ctx->audio_stream_sample_type = TCI_FLOAT32;
ctx->audio_stream_bytes_per_sample = 4;
sendframe_txt(client, char_buf);
}
else {
return 0;
}
return 0;
}
else if (strcmp(command, "audio_samplerate") == 0) {
if (atoi(arg1) != 48000)
i = atoi(arg1);
if (i != 48000 && i != 24000 && i != 12000 && i != 8000)
return 0;
ctx->audio_stream_samplerate = i;
sendframe_txt(client, char_buf);
return 0;
}
else if (strcmp(command, "audio_stream_channels") == 0) {
if (strcmp(arg1, "1") == 0)
@@ -228,23 +261,74 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx)
ctx->audio_stream_channels = 2;
else
return 0;
sendframe_txt(client, char_buf);
return 0;
}
else if (strcmp(command, "audio_stream_samples") == 0) {
return 0; // do not send
}
break;
case 'd':
if (strcmp(command, "dds") == 0) {
if (arg2) {
client_dds = atoll(arg2);
// Echo comes from quisk_tci_set_params()
}
else {
snprintf(char_buf, TCI_COMMAND_SIZE, "dds:0,%lld;", quisk_dds);
sendframe_txt(client, char_buf);
}
return 0;
}
break;
case 'i':
if (strcmp(command, "iq_start") == 0)
return 0; // do not send
else if (strcmp(command, "iq_stop") == 0)
return 0; // do not send
else if (strcmp(command, "iq_samplerate") == 0)
return 0; // do not send
if (strcmp(command, "iq_start") == 0) {
pthread_mutex_lock(&clients_list_mutex);
for (i = 0; i < tci_clients_count; i++) {
if (tci_clients_list[i] == client) {
ctx->send_IQ_stream = 1;
break;
}
}
pthread_mutex_unlock(&clients_list_mutex);
sendframe_txt(client, char_buf);
return 0;
}
else if (strcmp(command, "iq_stop") == 0) {
pthread_mutex_lock(&clients_list_mutex);
for (i = 0; i < tci_clients_count; i++) {
if (tci_clients_list[i] == client) {
ctx->send_IQ_stream = 0;
break;
}
}
pthread_mutex_unlock(&clients_list_mutex);
sendframe_txt(client, char_buf);
return 0;
}
else if (strcmp(command, "iq_samplerate") == 0) {
ctx->IQ_stream_samplerate = quisk_sound_state.sample_rate;
snprintf(char_buf, TCI_COMMAND_SIZE, "iq_samplerate:%d;", quisk_sound_state.sample_rate);
sendframe_txt(client, char_buf);
return 0;
}
else if (strcmp(command, "if") == 0) {
if (arg3) {
client_if = atoi(arg3);
// Echo comes from quisk_tci_set_params() UNIMPLEMENTED
}
else {
snprintf(char_buf, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if);
sendframe_txt(client, char_buf);
}
return 0;
}
break;
case 'm':
if (strcmp(command, "modulation") == 0) {
if (arg2) {
strncpy(client_modulation, arg2, TCI_COMMAND_SIZE - 1);
// Echo comes from quisk_tci_set_params()
}
else {
snprintf(char_buf, TCI_COMMAND_SIZE, "modulation:0,%.9s;", quisk_modulation);
@@ -260,6 +344,7 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx)
client_split_enable = 1;
else
client_split_enable = 0;
// Echo comes from quisk_tci_set_params()
}
else {
if (quisk_split_enable)
@@ -285,12 +370,20 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx)
tci_tx_audio_samples = 0;
tci_tx_audio_rate = ctx->audio_stream_samplerate;
pthread_mutex_unlock(&tx_buffer_mutex);
// Tx stream filters
memset(&ctx->HalfBandTx1,
0, sizeof(struct quisk_cHB45Filter));
memset(&ctx->HalfBandTx2,
0, sizeof(struct quisk_cHB45Filter));
quisk_filt_cInit(&ctx->filtTxInterp3,
quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double));
}
}
else if (client == tci_tx_audio_client) {
client_trx = 0;
tci_tx_audio_client = 0;
}
// Echo comes from quisk_tci_set_params()
}
else {
if (quisk_trx)
@@ -308,6 +401,7 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx)
if (strcmp(command, "vfo") == 0) {
if (arg3) {
client_vfo = atoll(arg3);
// Echo comes from quisk_tci_set_params()
}
else {
snprintf(char_buf, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo);
@@ -330,7 +424,9 @@ static void onopen(ws_cli_conn_t client)
ctx = malloc(sizeof(struct ClientData));
ctx->msg_length = 0;
ctx->send_Rx_audio_stream = 0;
ctx->send_IQ_stream = 0;
ctx->audio_stream_samplerate = 48000;
ctx->IQ_stream_samplerate = quisk_sound_state.sample_rate;
ctx->audio_stream_sample_type = TCI_FLOAT32;
ctx->audio_stream_channels = 2;
ctx->audio_stream_samples = 0;
@@ -346,9 +442,11 @@ static void onopen(ws_cli_conn_t client)
QuiskPrintf("TCI *Connection opened, addr: %s\n", cli);
}
sendframe_txt(client, "protocol:ESDR,1.4;");
sendframe_txt(client, "protocol:ExpertSDR3,2.0;");
//sendframe_txt(client, "vfo_limits:30000,30000000;"); Not known due to transverters
//sendframe_txt(client, "if_limits:-48000,48000;");
int if_limit = quisk_sound_state.sample_rate / 2;
snprintf(command, TCI_COMMAND_SIZE, "if_limits:%d,%d;", - if_limit, if_limit);
sendframe_txt(client, command);
sendframe_txt(client, "trx_count:1;");
sendframe_txt(client, "channel_count:1;");
sendframe_txt(client, "device:QuiskSDR;");
@@ -358,10 +456,10 @@ static void onopen(ws_cli_conn_t client)
sendframe_txt(client, "start;");
sendframe_txt(client, "tx_enable:0,true;");
//snprintf(command, TCI_COMMAND_SIZE, "dds:0,%d;", quisk_dds);
//sendframe_txt(client, command);
//snprintf(command, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if);
//sendframe_txt(client, command);
snprintf(command, TCI_COMMAND_SIZE, "dds:0,%lld;", quisk_dds);
sendframe_txt(client, command);
snprintf(command, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if);
sendframe_txt(client, command);
snprintf(command, TCI_COMMAND_SIZE, "modulation:0,%.9s;", quisk_modulation);
sendframe_txt(client, command);
snprintf(command, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo);
@@ -398,12 +496,14 @@ static void onclose(ws_cli_conn_t client)
static void onmessage(ws_cli_conn_t client, const unsigned char *msg, uint64_t size, int type)
{
char echo_copy[TCI_RX_BUF_SIZE];
complex double sample;
int n;
static complex double * cCopy = NULL;
static int cCopy_size = 0;
struct ClientData * ctx = ws_get_connection_context(client);
if (type == WS_FR_OP_TXT) {
if (size > TCI_RX_BUF_SIZE / 2)
return; // prevent overflow
struct ClientData * ctx = ws_get_connection_context(client);
if (ctx->msg_length + size >= TCI_RX_BUF_SIZE - 2)
ctx->msg_length = 0; // prevent overflow
memcpy(ctx->msg_buf + ctx->msg_length, msg, size); // append msg to buffer
@@ -437,7 +537,6 @@ static void onmessage(ws_cli_conn_t client, const unsigned char *msg, uint64_t s
const unsigned char * vpt2;
const unsigned char * vpt2_end;
int count2;
int buf_start=0, buf_end;
if (verbose >= 3) {
QuiskPrintf("\n\nMSG size %ld length %d\n 0 ", size, pt->length);
@@ -462,16 +561,20 @@ static void onmessage(ws_cli_conn_t client, const unsigned char *msg, uint64_t s
}
if (pt->type == TX_AUDIO_STREAM) {
n = pt->length * 7 + 100; // Make room for 6X interpolation
if (cCopy_size < n) {
if (cCopy)
free(cCopy);
cCopy_size = n;
cCopy = (complex double *)malloc(cCopy_size * sizeof(complex double));
}
if (pt->length > 0 && client == tci_tx_audio_client) {
pthread_mutex_lock(&tx_buffer_mutex);
if (verbose >= 2)
buf_start = CircularBuffer(0, &sample, 0, 0);
n = 0;
switch (TxChrono.format) {
case TCI_INT16:
break;
case TCI_FLOAT32:
// Version 1.4 does not return the number of channels. We assume two channels.
// pt->length is the number of floats in WSJT-X.
// pt->length is the number of floats
while (vpt < vpt_end && count < pt->length) {
re = *(float *)vpt;
vpt += sizeof(float);
@@ -484,22 +587,20 @@ static void onmessage(ws_cli_conn_t client, const unsigned char *msg, uint64_t s
else {
im = re;
}
sample = (re + I * im) * (CLIP32 / 2);
CircularBuffer(0, &sample, 0, 1);
cCopy[n++] = (re + I * im) * (CLIP32 / 2);
}
if (verbose && count != pt->length)
QuiskPrintf("TCI *TX count %ld %d %d\n", size, count, pt->length);
n = interpolate_tx_audio_stream(cCopy, n, ctx);
pthread_mutex_lock(&tx_buffer_mutex);
CircularBuffer(0, cCopy, 0, n);
pthread_mutex_unlock(&tx_buffer_mutex);
break;
}
if (verbose >= 2) {
buf_end = CircularBuffer(0, &sample, 0, 0);
QuiskPrintf("TCI Tx buffer add %4d start %5d end %5d\n", count, buf_start, buf_end);
}
pthread_mutex_unlock(&tx_buffer_mutex);
}
}
if (verbose)
PrintStream((const char *)msg, size);
PrintStream((const char *)msg, size, 0);
}
@@ -529,11 +630,60 @@ static void tci_startup(void)
return;
}
static int decimate_audio_stream(complex double * cSamples, int nSamples, struct ClientData * ctx)
{
switch(ctx->audio_stream_samplerate) { // Starting audio rate is 48000
case 48000:
break;
case 24000:
nSamples = quisk_cDecimate(cSamples, nSamples, &ctx->filtDecim48to24, 2);
break;
case 12000:
nSamples = quisk_cDecim2HB45(cSamples, nSamples, &ctx->HalfBand1);
nSamples = quisk_cDecimate(cSamples, nSamples, &ctx->filtDecim48to24, 2);
break;
case 8000:
nSamples = quisk_cDecim2HB45(cSamples, nSamples, &ctx->HalfBand1);
nSamples = quisk_cDecimate(cSamples, nSamples, &ctx->filtDecim3, 3);
break;
default:
nSamples = 0;
break;
}
return nSamples;
}
static int interpolate_tx_audio_stream(complex double * cSamples, int nSamples, struct ClientData * ctx)
{
switch(tci_tx_audio_rate) { // Ending audio rate is 48000
case 48000:
break;
case 24000:
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &ctx->HalfBandTx1);
break;
case 12000:
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &ctx->HalfBandTx1);
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &ctx->HalfBandTx2);
break;
case 8000:
nSamples = quisk_cInterpolate(cSamples, nSamples, &ctx->filtTxInterp3, 3);
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &ctx->HalfBandTx1);
break;
default:
nSamples = 0;
break;
}
return nSamples;
}
void tci_send_audio(complex double * cSamples, int nSamples) // called from the sound thread
{
int i, n;
struct _Stream stream;
struct ClientData * ctx;
static complex double * cCopy = NULL;
static int cCopy_size = 0;
complex double * cAudio;
if (tci_clients_count <= 0)
return;
@@ -555,15 +705,29 @@ void tci_send_audio(complex double * cSamples, int nSamples) // called from the
int bytes_per_sample = ctx->audio_stream_bytes_per_sample;
void * vpt = &stream.data;
void * vpt_end = vpt + TCI_STREAM_DATA_BYTES;
if (ctx->audio_stream_samplerate != 48000) { // decimation must not change cSamples
if (cCopy_size < nSamples) {
if (cCopy)
free(cCopy);
cCopy_size = nSamples * 2;
cCopy = (complex double *)malloc(cCopy_size * sizeof(complex double));
}
memcpy(cCopy, cSamples, nSamples * sizeof(complex double));
nSamples = decimate_audio_stream(cCopy, nSamples, ctx);
cAudio = cCopy;
}
else {
cAudio = cSamples;
}
switch (ctx->audio_stream_sample_type) {
case TCI_FLOAT32:
stream.length = 0; // make a frame
for (i = 0; i < nSamples; i++) {
*(float *)vpt = (float)(creal(cSamples[i]) * (1.0 / 2147483648.0 / 2));
*(float *)vpt = (float)(creal(cAudio[i]) * (1.0 / 2147483648.0 / 2));
vpt += bytes_per_sample;
stream.length++;
if (two_channels) {
*(float *)vpt = (float)(cimag(cSamples[i]) * (1.0 / 2147483648.0 / 2));
*(float *)vpt = (float)(cimag(cAudio[i]) * (1.0 / 2147483648.0 / 2));
vpt += bytes_per_sample;
stream.length++;
}
@@ -571,6 +735,7 @@ void tci_send_audio(complex double * cSamples, int nSamples) // called from the
sendframe_bin(tci_clients_list[n], (const char *)&stream,
(16 * sizeof(uint32_t) + stream.length * bytes_per_sample));
stream.length = 0;
vpt = &stream.data;
}
}
break;
@@ -579,6 +744,58 @@ void tci_send_audio(complex double * cSamples, int nSamples) // called from the
pthread_mutex_unlock(&clients_list_mutex);
}
void tci_send_iq(complex double * cSamples, int nSamples) // called from the sound thread
{
int i, n;
struct _Stream stream;
struct ClientData * ctx;
if (tci_clients_count <= 0)
return;
if (nSamples <= 0)
return;
pthread_mutex_lock(&clients_list_mutex);
for (n = 0; n < tci_clients_count; n++) {
ctx = ws_get_connection_context(tci_clients_list[n]);
if (ctx->send_IQ_stream == 0)
continue;
memset(&stream, 0, 16 * sizeof(uint32_t));
stream.sample_rate = ctx->IQ_stream_samplerate;
stream.format = ctx->audio_stream_sample_type;
stream.type = IQ_STREAM;
stream.channels = 2;
int bytes_per_sample = ctx->audio_stream_bytes_per_sample;
void * vpt = &stream.data;
void * vpt_end = vpt + TCI_STREAM_DATA_BYTES;
int original_length = nSamples;
switch (ctx->audio_stream_sample_type) {
case TCI_FLOAT32:
stream.length = 0; // make a frame
for (i = 0; i < nSamples; i++) {
*(float *)vpt = (float)(creal(cSamples[i]) * (1.0 / 2147483648.0 / 2));
vpt += bytes_per_sample;
stream.length++;
*(float *)vpt = (float)(cimag(cSamples[i]) * (1.0 / 2147483648.0 / 2));
vpt += bytes_per_sample;
stream.length++;
if (vpt >= vpt_end || i == nSamples - 1) {
sendframe_bin(tci_clients_list[n], (const char *)&stream,
(16 * sizeof(uint32_t) + stream.length * bytes_per_sample));
original_length -= stream.length / 2;
stream.length = 0;
vpt = &stream.data;
}
}
break;
}
if (verbose && original_length != 0)
QuiskPrintf("Error in tci_send_iq: Samples remain %d\n", original_length);
}
pthread_mutex_unlock(&clients_list_mutex);
}
int tci_get_mic(complex double * cSamples, int mic_count) // called from the sound thread
{
if (tci_tx_audio_client) {
@@ -608,16 +825,17 @@ int tci_get_mic(complex double * cSamples, int mic_count) // called from the sou
PyObject * quisk_tci_set_params(PyObject * self, PyObject * args, PyObject * keywds) // called from the GUI thread
{ /* Call with keyword arguments ONLY.
Sent from Quisk when parameters change. Broadcast the change to clients.*/
static char * kwlist[] = {"start", "verbose", "tci_dds", "tci_if", "tci_vfo", "tci_trx", "tci_split_enable",
static char * kwlist[] = {"start", "close", "verbose", "tci_dds", "tci_if", "tci_vfo", "tci_trx", "tci_split_enable",
"tci_modulation", NULL} ;
long long new_vfo=-1;
long long new_dds=-1;
int start=-1, new_if=-1, new_trx=-1, new_split_enable=-1;
int start=-1, close=-1, new_trx=-1, new_split_enable=-1;
int new_if = BAD_TCI_IF;
char * mode=NULL;
char char_buf[TCI_COMMAND_SIZE];
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiLiLiis", kwlist,
&start, &verbose, &new_dds, &new_if, &new_vfo, &new_trx, &new_split_enable,
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiLiLiis", kwlist,
&start, &close, &verbose, &new_dds, &new_if, &new_vfo, &new_trx, &new_split_enable,
&mode))
return NULL;
if (start == 1) {
@@ -628,6 +846,19 @@ PyObject * quisk_tci_set_params(PyObject * self, PyObject * args, PyObject * key
Py_INCREF (Py_None);
return Py_None;
}
if (close != -1) {
sendframe_txt_bcast(tci_port, "stop;");
}
if (new_dds != -1) {
quisk_dds = new_dds;
snprintf(char_buf, TCI_COMMAND_SIZE, "dds:0,%lld;", quisk_dds);
sendframe_txt_bcast(tci_port, char_buf);
}
if (new_if != BAD_TCI_IF) {
quisk_if = new_if;
snprintf(char_buf, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if);
sendframe_txt_bcast(tci_port, char_buf);
}
if (new_vfo != -1) {
quisk_vfo = new_vfo;
snprintf(char_buf, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo);
@@ -683,6 +914,10 @@ PyObject * quisk_tci_get_params(PyObject * self, PyObject * args) // called from
if (!PyArg_ParseTuple (args, "s", &name))
return NULL;
if (strcmp(name, "tci_clients_count") == 0)
return PyLong_FromLong(tci_clients_count);
if (tci_started == 0) {
}
else if (strcmp(name, "tci_vfo") == 0 && client_vfo >= 0) {
@@ -690,6 +925,16 @@ PyObject * quisk_tci_get_params(PyObject * self, PyObject * args) // called from
client_vfo = -1;
return PyLong_FromLongLong(ll);
}
else if (strcmp(name, "tci_dds") == 0 && client_dds >= 0) {
ll = client_dds;
client_dds = -1;
return PyLong_FromLongLong(ll);
}
else if (strcmp(name, "tci_if") == 0 && client_if != BAD_TCI_IF) {
ii = client_if;
client_if = BAD_TCI_IF;
return PyLong_FromLong(ii);
}
else if (strcmp(name, "tci_split_enable") == 0 && client_split_enable >= 0) {
ii = client_split_enable;
client_split_enable = -1;
@@ -718,7 +963,7 @@ PyObject * quisk_tci_get_params(PyObject * self, PyObject * args) // called from
strcpy(char_buf, "FM");
client_modulation[0] = 0;
if (char_buf[0])
return PyString_FromString(char_buf);
return PyUnicode_FromString(char_buf);
}
Py_INCREF (Py_None);
return Py_None;
+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)
+21 -33
View File
@@ -6,6 +6,7 @@
#include <sys/time.h>
#endif
#include <complex.h>
#include <string.h>
#include "quisk.h"
// Access to config file attributes.
@@ -71,40 +72,27 @@ double QuiskGetConfigDouble(const char * name, double deflt)
char * QuiskGetConfigString(const char * name, char * deflt)
{ // Return the UTF-8 configuration string. Return deflt for failure.
char * res;
PyObject * attr;
#if PY_MAJOR_VERSION < 3
static char retbuf[QUISK_SC_SIZE];
#endif
Py_ssize_t size;
if (!quisk_pyConfig || PyErr_Occurred())
return deflt;
attr = PyObject_GetAttrString(quisk_pyConfig, name);
if (attr) {
#if PY_MAJOR_VERSION >= 3
res = (char *)PyUnicode_AsUTF8(attr);
#else
if (PyUnicode_Check(attr)) {
PyObject * pystr = PyUnicode_AsUTF8String(attr);
strMcpy(retbuf, PyString_AsString(pystr), QUISK_SC_SIZE);
retbuf[QUISK_SC_SIZE - 1] = 0;
res = retbuf;
Py_DECREF(pystr);
}
else {
res = PyString_AsString(attr);
}
#endif
Py_DECREF(attr);
if (res)
return res; // success
else
PyErr_Clear();
}
else {
PyErr_Clear();
}
return deflt; // failure
if (!quisk_pyConfig || PyErr_Occurred())
return deflt; // failure
PyObject * attr = PyObject_GetAttrString(quisk_pyConfig, name);
if ( ! attr) {
PyErr_Clear();
return deflt; // failure
}
if ( ! PyUnicode_Check(attr)) {
Py_DECREF(attr);
return deflt; // failure
}
const char * utf8_str = PyUnicode_AsUTF8AndSize(attr, &size);
if ( ! utf8_str) {
Py_DECREF(attr);
PyErr_Clear();
return deflt; // failure
}
Py_DECREF(attr);
return (char *)utf8_str;
}
double QuiskTimeSec(void)