Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2085a8160 | ||
|
|
2b6b6fa9ea | ||
|
|
36210d4e4c | ||
|
|
eb9434c52a | ||
|
|
db928b6fcb | ||
|
|
a90f8b80bd | ||
|
|
faaeda54e5 | ||
|
|
c2304b23ee | ||
|
|
09154658ae | ||
|
|
20bd313041 | ||
|
|
d39722b170 | ||
|
|
c97ffa155e | ||
|
|
745882a506 | ||
|
|
674bbad1ab | ||
|
|
67c6fbcad9 | ||
|
|
84e194c644 | ||
|
|
b45a71b017 |
@@ -1,3 +1,32 @@
|
||||
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.
|
||||
It is unlikely that other digital software will connect without problems, but please test and let me know.
|
||||
|
||||
Quisk Version 4.2.51 May 2026
|
||||
==============================
|
||||
This version can use the TCI protocol to connect to WSJT-X. Please see Digital Modes in the documentation.
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
# Quisk Software Defined Radio by N2ADR
|
||||
**December 27, 2023**
|
||||
# Quisk Software Defined Radio
|
||||
|
||||
This is Quisk, a Software Defined Radio (SDR).
|
||||
You supply radio hardware such as Hermes Lite2 or SoftRock to convert
|
||||
the antenna voltage to I/Q samples.
|
||||
Then send the samples to a computer running Quisk.
|
||||
The Quisk software will read the I/Q data, tune it, filter it,
|
||||
demodulate it, and send the audio to headphones or speakers.
|
||||
Quisk has a microphone input and a key input so it can operate as a
|
||||
complete transceiver.
|
||||
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.
|
||||
|
||||
The web page for this project is [https://groups.io/g/n2adr-sdr](https://groups.io/g/n2adr-sdr).
|
||||
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.
|
||||
|
||||
The change log is [here](CHANGELOG.txt).
|
||||
|
||||
Installation instructions are [here](http://james.ahlstrom.name/quisk/docs.html#Installation).
|
||||
|
||||
The documentation is [here](http://james.ahlstrom.name/quisk/docs.html).
|
||||
|
||||
The help file is [here](http://james.ahlstrom.name/quisk/help.html).
|
||||
For more information see https://james.ahlstrom.name/quisk.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#Quisk version 4.2.51
|
||||
from .quisk import main
|
||||
#Quisk version 4.2.55
|
||||
from .quisk import main
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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_'):
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
#include <Python.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <complex.h>
|
||||
#include "quisk.h"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
After Width: | Height: | Size: 128 KiB |
@@ -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 |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 675 B |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
@@ -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,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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
#
|
||||
@@ -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)
|
||||
@@ -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.51'
|
||||
|
||||
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'],
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -199,14 +194,14 @@ static int quisk_open_capture(struct sound_dev * dev)
|
||||
DirectSoundCaptureEnumerate((LPDSENUMCALLBACK)DsEnumCapture, dev);
|
||||
if (errFound != DS_OK) {
|
||||
snprintf (dev->dev_errmsg, QUISK_SC_SIZE,
|
||||
"DirectSound capture device name %s not found", dev->name);
|
||||
"DirectSound capture device name %.200s not found", dev->name);
|
||||
if (quisk_sound_state.verbose_sound)
|
||||
QuiskPrintf("%s\n", dev->dev_errmsg);
|
||||
return 1;
|
||||
}
|
||||
if (errOpen != DS_OK) {
|
||||
snprintf (dev->dev_errmsg, QUISK_SC_SIZE,
|
||||
"DirectSound capture device %s open failed", dev->name);
|
||||
"DirectSound capture device %.200s open failed", dev->name);
|
||||
if (quisk_sound_state.verbose_sound)
|
||||
QuiskPrintf("%s\n", dev->dev_errmsg);
|
||||
return 1;
|
||||
@@ -226,7 +221,7 @@ static int quisk_open_capture(struct sound_dev * dev)
|
||||
}
|
||||
else {
|
||||
snprintf (dev->dev_errmsg, QUISK_SC_SIZE,
|
||||
"DirectSound capture device %s buffer create failed (0x%lX)", dev->name, hr);
|
||||
"DirectSound capture device %.200s buffer create failed (0x%lX)", dev->name, hr);
|
||||
return 1;
|
||||
}
|
||||
ptBuf = (LPDIRECTSOUNDCAPTUREBUFFER)dev->buffer;
|
||||
@@ -235,7 +230,7 @@ static int quisk_open_capture(struct sound_dev * dev)
|
||||
if (quisk_sound_state.verbose_sound)
|
||||
QuiskPrintf("Capture start error 0x%lX", hr);
|
||||
snprintf (dev->dev_errmsg, QUISK_SC_SIZE,
|
||||
"DirectSound capture device %s capture start failed", dev->name);
|
||||
"DirectSound capture device %.200s capture start failed", dev->name);
|
||||
return 1;
|
||||
}
|
||||
if (quisk_sound_state.verbose_sound) {
|
||||
@@ -270,18 +265,18 @@ static int quisk_open_playback(struct sound_dev * dev)
|
||||
DirectSoundEnumerate((LPDSENUMCALLBACK)DsEnumPlay, dev);
|
||||
if (errFound != DS_OK) {
|
||||
snprintf (dev->dev_errmsg, QUISK_SC_SIZE,
|
||||
"DirectSound play device name %s not found", dev->name);
|
||||
"DirectSound play device name %.200s not found", dev->name);
|
||||
return 1;
|
||||
}
|
||||
if (errOpen != DS_OK) {
|
||||
snprintf (dev->dev_errmsg, QUISK_SC_SIZE,
|
||||
"DirectSound play device %s open failed", dev->name);
|
||||
"DirectSound play device %.200s open failed", dev->name);
|
||||
return 1;
|
||||
}
|
||||
hr = IDirectSound_SetCooperativeLevel ((LPDIRECTSOUND8)dev->handle, quisk_mainwin_handle, DSSCL_PRIORITY);
|
||||
if (hr != DS_OK) {
|
||||
snprintf (dev->dev_errmsg, QUISK_SC_SIZE,
|
||||
"DirectSound play device %s cooperative level failed", dev->name);
|
||||
"DirectSound play device %.200s cooperative level failed", dev->name);
|
||||
return 1;
|
||||
}
|
||||
dev->sample_bytes = 4;
|
||||
@@ -299,7 +294,7 @@ static int quisk_open_playback(struct sound_dev * dev)
|
||||
}
|
||||
else {
|
||||
snprintf (dev->dev_errmsg, QUISK_SC_SIZE,
|
||||
"DirectSound play device %s buffer create failed (0x%X)", dev->name, (unsigned int)hr);
|
||||
"DirectSound play device %.180s buffer create failed (0x%X)", dev->name, (unsigned int)hr);
|
||||
return 1;
|
||||
}
|
||||
if (quisk_sound_state.verbose_sound) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2,34 +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_RX_BUF_SIZE 1024
|
||||
#define TCI_TX_BUFFER_MSEC 30 // Buffer delay in milliseconds
|
||||
#define TCI_COMMAND_SIZE 32
|
||||
#define TCI_STREAM_DATA_BYTES 16384
|
||||
#define TCI_RX_BUF_SIZE 1024
|
||||
#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 only sample type is FLOAT32, type 4. The FLOAT32 type is 3 in ExpertSDR3. We will always use 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 but the data is one channel in half the data length.
|
||||
// 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 Tx audio parameters are copied from the Rx audio stream.
|
||||
// The only sample type is FLOAT32 type 3.
|
||||
// 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.
|
||||
// The TCI modulations are: am, sam, dsb, lsb, usb, cw, nfm, wfm, spec, digl, digu, drm.
|
||||
|
||||
// These are TCI parameters:
|
||||
//static int quisk_dds;
|
||||
//static int quisk_if;
|
||||
static int quisk_vfo;
|
||||
static int client_vfo=-1;
|
||||
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;
|
||||
@@ -39,9 +42,15 @@ static char client_modulation[TCI_COMMAND_SIZE]={0};
|
||||
|
||||
static ws_cli_conn_t tci_clients_list[MAX_CLIENTS]; // list of TCI clients
|
||||
static int tci_clients_count;
|
||||
ws_cli_conn_t tci_tx_audio_client; // single client providing Tx audio
|
||||
static int tci_started; // Did we start the TCI server?
|
||||
|
||||
// Tx Audio
|
||||
ws_cli_conn_t tci_tx_audio_client; // single client providing Tx audio
|
||||
double tci_tx_audio_time; // seconds since the start of Tx audio
|
||||
uint64_t tci_tx_audio_samples; // number of samples requested since the start of Tx audio
|
||||
int tci_tx_audio_rate; // samples per second
|
||||
int tci_tx_audio_request; // the Stream.length to request in TX_CHRONO
|
||||
|
||||
static pthread_mutex_t clients_list_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_mutex_t tx_buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
@@ -59,7 +68,7 @@ enum SampleType // ExpertSDR3
|
||||
TCI_INT16 = 0, // 16-bit integral character type
|
||||
TCI_INT24 = 1, // 24-bit integral character type
|
||||
TCI_INT32 = 2, // 32-bit integral character type
|
||||
TCI_FLOAT32 = 3, // 32-bit type with a floating point (was 4 in SDR2)
|
||||
TCI_FLOAT32 = 3, // 32-bit type with a floating point
|
||||
} ;
|
||||
|
||||
struct ClientData { // data for each client
|
||||
@@ -71,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 {
|
||||
@@ -83,7 +102,7 @@ struct _Stream {
|
||||
uint32_t type; // stream type determined in StreamType
|
||||
uint32_t channels; // number of channels for ExpertSDR3
|
||||
uint32_t reserv[8]; // reserved
|
||||
uint8_t data[16384]; // samples
|
||||
uint8_t data[TCI_STREAM_DATA_BYTES]; // samples
|
||||
} ;
|
||||
|
||||
struct _TxChrono {
|
||||
@@ -106,7 +125,7 @@ static struct _TxChrono TxChrono = {
|
||||
.crc = 0,
|
||||
.length = 0,
|
||||
.type = TX_CHRONO,
|
||||
.channels = 1,
|
||||
.channels = 2,
|
||||
.reserv = {0, 0, 0, 0, 0, 0, 0, 0}
|
||||
} ;
|
||||
|
||||
@@ -124,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);
|
||||
@@ -156,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);
|
||||
}
|
||||
|
||||
@@ -169,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;
|
||||
@@ -185,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);
|
||||
@@ -199,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)
|
||||
@@ -220,25 +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) {
|
||||
for (i = 0; arg2[i]; i++)
|
||||
arg2[i] = toupper(arg2[i]);
|
||||
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);
|
||||
@@ -254,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)
|
||||
@@ -269,15 +360,30 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx)
|
||||
if (arg2) {
|
||||
if (strcmp(arg2, "true") == 0) {
|
||||
if (quisk_trx == 0) {
|
||||
pthread_mutex_lock(&tx_buffer_mutex);
|
||||
client_trx = 1;
|
||||
CircularBuffer(0, NULL, TCI_TX_BUFFER_MSEC * ctx->audio_stream_samplerate / 1000, 0);
|
||||
tci_tx_audio_request = TCI_STREAM_DATA_BYTES /
|
||||
ctx->audio_stream_bytes_per_sample / ctx->audio_stream_channels;
|
||||
CircularBuffer(0, NULL, tci_tx_audio_request * 2, 0);
|
||||
tci_tx_audio_client = client;
|
||||
tci_tx_audio_time = QuiskTimeSec();
|
||||
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)
|
||||
@@ -294,10 +400,11 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx)
|
||||
case 'v':
|
||||
if (strcmp(command, "vfo") == 0) {
|
||||
if (arg3) {
|
||||
client_vfo = atoi(arg3);
|
||||
client_vfo = atoll(arg3);
|
||||
// Echo comes from quisk_tci_set_params()
|
||||
}
|
||||
else {
|
||||
snprintf(char_buf, TCI_COMMAND_SIZE, "vfo:0,0,%d;", quisk_vfo);
|
||||
snprintf(char_buf, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo);
|
||||
sendframe_txt(client, char_buf);
|
||||
}
|
||||
return 0;
|
||||
@@ -317,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;
|
||||
@@ -333,25 +442,27 @@ 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, "vfo_limits:30000,30000000;");
|
||||
sendframe_txt(client, "if_limits:-48000,48000;");
|
||||
sendframe_txt(client, "protocol:ExpertSDR3,2.0;");
|
||||
//sendframe_txt(client, "vfo_limits:30000,30000000;"); Not known due to transverters
|
||||
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;");
|
||||
sendframe_txt(client, "receive_only:false;");
|
||||
sendframe_txt(client, "modulations_list:USB,LSB,CW,AM,FM,DIGL,DIGU;");
|
||||
sendframe_txt(client, "modulations_list:usb,lsb,cw,am,fm,digl,digu;");
|
||||
sendframe_txt(client, "ready;");
|
||||
|
||||
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,%d;", quisk_vfo);
|
||||
snprintf(command, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo);
|
||||
sendframe_txt(client, command);
|
||||
if (quisk_trx)
|
||||
sendframe_txt(client, "trx:0,true;");
|
||||
@@ -385,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
|
||||
@@ -419,40 +532,75 @@ static void onmessage(ws_cli_conn_t client, const unsigned char *msg, uint64_t s
|
||||
int channels = TxChrono.channels;
|
||||
int two_channels = channels == 2;
|
||||
int header = 16 * sizeof(uint32_t);
|
||||
const void * vpt = msg + header;
|
||||
const void * vpt_end = vpt + (size - header);
|
||||
const unsigned char * vpt = msg + header;
|
||||
const unsigned char * vpt_end = vpt + (size - header);
|
||||
const unsigned char * vpt2;
|
||||
const unsigned char * vpt2_end;
|
||||
int count2;
|
||||
|
||||
if (verbose >= 3) {
|
||||
QuiskPrintf("\n\nMSG size %ld length %d\n 0 ", size, pt->length);
|
||||
vpt2 = msg + header;
|
||||
vpt2_end = vpt2 + (size - header);
|
||||
count2 = 0;
|
||||
while (vpt2 < vpt2_end) {
|
||||
re = *(float *)vpt2;
|
||||
vpt2 += sizeof(float);
|
||||
im = *(float *)vpt2;
|
||||
vpt2 += sizeof(float);
|
||||
if (fabsf(re) > 1.1)
|
||||
re = 9;
|
||||
if (fabsf(im) > 1.1)
|
||||
im = 9;
|
||||
count2++;
|
||||
QuiskPrintf("%8.4f %8.4f ", re, im);
|
||||
if (count2 % 5 == 0)
|
||||
QuiskPrintf("\n%4d ", count2);
|
||||
}
|
||||
QuiskPrintf("\n");
|
||||
}
|
||||
|
||||
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);
|
||||
n = 0;
|
||||
switch (TxChrono.format) {
|
||||
case TCI_INT16:
|
||||
break;
|
||||
case TCI_FLOAT32: // FLOAT32 in version 2.0
|
||||
case 4: // FLOAT32 in version 1.4
|
||||
// Version 1.4 does not return the number of channels. We assume one channel.
|
||||
case TCI_FLOAT32:
|
||||
// pt->length is the number of floats
|
||||
while (vpt < vpt_end && count < pt->length) {
|
||||
re = *(float *)vpt;
|
||||
vpt += sizeof(float);
|
||||
count++;
|
||||
if (two_channels) {
|
||||
im = *(float *)vpt;
|
||||
vpt += sizeof(float);
|
||||
count++;
|
||||
}
|
||||
else {
|
||||
im = re;
|
||||
}
|
||||
sample = (re + I * im) * (CLIP32 / 2);
|
||||
count += CircularBuffer(0, &sample, 0, 1);
|
||||
cCopy[n++] = (re + I * im) * (CLIP32 / 2);
|
||||
}
|
||||
if (verbose && count != pt->length)
|
||||
QuiskPrintf("TCI TX count %d %d\n", 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;
|
||||
}
|
||||
pthread_mutex_unlock(&tx_buffer_mutex);
|
||||
}
|
||||
}
|
||||
if (verbose)
|
||||
PrintStream((const char *)msg, size);
|
||||
PrintStream((const char *)msg, size, 0);
|
||||
|
||||
}
|
||||
|
||||
@@ -482,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;
|
||||
@@ -507,16 +704,30 @@ void tci_send_audio(complex double * cSamples, int nSamples) // called from the
|
||||
int two_channels = stream.channels == 2;
|
||||
int bytes_per_sample = ctx->audio_stream_bytes_per_sample;
|
||||
void * vpt = &stream.data;
|
||||
void * vpt_end = vpt + 16384;
|
||||
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]) / 2147483648);
|
||||
*(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]) / 2147483648);
|
||||
*(float *)vpt = (float)(cimag(cAudio[i]) * (1.0 / 2147483648.0 / 2));
|
||||
vpt += bytes_per_sample;
|
||||
stream.length++;
|
||||
}
|
||||
@@ -524,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;
|
||||
@@ -532,14 +744,80 @@ 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) {
|
||||
TxChrono.length = mic_count;
|
||||
sendframe_bin(tci_tx_audio_client, (const char *)&TxChrono, 16 * sizeof(uint32_t));
|
||||
pthread_mutex_lock(&tx_buffer_mutex);
|
||||
mic_count = CircularBuffer(0, cSamples, mic_count, 0);
|
||||
pthread_mutex_lock(&tx_buffer_mutex); // Copy samples from the Tx buffer
|
||||
int count = CircularBuffer(0, cSamples, mic_count, 0);
|
||||
pthread_mutex_unlock(&tx_buffer_mutex);
|
||||
if (count < mic_count) {
|
||||
if (verbose >= 2)
|
||||
QuiskPrintf("TCI *TX replace mic %d %d\n", count, mic_count);
|
||||
while (count < mic_count)
|
||||
cSamples[count++] = 0;
|
||||
}
|
||||
// Ask for more samples
|
||||
double dtime = QuiskTimeSec();
|
||||
if (tci_tx_audio_samples < (dtime - tci_tx_audio_time) * tci_tx_audio_rate) {
|
||||
TxChrono.length = tci_tx_audio_request;
|
||||
sendframe_bin(tci_tx_audio_client, (const char *)&TxChrono, 16 * sizeof(uint32_t));
|
||||
//tci_tx_audio_samples += tci_tx_audio_request;
|
||||
tci_tx_audio_samples += tci_tx_audio_request / 2; // Stream.length is floats, not samples
|
||||
if (verbose >= 3)
|
||||
QuiskPrintf("TCI Request more Tx samples\n");
|
||||
}
|
||||
}
|
||||
return mic_count;
|
||||
}
|
||||
@@ -547,14 +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} ;
|
||||
int start=-1, new_dds=-1, new_if=-1, new_vfo=-1, new_trx=-1, new_split_enable=-1;
|
||||
long long new_vfo=-1;
|
||||
long long new_dds=-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, "|iiiiiiis", 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) {
|
||||
@@ -565,9 +846,22 @@ 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,%d;", quisk_vfo);
|
||||
snprintf(char_buf, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo);
|
||||
sendframe_txt_bcast(tci_port, char_buf);
|
||||
}
|
||||
if (new_split_enable != -1) {
|
||||
@@ -589,17 +883,22 @@ PyObject * quisk_tci_set_params(PyObject * self, PyObject * args, PyObject * key
|
||||
}
|
||||
if (mode) {
|
||||
if (strcmp(mode, "DGT-U") == 0)
|
||||
strcpy(quisk_modulation, "DIGU");
|
||||
strcpy(quisk_modulation, "digu");
|
||||
else if (strcmp(mode, "DGT-L") == 0)
|
||||
strcpy(quisk_modulation, "DIGL");
|
||||
strcpy(quisk_modulation, "digl");
|
||||
else if (strcmp(mode, "CWL") == 0 || strcmp(mode, "CWU") == 0)
|
||||
strcpy(quisk_modulation, "CW");
|
||||
else if (strcmp(mode, "LSB") == 0 || strcmp(mode, "USB") == 0 ||
|
||||
strcmp(mode, "AM") == 0 || strcmp(mode, "FM") == 0)
|
||||
strncpy(quisk_modulation, mode, TCI_COMMAND_SIZE - 1);
|
||||
strcpy(quisk_modulation, "cw");
|
||||
else if (strcmp(mode, "LSB") == 0)
|
||||
strcpy(quisk_modulation, "lsb");
|
||||
else if (strcmp(mode, "USB") == 0)
|
||||
strcpy(quisk_modulation, "usb");
|
||||
else if (strcmp(mode, "AM") == 0)
|
||||
strcpy(quisk_modulation, "am");
|
||||
else if (strcmp(mode, "FM") == 0)
|
||||
strcpy(quisk_modulation, "fm");
|
||||
else
|
||||
strcpy(quisk_modulation, "USB");
|
||||
snprintf(char_buf, TCI_COMMAND_SIZE, "modulation:0,%.9s;", quisk_modulation);
|
||||
strncpy(quisk_modulation, mode, TCI_COMMAND_SIZE - 1);
|
||||
snprintf(char_buf, TCI_COMMAND_SIZE, "modulation:0,%.9s", quisk_modulation);
|
||||
sendframe_txt_bcast(tci_port, char_buf);
|
||||
}
|
||||
Py_INCREF (Py_None);
|
||||
@@ -609,42 +908,62 @@ PyObject * quisk_tci_set_params(PyObject * self, PyObject * args, PyObject * key
|
||||
PyObject * quisk_tci_get_params(PyObject * self, PyObject * args) // called from the GUI thread
|
||||
{ /* Return client request for a parameter change */
|
||||
const char * name;
|
||||
int i;
|
||||
int ii;
|
||||
long long ll;
|
||||
char char_buf[TCI_COMMAND_SIZE];
|
||||
|
||||
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) {
|
||||
i = client_vfo;
|
||||
ll = client_vfo;
|
||||
client_vfo = -1;
|
||||
return PyLong_FromLong(i);
|
||||
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) {
|
||||
i = client_split_enable;
|
||||
ii = client_split_enable;
|
||||
client_split_enable = -1;
|
||||
return PyLong_FromLong(i);
|
||||
return PyLong_FromLong(ii);
|
||||
}
|
||||
else if (strcmp(name, "tci_trx") == 0 && client_trx >= 0) {
|
||||
i = client_trx;
|
||||
ii = client_trx;
|
||||
client_trx = -1;
|
||||
return PyLong_FromLong(i);
|
||||
return PyLong_FromLong(ii);
|
||||
}
|
||||
else if (strcmp(name, "tci_modulation") == 0 && client_modulation[0] != 0) {
|
||||
char_buf[0] = 0;
|
||||
if (strcmp(client_modulation, "DIGU") == 0)
|
||||
if (strcmp(client_modulation, "digu") == 0)
|
||||
strcpy(char_buf, "DGT-U");
|
||||
else if (strcmp(client_modulation, "DIGL") == 0)
|
||||
else if (strcmp(client_modulation, "digl") == 0)
|
||||
strcpy(char_buf, "DGT-L");
|
||||
else if (strcmp(client_modulation, "CW") == 0)
|
||||
else if (strcmp(client_modulation, "cw") == 0)
|
||||
strcpy(char_buf, "CWU");
|
||||
else if (strcmp(client_modulation, "LSB") == 0 || strcmp(client_modulation, "USB") == 0 ||
|
||||
strcmp(client_modulation, "AM") == 0 || strcmp(client_modulation, "FM") == 0)
|
||||
strncpy(char_buf, client_modulation, TCI_COMMAND_SIZE);
|
||||
else if (strcmp(client_modulation, "lsb") == 0)
|
||||
strcpy(char_buf, "LSB");
|
||||
else if (strcmp(client_modulation, "usb") == 0)
|
||||
strcpy(char_buf, "USB");
|
||||
else if (strcmp(client_modulation, "am") == 0)
|
||||
strcpy(char_buf, "AM");
|
||||
else if (strcmp(client_modulation, "fm") == 0)
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
This directory has files for the SDR-1000 clone synthesizer by
|
||||
UT3MK and US5NCJ
|
||||
@@ -0,0 +1 @@
|
||||
#
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -326,7 +314,7 @@ PyObject * QuiskPrintf(char * format, ...)
|
||||
int CircularBuffer(int channel, complex double * cSamples, int nRead, int nWrite)
|
||||
{
|
||||
// Initialize with cSamples NULL and nRead equal to the buffer delay in samples.
|
||||
// Read with nRead, write with nWrite. Only one can be non-zero.
|
||||
// Read with nRead, write with nWrite. Only one can be non-zero for read/write. If both are zero, return nBuf.
|
||||
// Return the number of samples read or written.
|
||||
static struct _channel {
|
||||
complex double * cBuf; // circular sample buffer; save samples until the buffer fills to nDelay
|
||||
@@ -342,16 +330,19 @@ int CircularBuffer(int channel, complex double * cSamples, int nRead, int nWrite
|
||||
|
||||
if ( ! cSamples) { // initialize
|
||||
ptChannel->nDelay = nRead;
|
||||
ptChannel->sizeBuf = nRead * 8;
|
||||
ptChannel->sizeBuf = nRead * 16;
|
||||
ptChannel->cBuf = (complex double *)realloc(ptChannel->cBuf, ptChannel->sizeBuf * sizeof(complex double));
|
||||
ptChannel->filling = 1;
|
||||
memset(ptChannel->cBuf, 0, ptChannel->sizeBuf * sizeof(complex double));
|
||||
ptChannel->nBuf = 0;
|
||||
ptChannel->Windex = 0;
|
||||
ptChannel->Rindex = 0;
|
||||
ptChannel->nBuf = 0;
|
||||
ptChannel->filling = 1;
|
||||
return 0;
|
||||
}
|
||||
if ( ! ptChannel->cBuf)
|
||||
return 0;
|
||||
if (nRead == 0 && nWrite == 0)
|
||||
return ptChannel->nBuf;
|
||||
num = 0; // number of samples read or written
|
||||
if (nWrite > 0) { // Copy samples from cSamples to cBuf
|
||||
for (i = 0; i < nWrite && ptChannel->nBuf < ptChannel->sizeBuf; i++) {
|
||||
@@ -363,13 +354,11 @@ int CircularBuffer(int channel, complex double * cSamples, int nRead, int nWrite
|
||||
}
|
||||
if (ptChannel->nBuf >= ptChannel->nDelay)
|
||||
ptChannel->filling = 0;
|
||||
//if (num != nWrite)
|
||||
// QuiskPrintf("Circ nWrite %d %d %d %d\n", num, nWrite, ptChannel->nBuf, ptChannel->sizeBuf);
|
||||
}
|
||||
else if (nRead > 0) {
|
||||
if (ptChannel->filling) { // return zero-valued samples
|
||||
for (num = 0; num < nRead; num++)
|
||||
cSamples[num] = 0;
|
||||
}
|
||||
else { // Copy samples from cBuf to cSamples
|
||||
if ( ! ptChannel->filling) { // Copy samples from cBuf to cSamples
|
||||
for (i = 0; i < nRead && ptChannel->nBuf > 0; i++) {
|
||||
cSamples[i] = ptChannel->cBuf[ptChannel->Rindex++];
|
||||
if (ptChannel->Rindex >= ptChannel->sizeBuf)
|
||||
|
||||
@@ -2030,7 +2030,7 @@ int ws_socket(struct ws_server *ws_srv)
|
||||
panic("Unable to listen!\n");
|
||||
|
||||
/* Wait for incoming connections. */
|
||||
printf("Waiting for incoming connections...\n");
|
||||
//printf("Waiting for incoming connections...\n");
|
||||
memset(client_socks, -1, sizeof(client_socks));
|
||||
|
||||
/* Accept connections. */
|
||||
|
||||