24 Commits
Author SHA1 Message Date
James C. Ahlstrom c2085a8160 Remove wheel tester. 2026-10-27 17:05:25 -04:00
James C. Ahlstrom 2b6b6fa9ea Merge branch 'master' of github.com:jimahlstrom/quisk
Merge mouse wheel logic.
2026-10-27 16:58:05 -04:00
James C. Ahlstrom 36210d4e4c Request by Jaroslav, OK2JRQ: alternate docs directory. 2026-10-27 16:56:09 -04:00
jimahlstrom eb9434c52a Merge pull request #4 from prof7bit/fix-wheel-steps
Fix symmetric mouse wheel tuning steps. Thanks to Bernd, DD4UG.
2026-10-27 16:49:51 -04:00
Bernd db928b6fcb Fix symmetric mouse wheel tuning steps 2026-10-24 13:45:22 +02:00
jimahlstrom a90f8b80bd Merge pull request #3 from argusSecurityBot/fix-bounds-checks-and-leak
Fix out-of-bounds accesses and a leak in quisk.c and afedrinet_io.c
2026-10-22 16:13:49 -04:00
argusSecurityBot faaeda54e5 Fix out-of-bounds accesses and a leak in quisk.c and afedrinet_io.c
- afedrinet_io.c open_rx_udp: use snprintf so a long ip string cannot
  overflow the 128-byte stack buffer.
- quisk.c set_multirx_mode, set_multirx_freq: reject a negative index.
- quisk.c set_multirx_play_channel: treat a negative channel like an
  out-of-range one.
- quisk.c measure_freq: keep ipeak inside [1, fft_size - 2] so that
  fft_average[ipeak - 1] and fft_average[ipeak + 1] stay in bounds when
  no peak is found.
- quisk.c record_app: free the previous multirx_fft_next_samples buffer
  before allocating a new one, like fft_window and current_graph.
2026-10-20 16:46:24 +00:00
James C. Ahlstrom c2304b23ee Minor correction to UT3MK radio 2026-10-19 13:43:22 -04:00
jimahlstrom 09154658ae Merge pull request #2 from zxalexis/master
Added UT3MK SDR radios
2026-10-19 13:22:35 -04:00
James C. Ahlstrom 20bd313041 Correction to makefile 2026-10-19 13:00:09 -04:00
zx d39722b170 Added UT3MK SDR radios 2026-10-18 22:02:37 +03:00
James C. Ahlstrom c97ffa155e Quisk version 4.2.55 2026-10-12 13:37:47 -04:00
jimahlstrom 745882a506 Add README for Quisk Software Defined Radio
Added introductory information about Quisk software defined radio, including its features and support links.
2026-09-27 16:19:11 -04:00
James C. Ahlstrom 674bbad1ab fix non-ascii char in setup.py 2026-08-20 14:08:21 -04:00
James C. Ahlstrom 67c6fbcad9 Quisk version 4.2.54 2026-08-04 12:01:45 -04:00
James C. Ahlstrom 84e194c644 Quisk version 4.2.53 2026-06-30 17:06:52 -04:00
James C. Ahlstrom b45a71b017 Quisk version 4.2.52 2026-05-30 11:18:52 -04:00
James C. Ahlstrom e6b9d9dd05 Quisk version 4.2.51 2026-05-10 15:22:33 -04:00
James C. Ahlstrom 1f68d05cd5 Quisk version 4.2.50 2025-12-29 14:01:36 -05:00
James C. Ahlstrom 018fc0e871 Quisk version 4.2.49 2025-12-22 15:12:47 -05:00
James C. Ahlstrom 1111d24333 Quisk version 4.2.48 2025-11-24 12:28:01 -05:00
James C. Ahlstrom d8aee9284d Quisk version 4.2.47 2025-11-22 13:39:10 -05:00
James C. Ahlstrom 7121dacbbd Quisk version 4.2.46 2025-10-12 12:43:21 -04:00
James C. Ahlstrom 0528e8bbbe Quisk version 4.2.45 2025-10-08 16:29:31 -04:00
77 changed files with 7169 additions and 1841 deletions
+92
View File
@@ -1,3 +1,95 @@
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.
Quisk Version 4.2.50 December 2025
===================================
This is a quick fix for a bug in 4.2.49 that results in the repeated message
AttributeError: 'Hardware' object has no attribute 'is_HermesLite2'
Quisk Version 4.2.49 December 2025
===================================
This version adds more features for the Anan 10E and other OpenHPSDR radios. Please test.
I added the forward power, SWR, temperature and PA current display to the OpenHPSDR radio. But
I don't have that radio so I need some help. I think the forward and reverse power connections
are correct, but it is still necessary to create a table to convert analog input codes to power.
Go to the Hardware screen and check the "Power meter calibration" setting. You will need to enter
"New" and create a suitable table. To see the ADC values, choose table "Show ADC".
There is no protocol standard to specify which analog input is used for temperature and PA current.
And AFAIK the Anan 10E has no such sensors. I am hoping to use the board ID to find the right input.
The ADC conversion is linear and the two points are specified on the Hardware screen.
I currently assigned the analog inputs as follows:
AIN1 Forward power
AIN2 Reverse power
AIN3 PA current
AIN5 Temperature
If you need to see the ADC codes 0-4095 for each input, go to hermes/quisk_hardware.py and
change DEBUG_SENSORS to one.
Quisk Version 4.2.48 November 2025
===================================
This is a quisk fix for a bug in the new OpenHPSDR radios. It fixes use_rx_udp that was reset to zero.
Quisk Version 4.2.47 November 2025
===================================
To improve the performance of Quisk with the Anan 10-E and other OpenHPSDR radios, I added
a new type of radio "OpenHPSDR". Use this type for the Anan 10-E and similar radios. It removes
all the protocol extensions added by the Hermes-Lite2 project. I removed the configuration
option "Hermes Lite2 protocol enable". This is now automatic.
I added buttons for antennas 0, 1, 2 and 3 for OpenHPSDR radios that support more antennas.
I added controls for the OpenHPSDR preamplifier and attenuator.
I do not have any Apache Labs radios, so I must depend on my users to report problems and solutions.
Quisk Version 4.2.46 October 2025
==================================
I fixed a bug in Hamlib rig 2 which happens with the new Pure Signal code. The Hamlib "f" command
now always returns the main receiver frequency, not one of the added receiver frequencies. This is
consistent with the Flex ZZ commands.
Quisk Version 4.2.45 October 2025
==================================
The new FreeDV including RADE mode is now a built-in mode of Quisk. Just press "FDV" to talk FreeDV.
In brief, install FreeDV-GUI from the web site. Then configure the usual Quisk digital input and
output that you use for digital modes. In FreeDV-GUI use the digital input and output. For the
speakers and microphone use the same ones as Quisk. Don't forget to configure your call sign in
FreeDV so it works with FreeDV Reporter. For detailed instructions see http://james.ahlstrom.name/rade.html.
The Quisk digital output device is now always active; a DGT mode is not required.
Quisk Version 4.2.44 September 2025
====================================
Quisk now has Pure Signal for the Hermes Lite2. This is baseband predistortion to reduce IMD products.
+9 -18
View File
@@ -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.
+2 -2
View File
@@ -1,2 +1,2 @@
#Quisk version 4.2.44
from .quisk import main
#Quisk version 4.2.55
from .quisk import main
+1 -1
View File
@@ -395,7 +395,7 @@ class ControlCommon(BaseHardware): # This is the Hardware class for the control
for idName in ("Mute", "NR2", "AGC", "Sqlch", "NB 1", "Notch", "Test 1", "Spot", "FDX", "PTT", "VOX"):
self.RemoteCtlSend("%s;%d\n" % (idName, app.idName2Button[idName].GetIndex()))
# Menus
for menu in (app.NB_menu, app.split_menu, app.freedv_menu, app.smeter_menu):
for menu in (app.NB_menu, app.split_menu, app.smeter_menu):
if menu:
for nid in menu.id2data:
menu_item = menu.FindItemById(nid)
+102 -8
View File
@@ -9,6 +9,7 @@
#include <Python.h>
#include <complex.h>
#include <math.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include <errno.h>
@@ -52,6 +53,41 @@ static int control_head_data_width; // app.data_width of control head for gr
static int packets_sent;
static int packets_recd;
///// The jitter buffer is thanks to Karl Heinz - K5KHK
// Jitter buffer for the microphone samples received from the control head. The network can hold
// packets for 100 msec or more (Wi-Fi power saving and scanning) and then deliver them in a burst,
// but the Hermes Tx buffer only has 50 msec of slack. Without a buffer here, each late burst is an
// underflow (a gap on the air) followed by an overflow (discarded samples). So buffer mic_jb_target
// frames before playing, and then release them at a steady MIC_JB_RATE.
// The buffer adds its delay to the transmit audio, so it is off unless the configuration item
// remote_radio_mic_jitter_ms is set. When it is off, samples are passed on as they arrive.
#define MIC_JB_RATE 8000 // frames per second sent by the control head
#define MIC_JB_SIZE 8000 // ring buffer size in stereo frames
#define MIC_JB_MAX_MS 500 // largest configurable delay; MIC_JB_SIZE must hold this plus MIC_JB_EXTRA
#define MIC_JB_EXTRA 2000 // trim back to mic_jb_target above mic_jb_target + this (clock drift): 250 msec
#define MIC_JB_IDLE 0.5 // seconds without packets that end a transmission
static int16_t mic_jb[MIC_JB_SIZE * 2];
static int mic_jb_target; // frames to buffer before playing, from remote_radio_mic_jitter_ms; zero for off
static int mic_jb_read, mic_jb_count, mic_jb_playing, mic_jb_starved;
static int mic_jb_underruns, mic_jb_overruns;
static double mic_jb_time, mic_jb_frac, mic_jb_last_packet;
static void mic_jb_reset(void)
{
int msec;
msec = QuiskGetConfigInt("remote_radio_mic_jitter_ms", 0);
if (msec < 0)
msec = 0;
else if (msec > MIC_JB_MAX_MS)
msec = MIC_JB_MAX_MS;
mic_jb_target = msec * MIC_JB_RATE / 1000;
mic_jb_read = mic_jb_count = mic_jb_playing = mic_jb_starved = 0;
mic_jb_underruns = mic_jb_overruns = 0;
mic_jb_frac = 0;
}
// Receive stereo 16-bit pcm radio speaker sound on the control head via UDP
int read_remote_radio_sound_socket(complex double * cSamples)
{
@@ -116,9 +152,9 @@ int read_remote_radio_sound_socket(complex double * cSamples)
// Receive stereo 16-bit pcm microphone samples at the remote radio via UDP
int read_remote_mic_sound_socket(complex double * cSamples)
{
int i, bytes, nInt16, nSamples;
int i, j, bytes, nInt16, nSamples, nFrames;
int16_t buf[UDP_SEND_INT16];
double samp_r, samp_l;
double now, due;
struct timeval tm_wait;
fd_set fds;
static struct quisk_cHB45Filter HalfBand;
@@ -133,8 +169,8 @@ int read_remote_mic_sound_socket(complex double * cSamples)
quisk_filt_cInit(&cFiltInterp3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double));
}
// read all available packets, one per loop
nSamples = 0;
// read all available packets into the jitter buffer, one per loop
now = QuiskTimeSec();
while (1) {
tm_wait.tv_sec = 0;
tm_wait.tv_usec = 0;
@@ -151,15 +187,71 @@ int read_remote_mic_sound_socket(complex double * cSamples)
break;
}
if (bytes > 0) {
if (mic_jb_starved) { // the buffer ran dry; count it unless the transmission had ended
mic_jb_starved = 0;
if (now - mic_jb_last_packet < MIC_JB_IDLE)
mic_jb_underruns++;
}
mic_jb_last_packet = now;
nInt16 = bytes / 2;
for (i = 0; i < nInt16; i += 2) {
samp_r = buf[i];
samp_l = buf[i + 1];
cSamples[nSamples++] = (samp_r + I * samp_l) / CLIP16 * CLIP32;
for (i = 0; i + 1 < nInt16; i += 2) {
if (mic_jb_count >= MIC_JB_SIZE) { // full: drop the oldest frame
mic_jb_read = (mic_jb_read + 1) % MIC_JB_SIZE;
mic_jb_count--;
}
j = (mic_jb_read + mic_jb_count) % MIC_JB_SIZE * 2;
mic_jb[j] = buf[i];
mic_jb[j + 1] = buf[i + 1];
mic_jb_count++;
}
}
} // while(1)
// release the frames that are due since the last call
nSamples = 0;
if (mic_jb_target == 0) { // the jitter buffer is off: pass on everything received
nFrames = mic_jb_count;
}
else if ( ! mic_jb_playing) {
nFrames = 0;
if (mic_jb_count >= mic_jb_target) {
mic_jb_playing = 1;
mic_jb_time = now;
mic_jb_frac = 0;
}
}
else {
due = (now - mic_jb_time) * MIC_JB_RATE + mic_jb_frac;
if (due < 0) // the clock was set back
due = 0;
else if (due > MIC_JB_RATE / 2) // limit to 0.5 sec, well within SAMP_BUFFER_SIZE after interpolation
due = MIC_JB_RATE / 2;
mic_jb_time = now;
nFrames = (int)due;
mic_jb_frac = due - nFrames;
if (nFrames > mic_jb_count) { // ran dry: play what is left, then buffer up again
nFrames = mic_jb_count;
mic_jb_playing = 0;
mic_jb_starved = 1;
}
}
for (i = 0; i < nFrames; i++) {
j = mic_jb_read * 2;
cSamples[nSamples++] = ((double)mic_jb[j] + I * (double)mic_jb[j + 1]) / CLIP16 * CLIP32;
mic_jb_read = (mic_jb_read + 1) % MIC_JB_SIZE;
}
mic_jb_count -= nFrames;
if (mic_jb_target && mic_jb_count > mic_jb_target + MIC_JB_EXTRA) { // the control head's clock is faster than ours
i = mic_jb_count - mic_jb_target;
mic_jb_read = (mic_jb_read + i) % MIC_JB_SIZE;
mic_jb_count -= i;
mic_jb_overruns++;
}
if ((mic_jb_underruns || mic_jb_overruns) && now - mic_jb_last_packet > MIC_JB_IDLE) {
QuiskPrintf("Remote mic jitter buffer: %d underruns, %d overruns\n", mic_jb_underruns, mic_jb_overruns);
mic_jb_underruns = mic_jb_overruns = 0;
}
nSamples = quisk_cInterpolate(cSamples, nSamples, &cFiltInterp3, 3);
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand);
@@ -715,6 +807,7 @@ PyObject * quisk_start_remote_radio_remote_sound(PyObject * self, PyObject * arg
packets_sent = 0;
packets_recd = 0;
mic_jb_reset();
return Py_None;
}
@@ -741,6 +834,7 @@ PyObject * quisk_stop_remote_radio_remote_sound(PyObject * self, PyObject * args
control_head_graph_socket_started = 0;
remote_radio_graph_socket_started = 0;
control_head_data_width = 0;
mic_jb_reset();
QuiskPrintf("total packets sent = %i, recd = %i\n", packets_sent, packets_recd);
+22 -3
View File
@@ -190,10 +190,20 @@ class Remot: # Remote comtrol base class
return
try: # Read any data from the socket
text = self.remote_ctl_connection.recv(1024)
except:
#traceback.print_exc()
# Thanks to Karl Heinz - K5KHK
except (BlockingIOError, InterruptedError, socket.timeout): # No data available now
return
except OSError: # For example ConnectionResetError: the Control Head is gone
print('Closing Remote Control connection: recv() failed')
self.RemoteCtlClose(False)
return
else: # We got some characters
if not text: # recv() returns nothing only when the Control Head has closed the connection
# Close now rather than waiting for the heartbeat timeout: a Control Head that
# crashed or lost its network may have left the transmitter keyed.
print('Closing Remote Control connection: closed by Control Head')
self.RemoteCtlClose(False)
return
if not isinstance(text, str):
text = text.decode('utf-8')
self.received += text
@@ -375,13 +385,22 @@ class Remot: # Remote comtrol base class
self.cw_next_keydown = None
def StopTransmit(self):
# TODO: Add code for modes other than CW
# PTT code added thanks to Karl Heinz - K5KHK
# Called whenever the remote session ends (RemoteCtlClose). Release everything the
# Control Head may have keyed: the CW key and, for voice and digital modes, PTT.
# Without the PTT release, a Control Head that disappears while transmitting (a crash,
# a lost network, a dead battery) leaves the transmitter keyed until someone unkeys it
# at the radio.
while len(self.cw_event_queue):
self.cw_event_queue.popleft()
self.cw_next_event_ts = None
self.cw_next_keydown = None
self.cw_key_down = 0
QS.set_remote_cwkey(0)
ptt = getattr(self.app, 'pttButton', None)
if ptt and ptt.GetValue():
print('Remote Control connection closed while transmitting: releasing PTT')
ptt.SetValue(0, True) # Same as releasing PTT at the radio; runs OnButtonPTT
def ProcessRadioBtn(self, command, cmd_text):
# Large and Small format screens send different button events for radio buttons.
+6 -5
View File
@@ -3,6 +3,7 @@
#ifdef MS_WINDOWS
#include <Winsock2.h>
#include <windows.h>
#include <string.h>
#else
#include <sys/types.h>
#include <sys/stat.h>
@@ -105,7 +106,7 @@ static PyObject * open_rx_udp(const char * ip, int port)
wVersionRequested = MAKEWORD(2, 2);
if (WSAStartup(wVersionRequested, &wsaData) != 0) {
sprintf(buf, "Failed to initialize Winsock (WSAStartup)");
return PyString_FromString(buf);
return PyUnicode_FromString(buf);
}
#endif
// quisk_using_udp = 1;
@@ -127,10 +128,10 @@ static PyObject * open_rx_udp(const char * ip, int port)
shutdown(rx_udp_socket, QUISK_SHUT_BOTH);
close(rx_udp_socket);
rx_udp_socket = INVALID_SOCKET;
sprintf(buf, "Failed to connect to UDP %s port %u", ip, port);
snprintf(buf, sizeof(buf), "Failed to connect to UDP %s port %u", ip, port);
}
else {
sprintf(buf, "Capture from UDP %s port %u", ip, port);
snprintf(buf, sizeof(buf), "Capture from UDP %s port %u", ip, port);
#if DEBUG_IO
if (getsockopt(rx_udp_socket, SOL_SOCKET, SO_RCVBUF, (char *)&intbuf, &bufsize) == 0)
{
@@ -145,7 +146,7 @@ static PyObject * open_rx_udp(const char * ip, int port)
else {
sprintf(buf, "Failed to open socket");
}
return PyString_FromString(buf);
return PyUnicode_FromString(buf);
}
static PyObject * close_rx_udp(PyObject * self, PyObject * args)
@@ -343,7 +344,7 @@ static PyObject * open_samples(PyObject * self, PyObject * args)
quisk_sample_source(NULL, NULL, &afedri_read_rx_udp);
//////////////
return open_rx_udp(ip, port); // AFEDRI specific
// return PyString_FromString(buf); // return a string message
// return PyUnicode_FromString(buf); // return a string message
}
// Miscellaneous functions needed by the SDR-IQ; called from the GUI thread as
+21 -4
View File
@@ -1,7 +1,24 @@
MODULE_NAME := afedrinet_io
OBJDIR := ../build/local
afedrinet2:
python2 setup.py build_ext --force --inplace
CFLAGS := $(shell python3 ../makefil.py --cflags)
LDFLAGS := $(shell python3 ../makefil.py --ldflags)
EXTENSION_SUFFIX := $(shell python3 ../makefil.py --suffix)
TARGET := $(MODULE_NAME)$(EXTENSION_SUFFIX)
afedrinet3:
# Default target: Link the shared library object
$(TARGET): afedrinet_io.o $(OBJDIR)/import_quisk_api.o $(OBJDIR)/is_key_down.o
$(CC) -o $@ afedrinet_io.o $(LDFLAGS) $(OBJDIR)/import_quisk_api.o $(OBJDIR)/is_key_down.o
# Compile the C source files into object files
afedrinet_io.o: afedrinet_io.c ../quisk.h
$(CC) -o $@ -c $< $(CFLAGS) -I..
.PHONY: clean
clean:
-rm -f *.o
-rm -f $(MODULE_NAME)*.so
.PHONY: afedrinet-old
afedrinet-old:
python3 setup.py build_ext --force --inplace
+155
View File
@@ -0,0 +1,155 @@
/*
* Base64 encoding/decoding (RFC1341)
* Copyright (c) 2005-2011, Jouni Malinen <j@w1.fi>
*
* This software may be distributed under the terms of the BSD license.
* See README for more details.
*/
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include "base64.h"
static const unsigned char base64_table[65] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/**
* base64_encode - Base64 encode
* @src: Data to be encoded
* @len: Length of the data to be encoded
* @out_len: Pointer to output length variable, or %NULL if not used
* Returns: Allocated buffer of out_len bytes of encoded data,
* or %NULL on failure
*
* Caller is responsible for freeing the returned buffer. Returned buffer is
* nul terminated to make it easier to use as a C string. The nul terminator is
* not included in out_len.
*/
unsigned char * base64_encode(const unsigned char *src, size_t len,
size_t *out_len)
{
unsigned char *out, *pos;
const unsigned char *end, *in;
size_t olen;
int line_len;
olen = len * 4 / 3 + 4; /* 3-byte blocks to 4-byte */
olen += olen / 72; /* line feeds */
olen++; /* nul termination */
if (olen < len)
return NULL; /* integer overflow */
out = malloc(olen);
if (out == NULL)
return NULL;
end = src + len;
in = src;
pos = out;
line_len = 0;
while (end - in >= 3) {
*pos++ = base64_table[in[0] >> 2];
*pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
*pos++ = base64_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)];
*pos++ = base64_table[in[2] & 0x3f];
in += 3;
line_len += 4;
if (line_len >= 72) {
*pos++ = '\n';
line_len = 0;
}
}
if (end - in) {
*pos++ = base64_table[in[0] >> 2];
if (end - in == 1) {
*pos++ = base64_table[(in[0] & 0x03) << 4];
*pos++ = '=';
} else {
*pos++ = base64_table[((in[0] & 0x03) << 4) |
(in[1] >> 4)];
*pos++ = base64_table[(in[1] & 0x0f) << 2];
}
*pos++ = '=';
line_len += 4;
}
if (line_len)
*pos++ = '\n';
*pos = '\0';
if (out_len)
*out_len = pos - out;
return out;
}
/**
* base64_decode - Base64 decode
* @src: Data to be decoded
* @len: Length of the data to be decoded
* @out_len: Pointer to output length variable
* Returns: Allocated buffer of out_len bytes of decoded data,
* or %NULL on failure
*
* Caller is responsible for freeing the returned buffer.
*/
unsigned char * base64_decode(const unsigned char *src, size_t len,
size_t *out_len)
{
unsigned char dtable[256], *out, *pos, block[4], tmp;
size_t i, count, olen;
int pad = 0;
memset(dtable, 0x80, 256);
for (i = 0; i < sizeof(base64_table) - 1; i++)
dtable[base64_table[i]] = (unsigned char) i;
dtable['='] = 0;
count = 0;
for (i = 0; i < len; i++) {
if (dtable[src[i]] != 0x80)
count++;
}
if (count == 0 || count % 4)
return NULL;
olen = count / 4 * 3;
pos = out = malloc(olen);
if (out == NULL)
return NULL;
count = 0;
for (i = 0; i < len; i++) {
tmp = dtable[src[i]];
if (tmp == 0x80)
continue;
if (src[i] == '=')
pad++;
block[count] = tmp;
count++;
if (count == 4) {
*pos++ = (block[0] << 2) | (block[1] >> 4);
*pos++ = (block[1] << 4) | (block[2] >> 2);
*pos++ = (block[2] << 6) | block[3];
count = 0;
if (pad) {
if (pad == 1)
pos--;
else if (pad == 2)
pos -= 2;
else {
/* Invalid padding */
free(out);
return NULL;
}
break;
}
}
}
*out_len = pos - out;
return out;
}
+19
View File
@@ -0,0 +1,19 @@
/*
* Base64 encoding/decoding (RFC1341)
* Copyright (c) 2005, Jouni Malinen <j@w1.fi>
*
* This software may be distributed under the terms of the BSD license.
* See README for more details.
*/
#ifndef BASE64_H
#define BASE64_H
#include <sys/types.h>
unsigned char * base64_encode(const unsigned char *src, size_t len,
size_t *out_len);
unsigned char * base64_decode(const unsigned char *src, size_t len,
size_t *out_len);
#endif /* BASE64_H */
+22 -36
View File
@@ -88,6 +88,7 @@ class Configuration:
platform_ignore = 'win_'
self.sections = []
self.receiver_data = []
self.receiver_explain = []
self.StatePath = conf.settings_file_path
if not self.StatePath:
self.StatePath = os.path.join(app.QuiskFilesDir, "quisk_settings.json")
@@ -158,9 +159,9 @@ class Configuration:
dlg.Destroy()
else:
radio_dict["use_soapy"] = '0'
if radio_type not in ("HiQSDR", "Hermes", "Red Pitaya", "Odyssey", "Odyssey2"):
if radio_type not in ("HiQSDR", "Hermes", "Red Pitaya", "Odyssey", "Odyssey2", "OpenHPSDR"):
radio_dict["use_rx_udp"] = '0'
if radio_type in ("Hermes", "Red Pitaya", "Odyssey2"):
if radio_type in ("Hermes", "Red Pitaya", "Odyssey2", "OpenHPSDR"):
if "Hermes_BandDict" not in radio_dict:
radio_dict["Hermes_BandDict"] = {}
if "Hermes_BandDictTx" not in radio_dict:
@@ -601,6 +602,7 @@ class Configuration:
# Receiver sections start with 16 #, "Receivers ", receiver name, explain
# self.receiver_data is a list of [receiver_name, receiver_data]
# receiver_data is a list of [data_name, text, fmt, help_text, values]
# self.receiver_explain is a list of the text in Receivers: the name, a comma and the explanation.
# Variable names start with ## variable_name variable_text, format
# The format is integer, number, text, boolean, integer choice, text choice, rfile
@@ -638,6 +640,7 @@ class Configuration:
continue
if line[0:27] == '################ Receivers ':
section = 'Receivers'
self.receiver_explain.append(line[27:].strip())
args = line[27:].split(',', 1)
rxname = args[0].strip()
section_data = []
@@ -724,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):
@@ -2298,8 +2290,9 @@ class ControlMixin:
if self.radio_name == Settings[1]: # changed for current radio
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', 'hermes_lite2_enable',
'fixed_tune_offset'):
'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',
'remote_radio_mic_jitter_ms'):
setattr(conf, name, x)
application.ImmediateChange(name)
elif name[0:4] in ('lin_', 'win_'):
@@ -2400,6 +2393,7 @@ class BaseWindow(wx.ScrolledWindow, ControlMixin):
def __init__(self, parent):
wx.ScrolledWindow.__init__(self, parent)
ControlMixin.__init__(self, parent)
self.SetScrollRate(8, 8)
class ConfigConfig(BaseWindow):
def __init__(self, parent, width):
@@ -2510,7 +2504,6 @@ The CW message will then repeat. A time of zero means no repeat.'
sl, btn = self.AddTextSliderHelp(4, " Repeat secs %.1f ", 0, 0, 100, self.OnPlayFileRepeat, help_text, span=2, scale=0.1)
self.NextRow()
self.FitInside()
self.SetScrollRate(1, 1)
def MakeFileButton(self, text, path, index, name, help_text):
if index < 10: # record buttons
cb = self.AddCheckBox(4, text, self.OnCheckRecPlay)
@@ -2688,7 +2681,6 @@ class ConfigTxAudio(BaseWindow):
application.CtrlTxAudioPreemph = sld
self.NextRow()
self.FitInside()
self.SetScrollRate(1, 1)
def OnTimer(self, event):
if not self.tmp_playing:
self.timer.Stop()
@@ -2737,8 +2729,8 @@ class Radios(BaseWindow): # The "Radios" first-level page
self.NextRow()
rb = self.AddRadioButton(1, "Add a new radio with the general type", self.NewTop)
choices = []
for name, data in local_conf.receiver_data:
choices.append(name)
for explain in local_conf.receiver_explain:
choices.append(explain)
choices.sort()
self.add_type = self.AddComboCtrl(2, '', choices=choices, no_edit=True)
self.add_type.SetSelection(0)
@@ -2766,7 +2758,6 @@ class Radios(BaseWindow): # The "Radios" first-level page
self.apply_btn = self.AddPushButton(3, "Apply", self.OnBtnApply)
self.NewRadioNames()
self.NewTop()
self.SetScrollRate(1, 1)
def NewTop(self, event=None):
if event:
btn = event.GetEventObject()
@@ -2810,7 +2801,7 @@ class Radios(BaseWindow): # The "Radios" first-level page
if not name or self.DuplicateName(name):
return
self.add_name.SetValue('')
typ = self.add_type.GetValue().strip()
typ = self.add_type.GetValue().split(',')[0].strip()
if local_conf.AddRadio(name, typ):
self.NewRadioNames()
local_conf.settings_changed = True
@@ -3003,7 +2994,6 @@ class RadioSection(BaseWindow): # The pages for each section in the second-leve
self.AddColSpacer(2, 20)
self.AddColSpacer(5, 20)
self.FitInside()
self.SetScrollRate(1, 1)
def OnButtonChangeFavorites(self, event):
if isinstance(event, (ChoiceCombo, ComboCtrl)):
path = event.GetValue()
@@ -3128,7 +3118,6 @@ class RadioHardwareBase(BaseWindow): # The Hardware page in the second-level no
self.NextRow(7)
self.AddColSpacer(2, 20)
self.AddColSpacer(5, 20)
self.SetScrollRate(1, 1)
def OnButtonChangeHardware(self, event):
if isinstance(event, (ChoiceCombo, ComboCtrl)):
path = event.GetValue()
@@ -3175,25 +3164,24 @@ class RadioHardware(RadioHardwareBase): # The Hardware page in the second-level
data_names = local_conf.GetReceiverData(radio_type)
col = 1
border = 2
hermes_board_id = 0
if radio_type == "Hermes":
try:
hermes_board_id = application.Hardware.hermes_board_id
except:
pass
try:
is_HermesLite2 = application.Hardware.is_HermesLite2
except:
is_HermesLite2 = False
if radio_name == Settings[1] and hasattr(application.Hardware, "ProgramGateware"):
help_text = "Choose an RBF file and program the Gateware (FPGA software) over Ethernet."
self.AddTextButtonHelp(1, "Gateware Update", "Program from RBF file..", application.Hardware.ProgramGateware, help_text)
col = 1
self.NextRow(self.row + 2)
for name, text, fmt, help_text, values in data_names:
text = text.replace("\\u2103", "\u2103")
if name in ('hardware_file_name', 'widgets_file_name'):
pass
elif name[0:4] == platform_ignore:
pass
elif name in ('Hermes_BandDictEnTx', ):
pass
elif 'Hl2_' in name and hermes_board_id != 6:
elif 'Hl2_' in name and not is_HermesLite2:
pass
elif fmt[0:4] in ('dict', 'list'):
pass
@@ -3222,7 +3210,7 @@ class RadioHardware(RadioHardwareBase): # The Hardware page in the second-level
col = 1
border = 2
self.NextRow()
if hermes_board_id == 6:
if is_HermesLite2:
if col == 4:
self.NextRow()
help_text = ('This controls the bias level for transistors in the final power amplifier. Enter a level from 0 to 255.'
@@ -3238,7 +3226,6 @@ class RadioHardware(RadioHardwareBase): # The Hardware page in the second-level
self.HermesWriteBiasButton = self.AddPushButton(7, "Write", self.OnButtonHermesWriteBias, border=0)
self.HermesWriteBiasButton.Enable(enbl)
self.FitInside()
self.SetScrollRate(1, 1)
def OnHermesChangeBias0(self, event):
value = self.HermesBias0.GetValue()
application.Hardware.ChangeBias0(value)
@@ -3852,7 +3839,6 @@ class RadioSound(BaseWindow): # The Sound page in the second-level notebook for
break
self.gbs = gbs
self.FitInside()
self.SetScrollRate(1, 1)
def ItemValue(self, row, col):
data_name = self.sound_names[row][col]
if col == 4: # Device names
@@ -4080,7 +4066,6 @@ The Tx bits are sent as the "Alex" filters and are sent to the J16 interface.'
self.AddBitField(col + 1, 7, 'Hermes_BandDictTx', band, bus, self.ChangeIO)
self.NextRow()
self.FitInside()
self.SetScrollRate(1, 1)
def SortCmp(self, item1):
# Numerical conversion of band name to megahertz
try:
@@ -4208,6 +4193,9 @@ class RadioRemote(BaseWindow): # The Remote page in the second-level notebook f
heading = None
self.NextRow()
self.col = 2
if not heading:
heading = self.AddTextHeading("TCI", text,
'Rig control commands and digital sample streams are sent to multiple clients using the TCI protocol.')
if not heading:
heading = self.AddTextHeading("Hamlib Rig 2", text,
'Rig control commands using "Hamlib Rig 2" are sent to a Quisk TCP port from multiple clients.')
@@ -4255,7 +4243,6 @@ class RadioRemote(BaseWindow): # The Remote page in the second-level notebook f
self.AddColSpacer(3, 20)
self.AddColSpacer(6, 20)
self.FitInside()
self.SetScrollRate(1, 1)
def OnChangePassword(self, event):
text = local_conf.globals.get('remote_radio_password', '')
dlg = wx.TextEntryDialog(self, "Please enter a password for remote access",
@@ -4337,7 +4324,6 @@ class xxRadioFilters(BaseWindow): # The Filters page in the second-level notebo
self.NextRow()
index += 1
self.FitInside()
self.SetScrollRate(1, 1)
def OnChangeFreq(self, event):
freq = event.GetString()
radio_dict = local_conf.GetRadioDict(self.radio_name)
+39 -7
View File
@@ -84,7 +84,7 @@ h3 {font-size:1.6rem;}
<div class="contents">
<h1>
<a id="Welcome"></a>
Welcome to QUISK (September 2025)
Welcome to QUISK (June 2026)
</h1>
<p>
@@ -197,18 +197,18 @@ 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.
A Quisk installation is needed for each version of Python you have.
<b>A Quisk installation is needed for each version of Python you have. If you upgrade to a newer Python
you must re-install Quisk. But your Quisk settings will be preserved.</b>
</p>
<br>
<h3 id="g0.2.2">Windows Initial Installation</h3>
<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
@@ -255,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>
@@ -305,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
@@ -376,17 +379,20 @@ python3 quisk.py
<br>
<p>
If "make" fails, you probably have missing packages or missing "-dev" packages. Try to figure out what is missing from the error messages.
You only clone github for the initial installation. To update Quisk, use pull:
You only clone github for the initial installation. To update Quisk, use pull followed by make:
</p>
<br>
<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.
@@ -1432,9 +1438,35 @@ is no longer needed
<br>
</p>
<h2 id="Digital">Digital Modes </h2>
<br>
<h3>Digital Modes with the TCI Protocol</h3>
<p>
Using the sound devices in a PC to transfer digital sample streams has always caused problems.
Several new methods are now becoming available. These are the <a href="https://github.com/ExpertSDR3/TCI">TCI protocol</a>
used by <a href="https://eesdr.com/en/">Expert Electronics</a> for
their SunSDR hardware
and <a href="https://www.vita.com/page-1855484">Vita49</a> used by <a href="https://www.flexradio.com">FlexRadio</a>.
And Hamlib is planning to add digital stream support.
</p>
<br>
<p>
Quisk can now connect to WSJT-X version 3.0.0 with the TCI protocol.
On the Quisk Config/radio/Remote screen enter IP address 127.0.0.1 and port 40001.
Use USB for digital modes. The DGT-* modes are for sound cards.
In WSJT-X choose the radio "TCI Client RX1", and for audio choose "TCI audio".
And enter the same IP and port. Everything then just works.
If your WSJT-X does not have "TCI Client RX1" you need to update to a more recent version.
This feature is new. To see the TCI conversation, use the "Debug level" on the Config/radio/Options screen.
</p>
<br>
<h3>Digital Modes with Sound Cards</h3>
<p>
Digital modes need to receive the audio from the radio to decode it.
They need to send audio to the radio to transmit.
These audio streams have traditionally been sent using PC sound cards or
an artificial sound card known as a Virtual Audio Cable.
Quisk has a number of modes "DGT-" to receive and transmit digital
signals. The modes "DGT-U" and "DGT-L" decode the signal as upper
signals on sound devices. The modes "DGT-U" and "DGT-L" decode the signal as upper
or lower sideband, and send the stereo audio to the digital sound
device. The left and right channel are the same.
The bandwidth is set with the filter buttons as
+1
View File
@@ -1,5 +1,6 @@
#include <Python.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <complex.h>
#include "quisk.h"
+1
View File
@@ -1,6 +1,7 @@
#include <Python.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include <complex.h> // Use native C99 complex type for fftw3
#include "quisk.h"
#include "filter.h"
+4 -4
View File
@@ -414,7 +414,7 @@ double quiskFilt111D4Coefs[196] = { 0.000022860383861326, 0.000070265173417164,
0.000878636067621920, 0.000694724116305488, 0.000487289901409396, 0.000301317969124407, 0.000160689233204503,
0.000070265173417164, 0.000022860383861326} ;
// Sample 53333 Hz, pass 20000, stop 24000, ripple 0.1 dB, atten 100 dB. For SDR-IQ. Stop 0.45000.
// Sample 53333 Hz, pass 20000, stop 24000, ripple 0.1 dB, atten 100 dB. For SDR-IQ. Pass 0.375. Stop 0.45000.
double quiskFilt53D1Coefs[55] = { 0.000318858391362187, 0.001662116369387873, -0.000065237353639775, -0.000854885424783294,
0.001693126335254083, -0.002034310867663472, 0.001354648789538254, 0.000509693525155950, -0.003079059747940221, 0.005242349925004462,
-0.005626507739484119, 0.003235398612833476, 0.001883583294529978, -0.008273775245721798, 0.013282431836060808, -0.013924391581141530,
@@ -426,7 +426,7 @@ double quiskFilt53D1Coefs[55] = { 0.000318858391362187, 0.001662116369387873, -0
-0.003079059747940221, 0.000509693525155950, 0.001354648789538254, -0.002034310867663472, 0.001693126335254083, -0.000854885424783294,
-0.000065237353639775, 0.001662116369387873, 0.000318858391362187 } ;
// Sample 53333 Hz, pass 10000, stop 12000, ripple 0.5 dB, atten 100 dB. For SDR-IQ. Stop 0.22500.
// Sample 53333 Hz, pass 10000, stop 12000, ripple 0.5 dB, atten 100 dB. For SDR-IQ. Pass 0.1875. Stop 0.22500.
double quiskFilt53D2Coefs[93] = { 0.000107966318729964, 0.000421475598772498, 0.000731343669484184, 0.000328611865414609,
-0.001457272471508304, -0.004143803290279123, -0.005736802939367581, -0.004303861070721504, -0.000415676176666607,
0.002561521351129964, 0.001705074918770091, -0.001794491466202784, -0.003398011312785532, -0.000595401366072839,
@@ -603,7 +603,7 @@ double quiskAudio48p6Coefs[71] = { 0.000324677779792651, 0.001038192405843851, 0
0.001132278163758895, 0.002811674671240099, 0.002938055116822665, 0.002082337464742640, 0.001038192405843851,
0.000324677779792651} ;
// Sample 96 kHz, pass 6, stop 40, ripple 0.1 dB, atten 100 dB. Stop 0.41667.
// Sample 96 kHz, pass 6, stop 40, ripple 0.1 dB, atten 100 dB. Pass 0.0625. Stop 0.41667.
double quiskAudio96Coefs[11] = { -0.004756607210954994, -0.022893760473075838, -0.023609616122334920,
0.078161168368381481, 0.277880235815005590, 0.388489826271597950, 0.277880235815005590,
0.078161168368381481, -0.023609616122334920, -0.022893760473075838, -0.004756607210954994} ;
@@ -663,7 +663,7 @@ double quiskAudioFmHpCoefs[309] = { 0.004847574947800705, -0.001211970386842814,
-0.000365311474947399, -0.000434052387522716, -0.000504808183463150, -0.000579117971409598, -0.000658731927598885, -0.000745515693376396,
-0.000841696354699768, -0.000949749961336003, -0.001072277374239886, -0.001211970386842814, 0.004847574947800705} ;
// Sample 24 kHz, pass 3, stop 4, ripple 0.1 dB, atten 100 dB. Stop 0.166667.
// Sample 24 kHz, pass 3, stop 4, ripple 0.1 dB, atten 100 dB. Pass 0.125. Stop 0.166667.
double quiskAudio24p3Coefs[100] = { 0.000040101217607380, 0.000144142945632828, 0.000313272623844654, 0.000472482653992188,
0.000478145332060253, 0.000196098255702949, -0.000370577202968848, -0.000996638165520275, -0.001297816111521122,
-0.000964536466628102, -0.000042557313824444, 0.000962997869327471, 0.001323534221298773, 0.000594056697947640,
+1 -681
View File
@@ -1,681 +1 @@
#include <Python.h>
#include <stdlib.h>
#include <math.h>
#include <complex.h> // Use native C99 complex type for fftw3
#include <sys/types.h>
#include "quisk.h"
#include "freedv.h"
#include <stdint.h>
int DEBUG;
#define MAX_RECEIVERS 2
typedef struct { // from comp.h
float real;
float imag;
} COMP;
struct freedv; // from freedv_api.h
typedef void (*freedv_callback_rx)(void *, char);
typedef char (*freedv_callback_tx)(void *);
/* Protocol bits are packed MSB-first */
/* Called when a frame containing protocol data is decoded */
typedef void (*freedv_callback_protorx)(void *, char *);
/* Called when a frame containing protocol data is to be sent */
typedef void (*freedv_callback_prototx)(void *, char *);
/* Data packet callbacks */
/* Called when a packet has been received */
typedef void (*freedv_callback_datarx)(void *, unsigned char *packet, size_t size);
/* Called when a new packet can be send */
typedef void (*freedv_callback_datatx)(void *, unsigned char *packet, size_t *size);
/* advanced freedv open options required by some modes */
struct freedv_advanced {
int interleave_frames; // now unused but remains to prevent breaking API for
// legacy apps
// parameters for FREEDV_MODE_FSK_LDPC
int M; // 2 or 4 FSK
int Rs; // Symbol rate Hz
int Fs; // Sample rate Hz
int first_tone; // Freq of first tone Hz
int tone_spacing; // Spacing between tones Hz
char *codename; // LDPC codename, from codes listed in ldpc_codes.c
};
#ifdef MS_WINDOWS
#include <windows.h>
HMODULE hLib;
#define GET_HANDLE1 hLib = LoadLibrary("libcodec2.dll")
#define GET_HANDLE2 hLib = LoadLibrary(".\\freedvpkg\\libcodec2.dll")
#define GET_HANDLE3 hLib = LoadLibrary(".\\freedvpkg\\libcodec2_32.dll")
#define GET_HANDLE4 hLib = LoadLibrary(".\\freedvpkg\\libcodec2_64.dll")
#define GET_ADDR(name) (void *)GetProcAddress(hLib, name)
#define CLOSE_LIB FreeLibrary(hLib)
#else
#include <dlfcn.h>
void * hLib;
#define GET_HANDLE1 hLib = dlopen("libcodec2.so", RTLD_LAZY)
#define GET_HANDLE2 hLib = dlopen("./freedvpkg/libcodec2.so", RTLD_LAZY)
#define GET_HANDLE3 hLib = dlopen("./freedvpkg/libcodec2_32.so", RTLD_LAZY)
#define GET_HANDLE4 hLib = dlopen("./freedvpkg/libcodec2_64.so", RTLD_LAZY)
#define GET_ADDR(name) dlsym(hLib, name)
#define CLOSE_LIB dlclose(hLib)
#endif
static int requested_mode = -1; // requested mode
int freedv_current_mode = -1; // the current running mode
static int quisk_freedv_squelch;
static int interleave_frames = 1;
static int freedv_version = -1;
static int quisk_set_tx_bpf = 1;
static int handle_index = -1;
// need access to these here and in quisk.c
int n_speech_sample_rate = 8000;
int n_modem_sample_rate = 8000;
int n_max_modem_samples = 1000;
#define SPEECH_BUF_SIZE 12000 // speech buffer size increased from 3000
#define MAX_MODEM_SAMPLES 8192 // maximum of n_max_modem_samples for any mode
#define MAX_SPEECH_SAMPLES 8192 // maximum of n_speech_samples for any mode
static struct _rx_channel{
struct freedv * hFreedv;
short * demod_in;
int rxdata_index;
short speech_out[SPEECH_BUF_SIZE]; // output buffer
int speech_available; // number of samples in output buffer
int playing; // are we currently returning speech samples?
} rx_channel[MAX_RECEIVERS] = { 0 }; // safe initialisation to 0
// freedv_version is the library version number, or
// -1 no library was found
// -2 a library was found, but freedv_get_version is missing
// FreeDV API functions:
// open, close
struct freedv * (*freedv_open)(int mode);
struct freedv * (*freedv_open_advanced)(int mode, struct freedv_advanced *adv);
void (*freedv_close)(struct freedv *freedv);
// Transmit
void (*freedv_tx)(struct freedv *freedv, short *, short *);
void (*freedv_comptx)(struct freedv *freedv, COMP *, short *);
// Receive
int (*freedv_nin)(struct freedv *freedv);
int (*freedv_rx)(struct freedv *freedv, short *, short demod_in[]);
int (*freedv_floatrx)(struct freedv *freedv, short *, float demod_in[]);
int (*freedv_comprx)(struct freedv *freedv, short *, COMP demod_in[]);
// Set parameters
void (*freedv_set_callback_txt) (struct freedv *freedv, freedv_callback_rx rx, freedv_callback_tx tx, void *callback_state);
void (*freedv_set_test_frames) (struct freedv *freedv, int test_frames);
void (*freedv_set_smooth_symbols) (struct freedv *freedv, int smooth_symbols);
void (*freedv_set_squelch_en) (struct freedv *freedv, int squelch_en);
void (*freedv_set_snr_squelch_thresh) (struct freedv *freedv, float snr_squelch_thresh);
void (*freedv_set_tx_bpf) (struct freedv *freedv, int val);
// Get parameters
int (*freedv_get_version)(void);
void (*freedv_get_modem_stats)(struct freedv *freedv, int *sync, float *snr_est);
int (*freedv_get_test_frames) (struct freedv *freedv);
int (*freedv_get_n_speech_samples) (struct freedv *freedv);
int (*freedv_get_n_max_modem_samples) (struct freedv *freedv);
int (*freedv_get_n_nom_modem_samples) (struct freedv *freedv);
int (*freedv_get_total_bits) (struct freedv *freedv);
int (*freedv_get_total_bit_errors) (struct freedv *freedv);
int (*freedv_get_modem_sample_rate) (struct freedv *freedv);
int (*freedv_get_speech_sample_rate) (struct freedv *freedv);
//
int (*freedv_get_sync) (struct freedv *freedv);
void (*freedv_set_callback_protocol) (struct freedv *freedv, freedv_callback_protorx rx, freedv_callback_prototx tx, void *callback_state);
void (*freedv_set_callback_data) (struct freedv *freedv, freedv_callback_datarx datarx, freedv_callback_datatx datatx, void *callback_state);
//
// checkAvxSupport
//
// This function was copied from freedv, file fdmdv2_main.cpp and tweaked for use here
//
// Tests the underlying platform for AVX support. 2020 needs AVX support to run
// in real-time, and old processors do not offer AVX support
//
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386) || defined(_M_IX86)
#include <cpuid.h>
static int checkAvxSupport(void)
{
int isAvxPresent = 0;
uint32_t eax, ebx, ecx, edx;
eax = ebx = ecx = edx = 0;
__cpuid(1, eax, ebx, ecx, edx);
if (ecx & (1<<27) && ecx & (1<<28)) {
// CPU supports XSAVE and AVX
uint32_t xcr0, xcr0_high;
asm("xgetbv" : "=a" (xcr0), "=d" (xcr0_high) : "c" (0));
isAvxPresent = (xcr0 & 6) == 6; // AVX state saving enabled?
}
return isAvxPresent;
}
#else
static int checkAvxSupport(void)
{
return 0;
}
#endif
/* Called when a new packet can be sent */
void my_datatx(void *callback_state, unsigned char *packet, size_t *size) {
*size = 0;
}
static void GetAddrs(void)
{
handle_index = 1;
GET_HANDLE1;
if ( ! hLib) {
handle_index = 2;
GET_HANDLE2;
if ( ! hLib) {
handle_index = 4;
GET_HANDLE4;
}
}
if (hLib) {
freedv_get_version = GET_ADDR("freedv_get_version");
if (freedv_get_version != NULL) {
freedv_version = freedv_get_version();
if (DEBUG)
printf("FreeDV codec2 library %d found, version %d\n", handle_index, freedv_version);
}
else {
freedv_version = -2;
if (DEBUG)
printf("FreeDV codec2 library %d found, but no freedv_get_version() !!!\n", handle_index);
CLOSE_LIB;
return;
}
}
else {
handle_index = -1;
if (DEBUG)
printf("Could not find the FreeDV codec2 shared library\n");
return;
}
// open, close
freedv_open = GET_ADDR("freedv_open");
freedv_open_advanced = GET_ADDR("freedv_open_advanced");
freedv_close = GET_ADDR("freedv_close");
// Transmit
freedv_tx = GET_ADDR("freedv_tx");
freedv_comptx = GET_ADDR("freedv_comptx");
// Receive
freedv_nin = GET_ADDR("freedv_nin");
freedv_rx = GET_ADDR("freedv_rx");
freedv_floatrx = GET_ADDR("freedv_floatrx");
freedv_comprx = GET_ADDR("freedv_comprx");
// Set parameters
freedv_set_callback_txt = GET_ADDR("freedv_set_callback_txt");
freedv_set_callback_protocol = GET_ADDR("freedv_set_callback_protocol");
freedv_set_callback_data = GET_ADDR("freedv_set_callback_data");
freedv_set_test_frames = GET_ADDR("freedv_set_test_frames");
freedv_set_smooth_symbols = GET_ADDR("freedv_set_smooth_symbols");
freedv_set_squelch_en = GET_ADDR("freedv_set_squelch_en");
freedv_set_snr_squelch_thresh = GET_ADDR("freedv_set_snr_squelch_thresh");
freedv_set_tx_bpf = GET_ADDR("freedv_set_tx_bpf");
// Get parameters
freedv_get_modem_stats = GET_ADDR("freedv_get_modem_stats");
freedv_get_test_frames = GET_ADDR("freedv_get_test_frames");
freedv_get_n_speech_samples = GET_ADDR("freedv_get_n_speech_samples");
freedv_get_n_max_modem_samples = GET_ADDR("freedv_get_n_max_modem_samples");
freedv_get_n_nom_modem_samples = GET_ADDR("freedv_get_n_nom_modem_samples");
freedv_get_total_bits = GET_ADDR("freedv_get_total_bits");
freedv_get_total_bit_errors = GET_ADDR("freedv_get_total_bit_errors");
freedv_get_sync = GET_ADDR("freedv_get_sync"); // requires version 11
if( freedv_version >= 11)
freedv_get_modem_sample_rate = GET_ADDR("freedv_get_modem_sample_rate"); // requires version 11 or later
if (freedv_version >= 12)
freedv_get_speech_sample_rate = GET_ADDR("freedv_get_speech_sample_rate"); // requires version 12
return;
}
static int quisk_freedv_rx(short * sSamples, double * dsamples, int count, int bank) // Called from the sound thread.
{ // Input digital modulation is sSamples; decoded voice is dsamples. Each "bank" is a stream of audio.
// caller will have decimated input audio down to n_modem_sample_rate and reduced block size to match reduction
// output data will be returned at n_speech_sample_rate with corresponding block sizes
int i, nout, need, have, sync;
int n_speech_samples;
struct freedv * hF;
struct _rx_channel * pCh;
int nCountOut = 1; // In case we have a higher (or lower) value for o/p count
if (sSamples == NULL) { // shutdown
for (i = 0; i < MAX_RECEIVERS; i++) {
if (rx_channel[i].demod_in) {
free(rx_channel[i].demod_in);
rx_channel[i].demod_in = NULL;
}
}
return 0;
}
if (bank < 0 || bank >= MAX_RECEIVERS)
return 0;
hF = rx_channel[bank].hFreedv;
if ( ! hF)
return 0;
pCh = rx_channel + bank;
n_speech_samples = freedv_get_n_speech_samples(hF);
// if (say) input modem rate is the 2020 mode rate of 8000 sa/sec
// and output rate is the corresponding 16000 sa/sec then for an input
// block size of N, outbut 2N samples block size.
// Apply a multiplier/divisor to 'count' for the output samples count
if(n_speech_sample_rate >= n_modem_sample_rate ) {
int nRatio = n_speech_sample_rate / n_modem_sample_rate;
if( nRatio >= 1 && nRatio <= 6)
nCountOut = nRatio * count;
}
else {
int nRatio = n_modem_sample_rate / n_speech_sample_rate;
if( nRatio >= 1 && nRatio <= 6)
nCountOut = count / nRatio;
}
nout = 0;
need = freedv_nin(hF);
for (i = 0; i < count; i++) {
pCh->demod_in[pCh->rxdata_index] = sSamples[i];
pCh->rxdata_index++;
if (pCh->rxdata_index >= need) {
if (pCh->speech_available + n_speech_samples < SPEECH_BUF_SIZE) { // check for buffer space
have = freedv_rx(hF, pCh->speech_out + pCh->speech_available, pCh->demod_in);
if (freedv_version > 10)
sync = freedv_get_sync(hF);
else
freedv_get_modem_stats(hF, &sync, NULL);
if (freedv_current_mode == 0) { // mode 1600
if (sync) // throw away speech if not in sync
pCh->speech_available += have;
}
else if (pCh->speech_available < SPEECH_BUF_SIZE * 2 / 3) {
pCh->speech_available += have; // keep speech if there is space
}
else {
if (DEBUG) printf("Close to maximum in speech output buffer\n");
}
}
else { // no space in buffer
if (DEBUG) printf("Overflow in speech output buffer\n");
}
pCh->rxdata_index = 0;
need = freedv_nin(hF);
}
}
if ( ! pCh->playing) {
if (pCh->speech_available >= 2 * n_speech_samples) {
pCh->playing = 1;
}
else { // return zero samples
for (i = 0; i < nCountOut; i++)
dsamples[i] = 0;
//if (DEBUG) printf("Rx buffer playing %d available %d\n", pCh->playing, pCh->speech_available);
return nCountOut;
}
}
for (nout = 0; nout < pCh->speech_available && nout < nCountOut; nout++)
dsamples[nout] = pCh->speech_out[nout] * ((double)CLIP32 / CLIP16 * 0.99);
if (nout) {
pCh->speech_available -= nout;
memmove(pCh->speech_out, pCh->speech_out + nout, (pCh->speech_available) * sizeof(short));
}
if ( ! pCh->speech_available) {
pCh->playing = 0;
while (nout < nCountOut)
dsamples[nout++] = 0;
}
//if (DEBUG) printf("Rx buffer playing %d available %d\n", pCh->playing, pCh->speech_available);
return nout;
}
static int quisk_freedv_tx(complex double * cSamples, double * dsamples, int count, int is_real) // Called from the sound thread.
{ // Input voice samples are dsamples; output digital modulation is cSamples.
int i, nout;
int n_speech_samples;
int n_nom_modem_samples;
static COMP * mod_out = NULL;
static short * speech_in = NULL;
static int speech_index=0, mod_index=0;
// g8kbb - add for 800XA
static short *real_mod_out = NULL;
if (dsamples == NULL) { // shutdown
return 0; // nothing to do
}
if ( ! rx_channel[0].hFreedv)
return 0;
n_speech_samples = freedv_get_n_speech_samples(rx_channel[0].hFreedv);
n_nom_modem_samples = freedv_get_n_nom_modem_samples(rx_channel[0].hFreedv);
// find if need to output at faster rate
int nRatio = n_modem_sample_rate / n_speech_sample_rate;
if( nRatio < 1 || nRatio > 6 )
nRatio = 1;
if (mod_out == NULL) { // initialize
mod_out = (COMP *)malloc(sizeof(COMP) * MAX_MODEM_SAMPLES);
memset(mod_out, 0, sizeof(COMP) * MAX_MODEM_SAMPLES);
speech_in = (short*)malloc(sizeof(short) * MAX_SPEECH_SAMPLES);
speech_index=0;
mod_index=0;
real_mod_out = (short *)malloc(sizeof(short) * MAX_MODEM_SAMPLES);
memset(real_mod_out, 0, sizeof(short) * MAX_MODEM_SAMPLES);
//if( DEBUG ) printf("Initialise tx buffer size = %d, i/p sample size = %d\n", n_nom_modem_samples, n_speech_samples );
}
nout = 0;
for (i = 0; i < count; i++) {
speech_in[speech_index++] = (short)dsamples[i];
if (speech_index >= n_speech_samples) {
// Calculate a new block, but first write out the rest of the old block
// 800xa uses real not complex
if( is_real ) {
for ( ; mod_index < n_nom_modem_samples; mod_index++)
cSamples[nout++] = real_mod_out[mod_index];
freedv_tx(rx_channel[0].hFreedv, real_mod_out, speech_in);
}
else {
for ( ; mod_index < n_nom_modem_samples; mod_index++)
cSamples[nout++] = mod_out[mod_index].real + I * mod_out[mod_index].imag;
freedv_comptx(rx_channel[0].hFreedv, mod_out, speech_in);
}
//if( DEBUG ) printf("Block processed, speech index=%d, n_speech_samples=%d, count=%d\n", speech_index, n_speech_samples, count );
mod_index = 0;
speech_index = 0;
}
else { // write out samples
for( int j=0; j<nRatio; j++) {
if (mod_index < n_nom_modem_samples ) {
if( is_real ) {
cSamples[nout++] = real_mod_out[mod_index];
}
else {
cSamples[nout++] = mod_out[mod_index].real + I * mod_out[mod_index].imag;
}
mod_index++;
}
}
}
}
if (rxMode == FDV_L)
for (i = 0; i < nout; i++)
cSamples[i] = conj(cSamples[i]);
return nout;
}
#define TX_MSG_SIZE 80
static char quisk_tx_msg[TX_MSG_SIZE];
static char get_next_tx_char(void * callback_state)
{
char c;
static int index = 0;
c = quisk_tx_msg[index++];
if (index >= TX_MSG_SIZE)
index = 0;
if ( ! c) {
index = 0;
c = quisk_tx_msg[index++];
}
return c;
}
#define RX_MSG_SIZE 80
static char quisk_rx_msg[RX_MSG_SIZE + 1];
static void put_next_rx_char(void * callback_state, char ch)
{
if (ch == '\n' || ch == '\r')
ch = ' ';
if (ch < 32 || ch > 126) // printable characters
return;
if (strlen(quisk_rx_msg) < RX_MSG_SIZE)
strncat(quisk_rx_msg, &ch, 1);
}
PyObject * quisk_freedv_get_rx_char(PyObject * self, PyObject * args) // Called from the GUI thread.
{
PyObject * txt;
if (!PyArg_ParseTuple (args, ""))
return NULL;
txt = PyString_FromString(quisk_rx_msg);
quisk_rx_msg[0] = 0;
return txt;
}
static void CloseFreedv(void) // Called from the GUI thread or sound thread
{
int i;
for (i = 0; i < MAX_RECEIVERS; i++) {
if (rx_channel[i].hFreedv) {
freedv_close(rx_channel[i].hFreedv);
rx_channel[i].hFreedv = NULL;
}
}
quisk_freedv_rx(NULL, NULL, 0, 0);
quisk_freedv_tx(NULL, NULL, 0, 0);
freedv_current_mode = -1;
}
static int OpenFreedv(void) // Called from the GUI thread or sound thread
{
int i;
struct freedv * hF;
if ( ! hLib)
GetAddrs(); // Get the entry points for funtions in the codec2 library
if (DEBUG) {
switch (handle_index) {
case 1:
printf("freedv_open: system codec2 library found, version %d\n", freedv_version);
break;
case 2:
printf("freedv_open: codec2 library freedvpkg/libcodec2.dll|so found, version %d\n", freedv_version);
break;
case 3:
case 4:
printf("freedv_open: codec2 library freedvpkg/libcodec2_64 found, version %d\n", freedv_version);
break;
default:
printf("freedv_open: Could not find the FreeDV codec2 shared library\n");
break;
}
}
if (freedv_version < 10) {
CloseFreedv();
requested_mode = -1;
if (DEBUG)
printf("freedv_open: Failure because version is less than 10\n");
return 0; // failure
}
if (requested_mode == FREEDV_MODE_2020) {
if (checkAvxSupport() == 0) {
CloseFreedv();
requested_mode = -1;
if (DEBUG)
printf("freedv_open: Failure because mode 2020 requires Avx support\n");
return 0; // failure
}
if (handle_index >= 3) { // Quisk provided codec2
CloseFreedv();
requested_mode = -1;
if (DEBUG)
printf("freedv_open: Failure because mode 2020 requires a system installation of codec2\n");
return 0; // failure
}
}
if ((requested_mode == FREEDV_MODE_700D || requested_mode == FREEDV_MODE_700E) && freedv_open_advanced) {
struct freedv_advanced adv;
memset(&adv, 0, sizeof(adv));
adv.interleave_frames = interleave_frames;
hF = freedv_open_advanced(requested_mode, &adv);
}
else {
hF = freedv_open(requested_mode);
}
if (hF == NULL) {
CloseFreedv();
requested_mode = -1;
return 0; // failure
}
rx_channel[0].hFreedv = hF;
quisk_dvoice_freedv(&quisk_freedv_rx, &quisk_freedv_tx);
if (quisk_tx_msg[0])
freedv_set_callback_txt(hF, &put_next_rx_char, &get_next_tx_char, NULL);
else
freedv_set_callback_txt(hF, &put_next_rx_char, NULL, NULL);
if (freedv_set_callback_protocol)
freedv_set_callback_protocol(hF, NULL, NULL, NULL);
if (freedv_set_callback_data)
freedv_set_callback_data(hF, NULL, my_datatx, NULL);
freedv_set_squelch_en(hF, quisk_freedv_squelch);
if (freedv_set_tx_bpf)
freedv_set_tx_bpf(hF, quisk_set_tx_bpf);
n_max_modem_samples = freedv_get_n_max_modem_samples(hF);
n_speech_sample_rate = 8000;
if (freedv_version >= 12 && freedv_get_speech_sample_rate)
n_speech_sample_rate = freedv_get_speech_sample_rate(hF);
n_modem_sample_rate = 8000;
if (freedv_version >= 11 && freedv_get_modem_sample_rate)
n_modem_sample_rate = freedv_get_modem_sample_rate(hF);
for (i = 0; i < MAX_RECEIVERS; i++) {
rx_channel[i].rxdata_index = 0;
rx_channel[i].speech_available = 0;
rx_channel[i].playing = 0;
if (rx_channel[i].demod_in)
free(rx_channel[i].demod_in);
rx_channel[i].demod_in = (short *)malloc(sizeof(short) * n_max_modem_samples);
if( rx_channel[i].demod_in == NULL) {
CloseFreedv();
requested_mode = -1;
return 0; // failure
}
if (i > 0) {
rx_channel[i].hFreedv = freedv_open(requested_mode);
if (rx_channel[i].hFreedv)
freedv_set_squelch_en(rx_channel[i].hFreedv, quisk_freedv_squelch);
}
}
if (DEBUG) printf("n_nom_modem_samples %d\n", freedv_get_n_nom_modem_samples(rx_channel[0].hFreedv));
if (DEBUG) printf("n_speech_samples %d\n", freedv_get_n_speech_samples(rx_channel[0].hFreedv));
if (DEBUG) printf("n_max_modem_samples %d\n", n_max_modem_samples);
if (DEBUG) {
if (freedv_version >= 11) printf("modem_sample_rate %d\n", n_modem_sample_rate);
if (freedv_version >= 12) printf("speech_sample_rate %d\n", n_speech_sample_rate);
}
freedv_current_mode = requested_mode;
return 1; // success
}
void quisk_check_freedv_mode(void)
{ // see if we need to change the mode
if (requested_mode == freedv_current_mode)
return;
if (DEBUG) printf("Change in mode to %d\n", requested_mode);
CloseFreedv();
if (requested_mode >= 0)
OpenFreedv();
else
requested_mode = -1;
}
PyObject * quisk_freedv_open(PyObject * self, PyObject * args) // Called from the GUI thread before freedv is open
{
if (!PyArg_ParseTuple (args, ""))
return NULL;
return PyInt_FromLong(OpenFreedv());
}
PyObject * quisk_freedv_close(PyObject * self, PyObject * args) // Called from the GUI thread.
{
if (!PyArg_ParseTuple (args, ""))
return NULL;
requested_mode = -1; // request close
Py_INCREF (Py_None);
return Py_None;
}
PyObject * quisk_freedv_set_options(PyObject * self, PyObject * args, PyObject * keywds) // Called from the GUI thread.
{ // Call with keyword arguments ONLY to change parameters. Call before quisk_freedv_open() to set an initial mode.
int mode=-1; // Call again to change the mode.
int bpf=-1;
char * ptMsg=NULL;
static char * kwlist[] = {"mode", "tx_msg", "DEBUG", "squelch", "interleave_frames", "set_tx_bpf", NULL} ;
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|isiiii", kwlist, &mode, &ptMsg, &DEBUG, &quisk_freedv_squelch, &interleave_frames, &bpf))
return NULL;
if (ptMsg)
strMcpy(quisk_tx_msg, ptMsg, TX_MSG_SIZE);
if (bpf != -1) {
quisk_set_tx_bpf = bpf;
if (freedv_set_tx_bpf && rx_channel[0].hFreedv)
freedv_set_tx_bpf(rx_channel[0].hFreedv, quisk_set_tx_bpf);
}
if (mode == -1) { // mode was not entered
;
}
else if (mode == FREEDV_MODE_700E && freedv_version >= 14) {
requested_mode = mode;
}
else if (mode == FREEDV_MODE_2020) {
if (checkAvxSupport() && handle_index <= 2 && freedv_version >= 13)
requested_mode = mode;
}
else if (freedv_version == 10) {
if (mode == 0)
requested_mode = mode;
}
else if (freedv_version == 11) {
if (mode <= 2)
requested_mode = mode;
}
else {
requested_mode = mode;
}
return PyInt_FromLong(requested_mode); // Return the mode
}
PyObject * quisk_freedv_get_snr(PyObject * self, PyObject * args) // Called from the GUI thread.
{
float snr_est = 0.0;
if (!PyArg_ParseTuple (args, ""))
return NULL;
if (rx_channel[0].hFreedv)
freedv_get_modem_stats(rx_channel[0].hFreedv, NULL, &snr_est);
return PyFloat_FromDouble(snr_est);
}
PyObject * quisk_freedv_get_version(PyObject * self, PyObject * args) // Called from the GUI thread.
{
if (!PyArg_ParseTuple (args, ""))
return NULL;
if ( ! hLib)
GetAddrs(); // Get the entry points for funtions in the codec2 library
return PyInt_FromLong(freedv_version);
}
PyObject * quisk_freedv_set_squelch_en(PyObject * self, PyObject * args) // Called from the GUI thread.
{
int i,state;
if (!PyArg_ParseTuple (args, "i", &state))
return NULL;
quisk_freedv_squelch = state;
for (i = 0; i < MAX_RECEIVERS; i++) {
if( rx_channel[i].hFreedv )
freedv_set_squelch_en(rx_channel[i].hFreedv, state);
}
return PyInt_FromLong(state);
}
// This file is no longer used.
+1 -24
View File
@@ -1,24 +1 @@
extern int freedv_current_mode; // the current FreeDV running mode
// available speech modes
#define FREEDV_MODE_1600 0
#define FREEDV_MODE_2400A 3
#define FREEDV_MODE_2400B 4
#define FREEDV_MODE_800XA 5
#define FREEDV_MODE_700C 6
#define FREEDV_MODE_700D 7
#define FREEDV_MODE_700E 13
#define FREEDV_MODE_2020 8
#define FREEDV_MODE_2020B 16
// available data modes
#define FREEDV_MODE_FSK_LDPC 9
#define FREEDV_MODE_DATAC1 10
#define FREEDV_MODE_DATAC3 12
#define FREEDV_MODE_DATAC0 14
#define FREEDV_MODE_DATAC4 18
#define FREEDV_MODE_DATAC13 19
// Sample rates used
#define FREEDV_FS_8000 8000
#define FREEDV_FS_16000 16000
// This header file is no longer used.
+1 -129
View File
@@ -1,129 +1 @@
Note: The directory freedvpkg no longer contains source files. It only contains copies of
the codec2 libraries for use by Quisk. If any other files are present, delete them.
FreeDV and Directory freedvpkg
==============================
FreeDV is the combination of the codec2 codec and the fdmdv modem. It provides digital voice
suitable for HF and VHF transmission. Quisk has native (built-in) support for
FreeDV. Just push the FDV mode and talk. This freedvpkg directory contains copies of the
codec2 libraries for use by Quisk.
You can also use the separate FreeDV program available at freedv.org, and the Quisk DGT-U mode
which attaches to external digital programs. The setup is identical to fldigi and other external
digital programs.
Quisk will add the FDV button if specified on the Config/radio/Controls tab.
If there is a problem with the freedv module, the button will be grayed out.
The freedv module requires the codec2 library. This library is included for Windows and for
Ubuntu 18.04 LTS 32-bit and 64-bit. For other systems (such as ARM) you will need to build another
codec2. Just try the FDV mode and see if it works. It should always work on Windows, and may work
on Linux. If the FDV button is grayed out, you need a different codec2 than the one included. Make
a new codec2 library, and copy it to freedvpkg or to the correct system directory.
2020 mode
=========
You can now use 2020 mode with Quisk. Note however that 2020 mode requires a processor with AVX.
Also note a Raspberry PI (model 4B) does not have enough processing power even if neon is used
and it is overclocked.
There are two parts to this:
1. build LPCNet
2. rebuild codec2
The details are found here: https://github.com/drowe67/codec2#freedv-2020-support-building-with-lpcnet
Both liblpcnetfreedv and libcodec2 are needed. If you are running under windows, the resultant
liblpcnetfreedv.dll file needs to be placed in the quisk folder. Libcodec2 may be placed in the
quisk freedvpkg folder. Or install the DLL's in the correct system folders.
Search Order
============
Quisk will search for a valid codec2 library in this order on Windows:
1. The system codec2 library installed outside of Quisk by another program.
2. freedvpkg/libcodec2.dll. Not included. You can copy your own codec2 here but a system-wide installation is better.
3. freedvpkg/libcodec2_32.dll. The 32-bit codec2 shipped with Quisk.
4. freedvpkg/libcodec2_64.dll. The 64-bit codec2 shipped with Quisk.
Quisk will search for a valid codec2 library in this order on Linux:
1. The system codec2 library installed outside of Quisk by another program.
2. freedvpkg/libcodec2.so. Not included. You can copy your own codec2 here but a system-wide installation is better.
3. freedvpkg/libcodec2_32.so. The 32-bit codec2 shipped with Quisk.
4. freedvpkg/libcodec2_64.so. The 64-bit codec2 shipped with Quisk.
How to Build a New codec2
=========================
The source for codec2 is available on github. Or google for other sources or perhaps a pre-built library.
If you need to compile codec2 from source, first change to a suitable directory (not the Quisk directory)
and download the source with git:
git clone https://github.com/drowe67/codec2.git
Then build codec2 using the directions found at https://github.com/drowe67/codec2/blob/master/INSTALL.
The directions given below are current as of June 2020, but check for changes.
Then copy the codec2 library to the freedvpkg directory under Quisk.
Build a New codec2 on Linux
===========================
Create the codec2 shared library. This assumes a 64-bit linux. Change the directory
name from build_linux64 to build_linux32 for 32-bit linux. Note the "../".
cd codec2
mkdir build_linux64
cd build_linux64
cmake -DCMAKE_BUILD_TYPE=Release ../
make
cd src
cp libcodec2.so my-quisk-directory/freedvpkg
Build a New 32-bit codec2 on 64-bit Linux
=========================================
Make sure package libc6-dev-i386 is installed.
Create the codec2 shared library. Note the "../".
cd codec2
mkdir build_linux32
cd build_linux32
export CFLAGS=-m32
cmake -DCMAKE_BUILD_TYPE=Release ../
make
cd src
cp libcodec2.so my-quisk-directory/freedvpkg
Build a New codec2 on Windows
=============================
For Windows you need to install MinGW-w64, MSYS2, and g++. Use the mingw shell. The Speex libraries
are not needed by codec2, but are required for the Unit Test modules. To build the Unit Test modules,
you need to install Speex and add -DSPEEXDSP_INCLUDE_DIR=../speex/include/speex -DSPEEXDSP_LIBRARY=../speex/bin.
# Use mingw32.exe 32-bit shell:
cd codec2
mkdir build_win32
cd build_win32
cmake -G "MSYS Makefiles" -DCMAKE_BUILD_TYPE=Release -DUNITTEST=OFF ../
make codec2
cd src
cp libcodec2.dll my-quisk-directory/freedvpkg
# Use mingw64.exe 64-bit shell:
cd codec2
mkdir build_win64
cd build_win64
cmake -G "MSYS Makefiles" -DCMAKE_BUILD_TYPE=Release -DUNITTEST=OFF -DCMAKE_SYSTEM_PROCESSOR=x86_64 ../
make codec2
cd src
cp libcodec2.dll my-quisk-directory/freedvpkg
Testing
=======
You can just start Quisk and see if the FDV button is not grayed out, and FDV works. Or you can
test the import of freedv and look for error messages.
cd my-quisk-directory
python # Use the correct Python 2 or 3 in 32 or 64 bit
import _quisk
_quisk.freedv_get_version() # This should return 13 or higher for a recent codec2
This directory is no longer used by FreeDV. Please see the documentation.
Binary file not shown.
Binary file not shown.
+144
View File
@@ -0,0 +1,144 @@
/*
* Copyright (C) 2016-2024 Davidson Francis <davidsondfgl@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#define _POSIX_C_SOURCE 200809L
#include "base64.h"
#include "sha1.h"
#include "ws.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
/**
* @dir src/
* @brief Handshake routines directory
*
* @file handshake.c
* @brief Handshake routines.
*/
/**
* @brief Gets the field Sec-WebSocket-Accept on response, by
* an previously informed key.
*
* @param wsKey Sec-WebSocket-Key
* @param dest source to be stored the value.
*
* @return Returns 0 if success and a negative number
* otherwise.
*
* @attention This is part of the internal API and is documented just
* for completeness.
*/
int get_handshake_accept(char *wsKey, unsigned char **dest)
{
unsigned char hash[SHA1HashSize]; /* SHA-1 Hash. */
SHA1Context ctx; /* SHA-1 Context. */
char *str; /* WebSocket key + magic string. */
/* Invalid key. */
if (!wsKey)
return (-1);
str = calloc(1, sizeof(char) * (WS_KEY_LEN + WS_MS_LEN + 1));
if (!str)
return (-1);
strncpy(str, wsKey, WS_KEY_LEN);
strcat(str, MAGIC_STRING);
SHA1Reset(&ctx);
SHA1Input(&ctx, (const uint8_t *)str, WS_KEYMS_LEN);
SHA1Result(&ctx, hash);
*dest = base64_encode(hash, SHA1HashSize, NULL);
*(*dest + strlen((const char *)*dest) - 1) = '\0';
free(str);
return (0);
}
/**
* @brief Finds the ocorrence of @p needle in @p haystack, case
* insensitive.
*
* @param haystack Target string to be searched.
* @param needle Substring to search for.
*
* @returns If found, returns a pointer at the beginning of the
* found substring. Otherwise, returns NULL.
*/
static const char *strstricase(const char *haystack, const char *needle)
{
size_t length;
for (length = strlen(needle); *haystack; haystack++)
if (!strncasecmp(haystack, needle, length))
return haystack;
return (NULL);
}
/**
* @brief Gets the complete response to accomplish a succesfully
* handshake.
*
* @param hsrequest Client request.
* @param hsresponse Server response.
*
* @return Returns 0 if success and a negative number
* otherwise.
*
* @attention This is part of the internal API and is documented just
* for completeness.
*/
int get_handshake_response(char *hsrequest, char **hsresponse)
{
unsigned char *accept; /* Accept message. */
char *saveptr; /* strtok_r() pointer. */
char *s; /* Current string. */
int ret; /* Return value. */
saveptr = NULL;
for (s = strtok_r(hsrequest, "\r\n", &saveptr); s != NULL;
s = strtok_r(NULL, "\r\n", &saveptr))
{
if (strstricase(s, WS_HS_REQ) != NULL)
break;
}
/* Ensure that we have a valid pointer. */
if (s == NULL)
return (-1);
saveptr = NULL;
s = strtok_r(s, " ", &saveptr);
s = strtok_r(NULL, " ", &saveptr);
ret = get_handshake_accept(s, &accept);
if (ret < 0)
return (ret);
*hsresponse = malloc(sizeof(char) * WS_HS_ACCLEN);
if (*hsresponse == NULL)
return (-1);
strcpy(*hsresponse, WS_HS_ACCEPT);
strcat(*hsresponse, (const char *)accept);
strcat(*hsresponse, "\r\n\r\n");
free(accept);
return (0);
}
+157
View File
@@ -0,0 +1,157 @@
# Please do not change this widgets module for Quisk. Instead copy
# it to your own quisk_widgets.py and make changes there.
#
# This module is used to add extra widgets to the QUISK screen.
import wx, math
class BottomWidgets: # Add extra widgets to the bottom of the screen
def __init__(self, app, hardware, conf, frame, gbs, vertBox):
self.config = conf
self.hardware = hardware
self.application = app
self.start_row = app.widget_row # The first available row
self.start_col = app.button_start_col # The start of the button columns
self.num_rows_added = 1
self.btnPreamp = app.QuiskCheckbutton(frame, self.OnBtnPreamp, text='Preamp')
gbs.Add(self.btnPreamp, (self.start_row, self.start_col), flag=wx.EXPAND)
bw, bh = self.btnPreamp.GetMinSize()
init = app.hermes_atten_dB
self.sliderAtten = app.SliderBoxHH(frame, 'Atten %d dB', init, 0, 31, self.OnAtten, True)
self.sliderAtten.idName = "RfAtten"
app.midiControls["RfAtten"] = (self.sliderAtten, self.OnAtten)
hardware.ChangeAtten(init)
gbs.Add(self.sliderAtten, (self.start_row, self.start_col + 2), (1, 6), flag=wx.EXPAND)
if conf.button_layout == "Small screen":
# Display four data items in a single window
self.text_temperature = app.QuiskText1(frame, '', bh)
self.text_pa_current = app.QuiskText1(frame, '', bh)
self.text_fwd_power = app.QuiskText1(frame, '', bh)
self.text_swr = app.QuiskText1(frame, '', bh)
self.text_data = self.text_temperature
self.text_pa_current.Hide()
self.text_fwd_power.Hide()
self.text_swr.Hide()
b = app.QuiskPushbutton(frame, self.OnTextDataMenu, '..')
szr = self.data_sizer = wx.BoxSizer(wx.HORIZONTAL)
szr.Add(self.text_data, 1, flag=wx.ALIGN_CENTER_VERTICAL)
szr.Add(b, 0, flag=wx.ALIGN_CENTER_VERTICAL)
gbs.Add(szr, (self.start_row, self.start_col + 10), (1, 2), flag=wx.EXPAND)
# Make a popup menu for the data window
self.text_data_menu = wx.Menu()
item = self.text_data_menu.Append(-1, 'Temperature')
app.Bind(wx.EVT_MENU, self.OnDataTemperature, item)
item = self.text_data_menu.Append(-1, 'PA Current')
app.Bind(wx.EVT_MENU, self.OnDataPaCurrent, item)
item = self.text_data_menu.Append(-1, 'Fwd Power')
app.Bind(wx.EVT_MENU, self.OnDataFwdPower, item)
item = self.text_data_menu.Append(-1, 'SWR')
app.Bind(wx.EVT_MENU, self.OnDataSwr, item)
else:
szr = wx.BoxSizer(wx.HORIZONTAL)
gbs.Add(szr, (self.start_row, self.start_col + 10), (1, 18), flag=wx.EXPAND)
text_temperature = wx.StaticText(frame, -1, ' Temp 100DC XX', style=wx.ST_NO_AUTORESIZE)
size = text_temperature.GetBestSize()
text_temperature.Destroy()
self.text_temperature = wx.StaticText(frame, -1, '', size=size, style=wx.ST_NO_AUTORESIZE)
self.text_pa_current = wx.StaticText(frame, -1, '', size=size, style=wx.ST_NO_AUTORESIZE)
self.text_fwd_power = wx.StaticText(frame, -1, '', size=size, style=wx.ST_NO_AUTORESIZE)
self.text_swr = wx.StaticText(frame, -1, '', size=size, style=wx.ST_NO_AUTORESIZE)
flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL
szr.Add(self.text_temperature, 0, flag=flag)
szr.Add(self.text_pa_current, 0, flag=flag)
szr.Add(self.text_fwd_power, 0, flag=flag)
szr.Add(self.text_swr, 0, flag=flag)
def OnBtnPreamp(self, event):
self.hardware.ChangePreamp(self.btnPreamp.GetValue())
def OnAtten(self, event):
value = self.sliderAtten.GetValue()
self.hardware.ChangeAtten(value)
self.application.hermes_atten_dB = value
def Code2FwdRevWatts(self, fwd, rev): # Convert the fwd/rev power code to watts forward and reverse
#print (self.hardware.hermes_rev_power, self.hardware.hermes_fwd_power)
fwd = self.hardware.InterpolatePower(fwd)
rev = self.hardware.InterpolatePower(rev)
# Which voltage is forward and reverse depends on the polarity of the current sense transformer
if fwd >= rev:
return fwd, rev
else:
return rev, fwd
def UpdateText(self): # Called at intervals
conf = self.config
# Temperature
temp = self.hardware.hermes_temperature
if conf.calibrate_temp_20 == conf.calibrate_temp_40:
temp = " Temp ADC %4d" % temp
else:
m = 20.0 / (conf.calibrate_temp_40 - conf.calibrate_temp_20)
b = 20.0 - m * conf.calibrate_temp_20
temp = m * temp + b
temp = (" Temp %3.0f" % temp) + u'\u2103'
self.text_temperature.SetLabel(temp)
# power amp current
current = self.hardware.hermes_pa_current
if conf.calibrate_current_0 == conf.calibrate_current_1:
current = " Amps ADC %4d" % current
else:
m = 1.0 / (conf.calibrate_current_1 - conf.calibrate_current_0)
b = - m * conf.calibrate_current_0
current = m * current + b
current = " PA %4.0f ma" % (1000*current)
self.text_pa_current.SetLabel(current)
# forward and reverse peak power
if not self.hardware.power_interpolator: # For no interpolator, show ADC
fwd = self.hardware.hermes_fwd_power
rev = self.hardware.hermes_rev_power
self.text_fwd_power.SetLabel("Fwd ADC %4d" % fwd)
self.text_swr.SetLabel("Rev ADC %4d" % rev)
return
fwd, rev = self.Code2FwdRevWatts(self.hardware.hermes_fwd_peak, self.hardware.hermes_rev_peak)
# forward less reverse power
power = fwd - rev
if power < 0.0:
power = 0.0
text = " PEP %3.1f watts" % power
self.text_fwd_power.SetLabel(text)
# SWR based on average power
fwd, rev = self.Code2FwdRevWatts(self.hardware.hermes_fwd_power, self.hardware.hermes_rev_power)
if fwd >= 0.05:
gamma = math.sqrt(rev / fwd)
if gamma < 0.98:
swr = (1.0 + gamma) / (1.0 - gamma)
else:
swr = 99.0
if swr < 9.95:
text = " SWR %4.2f" % swr
else:
text = " SWR %4.0f" % swr
else:
text = " SWR ---"
self.text_swr.SetLabel(text)
def OnTextDataMenu(self, event):
btn = event.GetEventObject()
btn.PopupMenu(self.text_data_menu, (0,0))
def OnDataTemperature(self, event):
self.data_sizer.Replace(self.text_data, self.text_temperature)
self.text_data.Hide()
self.text_data = self.text_temperature
self.text_data.Show()
self.data_sizer.Layout()
def OnDataPaCurrent(self, event):
self.data_sizer.Replace(self.text_data, self.text_pa_current)
self.text_data.Hide()
self.text_data = self.text_pa_current
self.text_data.Show()
self.data_sizer.Layout()
def OnDataFwdPower(self, event):
self.data_sizer.Replace(self.text_data, self.text_fwd_power)
self.text_data.Hide()
self.text_data = self.text_fwd_power
self.text_data.Show()
self.data_sizer.Layout()
def OnDataSwr(self, event):
self.data_sizer.Replace(self.text_data, self.text_swr)
self.text_data.Hide()
self.text_data = self.text_swr
self.text_data.Show()
self.data_sizer.Layout()
+43 -15
View File
@@ -15,6 +15,7 @@ from quisk_hardware_model import Hardware as BaseHardware
DEBUG = 0
DEBUG_I2C = 0
DEBUG_SENSORS = 0
class IOBoard:
"This class controls the N2ADR IO Board for the HermesLite 2"
@@ -46,6 +47,8 @@ class IOBoard:
self.old_receive = None
self.slow = 0
def HeartBeat(self): # Called at 10 Hz for housekeeping tasks
if not self.hardware.is_HermesLite2:
return
if not QS.get_params('rx_udp_started'):
return
if self.have_IO_Board is None:
@@ -154,6 +157,7 @@ class Hardware(BaseHardware):
self.hermes_ip = ""
self.hermes_code_version = -1
self.hermes_board_id = -1
self.is_HermesLite2 = False
self.hermes_temperature = 0.0
self.hermes_fwd_power = 0.0
self.hermes_rev_power = 0.0
@@ -166,7 +170,10 @@ class Hardware(BaseHardware):
self.vna_count = 0
self.vna_started = False
self.repeater_freq = None # original repeater output frequency
self.antenna_labels = ('Ant 0', 'Ant 1') # labels for the antenna button
if conf.hardware_file_type == "Red Pitaya":
self.antenna_labels = ('Ant 0', 'Ant 1') # labels for the antenna button
else:
self.antenna_labels = ('Ant 0', 'Ant 1', 'Ant 2', 'Ant 3')
self.antenna_index = 0 # index of antenna in use
self.delay_config = True # Delay sending message to HL2 until after sound starts
self.TFRC_counter = 0 # Call for power etc. at intervals
@@ -271,6 +278,7 @@ class Hardware(BaseHardware):
self.hermes_ip = addr[0]
self.hermes_code_version = data[9]
self.hermes_board_id = data[10]
self.is_HermesLite2 = self.hermes_board_id == 6 and self.conf.hardware_file_type == "Hermes"
QS.set_hermes_id(data[9], data[10])
if data[0x16] >> 6 == 0:
QS.set_params(bandscopeScale = 2048)
@@ -317,6 +325,7 @@ class Hardware(BaseHardware):
bid = 6
self.hermes_code_version = code
self.hermes_board_id = bid
self.is_HermesLite2 = self.hermes_board_id == 6 and self.conf.hardware_file_type == "Hermes"
QS.set_hermes_id(code, bid)
st = 'Capture from Hermes device at specified IP %s' % self.hermes_ip
found = True
@@ -331,9 +340,13 @@ class Hardware(BaseHardware):
def open(self):
self.delay_config = True # Delay sending message to HL2 until after sound starts
# This list only changes control bits; no use of WriteQueue()
for name in ('keyupDelay',
'hermes_lowpwr_tr_enable', 'hermes_PWM', 'hermes_disable_sync', 'hermes_power_amp', 'Hermes_BandDictEnTx'):
self.ImmediateChange(name)
self.ImmediateChange('keyupDelay')
self.ImmediateChange('Hermes_BandDictEnTx')
if self.is_HermesLite2:
for name in ('hermes_lowpwr_tr_enable', 'hermes_PWM', 'hermes_disable_sync', 'hermes_power_amp'):
self.ImmediateChange(name)
#else: # DEBUG: Turn on power amp for testing OpenHPSDR radio with my Hermes Lite2
# self.SetControlBit(0x09, 19, 1)
return self.config_text
def GetValue(self, name): # return values stored in the hardware
if name == 'Hware_Hl2_EepromIP':
@@ -492,7 +505,15 @@ class Hardware(BaseHardware):
self.key_was_down = key_down
if self.TFRC_counter >= 3:
self.TFRC_counter = 0
self.hermes_temperature, self.hermes_fwd_power, self.hermes_rev_power, self.hermes_pa_current, self.hermes_fwd_peak, self.hermes_rev_peak = QS.get_hermes_TFRC()
AIN1_avg, AIN1_peak, AIN2_avg, AIN2_peak, AIN3, AIN4, AIN5, AIN6 = QS.get_hermes_TFRC()
if self.hermes_board_id == 6:
(self.hermes_fwd_power, self.hermes_fwd_peak, self.hermes_rev_power, self.hermes_rev_peak,
self.hermes_pa_current, self.hermes_temperature) = AIN1_avg, AIN1_peak, AIN2_avg, AIN2_peak, AIN3, AIN5
else:
(self.hermes_fwd_power, self.hermes_fwd_peak, self.hermes_rev_power, self.hermes_rev_peak,
self.hermes_pa_current, self.hermes_temperature) = AIN1_avg, AIN1_peak, AIN2_avg, AIN2_peak, AIN3, AIN5
if DEBUG_SENSORS:
print("AIN 1-6 %4d %4d %4d %4d %4d %4d" % (AIN1_avg, AIN2_avg, AIN3, AIN4, AIN5, AIN6))
if self.application.bottom_widgets:
self.application.bottom_widgets.UpdateText()
if self.delay_config and QS.get_params('rx_udp_started'):
@@ -529,7 +550,9 @@ class Hardware(BaseHardware):
def ChangeBandFilters(self):
if not hasattr(self.application, "multi_rx_screen"):
return # Needed for the VNA program
self.SetControlBit(0x00, 13, self.antenna_index)
self.SetControlBit(0x00, 13, self.antenna_index & 0x01)
self.SetControlBit(0x00, 14, self.antenna_index & 0x02)
self.SetControlBit(0x00, 15, self.antenna_index > 0)
highest = self.band
freq = self.conf.BandEdge.get(highest, (0, 0))[0]
for pane in self.application.multi_rx_screen.receiver_list:
@@ -606,12 +629,7 @@ class Hardware(BaseHardware):
return (48000, 384000)
## Hardware AGC is no longer supported in HL2 identifying as version >=40
def ChangeAGC(self, value):
if value:
self.pc2hermes[2] |= 0x10 # C0 index == 0, C3[4]: AGC enable
else:
self.pc2hermes[2] &= ~0x10
QS.pc_to_hermes(self.pc2hermes)
if DEBUG: print ("Change AGC to", value)
pass
## Simpler LNA setting for HL2 identifying as version >=40, see HL2 wiki for details
def ChangeLNA(self, value): # LNA for Rx
# value is -12 to +48
@@ -624,7 +642,7 @@ class Hardware(BaseHardware):
value = 51 - value
else:
value = ((value+12) & 0x3f) | 0x40
self.pc2hermes[4 * 10 + 3] = value # C0 index == 0x1010, C4[4:0] LNA 0-32 dB gain
self.pc2hermes[4 * 10 + 3] = value # C0 index == 0b1010, C4[4:0] LNA 0-32 dB gain
QS.pc_to_hermes(self.pc2hermes)
if DEBUG: print ("Change LNA to", value)
def ChangeTxLNA(self, value): # LNA for Tx
@@ -634,9 +652,17 @@ class Hardware(BaseHardware):
elif value > 48:
value = 48
value = ((value+12) & 0x3f) | 0x40 | 0x80
self.SetControlByte(0x0e, 3, value, False) # C0 index == 0x0e, C3
self.SetControlByte(0x0e, 3, value, False) # C0 index == 0b1110 == 0x0e, C3
QS.pc_to_hermes(self.pc2hermes)
if DEBUG: print ("Change Tx LNA to", value)
def ChangeAtten(self, value):
# value is 0 to 31
self.pc2hermes[4 * 10 + 3] = value & 0x1F # C0 index == 0b1010, C4[4:0] attenuator, 0 to 31 dB
self.SetControlBit(0b1010, 5, value) # enable attenuator
QS.pc_to_hermes(self.pc2hermes)
if DEBUG: print ("Change attenuator to", value)
def ChangePreamp(self, value):
self.SetControlBit(0b0000, 10, value) # enable preamp
def SetTxLevel(self):
try:
tx_level = self.conf.tx_level[self.band]
@@ -653,7 +679,7 @@ class Hardware(BaseHardware):
tx_level = 0
elif tx_level > 255:
tx_level = 255
self.pc2hermes[4 * 9] = tx_level # C0 index == 0x1001, C1[7:0] Tx level
self.pc2hermes[4 * 9] = tx_level # C0 index == 0b1001, C1[7:0] Tx level
QS.pc_to_hermes(self.pc2hermes)
if DEBUG: print("Change tx_level to", tx_level)
def MultiRxCount(self, count): # count == number of additional receivers besides the Tx/Rx receiver: 1, 2, 3
@@ -876,6 +902,8 @@ class Hardware(BaseHardware):
QS.set_hermeslite_writepointer(0)
return False
def WriteQueue(self, wait=False):
if not self.is_HermesLite2:
return False
self._wait_queue()
# Send next write
self.old_writequeue = self.pc2hermeslitewritequeue[:]
+6
View File
@@ -154,6 +154,12 @@ class BottomWidgets: # Add extra widgets to the bottom of the screen
current = " PA %4.0f ma" % (1000*current)
self.text_pa_current.SetLabel(current)
# forward and reverse peak power
if not self.hardware.power_interpolator: # For no interpolator, show ADC
fwd = self.hardware.hermes_fwd_power
rev = self.hardware.hermes_rev_power
self.text_fwd_power.SetLabel("Fwd ADC %4d" % fwd)
self.text_swr.SetLabel("Rev ADC %4d" % rev)
return
fwd, rev = self.Code2FwdRevWatts(self.hardware.hermes_fwd_peak, self.hardware.hermes_rev_peak)
# forward less reverse power
power = fwd - rev
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

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

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

+2
View File
@@ -32,6 +32,8 @@ with this:
*/
#include <Python.h>
#include <string.h>
#include <stdio.h>
void ** Quisk_API; // array of pointers to functions and variables from module _quisk
struct sound_conf * pt_quisk_sound_state; // pointer to quisk_sound_state
+1
View File
@@ -1,5 +1,6 @@
#include <Python.h> // used by quisk.h
#include <complex.h> // Used by quisk.h
#include <string.h>
#include "quisk.h"
// This module provides methods to access the state of the key.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+70
View File
@@ -0,0 +1,70 @@
# This provides helper functions for our makefile.
# Changes for MacOS support thanks to Mario, DL3LSM.
# Changes for building from macports provided by Eric, KM4DSJ
# Updated code for a Mac build contributed by Christoph, DL1YCF, December 2020.
# Converted from setup.py to makefile by Jim Ahlstrom, June 2026
import sys, os, subprocess, sysconfig
if len(sys.argv) < 2:
sys.exit(2)
job = sys.argv[1]
def flags ():
# Fetch compiler flags and link flags from Python
result = subprocess.run(["python3-config", "--cflags"], capture_output=True, text=True, check=True)
cflags = result.stdout.strip()
cflags += " -fPIC"
result = subprocess.run(["python3-config", "--ldflags"], capture_output=True, text=True, check=True)
ldflags = result.stdout.strip()
ldflags += " -shared -lm -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-z,relro -g -fwrapv -O2 -lfftw3"
if sys.platform == "darwin": # Build for Macintosh
cflags += " -DQUISK_HAVE_PORTAUDIO "
ldflags += " -lportaudio "
if os.path.isdir('/opt/local/include'): # MacPorts
base_dir = '/opt/local'
elif os.path.isdir('/usr/local/include'): # HomeBrew on macOS Intel
base_dir = '/usr/local'
elif os.path.isdir('/opt/homebrew/include'): # HomeBrew on Apple Silicon
base_dir = '/opt/homebrew'
else: # Regular build?
base_dir = '/usr'
if os.path.isfile(base_dir + "/include/pulse/pulseaudio.h"):
cflags += " -DQUISK_HAVE_PULSEAUDIO "
ldflags += " -lpulse "
cflags += " -I. -I%s/include " % base_dir
ldflags += " -L. -L%s/lib " % base_dir
elif "freebsd" in sys.platform: #Build for FreeBSD
cflags += " -DQUISK_HAVE_PULSEAUDIO -I. -I/usr/local/include"
ldflags += " -lpulse -L. -L/usr/local/lib"
else: # Linux
cflags += " -DQUISK_HAVE_ALSA -DQUISK_HAVE_PULSEAUDIO"
ldflags += " -lasound -lpulse"
if os.path.isfile("/usr/include/portaudio.h"):
cflags += " -DQUISK_HAVE_PORTAUDIO"
ldflags += " -lportaudio"
return cflags, ldflags
if job == "--soapy":
# See if soapy is available
if os.path.isdir("/usr/include/SoapySDR") or os.path.isdir("/usr/local/include/SoapySDR"):
print('1')
else:
print('0')
elif job == "--suffix":
# Determine the platform-specific extension suffix (e.g., .cpython-311-x86_64-linux-gnu.so)
print(sysconfig.get_config_var('EXT_SUFFIX'))
elif job == "--cflags":
cc, ld = flags()
print(cc)
elif job == "--ldflags":
cc, ld = flags()
print(ld)
else:
sys.exit(1)
+91 -16
View File
@@ -1,18 +1,93 @@
.PHONY: quisk
# This makefile replaces the old Python "setup.py build_ext" which has been deprecated.
# See "quisk-old" below to make Quisk using the old setup.py.
quisk:
MODULE_NAME := _quisk
OBJDIR := build/local
SUBDIRS := afedrinet
ifeq ($(shell python3 makefil.py --soapy), 1)
SUBDIRS := $(SUBDIRS) soapypkg
endif
SRCS := \
base64.c \
extdemod.c \
filter.c \
freedv.c \
handshake.c \
is_key_down.c \
microphone.c \
quisk.c \
quisk_wdsp.c \
sha1.c \
sound.c\
sound_alsa.c \
sound_directx.c \
sound_portaudio.c \
sound_pulseaudio.c \
sound_wasapi.c \
tci.c \
utf8.c \
utility.c \
ws.c
CFLAGS := $(shell python3 makefil.py --cflags)
LDFLAGS := $(shell python3 makefil.py --ldflags)
EXTENSION_SUFFIX := $(shell python3 makefil.py --suffix)
TARGET := $(MODULE_NAME)$(EXTENSION_SUFFIX)
#$(info Info CFLAGS $(CFLAGS))
#$(info Info LDFLAGS $(LDFLAGS))
#$(info Info TARGET $(TARGET))
#$(info Info SUBDIRS $(SUBDIRS))
OBJS := $(patsubst %.c, $(OBJDIR)/%.o, $(SRCS))
.PHONY: all clean test
.PHONY: $(SUBDIRS)
# Default target
all: $(OBJDIR) $(OBJDIR)/import_quisk_api.o ac2yd/remote.o $(TARGET) $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $@
$(OBJDIR):
@mkdir -p $(OBJDIR)
# Link the shared library object
$(TARGET): $(OBJS) ac2yd/remote.o
$(CC) -o $@ $(OBJS) ac2yd/remote.o $(LDFLAGS)
# Define the location for dependency files
DEPS = $(patsubst %.o,$(OBJDIR)/%.d,$(notdir $(OBJS)))
DEPFLAGS = -MMD -MP -MF $(OBJDIR)/$(notdir $*).d
-include $(DEPS)
# Compile the C source files into object files
ac2yd/remote.o: ac2yd/remote.c quisk.h filter.h
$(CC) -o $@ -c $< $(CFLAGS) $(DEPFLAGS)
$(OBJDIR)/import_quisk_api.o: import_quisk_api.c
$(CC) -o $@ -c $< $(CFLAGS) $(DEPFLAGS)
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) -o $@ -c $< $(CFLAGS) $(DEPFLAGS)
clean:
-rm -f $(OBJDIR)/*.o $(OBJDIR)/*.d
-rm -f ac2yd/*.o
-rm -f $(MODULE_NAME)*.so
for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir clean; \
done
# Run a test script to verify that the module can be imported
test: $(TARGET)
@python3 -c "import $(MODULE_NAME); print('Successfully imported:', $(MODULE_NAME).__file__)"
.PHONY: quisk-old
quisk-old:
python3 setup.py build_ext --force --inplace
@echo
quisk3:
python3 setup.py build_ext --force --inplace
@echo
soapy3:
(cd soapypkg; make soapy3)
afedrinet3:
(cd afedrinet; make afedrinet3)
perseus3:
(cd perseuspkg; make perseus3)
+9 -150
View File
@@ -3,12 +3,12 @@
#include <math.h>
#include <sys/time.h>
#include <complex.h>
#include <string.h>
#include <fftw3.h>
#include "quisk.h"
#include <sys/types.h>
#include "microphone.h"
#include "filter.h"
#include "freedv.h"
#ifdef MS_WINDOWS
#include <winsock2.h>
@@ -245,11 +245,9 @@ static void init_alc(struct alc * pt, int size)
case DGT_U:
case DGT_L:
case DGT_IQ:
pt->gain_now[i] = 1.4;
break;
case FDV_U:
case FDV_L:
pt->gain_now[i] = 2.0;
pt->gain_now[i] = 1.4;
break;
default:
pt->gain_now[i] = 1.0;
@@ -617,7 +615,7 @@ static int tx_filter_digital(complex double * filtered, int count)
quisk_filt_dInit(&filter1, quiskDgtFilt48Coefs, sizeof(quiskDgtFilt48Coefs)/sizeof(double)); // pass 1350, stop 1650
}
if ( ! filtered) { // Change to rxMode
quisk_filt_tune(&filter1, 1650.0 / 48000, rxMode != DGT_L && rxMode != LSB);
quisk_filt_tune(&filter1, 1650.0 / 48000, rxMode != DGT_L && rxMode != LSB && rxMode != FDV_L);
return 0;
}
for (i = 0; i < count; i++) // FIR bandpass filter; separate into I and Q
@@ -626,140 +624,6 @@ static int tx_filter_digital(complex double * filtered, int count)
return count;
}
static int tx_filter_freedv(complex double * filtered, int count, int encode)
{ // Input samples are creal(filtered), output is filtered.
// This filter is used for digital voice.
// Changes by Dave Roberts, G8KBB, for additional modes June, 2020.
// Modified by N2ADR January, 2024.
int i;
// The input sample rate is mic_sample_rate, either 8000 or 48000 sps.
// The FreeDV codec requires an input speech_sample_rate of either 8000 or 16000 sps depending on the mode. Input is always real.
// The FreeDV codec will output samples at modem_sample_rate of 8000 or 48000 sps depending on the mode, and output may be real or complex.
double dtmp, magn, dsample;
int modem_sample_rate, speech_sample_rate, use_filter, is_real;
static int samples_size = 0;
static int do_init = 1;
static double aaa, bbb, ccc, Xmin, Xmax, Ymax;
static double time_long, time_short;
static double inMax=0.3;
static double * dsamples = NULL;
static struct quisk_cFilter filter1, filter2;
static struct quisk_dFilter filtDecim48to16, filtDecim16to8;;
static struct quisk_cFilter cfiltInterp8to48;
if (do_init) { // initialization
//QuiskWavWriteOpen(&hWav, "jim.wav", 3, 1, 4, 8000, 1.0 / CLIP16);
do_init = 0;
quisk_filt_cInit(&filter1, quiskLpFilt48Coefs, sizeof(quiskLpFilt48Coefs)/sizeof(double)); // at 48000 sps pass 3000 stop 4000 times two plus 3000
quisk_filt_cInit(&filter2, quiskFilt53D2Coefs, sizeof(quiskFilt53D2Coefs)/sizeof(double)); // at 8000 sps pass 1500 stop 1800 times two plus 1500
quisk_filt_cInit(&cfiltInterp8to48, quiskLpFilt48Coefs, sizeof(quiskLpFilt48Coefs)/sizeof(double));
quisk_filt_dInit(&filtDecim48to16, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double)); // pass 6000 stop 8000
quisk_filt_dInit(&filtDecim16to8, quiskFilt16dec8Coefs, sizeof(quiskFilt16dec8Coefs)/sizeof(double)); // pass 3000 stop 4000
Ymax = pow(10.0, - 1 / 20.0); // maximum y
Xmax = pow(10.0, 3 / 20.0); // x where slope is zero; for x > Xmax, y == Ymax
Xmin = Ymax - fabs(Ymax - Xmax); // x where slope is 1 and y = x; start of compression
//printf ("Xmin %f\n", Xmin);
aaa = 1.0 / (2.0 * (Xmin - Xmax)); // quadratic
bbb = -2.0 * aaa * Xmax;
ccc = Ymax - aaa * Xmax * Xmax - bbb * Xmax;
}
switch (freedv_current_mode) { // n_modem_sample_rate and n_speech_sample_rate may not be valid
case FREEDV_MODE_2400A:
case FREEDV_MODE_2400B:
use_filter = 1; // center frequency 3000
is_real = 1;
modem_sample_rate = 48000;
speech_sample_rate = 8000;
break;
case FREEDV_MODE_2020:
use_filter = 0;
is_real = 0;
modem_sample_rate = 8000;
speech_sample_rate = 16000;
break;
case FREEDV_MODE_800XA:
use_filter = 2; // center frequency 1500
is_real = 1;
modem_sample_rate = 8000;
speech_sample_rate = 8000;
break;
default:
use_filter = 0;
is_real = 0;
modem_sample_rate = 8000;
speech_sample_rate = 8000;
break;
}
if ( ! filtered) { // rxMode changed
quisk_filt_tune((struct quisk_dFilter *)&filter1, 3000.0 / 48000.0, rxMode != FDV_L);
quisk_filt_tune((struct quisk_dFilter *)&filter2, 1500.0 / 8000.0, rxMode != FDV_L);
return 0;
}
// check size of dsamples[] buffer
if (count > samples_size) {
samples_size = count * 2;
if (dsamples)
free(dsamples);
dsamples = (double *)malloc(samples_size * sizeof(double));
}
// copy to dsamples[]
for (i = 0; i < count; i++)
dsamples[i] = creal(filtered[i]) / CLIP16; // normalize to 1.0000
// Decimate to 8000 Hz or to 16000 depending on mode (and hence setting of sample rate)
if (quisk_sound_state.mic_sample_rate == 48000) {
switch (speech_sample_rate) {
case 16000:
count = quisk_dDecimate(dsamples, count, &filtDecim48to16, 3);
break;
case 8000:
count = quisk_dDecimate(dsamples, count, &filtDecim48to16, 3);
count = quisk_dDecimate(dsamples, count, &filtDecim16to8, 2);
break;
default:
QuiskPrintf("Failure to convert speech input rate in tx_filter_freedv\n");
break;
}
}
else if (quisk_sound_state.mic_sample_rate == 8000 && speech_sample_rate != 8000) {
QuiskPrintf("Failure to convert input rate in tx_filter_freedv\n");
}
// Measure average peak input audio level and limit
dtmp = 1.0 / speech_sample_rate; // sample time
time_long = 1.0 - exp(- dtmp / 3.000);
time_short = 1.0 - exp(- dtmp / 0.005);
for (i = 0; i < count; i++) {
dsample = dsamples[i];
magn = fabs(dsample);
if (magn > inMax)
inMax = inMax * (1 - time_short) + time_short * magn;
else if(magn > mic_agc_level)
inMax = inMax * (1 - time_long) + time_long * magn;
else
inMax = inMax * (1 - time_long) + time_long * mic_agc_level;
dsample = dsample / inMax * Xmin * 0.7;
magn = fabs(dsample);
if (magn < Xmin)
dsamples[i] = dsample;
else if (magn > Xmax)
dsamples[i] = copysign(Ymax, dsample);
else
dsamples[i] = copysign(aaa * magn * magn + bbb * magn + ccc, dsample);
dsamples[i] = dsamples[i] * CLIP16;
}
//QuiskWavWriteD(&hWav, dsamples, count);
if (encode && pt_quisk_freedv_tx) // Encode audio into digital modulation
count = (* pt_quisk_freedv_tx)(filtered, dsamples, count, is_real);
//quisk_calc_audio_graph(CLIP16, filtered, NULL, count, 0);
if (use_filter == 1) // convert float samples to complex with an analytic filter
count = quisk_cCDecimate(filtered, count, &filter1, 1);
else if (use_filter == 2)
count = quisk_cCDecimate(filtered, count, &filter2, 1);
// Interpolate up to 48000
if (MIC_OUT_RATE != modem_sample_rate )
count = quisk_cInterpolate(filtered, count, &cfiltInterp8to48, MIC_OUT_RATE / modem_sample_rate);
return count;
}
PyObject * quisk_get_tx_filter(PyObject * self, PyObject * args)
{ // return the TX filter response to display on the graph
// This is for debugging. Change quisk.py to call QS.get_tx_filter() instead
@@ -1356,6 +1220,10 @@ int quisk_process_microphone(int mic_sample_rate, complex double * cSamples, int
count *= interp;
transmit_mic_carrier(cSamples, count, quiskSpotLevel / 1000.0);
}
else if (tci_tx_audio_client) {
count = tx_filter_digital(cSamples, count); // filter samples, minimal processing
process_alc(cSamples, count, &tx_alc, DGT_U);
}
else switch (rxMode) {
case LSB: // LSB
case USB: // USB
@@ -1397,6 +1265,8 @@ int quisk_process_microphone(int mic_sample_rate, complex double * cSamples, int
case DGT_L:
case DGT_IQ:
case DGT_FM:
case FDV_U: // FreeDV
case FDV_L:
count = tx_filter_digital(cSamples, count); // filter samples, minimal processing
process_alc(cSamples, count, &tx_alc, rxMode);
break;
@@ -1404,11 +1274,6 @@ int quisk_process_microphone(int mic_sample_rate, complex double * cSamples, int
count *= interp;
transmit_mic_imd(cSamples, count, quiskImdLevel / 1000.0);
break;
case FDV_U: // FDV
case FDV_L:
count = tx_filter_freedv(cSamples, count, 1);
process_alc(cSamples, count, &tx_alc, rxMode);
break;
default:
break;
}
@@ -1499,11 +1364,6 @@ int quisk_process_microphone(int mic_sample_rate, complex double * cSamples, int
count = tx_filter(cSamples, count);
process_alc(cSamples, count, &tx_alc, rxMode);
break;
case FDV_U: // FDV
case FDV_L:
count = tx_filter_freedv(cSamples, count, 0);
process_alc(cSamples, count, &tx_alc, rxMode);
break;
default:
for (i = 0; i < count; i++)
cSamples[i] = 0;
@@ -1673,7 +1533,6 @@ void quisk_set_tx_mode(void) // called when the mode rxMode is changed
tx_filter(NULL, 0);
tx_filter_digital(NULL, 0);
transmit_udp(NULL, 0);
tx_filter_freedv(NULL, 0, 0);
serial_key_samples(NULL, 0);
}
+3 -9
View File
@@ -1,8 +1,5 @@
# This is the hardware control file for my shack.
# It is for the Hermes-Lite2 5 watt output which uses only the antenna tuner.
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
# It is for the Hermes-Lite2 RF1 output and Alt Rx input.
from hermes.quisk_hardware import Hardware as BaseHw
from n2adr import station_hardware
@@ -17,7 +14,7 @@ class Hardware(BaseHw):
self.controlbox = station_hardware.ControlBox(app, conf) # Control my Station Control Box
self.v2filter = station_hardware.FilterBoxV2(app, conf) # Control V2 filter box
def open(self):
if False:
if False: # Start the station control GUI
from n2adr.station_hardware import StationControlGUI
self.GUI = StationControlGUI(self.application.main_frame, self, self.application, self.conf)
self.GUI.Show()
@@ -29,12 +26,11 @@ class Hardware(BaseHw):
return BaseHw.close(self)
def ChangeFilterFrequency(self, tx_freq):
if tx_freq and tx_freq > 0:
self.v2filter.SetTxFreq(tx_freq)
if self.GUI:
self.GUI.SetTxFreq(tx_freq)
self.GUI.freq_entry.ChangeValue("%.3f" % (tx_freq * 1E-6))
else:
self.anttuner.SetTxFreq(tx_freq)
self.v2filter.SetTxFreq(tx_freq)
def ChangeFrequency(self, tx_freq, vfo_freq, source='', band='', event=None):
self.ChangeFilterFrequency(tx_freq)
return BaseHw.ChangeFrequency(self, tx_freq, vfo_freq, source, band, event)
@@ -42,8 +38,6 @@ class Hardware(BaseHw):
# band is a string: "60", "40", "WWV", etc.
ret = BaseHw.ChangeBand(self, band)
self.anttuner.ChangeBand(band)
#self.lpfilter.ChangeBand(band)
#self.hpfilter.ChangeBand(band)
self.v2filter.ChangeBand(band)
self.CorrectSmeter()
return ret
+4 -50
View File
@@ -1,56 +1,10 @@
# Please do not change this widgets module for Quisk. Instead copy
# it to your own quisk_widgets.py and make changes there.
#
# This module is used to add extra widgets to the QUISK screen.
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
import wx, time
import _quisk as QS
class BottomWidgets: # Add extra widgets to the bottom of the screen
from hermes.quisk_widgets import BottomWidgets as BaseWidgets
class BottomWidgets(BaseWidgets): # Add extra widgets to the bottom of the screen
def __init__(self, app, hardware, conf, frame, gbs, vertBox):
#self.config = conf
#self.hardware = hardware
#self.application = app
#start_row = app.widget_row # The first available row
#start_col = app.button_start_col # The start of the button columns
#b = app.QuiskCycleCheckbutton(frame, self.OnAntTuner, ('Antenna', 'Ant 0', 'Ant 1'))
#bw, bh = b.GetMinSize()
#gbs.Add(b, (start_row, start_col), (1, 2), flag=wx.EXPAND)
#b = app.QuiskPushbutton(frame, self.OnAntTuner, 'L+')
#b.Enable(0)
#gbs.Add(b, (start_row, start_col + 2), (1, 2), flag=wx.EXPAND)
#b = app.QuiskPushbutton(frame, self.OnAntTuner, 'L-')
#b.Enable(0)
#gbs.Add(b, (start_row, start_col + 4), (1, 2), flag=wx.EXPAND)
#b = app.QuiskPushbutton(frame, self.OnAntTuner, 'C+')
#b.Enable(0)
#gbs.Add(b, (start_row, start_col + 6), (1, 2), flag=wx.EXPAND)
#b = app.QuiskPushbutton(frame, self.OnAntTuner, 'C-')
#b.Enable(0)
#gbs.Add(b, (start_row, start_col + 8), (1, 2), flag=wx.EXPAND)
#b = app.QuiskPushbutton(frame, self.OnAntTuner, 'Save')
#b.Enable(0)
#gbs.Add(b, (start_row, start_col + 10), (1, 2), flag=wx.EXPAND)
#self.swr_label = app.QuiskText(frame, 'Watts 000 SWR 10.1 Zh Ind 22 Cap 33 Freq 28100 (7777)', bh)
#gbs.Add(self.swr_label, (start_row, start_col + 2), (1, 10), flag=wx.EXPAND)
#b = app.QuiskCheckbutton(frame, None, text='')
#gbs.Add(b, (start_row, start_col + 25), (1, 2), flag=wx.EXPAND)
# Example of a horizontal slider:
# lab = wx.StaticText(frame, -1, 'Preamp', style=wx.ALIGN_CENTER)
# gbs.Add(lab, (5,0), flag=wx.EXPAND)
# sl = wx.Slider(frame, -1, 1024, 0, 2048) # parent, -1, initial, min, max
# gbs.Add(sl, (5,1), (1, 5), flag=wx.EXPAND)
# sl.Bind(wx.EVT_SCROLL, self.OnPreamp)
# def OnPreamp(self, event):
# print event.GetPosition()
self.num_rows_added = 0
#def OnAntTuner(self, event):
# btn = event.GetEventObject()
# text = btn.GetLabel()
# self.hardware.OnAntTuner(text)
#def UpdateText(self, text):
# self.swr_label.SetLabel(text)
BaseWidgets.__init__(self, app, hardware, conf, frame, gbs, vertBox)
+85 -123
View File
@@ -1,9 +1,5 @@
# This file supports various hardware boxes at my shack
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
import sys, struct, math, socket, select, time, traceback, os
import wx, wx.lib.buttons
@@ -411,18 +407,15 @@ class AntennaTuner: # Control my homebrew antenna tuner and my KI8BV dipole
self.set_HighZ = 0
self.antnum = 0 # Antenna number 0 or 1
self.dipole2 = AntennaControl(app, conf) # Control the KI8BV dipole
if False and conf.use_rx_udp == 10: # Hermes UDP protocol for Hermes-Lite2
path = 'TunerLCZ_HL2.txt'
else:
path = 'TunerLCZ.txt'
path = 'TunerLCZ.txt'
if sys.platform == "win32":
path = 'C:/pub/' + path
self.path = 'C:/pub/' + path
else:
path = '/home/jim/pub/' + path
fp = open(path, "r")
self.path = '/home/jim/pub/' + path
fp = open(self.path, "r")
lines = fp.readlines()
fp.close()
self.TunerLCZ = [(0, 0, 0, 0), (999111000, 0, 0, 0)] # Add dummy first and last entry.
self.TunerLCZ = []
for line in lines:
freq, antL, antC, hilo = line.split()
freq = int(freq)
@@ -451,7 +444,12 @@ class AntennaTuner: # Control my homebrew antenna tuner and my KI8BV dipole
pass
def ChangeBand(self, band):
pass
def FindLCZ(self, tx_freq):
def SaveLCZ(self):
fp = open(self.path, 'w')
for tup in self.TunerLCZ:
fp.write("%12d %2d %2d %d\n" % tup)
fp.close()
def FindIndexTup(self, tx_freq):
i1 = 0
i2 = len(self.TunerLCZ) - 1
while 1: # binary partition
@@ -462,20 +460,14 @@ class AntennaTuner: # Control my homebrew antenna tuner and my KI8BV dipole
i2 = i
if i2 - i1 <= 1:
break
# The correct setting is between i1 and i2.
# The correct setting is between i1 and i2, or below i1 or above i2.
# Choose the index that is closest in frequency.
delta1 = tx_freq - self.TunerLCZ[i1][0]
delta2 = self.TunerLCZ[i2][0] - tx_freq
if i1 == 0: # below the first frequency
if abs(tx_freq - self.TunerLCZ[i1][0]) < abs(tx_freq - self.TunerLCZ[i2][0]):
index = i1
elif i2 == len(self.TunerLCZ) - 1: # above the last frequency
else:
index = i2
elif delta2 < delta1: # i2 is closer
index = i2
else: # i1 is closer
index = i1
if DEBUG: print ("AntennaTuner FindLCZ", tx_freq, i1, i2, len(self.TunerLCZ), self.TunerLCZ[index])
return self.TunerLCZ[index]
if DEBUG: print ("StatuinControlGUI FindIndexTup", tx_freq, i1, i2, len(self.TunerLCZ), index, self.TunerLCZ[index])
return index, self.TunerLCZ[index]
def SetTxFreq(self, tx_freq, no_tune=False):
#if DEBUG: print("AntennaTuner SetTxFreq", tx_freq)
self.tx_freq = tx_freq
@@ -484,10 +476,10 @@ class AntennaTuner: # Control my homebrew antenna tuner and my KI8BV dipole
if tx_freq < 17000000:
if DEBUG and self.antnum == 1: print ("antnum 0")
self.antnum = 0
else:
elif self.dipole2:
if DEBUG and self.antnum == 0: print ("antnum 1")
self.antnum = 1
if self.antnum == 1:
if self.antnum == 1 and self.dipole2:
self.dipole2.SetTxFreq(tx_freq)
if not self.socket:
return
@@ -496,7 +488,7 @@ class AntennaTuner: # Control my homebrew antenna tuner and my KI8BV dipole
if abs(self.tune_freq - tx_freq) < 5000: # ignore small changes
return
self.tune_freq = tx_freq
f, newL, newC, newH = self.FindLCZ(tx_freq)
index, (f, newL, newC, newH) = self.FindIndexTup(tx_freq)
if newH != self.set_HighZ or self.set_C != newC or self.set_L != newL:
self.set_HighZ = newH
self.set_C = newC
@@ -521,7 +513,8 @@ class AntennaTuner: # Control my homebrew antenna tuner and my KI8BV dipole
self.timer = 0
else:
self.timer = 0
self.dipole2.HeartBeat()
if self.dipole2:
self.dipole2.HeartBeat()
def Send(self):
try:
self.socket.send(self.want_data)
@@ -539,26 +532,7 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
self.conf = conf
self.data_saving = False
self.tx_freq = 0
if sys.platform == "win32":
self.filename = 'C:/pub/TunerLCZ.tmp'
else:
self.filename = '/home/jim/pub/TunerLCZ.tmp'
try:
fp = open(self.filename, "r")
except FileNotFoundError:
lines = ()
else:
lines = fp.readlines()
fp.close()
self.TunerLCZ = [(0, 0, 0, 0), (999111000, 0, 0, 0)] # Add dummy first and last entry.
for line in lines:
freq, antL, antC, hilo = line.split()
freq = int(freq)
antL = int(antL)
antC = int(antC)
hilo = int(hilo)
self.TunerLCZ.append((freq, antL, antC, hilo))
self.TunerLCZ.sort()
self.anttuner = self.hware.anttuner
self.SetBackgroundColour('light steel blue')
self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyDown)
sizer = wx.GridBagSizer(hgap=5, vgap=3)
@@ -568,6 +542,7 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
self.bands = []
for band in ('160', '80', '60', '40', '30', '20', '17', '15', '12', '10'):
b = wx.lib.buttons.GenToggleButton(self, -1, band)
b.Enable(False)
b.SetUseFocusIndicator(False)
b.SetBezelWidth(4)
self.bands.append(b)
@@ -604,6 +579,7 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
szr = wx.BoxSizer(wx.HORIZONTAL)
szr.AddSpacer(20)
b = wx.lib.buttons.GenToggleButton(self, -1, "Key Down")
b.Enable(False)
b.SetFont(font)
b.SetUseFocusIndicator(False)
b.SetBezelWidth(4)
@@ -611,6 +587,7 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
szr.Add(b)
szr.AddSpacer(10)
b = wx.lib.buttons.GenToggleButton(self, -1, "FilterV2 Preamp")
b.Enable(False)
b.SetFont(font)
b.SetUseFocusIndicator(False)
b.SetBezelWidth(4)
@@ -622,16 +599,31 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
b.SetFont(font)
szr.Add(b, flag=wx.ALIGN_CENTER_VERTICAL)
szr.AddSpacer(10)
self.freq_entry = wx.TextCtrl(self, value='0000.000', style=wx.TE_PROCESS_ENTER)
self.freq_entry = wx.StaticText(self, -1, "0000.000")
#self.freq_entry = wx.TextCtrl(self, value='0000.000', style=wx.TE_PROCESS_ENTER)
self.freq_entry.SetFont(font)
self.Bind(wx.EVT_TEXT_ENTER, self.OnFreqEntry, self.freq_entry)
szr.Add(self.freq_entry)
#self.Bind(wx.EVT_TEXT_ENTER, self.OnFreqEntry, self.freq_entry)
szr.Add(self.freq_entry, flag=wx.ALIGN_CENTER_VERTICAL)
szr.AddSpacer(10)
b = wx.lib.buttons.GenButton(self, -1, "Search Freq")
b = wx.lib.buttons.GenButton(self, -1, "Freq-")
b.SetUseFocusIndicator(False)
b.SetBezelWidth(4)
b.SetFont(font)
self.Bind(wx.EVT_BUTTON, self.OnButtonSearch, b)
self.Bind(wx.EVT_BUTTON, self.OnButtonDown, b)
szr.Add(b)
szr.AddSpacer(10)
b = wx.lib.buttons.GenButton(self, -1, "Freq+")
b.SetUseFocusIndicator(False)
b.SetBezelWidth(4)
b.SetFont(font)
self.Bind(wx.EVT_BUTTON, self.OnButtonUp, b)
szr.Add(b)
szr.AddSpacer(10)
b = wx.lib.buttons.GenButton(self, -1, "Delete")
b.SetUseFocusIndicator(False)
b.SetBezelWidth(4)
b.SetFont(font)
self.Bind(wx.EVT_BUTTON, self.OnButtonDelete, b)
szr.Add(b)
sizer.Add(szr, pos=(row, 0), span=(1, 3))
if frame is None:
@@ -647,46 +639,22 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
self.Bind(wx.EVT_TIMER, self.HeartBeat)
self.timer.Start(100)
def SetTxFreq(self, tx_freq):
#print("StationControlGUI SetTxFreq", tx_freq)
tx_freq = ((tx_freq + 500) // 1000) * 1000
self.tx_freq = tx_freq
index, tup = self.FindIndexTup(tx_freq)
anttuner = self.anttuner
index, tup = anttuner.FindIndexTup(tx_freq)
self.sliderL.SetValue(tup[1])
self.sliderC.SetValue(tup[2])
self.btnHiLo.SetValue(tup[3])
anttuner = self.hware.anttuner
fff, anttuner.set_L, anttuner.set_C, anttuner.set_HighZ = tup
if tx_freq < 17000000:
anttuner.antnum = 0
else:
elif anttuner.dipole2:
anttuner.antnum = 1
if anttuner.antnum == 1:
if anttuner.antnum == 1 and anttuner.dipole2:
anttuner.dipole2.SetTxFreq(tx_freq)
anttuner.WantData()
def FindIndexTup(self, tx_freq):
i1 = 0
i2 = len(self.TunerLCZ) - 1
while 1: # binary partition
i = (i1 + i2) // 2
if self.TunerLCZ[i][0] < tx_freq:
i1 = i
else:
i2 = i
if i2 - i1 <= 1:
break
# The correct setting is between i1 and i2.
# Choose the index that is closest in frequency.
delta1 = tx_freq - self.TunerLCZ[i1][0]
delta2 = self.TunerLCZ[i2][0] - tx_freq
if i1 == 0: # below the first frequency
index = i1
elif i2 == len(self.TunerLCZ) - 1: # above the last frequency
index = i2
elif delta2 < delta1: # i2 is closer
index = i2
else: # i1 is closer
index = i1
if DEBUG: print ("StatuinControlGUI FindLCZ", tx_freq, i1, i2, len(self.TunerLCZ), index, self.TunerLCZ[index])
return index, self.TunerLCZ[index]
self.freq_entry.SetLabel("%.3f" % (tx_freq * 1E-6))
def OnBtnClose(self, event):
self.hware.anttuner.close()
self.hware.controlbox.close()
@@ -730,16 +698,15 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
f = ((f + 500) // 1000) * 1000
if self.hware.v2filter:
self.hware.v2filter.SetTxFreq(f)
self.hware.anttuner.SetTxFreq(f, no_tune=True)
self.anttuner.SetTxFreq(f, no_tune=True)
self.freq_entry.ChangeValue("%.3f" % (f * 1E-6))
self.SetTxFreq(f)
def OnButtonHiLo(self, event):
anttuner = self.hware.anttuner
if self.btnHiLo.GetValue():
anttuner.set_HighZ = 1
self.anttuner.set_HighZ = 1
else:
anttuner.set_HighZ = 0
anttuner.WantData()
self.anttuner.set_HighZ = 0
self.anttuner.WantData()
def OnFreqEntry(self, event=None):
freq = self.freq_entry.GetValue()
freq = float(freq) * 1E6
@@ -751,11 +718,27 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
freq = int(freq + 0.1)
self.tx_freq = freq
self.SetTxFreq(self.tx_freq)
def OnButtonDown(self, event):
index, tup = self.anttuner.FindIndexTup(self.tx_freq)
index -= 1
if index >= 0:
self.SetTxFreq(self.anttuner.TunerLCZ[index][0])
def OnButtonUp(self, event):
index, tup = self.anttuner.FindIndexTup(self.tx_freq)
index += 1
if index < len(self.anttuner.TunerLCZ):
self.SetTxFreq(self.anttuner.TunerLCZ[index][0])
def OnButtonDelete(self, event):
index, tup = self.anttuner.FindIndexTup(self.tx_freq)
if self.tx_freq == self.anttuner.TunerLCZ[index][0]:
del self.anttuner.TunerLCZ[index]
if index > 0:
self.SetTxFreq(self.anttuner.TunerLCZ[index - 1][0])
elif len(self.anttuner.TunerLCZ) > 0:
self.SetTxFreq(self.anttuner.TunerLCZ[0][0])
else:
self.SetTxFreq(0)
def OnButtonSave(self, event):
freq = self.freq_entry.GetValue()
freq = float(freq) * 1E6
freq = int(freq + 0.1)
self.tx_freq = freq
L = self.sliderL.GetValue()
C = self.sliderC.GetValue()
if self.btnHiLo.GetValue():
@@ -763,22 +746,19 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
else:
hilo = 0
tup = (self.tx_freq, L, C, hilo)
index, t = self.FindIndexTup(self.tx_freq)
if self.tx_freq == self.TunerLCZ[index][0]:
self.TunerLCZ[index] = tup
index, t = self.anttuner.FindIndexTup(self.tx_freq)
if self.tx_freq == self.anttuner.TunerLCZ[index][0]:
self.anttuner.TunerLCZ[index] = tup
else:
self.TunerLCZ.append(tup)
self.TunerLCZ.sort()
fp = open(self.filename, 'w')
for tup in self.TunerLCZ[1:-1]: # Throw away dummy entries
fp.write("%12d %2d %2d %d\n" % tup)
fp.close()
self.anttuner.TunerLCZ.append(tup)
self.anttuner.TunerLCZ.sort()
self.anttuner.SaveLCZ()
def OnSliderL(self, event=None):
self.hware.anttuner.set_L = self.sliderL.GetValue()
self.hware.anttuner.WantData()
self.anttuner.set_L = self.sliderL.GetValue()
self.anttuner.WantData()
def OnSliderC(self, event=None):
self.hware.anttuner.set_C = self.sliderC.GetValue()
self.hware.anttuner.WantData()
self.anttuner.set_C = self.sliderC.GetValue()
self.anttuner.WantData()
def OnBtnV2Preamp(self, event):
if self.hware.v2filter:
self.hware.v2filter.SetPreamp(event.GetEventObject().GetValue())
@@ -1072,21 +1052,3 @@ class AT200PC: # Control an AT-200PC autotuner made by LDG
self.set_C += 1
elif text == 'C-':
self.set_C -= 1
class App(wx.App):
def OnInit(self):
if sys.path[0] != "'.'": # Make sure the current working directory is on path
sys.path.insert(0, '.')
import quisk_conf_defaults as conf
import quisk_hardware_model as hardware
self.bottom_widgets = None
hardware.anttuner = AntennaTuner(self, conf) # Control the antenna tuner
hardware.v2filter = FilterBoxV2(self, conf) # Control V2 filter box
hardware.controlbox = ControlBox(self, conf) # Control my Station Control Box
self.main_frame = frame = StationControlGUI (None, hardware, self, conf)
frame.Open()
frame.Show()
return True
if __name__ == '__main__':
App().MainLoop()
+21 -3
View File
@@ -1,7 +1,25 @@
MODULE_NAME := perseus
OBJDIR := ../build/local
perseus2:
python2 setup.py build_ext --force --inplace
CFLAGS := $(shell python3 ../makefil.py --cflags)
LDFLAGS := $(shell python3 ../makefil.py --ldflags)
EXTENSION_SUFFIX := $(shell python3 ../makefil.py --suffix)
TARGET := $(MODULE_NAME)$(EXTENSION_SUFFIX)
perseus3:
# Default target: Link the shared library object
$(TARGET): perseus.o $(OBJDIR)/import_quisk_api.o
$(CC) -o $@ perseus.o $(LDFLAGS) $(OBJDIR)/import_quisk_api.o
# Compile the C source files into object files
perseus.o: perseus.c ../quisk.h ../filter.h
$(CC) -o $@ -c $< $(CFLAGS) -I..
.PHONY: clean
clean:
-rm -f *.o
-rm -f $(TARGET)
.PHONY: perseus-old
perseus-old:
python3 setup.py build_ext --force --inplace
+2 -2
View File
@@ -297,11 +297,11 @@ static PyObject * open_device(PyObject * self, PyObject * args)
main_cleanup:
return PyString_FromString("ERROR");
return PyUnicode_FromString("ERROR");
exit_success:
return PyString_FromString(buf128);
return PyUnicode_FromString(buf128);
}
+1 -1
View File
@@ -18,7 +18,7 @@ from quisk_hardware_model import Hardware as BaseHardware
DEBUG = 1
# Define the name of the hardware and the items on the hardware screen (see quisk_conf_defaults.py):
################ Receivers PerseusSDR, The PerseusSDR interface to multiple hardware SDRs.
################ Receivers PerseusSDR, The PerseusSDR interface to multiple SDRs.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'perseuspkg/quisk_hardware.py'
+47
View File
@@ -0,0 +1,47 @@
[build-system]
requires = ["setuptools>=61.0.0"]
build-backend = "setuptools.build_meta"
[project]
name = "quisk"
version = "4.2.55"
requires-python = ">=3.13"
description = 'QUISK is a Software Defined Radio (SDR) transceiver that can control various radio hardware.'
license = "GPL-2.0-or-later"
license-files = ["license.txt"]
authors = [
{name = "James C. Ahlstrom", email="jahlstr@gmail.com"}
]
keywords = ['quisk', 'sdr', 'software defined radio']
classifiers = [
'Development Status :: 6 - Mature',
'Environment :: X11 Applications',
'Environment :: Win32 (MS Windows)',
'Intended Audience :: End Users/Desktop',
'Natural Language :: English',
'Operating System :: POSIX :: Linux',
'Operating System :: Microsoft :: Windows',
'Programming Language :: Python :: 3',
'Programming Language :: C',
'Topic :: Communications :: Ham Radio',
]
dynamic = ["readme"]
[project.gui-scripts]
quisk = "quisk.quisk:main"
[project.urls]
Homepage = "https://james.ahlstrom.name/quisk/index.html"
Documentation = "https://james.ahlstrom.name/quisk/docs.html"
Repository = "https://github.com/jimahlstrom/quisk"
Changelog = "https://james.ahlstrom.name/quisk/CHANGELOG.txt"
[tool.setuptools.package-data]
"quisk.icons" = ["*"]
[tool.setuptools]
package-dir = {"quisk" = "."}
packages = ["quisk", "quisk.n2adr", "quisk.softrock", "quisk.freedvpkg",
"quisk.hermes", "quisk.hiqsdr", "quisk.afedrinet", "quisk.soapypkg",
"quisk.sdrmicronpkg", "quisk.perseuspkg", "quisk.ac2yd", "quisk.multuspkg"]
+95 -185
View File
@@ -3,6 +3,7 @@
#include <stdlib.h>
#include <math.h>
#include <complex.h> // Use native C99 complex type for fftw3
#include <string.h>
#include <fftw3.h>
#include <sys/types.h>
#include <stdbool.h>
@@ -151,7 +152,6 @@ static int waterfall_scroll_mode = 1; // draw the first lines multiple times
static int quisk_use_sidetone; // is there a sidetone volume control?
static int hl2_txbuf_errors; // errors in the Hermes-Lite2 Tx buffer
static int hl2_txbuf_state; // state machine for errors in the Hermes-Lite2 Tx buffer
static int freedv_monitor; // pass the Freedv audio to the speakers instead of to the demodulator
static int softrock_correct_freq; // gain and phase corrections for SoftRock
static double softrock_correct_gain;
static double softrock_correct_phase;
@@ -219,14 +219,17 @@ static unsigned char quisk_hermes_to_pc[5 * 4]; // data received from the Herme
static unsigned char quisk_hermeslite_response[5]; // response from Hermes-Lite commands
unsigned int quisk_hermes_code_version = -1; // code version returned by the Hermes hardware
unsigned int quisk_hermes_board_id = -1; // board ID returned by the Hermes hardware
static double hermes_temperature; // average temperature
static double hermes_fwd_power; // average forward power
static double hermes_rev_power; // average reverse power
static double hermes_fwd_peak; // peak forward power
static double hermes_rev_peak; // peak reverse power
static double hermes_pa_current; // average power amp current
static int hermes_count_temperature; // number of temperature samples
static int hermes_count_current; // number of current samples
static double hpsdr_AIN1_avg; // average forward power
static double hpsdr_AIN1_peak; // peak forward power
static double hpsdr_AIN2_avg; // average reverse power
static double hpsdr_AIN2_peak; // peak reverse power
static double hpsdr_AIN3; // average power amp current
static double hpsdr_AIN4;
static double hpsdr_AIN5; // average temperature
static double hpsdr_AIN6;
static int hpsdr_AIN3_count; // number of current samples
static int hpsdr_AIN4_count; // number of current samples
static int hpsdr_AIN5_count; // number of current samples
static int hardware_ptt; // hardware PTT switch
int quisk_hardware_cwkey; // hardware CW key from UDP or USB
@@ -569,15 +572,10 @@ void QuiskWavClose(struct QuiskWav * hWav)
}
#endif
// These are used for digital voice codecs
ty_dvoice_codec_rx pt_quisk_freedv_rx;
ty_dvoice_codec_tx pt_quisk_freedv_tx;
void quisk_dvoice_freedv(ty_dvoice_codec_rx rx, ty_dvoice_codec_tx tx)
void quisk_dvoice_freedv(void) // Not used.
{
pt_quisk_freedv_rx = rx;
pt_quisk_freedv_tx = tx;
}
#if 0
static int fFracDecim(double * dSamples, int nSamples, double fdecim)
{ // fractional decimation by fdecim > 1.0
@@ -1851,10 +1849,8 @@ static int quisk_process_decimate(complex double * cSamples, int nSamples, int b
static int quisk_process_demodulate(complex double * cSamples, double * dsamples, int nSamples, int bank, int nFilter, rx_mode_type rx_mode)
{ // Changes here will require changes to get_filter_rate();
int i;
bool isLSB;
complex double cx;
double d, di, dd;
static struct AgcState Agc1 = {0.3, 16000, 0}, Agc2 = {0.3, 16000, 0};
//static int count=0;
//static double phase=0;
static struct stStorage {
@@ -2090,6 +2086,7 @@ static int quisk_process_demodulate(complex double * cSamples, double * dsamples
MeasureSquelch[bank].squelch_active = MeasureSquelch[bank].squelch < squelch_level;
break;
case DGT_U: // digital mode DGT-U at 48 ksps
case FDV_U:
if (filter_bandwidth[nFilter] < DGT_NARROW_FREQ) { // filter at 6 ksps
quisk_filter_srate = quisk_decim_srate / 8;
nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand5);
@@ -2116,6 +2113,7 @@ static int quisk_process_demodulate(complex double * cSamples, double * dsamples
}
break;
case DGT_L: // digital mode DGT-L
case FDV_L:
if (filter_bandwidth[nFilter] < DGT_NARROW_FREQ) { // filter at 6 ksps
quisk_filter_srate = quisk_decim_srate / 8;
nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand5);
@@ -2154,102 +2152,6 @@ static int quisk_process_demodulate(complex double * cSamples, double * dsamples
}
}
break;
case FDV_U: // digital voice
case FDV_L: // Extra modes added by Dave Roberts, G8KBB, June 2020. Modified by N2ADR 2024.
quisk_check_freedv_mode();
// current coding assumes input rate n_modem_sample_rate is 8000 or 48000.
// decimate:
switch (n_modem_sample_rate) {
case 8000:
quisk_filter_srate = quisk_decim_srate / 6;
nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim48to16, 3);
nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim16to8, 2);
break;
case 48000:
quisk_filter_srate = quisk_decim_srate;
break;
default:
quisk_filter_srate = quisk_decim_srate / 6;
QuiskPrintf("Bad FreeDV modem rate in quisk_process_demodulate\n");
break;
}
// filter and demodulate to mono audio:
isLSB = (rx_mode == FDV_L);
complex double cBuf[nSamples];
for (i = 0; i < nSamples; i++) {
cx = cRxFilterOut(cSamples[i], bank, nFilter);
if (isLSB)
dd = creal(cx) + cimag(cx);
else
dd = creal(cx) - cimag(cx);
if(bank == 0) {
measure_audio_sum += dd * dd;
measure_audio_count += 1;
}
cBuf[i] = dd;
}
// Perhaps decimate by an additional fraction
if (quisk_decim_srate != 48000) {
dd = quisk_decim_srate / 48000.0;
nSamples = cFracDecim(cBuf, nSamples, dd);
quisk_decim_srate = 48000;
}
if (bank == 0)
process_agc(&Agc1, cBuf, nSamples, 0);
else
process_agc(&Agc2, cBuf, nSamples, 0);
// pass data decimated down to n_modem_sample_rate
// return data is at n_speech_sample_rate
#if 0
static double dmax_in = 0;
for (i = 0; i < nSamples; i++) {
dd = fabs(creal(cBuf[i])) / CLIP16;
if (dd > dmax_in)
dmax_in = dd;
}
if (bank == 0 && measure_audio_count >= quisk_filter_srate * measure_audio_time) {
//printf("Max sample in %.0lf\n", dmax_in);
dmax_in = 0;
}
#endif
if (nSamples > n_max_modem_samples) {
printf ("FreeDV: nSamples %d exceeds max_modem_samples %d\n", nSamples, n_max_modem_samples);
nSamples = n_max_modem_samples;
}
short sBuf[nSamples];
for (i = 0; i < nSamples; i++)
sBuf[i] = (short)(creal(cBuf[i]) / CLIP16);
if (freedv_monitor) {
for (i = 0; i < nSamples; i++)
dsamples[i] = (float)sBuf[i] * CLIP16;
}
else {
if (pt_quisk_freedv_rx)
nSamples = (* pt_quisk_freedv_rx)(sBuf, dsamples, nSamples, bank);
}
#if 0
static double dmax_out = 0;
for (i = 0; i < nSamples; i++) {
dd = fabs(dsamples[i]) / CLIP16;
if (dd > dmax_out)
dmax_out = dd;
}
if (bank == 0 && measure_audio_count >= quisk_filter_srate * measure_audio_time) {
//printf(" Max sample out %.0lf\n", dmax_out);
dmax_out = 0;
}
#endif
// current coding assumes output rate n_speech_sample_rate is 8000 or 16000
switch (n_speech_sample_rate) {
case 8000:
nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio24p3, 3);
nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7);
break;
case 16000:
nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio24p3, 3);
break;
}
break;
}
if (bank == 0 && measure_audio_count >= quisk_filter_srate * measure_audio_time) {
measured_audio = sqrt(measure_audio_sum / measure_audio_count) / CLIP32 * 1e6;
@@ -2785,9 +2687,6 @@ start_agc:
if (rxMode == EXT || rxMode == DGT_IQ) { // Ext and DGT-IQ stereo sound
process_agc(&Agc1, cSamples, nSamples, 1);
}
else if (rxMode == FDV_U || rxMode == FDV_L) { // Agc already done
;
}
else if (split_rxtx || multirx_play_channel >= 0) { // separate AGC for left and right channels
for (i = 0; i < nSamples; i++) {
orig_cSamples[i] = cimag(cSamples[i]);
@@ -2945,6 +2844,8 @@ static PyObject * get_filter_rate(PyObject * self, PyObject * args)
break;
case DGT_U: // digital modes DGT-*
case DGT_L:
case FDV_U: // digital voice
case FDV_L:
if (bandwidth < DGT_NARROW_FREQ)
filter_srate = decim_srate / 8;
else
@@ -2953,10 +2854,6 @@ static PyObject * get_filter_rate(PyObject * self, PyObject * args)
case DGT_IQ: // digital mode at 48 ksps
filter_srate = decim_srate;
break;
case FDV_U: // digital voice
case FDV_L:
filter_srate = n_modem_sample_rate;
break;
}
//QuiskPrintf("Filter rate %d\n", filter_srate);
return PyInt_FromLong(filter_srate);
@@ -3196,14 +3093,13 @@ static PyObject * set_params(PyObject * self, PyObject * args, PyObject * keywds
{ /* Call with keyword arguments ONLY; change local parameters */
static char * kwlist[] = {"quisk_is_vna", "rx_bytes", "rx_endian", "read_error", "clip",
"bscope_bytes", "bscope_endian", "bscope_size", "bandscopeScale", "hermes_pause",
"freedv_monitor", NULL} ;
NULL} ;
int i, nbytes, read_error, clip, bscope_size, hermes_pause;
nbytes = read_error = clip = bscope_size = hermes_pause = -1;
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiiiiiidii", kwlist,
&quisk_is_vna, &nbytes, &py_sample_rx_endian, &read_error, &clip,
&py_bscope_bytes, &py_bscope_endian, &bscope_size, &bandscopeScale, &hermes_pause,
&freedv_monitor))
&py_bscope_bytes, &py_bscope_endian, &bscope_size, &bandscopeScale, &hermes_pause))
return NULL;
if (nbytes != -1) {
py_sample_rx_bytes = nbytes;
@@ -3244,31 +3140,43 @@ static PyObject * get_hermes_TFRC(PyObject * self, PyObject * args)
if (!PyArg_ParseTuple (args, ""))
return NULL;
if (hermes_count_temperature > 0) {
hermes_temperature /= hermes_count_temperature;
hermes_fwd_power /= hermes_count_temperature;
if (hpsdr_AIN5_count > 0) {
hpsdr_AIN5 /= hpsdr_AIN5_count;
hpsdr_AIN1_avg /= hpsdr_AIN5_count;
}
else {
hermes_temperature = 0.0;
hermes_fwd_power = 0.0;
hpsdr_AIN5 = 0.0;
hpsdr_AIN1_avg = 0.0;
}
if (hermes_count_current > 0) {
hermes_rev_power /= hermes_count_current;
hermes_pa_current /= hermes_count_current;
if (hpsdr_AIN3_count > 0) {
hpsdr_AIN2_avg /= hpsdr_AIN3_count;
hpsdr_AIN3 /= hpsdr_AIN3_count;
}
else {
hermes_rev_power = 0.0;
hermes_pa_current = 0.0;
hpsdr_AIN2_avg = 0.0;
hpsdr_AIN3 = 0.0;
}
ret = Py_BuildValue("dddddd", hermes_temperature, hermes_fwd_power, hermes_rev_power, hermes_pa_current, hermes_fwd_peak, hermes_rev_peak);
hermes_temperature = 0;
hermes_fwd_power = 0;
hermes_rev_power = 0;
hermes_fwd_peak = 0;
hermes_rev_peak = 0;
hermes_pa_current = 0;
hermes_count_temperature = 0;
hermes_count_current = 0;
if (hpsdr_AIN4_count > 0) {
hpsdr_AIN4 /= hpsdr_AIN4_count;
hpsdr_AIN6 /= hpsdr_AIN4_count;
}
else {
hpsdr_AIN4 = 0.0;
hpsdr_AIN6 = 0.0;
}
ret = Py_BuildValue("dddddddd", hpsdr_AIN1_avg, hpsdr_AIN1_peak, hpsdr_AIN2_avg, hpsdr_AIN2_peak,
hpsdr_AIN3, hpsdr_AIN4, hpsdr_AIN5, hpsdr_AIN6);
hpsdr_AIN1_avg = 0;
hpsdr_AIN1_peak = 0;
hpsdr_AIN2_avg = 0;
hpsdr_AIN2_peak = 0;
hpsdr_AIN3 = 0;
hpsdr_AIN4 = 0;
hpsdr_AIN5 = 0;
hpsdr_AIN6 = 0;
hpsdr_AIN3_count = 0;
hpsdr_AIN4_count = 0;
hpsdr_AIN5_count = 0;
return ret;
}
@@ -3816,19 +3724,24 @@ static int read_rx_udp10(complex double * samp) // Read samples from UDP using t
quisk_set_play_state();
}
}
else if(dindex == 1) { // temperature and forward power
hermes_temperature += quisk_hermes_to_pc[4] << 8 | quisk_hermes_to_pc[5];
else if(dindex == 1) {
hpsdr_AIN5 += quisk_hermes_to_pc[4] << 8 | quisk_hermes_to_pc[5];
power = quisk_hermes_to_pc[6] << 8 | quisk_hermes_to_pc[7];
hermes_fwd_power += power;
hermes_fwd_peak = fmax(hermes_fwd_peak, (double)power);
hermes_count_temperature++;
hpsdr_AIN1_avg += power;
hpsdr_AIN1_peak = fmax(hpsdr_AIN1_peak, (double)power);
hpsdr_AIN5_count++;
}
else if (dindex == 2) { // reverse power and current
else if (dindex == 2) {
power = quisk_hermes_to_pc[8] << 8 | quisk_hermes_to_pc[9];
hermes_rev_power += power;
hermes_rev_peak = fmax(hermes_rev_peak, (double)power);
hermes_pa_current += quisk_hermes_to_pc[10] << 8 | quisk_hermes_to_pc[11];
hermes_count_current++;
hpsdr_AIN2_avg += power;
hpsdr_AIN2_peak = fmax(hpsdr_AIN2_peak, (double)power);
hpsdr_AIN3 += quisk_hermes_to_pc[10] << 8 | quisk_hermes_to_pc[11];
hpsdr_AIN3_count++;
}
else if (dindex == 3) {
hpsdr_AIN4 += quisk_hermes_to_pc[12] << 8 | quisk_hermes_to_pc[13];
hpsdr_AIN6 += quisk_hermes_to_pc[14] << 8 | quisk_hermes_to_pc[15];
hpsdr_AIN4_count++;
}
// convert 24-bit samples to 32-bit samples; int must be 32 bits.
index = start + 5;
@@ -4115,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;
@@ -4164,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)
@@ -4378,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)
@@ -4581,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);
}
@@ -4605,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);
}
@@ -4917,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;
@@ -4929,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;
@@ -4947,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;
@@ -5725,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;
@@ -5748,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");
@@ -5783,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);
@@ -5824,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;
}
@@ -6097,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)
@@ -6269,13 +6184,6 @@ static PyMethodDef QuiskMethods[] = {
{"set_cwkey", set_hardware_cwkey, METH_VARARGS, "Change the CW key up/down state."},
{"set_remote_cwkey", set_remote_cwkey, METH_VARARGS, "Change the remote control CW key up/down state."},
{"set_PTT", set_PTT, METH_VARARGS, "Record the PTT button state."},
{"freedv_open", quisk_freedv_open, METH_VARARGS, "Open FreeDV."},
{"freedv_close", quisk_freedv_close, METH_VARARGS, "Close FreeDV."},
{"freedv_get_snr", quisk_freedv_get_snr, METH_VARARGS, "Return the signal to noise ratio in dB."},
{"freedv_get_version", quisk_freedv_get_version, METH_VARARGS, "Return the codec2 API version."},
{"freedv_get_rx_char", quisk_freedv_get_rx_char, METH_VARARGS, "Get text characters received from freedv."},
{"freedv_set_options", (PyCFunction)quisk_freedv_set_options, METH_VARARGS|METH_KEYWORDS, "Set the freedv parameters."},
{"freedv_set_squelch_en", quisk_freedv_set_squelch_en, METH_VARARGS, "Enable or disable FreeDV squelch."},
{"wdsp_set_parameter", (PyCFunction)quisk_wdsp_set_parameter, METH_VARARGS|METH_KEYWORDS, "Set parameters for the WDSP SDR library."},
{"tmp_record_save", tmp_record_save, METH_VARARGS, "Save the temporary recording in a WAV file."},
{"watfall_RgbData", watfall_RgbData, METH_VARARGS, "Return a cookie for the Waterfall pixel data."},
@@ -6283,6 +6191,8 @@ static PyMethodDef QuiskMethods[] = {
{"watfall_GetPixels", watfall_GetPixels, METH_VARARGS, "Write the Waterfall image to be displayed."},
{"write_fftw_wisdom", write_fftw_wisdom, METH_VARARGS, "Write the current fftw wisdom to the wisdom file."},
{"read_fftw_wisdom", read_fftw_wisdom, METH_VARARGS, "Return the current fftw wisdom as a byte array."},
{"tci_get_params", (PyCFunction)quisk_tci_get_params, METH_VARARGS, "Return parameters from TCI."},
{"tci_set_params", (PyCFunction)quisk_tci_set_params, METH_VARARGS|METH_KEYWORDS, "Set parameters for TCI."},
// Remote Quisk control head and slave by Ben, AC2YD
{"start_control_head_remote_sound", quisk_start_control_head_remote_sound, METH_VARARGS, "Start running UDP remote sound on control_head."},
{"stop_control_head_remote_sound", quisk_stop_control_head_remote_sound, METH_VARARGS, "Stop running UDP remote sound on control_head."},
+13 -20
View File
@@ -29,6 +29,8 @@
#define QUISK_CWKEY_DOWN (quisk_hardware_cwkey || quisk_serial_key_down || quisk_midi_cwkey || quisk_remote_cwkey)
#include <stdio.h>
#include <stdlib.h>
#ifdef MS_WINDOWS
#define QUISK_SHUT_RD SD_RECEIVE
#define QUISK_SHUT_BOTH SD_BOTH
@@ -260,6 +262,14 @@ extern PyObject * quisk_stop_remote_radio_remote_sound(PyObject * self, PyObject
extern int receive_graph_data(double * fft_avg);
extern void send_graph_data(double * fft_avg, int fft_size, double zoom, double deltaf, int fft_sample_rate, double scale);
// TCI server
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.
void QuiskWavClose(struct QuiskWav *);
int QuiskWavWriteOpen(struct QuiskWav *, char *, short, short, short, int, double);
void QuiskWavWriteC(struct QuiskWav *, complex double *, int);
@@ -312,10 +322,6 @@ extern int quisk_active_sidetone; // Whether and how to generate a sidetone
extern int quisk_isFDX; // Are we in full duplex mode?
extern int quisk_use_serial_port; // Are we using the serial port for CW key or PTT?
extern play_state_t quisk_play_state; // startup, receiving, sidetone
extern int freedv_current_mode; // current FreeDV mode; 700D, @)@) etc.
extern int n_modem_sample_rate; // Receive data, decimate to modem_sample_rate, FreeDV codec output data at speech_sample_rate
extern int n_speech_sample_rate; // Microphone decimate to speech_sample_rate, Freedv codec output data at modem_sample_rate, interpolate to 48000
extern int n_max_modem_samples; // maximum input to freedv_rx()
extern int quisk_start_cw_delay; // milliseconds to delay output on serial or MIDI CW key down
extern int quisk_start_ssb_delay; // milliseconds to discard output for all modes except CW
extern struct sound_dev * quiskPlaybackDevices[]; // array of Playback sound devices
@@ -346,14 +352,7 @@ extern PyObject * quisk_set_hermes_filter(PyObject *, PyObject *);
extern PyObject * quisk_set_alex_hpf(PyObject *, PyObject *);
extern PyObject * quisk_set_alex_lpf(PyObject *, PyObject *);
extern PyObject * quisk_freedv_open(PyObject *, PyObject *);
extern PyObject * quisk_freedv_close(PyObject *, PyObject *);
extern PyObject * quisk_freedv_get_snr(PyObject *, PyObject *);
extern PyObject * quisk_freedv_get_version(PyObject *, PyObject *);
extern PyObject * quisk_freedv_get_rx_char(PyObject *, PyObject *);
extern PyObject * quisk_freedv_set_options(PyObject *, PyObject *, PyObject *);
extern PyObject * quisk_set_sparams(PyObject *, PyObject *, PyObject *);
extern PyObject * quisk_freedv_set_squelch_en(PyObject *, PyObject *);
extern PyObject * quisk_open_key(PyObject *, PyObject *, PyObject *);
extern PyObject * quisk_close_key(PyObject *, PyObject *);
extern PyObject * quisk_set_sound_name(PyObject *, PyObject *);
@@ -397,7 +396,6 @@ void quisk_file_playback(complex double *, int, double);
void quisk_tmp_playback(complex double *, int, double);
void quisk_hermes_tx_send(int, int *);
void quisk_udp_mic_error(char *);
void quisk_check_freedv_mode(void);
void quisk_calc_audio_graph(double, complex double *, double *, int, int);
double QuiskDeltaSec(int);
void * quisk_make_sidetone(struct sound_dev *, int);
@@ -406,12 +404,7 @@ int quisk_play_sidetone(struct sound_dev *);
void quisk_set_play_state(void);
void quisk_poll_hardware_key(void);
void PreDistort(complex double * amp_in_samples, complex double * amp_out_samples, int nSamples, complex double * tx_samples, int num_tx);
// Functions supporting digital voice codecs
typedef int (* ty_dvoice_codec_rx)(short *, double *, int, int);
typedef int (* ty_dvoice_codec_tx)(complex double *, double *, int, int);
extern ty_dvoice_codec_rx pt_quisk_freedv_rx;
extern ty_dvoice_codec_tx pt_quisk_freedv_tx;
int CircularBuffer(int channel, complex double * cSamples, int nRead, int nWrite);
// Driver function definitions=================================================
int quisk_read_alsa(struct sound_dev *, complex double *);
@@ -468,7 +461,7 @@ int import_quisk_api(void); // used to initialize Quisk_API
#define QuiskSleepMicrosec (*( void (*) (int) )Quisk_API[5])
#define QuiskPrintTime (*( void (*) (const char *, int) )Quisk_API[6])
#define quisk_sample_source (*( void (*) (ty_sample_start, ty_sample_stop, ty_sample_read) )Quisk_API[7])
#define quisk_dvoice_freedv (*( void (*) (ty_dvoice_codec_rx, ty_dvoice_codec_tx) )Quisk_API[8])
#define quisk_dvoice_freedv (*( void (*) (void) )Quisk_API[8])
#define quisk_is_key_down (*( int (*) (void) )Quisk_API[9])
#define quisk_sample_source4 (*( void (*) (ty_sample_start, ty_sample_stop, ty_sample_read, ty_sample_write) )Quisk_API[10])
#define strMcpy (*( char * (*) (char *, const char *, size_t) )Quisk_API[11])
@@ -483,7 +476,7 @@ double QuiskTimeSec(void);
void QuiskSleepMicrosec(int);
void QuiskPrintTime(const char *, int);
void quisk_sample_source(ty_sample_start, ty_sample_stop, ty_sample_read);
void quisk_dvoice_freedv(ty_dvoice_codec_rx, ty_dvoice_codec_tx);
void quisk_dvoice_freedv(void);
int quisk_is_key_down(void);
void quisk_sample_source4(ty_sample_start, ty_sample_stop, ty_sample_read, ty_sample_write);
char * strMcpy(char *, const char *, size_t);
+135 -158
View File
@@ -39,6 +39,7 @@ from filters import Filters
import dxcluster
import configure
import quisk_conf_defaults as conf
import quisk_utils
import quisk_wdsp
for name in configure.name2format: # Add defaults. These may be overwritten by a user config file.
@@ -292,9 +293,9 @@ class HamlibHandlerSerial:
# Additional logic contributed by Dr. Karsten Schmidt
Mo2CoKen = {'CWL':7, 'CWU':3, 'LSB':1, 'USB':2, 'AM':5, 'FM':4, 'DGT-U':9, 'DGT-L':6, 'DGT-FM':4, 'DGT-IQ':9}
Mo2CoKen = {'CWL':7, 'CWU':3, 'LSB':1, 'USB':2, 'AM':5, 'FM':4, 'DGT-U':9, 'FDV-U':9, 'DGT-L':6, 'FDV-L':6, 'DGT-FM':4, 'DGT-IQ':9}
Co2MoKen = {1:'LSB', 2:'USB', 3:'CWU', 4:'FM', 5:'AM', 6:'DGT-L', 7:'CWL', 9:'DGT-U'}
Mo2CoFlex = {'CWL':3, 'CWU':4, 'LSB':0, 'USB':1, 'AM':6, 'FM':5, 'DGT-U':7, 'DGT-L':9, 'DGT-FM':5, 'DGT-IQ':7}
Mo2CoFlex = {'CWL':3, 'CWU':4, 'LSB':0, 'USB':1, 'AM':6, 'FM':5, 'DGT-U':7, 'FDV-U':7, 'DGT-L':9, 'FDV-L':9, 'DGT-FM':5, 'DGT-IQ':7}
Co2MoFlex = {0:'LSB', 1:'USB', 3:'CWL', 4:'CWU', 5:'FM', 6:'AM', 7:'DGT-U', 9:'DGT-L'}
ConvertZZAC_P1_2_Step = { 0: 1,
1: 10,
@@ -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 = []
@@ -995,17 +999,12 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
else:
self.Send('0\n')
def GetFreq(self): # The Rx frequency
rx = self.app.multi_rx_screen.receiver_list
if rx:
rx = rx[0]
self.Reply('Frequency', rx.txFreq + rx.VFO, 0)
if HAMLIB_DEBUG:
print ("GetFreq rx", rx.txFreq, rx.VFO)
else:
self.Reply('Frequency', self.app.rxFreq + self.app.VFO, 0)
if HAMLIB_DEBUG:
print ("GetFreq app", self.app.rxFreq, self.app.txFreq, self.app.VFO)
# This is always the main Rx frequency without reference to added receivers.
self.Reply('Frequency', self.app.rxFreq + self.app.VFO, 0)
if HAMLIB_DEBUG:
print ("GetFreq app", self.app.rxFreq, self.app.txFreq, self.app.VFO)
def SetFreq(self): # The Rx frequency
# This is always the main Rx frequency without reference to added receivers.
freq = self.GetParamNumber()
try:
freq = float(freq)
@@ -1014,12 +1013,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
self.ErrParam()
else:
freq = int(freq + 0.5)
# For multiple receivers this controls the first added receiver frequency; else it controls split Rx/Tx frequency.
rx = self.app.multi_rx_screen.receiver_list
if rx:
rx[0].ChangeRxTxFrequency(freq)
else:
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
@@ -1032,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):
@@ -1061,9 +1056,9 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
mode = 'CW'
elif mode == 'CWL': # Is this what CWR means?
mode = 'CWR'
elif mode == 'DGT-U':
elif mode in ('DGT-U', 'FDV-U'):
mode = 'PKTUSB'
elif mode == 'DGT-L':
elif mode in ('DGT-L', 'FDV-L'):
mode = 'PKTLSB'
elif mode == 'DGT-FM':
mode = 'PKTFM'
@@ -1117,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):
@@ -2574,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:
@@ -2584,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:
@@ -3652,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):
@@ -3727,8 +3738,8 @@ class App(wx.App):
'txAudioPreemphUsb', 'txAudioPreemphAm', 'txAudioPreemphFm', 'txAudioPreemphFdv',
'wfallScaleZ', 'wfallGrScaleZ', 'graphScaleZ', 'split_rxtx_play', 'modeFilter',
'file_name_rec_audio', 'file_name_rec_samples', 'file_name_rec_mic', 'file_name_rec_tmp',
'file_name_play_audio', 'file_name_play_samples', 'file_name_play_cq', 'freedv_mode',
'file_play_source', 'hermes_LNA_dB']
'file_name_play_audio', 'file_name_play_samples', 'file_name_play_cq',
'file_play_source', 'hermes_LNA_dB', 'hermes_atten_dB']
def __init__(self):
global application
QS.AppStatus(1)
@@ -3948,9 +3959,9 @@ class App(wx.App):
self.hot_key_ptt_active = False
self.serial_ptt_active = False
self.vox_ptt_active = False
self.freedv_mode = 'Mode 700D' # restore FreeDV mode setting
self.freedv_menu = None
self.tci_ptt_active = False
self.hermes_LNA_dB = 20
self.hermes_atten_dB = 0
if hasattr(Hardware, "OnChangeRxTx"):
self.want_RxTx = True
else:
@@ -4039,6 +4050,10 @@ class App(wx.App):
self.file_name_play_samples = ''
self.file_name_play_cq = ''
self.midiControls = {} # Control object and associated function for Midi control name
self.tci_vfo = 0
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)
@@ -4286,6 +4301,10 @@ class App(wx.App):
self.main_frame.SetClientSize(wx.Size(width, self.height))
if hasattr(Hardware, 'pre_open'): # pre_open() is called before open()
Hardware.pre_open()
try:
self.is_HermesLite2 = Hardware.is_HermesLite2 # This is set in pre_open()
except:
self.is_HermesLite2 = False
if self.local_conf.GetWidgets(self, Hardware, conf, frame, gbs, vertBox):
pass
elif conf.quisk_widgets:
@@ -4360,6 +4379,19 @@ class App(wx.App):
if hasattr(bw, "sliderLNA"):
self.midiControls["RfLna"] = (bw.sliderLNA, bw.OnLNA)
self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyHook)
# Start the TCI server
if conf.tci_ip and conf.tci_port:
value = getattr(conf, "pulse_audio_verbose_output")
QS.tci_set_params(verbose=value)
QS.tci_set_params(start=1);
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)
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
def OnStartWsjtx(self, ctrl):
method = ctrl.GetValue()
@@ -4684,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:
@@ -4711,8 +4745,6 @@ class App(wx.App):
self.hamlib_com1_handler.close()
if self.hamlib_com2_handler:
self.hamlib_com2_handler.close()
if conf.add_freedv_button:
QS.freedv_close()
msg = QS.GetQuiskPrintf()
if msg:
print(msg, end='')
@@ -4771,6 +4803,7 @@ class App(wx.App):
def ImmediateChange(self, name):
if name == "pulse_audio_verbose_output":
value = getattr(conf, name)
QS.tci_set_params(verbose=value)
if value == 0:
self.std_out_err.Show(False)
else:
@@ -5051,14 +5084,9 @@ class App(wx.App):
self.config_screen.config.EnableRecPlay()
### Right bank of buttons
mode_names = ['CWL', 'CWU', 'LSB', 'USB', 'AM', 'FM', 'DGT-U', 'DGT-L', 'DGT-FM', 'DGT-IQ', 'FDV-U', 'FDV-L', 'IMD']
labels = [('CWL', 'CWU'), ('LSB', 'USB'), 'AM', 'FM', ('DGT-U', 'DGT-L', 'DGT-FM', 'DGT-IQ')]
shortcuts = ['C', 'S', 'A', 'M', 'D']
count = 5 # There is room for seven buttons
if conf.add_freedv_button:
n_freedv = count
count += 1
labels.append('FDV-U')
shortcuts.append('F')
labels = [('CWL', 'CWU'), ('LSB', 'USB'), 'AM', 'FM', ('DGT-U', 'DGT-L', 'DGT-FM', 'DGT-IQ'), ('FDV-U', 'FDV-L')]
shortcuts = ['C', 'S', 'A', 'M', 'D', 'F']
count = 6 # There is room for seven buttons
if conf.add_imd_button:
n_imd = count
count += 1
@@ -5080,43 +5108,11 @@ class App(wx.App):
self.modeButns.buttons[0].idName = "CW U/L"
self.modeButns.buttons[1].idName = "SSB U/L"
self.modeButns.buttons[4].idName = "DGT"
self.modeButns.buttons[5].idName = "FDV"
else:
labels = ['CWL', 'CWU', 'LSB', 'USB', 'AM', 'FM', 'DGT-U', 'DGT-L', 'DGT-FM', 'DGT-IQ', 'FDV-U', 'IMD']
self.modeButns = RadioBtnPopup(frame, self.OnBtnMode, labels, None, 'modeButns')
self.modeButns.idName = "modeButns"
self.freedv_menu_items = {}
if conf.add_freedv_button:
self.freedv_menu = QuiskMenu("freedv_menu")
msg = conf.freedv_tx_msg
QS.freedv_set_options(mode=conf.freedv_modes[0][1], tx_msg=msg, DEBUG=0, squelch=1)
self.freedv_menu.AppendCheckItem("Monitor", self.OnFreedvMonitor)
self.freedv_menu.AppendSeparator()
for mode, index in conf.freedv_modes:
item = self.freedv_menu.AppendRadioItem(mode, self.OnFreedvMenu, mode == self.freedv_mode)
self.freedv_menu_items[index] = item
if mode == self.freedv_mode: # Restore mode
QS.freedv_set_options(mode=index)
if mode in ("Mode 2020", "Mode 2400A", "Mode 2400B"):
item.Enable(False)
if conf.button_layout == 'Large screen':
b = QuiskCycleCheckbutton(frame, None, ('FDV-U', 'FDV-L'), is_radio=True)
b.idName = "FDV"
b.char_shortcut = 'F'
self.btnFreeDV = WrapMenu(b, self.freedv_menu)
self.modeButns.ReplaceButton(n_freedv, self.btnFreeDV)
else:
self.btnFreeDV = self.modeButns.AddMenu('FDV-U', self.freedv_menu)
try:
ok = QS.freedv_open()
except:
traceback.print_exc()
ok = 0
if not ok:
conf.add_freedv_button = False
if conf.button_layout == 'Large screen':
self.modeButns.GetButtons()[n_freedv].Enable(0)
else:
self.modeButns.Enable('FDV-U', False)
if conf.add_imd_button:
val = 1000
QS.set_imd_level(val)
@@ -5395,16 +5391,6 @@ class App(wx.App):
t = '%13.2f' % (QS.measure_frequency(-1) + vfo)
t = t[0:4] + ' ' + t[4:7] + ' ' + t[7:] + ' Hz'
self.smeter.SetLabel(t)
def NewDVmeter(self):
if conf.add_freedv_button:
snr = QS.freedv_get_snr()
txt = QS.freedv_get_rx_char()
self.graph.ScrollMsg(txt)
self.waterfall.ScrollMsg(txt)
else:
snr = 0.0
t = " SNR %3.0f" % snr
self.smeter.SetLabel(t)
def NewSmeter(self):
self.smeter_db_count += 1 # count for average
x = QS.get_smeter()
@@ -5574,66 +5560,6 @@ class App(wx.App):
self.multi_rx_screen.waterfall.pane2.filter_center = center
if self.screen is self.filter_screen:
self.screen.NewFilter()
def OnFreedvMonitor(self, event):
for item in self.freedv_menu.GetMenuItems():
if item.GetKind() == wx.ITEM_CHECK:
if item.IsChecked():
QS.set_params(freedv_monitor=1)
else:
QS.set_params(freedv_monitor=0)
break
def OnFreedvMenu(self, event):
text = ''
for item in self.freedv_menu.GetMenuItems():
kind = item.GetKind()
if kind != wx.ITEM_RADIO:
continue
if item.IsChecked():
text = item.GetItemLabel()
break
for mode, index in conf.freedv_modes:
if mode == text:
break
else:
print ("Failure in OnFreedvMenu")
return
mode = QS.freedv_set_options(mode=index)
if mode == index:
self.freedv_mode = text
elif not self.remote_control_slave: # change to new mode failed
self.freedv_menu_items[mode].Check(1)
pos = (self.width//2, self.height//2)
if index == 8:
dlg = wx.MessageDialog(self.main_frame, "Quisk does not install %s. Please install a system-wide codec2." % text, "FreeDV Modes", wx.OK, pos)
else:
dlg = wx.MessageDialog(self.main_frame, "No codec2 support for " + text, "FreeDV Modes", wx.OK, pos)
dlg.ShowModal()
else:
self.freedv_menu_items[mode].Check(1)
print ("FreeDV change mode failed.")
self.OnChangeFilter()
def OnChangeFilter(self):
# Used by OnFreedvMenu because the FreeDV mode may change the filter sample rate
bw = int(self.filterButns.GetLabel())
# We can not use QS.get_filter_srate() because the FreeDV mode is not changed yet.
if self.freedv_mode in ("Mode 2400A", "Mode 2400B"):
frate = 48000;
else:
frate = 8000;
bw = min(bw, frate // 2)
self.filter_bandwidth = bw
center = self.GetFilterCenter(self.mode, bw)
filtI, filtQ = self.MakeFilterCoef(frate, None, bw, center)
lower_edge = center - bw // 2
QS.set_filters(filtI, filtQ, bw, lower_edge, 0)
self.multi_rx_screen.graph.filter_bandwidth = bw
self.multi_rx_screen.graph.filter_center = center
self.multi_rx_screen.waterfall.pane1.filter_bandwidth = bw
self.multi_rx_screen.waterfall.pane1.filter_center = center
self.multi_rx_screen.waterfall.pane2.filter_bandwidth = bw
self.multi_rx_screen.waterfall.pane2.filter_center = center
if self.screen is self.filter_screen:
self.screen.NewFilter()
def OnBtnHelp(self, event):
if event.GetEventObject().GetValue():
self.OnBtnScreen(None, 'Help')
@@ -5894,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
@@ -6098,11 +6024,6 @@ class App(wx.App):
QS.set_squelch(self.levelSquelch / 12.0 - 120.0)
else:
QS.set_squelch(-999.0)
elif self.mode[0:3] == 'FDV':
if value:
QS.freedv_set_squelch_en(1)
else:
QS.freedv_set_squelch_en(0)
else:
self.levelSquelchSSB = btn.slider_value
if value:
@@ -6236,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:
@@ -6247,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
@@ -6437,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:
@@ -6459,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):
@@ -6798,11 +6721,13 @@ class App(wx.App):
if not self.tx_indicator:
self.tx_indicator = True
self.pttButton.Tx.TurnOn(True)
QS.tci_set_params(tci_trx=1)
else:
if self.tx_indicator:
self.tx_indicator = False
self.pttButton.Tx.TurnOn(False)
if True: # Manage the PTT button using serial port, VOX, hot keys, WAV file play, PTT button, MIDI, CAT
QS.tci_set_params(tci_trx=0)
if True: # Manage the PTT button using serial port, VOX, hot keys, WAV file play, PTT button, MIDI, CAT, TCI
ptt_button_down = self.pttButton.GetValue()
ptt = None
if conf.quisk_serial_cts[0:4] == "PTT " or conf.quisk_serial_dsr[0:4] == "PTT ":
@@ -6857,6 +6782,14 @@ class App(wx.App):
self.hot_key_ptt_pressed = False
if self.tx_inhibit:
ptt = False
tci = QS.tci_get_params("tci_trx")
if tci is not None:
if tci:
self.tci_ptt_active = True
ptt = True
elif self.tci_ptt_active:
self.tci_ptt_active = False
ptt = False
if ptt is True and not ptt_button_down:
self.SetPTT(True)
elif ptt is False and ptt_button_down:
@@ -6896,10 +6829,7 @@ class App(wx.App):
d = 1E-10
self.smeter.SetLabel(" ADC %.0f%% %.0fdB" % (d * 100.0, 20 * math.log10(d)))
elif self.smeter_usage == "smeter": # update the S-meter
if self.mode in ('FDV-U', 'FDV-L'):
self.NewDVmeter()
else:
self.NewSmeter()
self.NewSmeter()
elif self.smeter_usage == "freq":
self.MeasureFrequency() # display measured frequency
else:
@@ -6924,10 +6854,57 @@ 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 conf.hermes_lite2_enable:
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)
# tci_trx handled elsewhere
if self.is_HermesLite2:
self.tx_inhibit = QS.get_params('quisk_tx_inhibit')
else:
self.tx_inhibit = 0
+179 -40
View File
@@ -48,7 +48,7 @@ quisk_widgets = None
################ Receivers SoftRock USB, Devices controlled by USB that capture samples from a sound card, and (for Tx) play samples to a sound card
################ Receivers SoftRock USB, Sound card radios controlled by USB.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'softrock/hardware_usb.py'
@@ -133,7 +133,7 @@ digital_tx_level = 100
################ Receivers SoftRock Fixed, Fixed frequency devices that capture samples from a sound card, and (for Tx) play samples to a sound card
################ Receivers SoftRock Fixed, Fixed frequency sound card radios.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'quisk_hardware_fixed.py'
@@ -172,9 +172,25 @@ 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 hardware and the improved HiQSDR using UDP
################ Receivers HiQSDR, The original N2ADR and HiQSDR radios.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'hiqsdr/quisk_hardware.py'
@@ -258,7 +274,7 @@ sndp_active = True
################ Receivers Hermes, The Hermes-Lite Project and possibly other hardware with the Hermes FPGA code.
################ Receivers Hermes, The Hermes-Lite project by Steve Haynal.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'hermes/quisk_hardware.py'
@@ -275,7 +291,7 @@ sndp_active = True
# You can set these options:
## use_rx_udp Hardware type, integer choice
# This is the type of UDP hardware. Use 10 for the Hermes protocol.
# This is the type of UDP hardware. Use 10 for the Hermes Lite2 protocol.
#use_rx_udp = 10
## udp_rx_ip Hermes known IP, text
@@ -373,6 +389,7 @@ hermes_disable_sync = False
# be sure to write it down. To use this address you must set "Eeprom IP Usage".
# And you may want to set "Hermes known IP" too. Make sure the address does not conflict
# with your DHCP server.
# Power cycle the HL2 after changing this item.
Hware_Hl2_EepromIP = '192.168.1.6'
#Hware_Hl2_EepromIP = '192.168.1.241'
@@ -380,6 +397,7 @@ Hware_Hl2_EepromIP = '192.168.1.6'
# This is the way the EEPROM IP address is used at power on.
# "Ignore" means it is not used at all. "Set address" means DHCP is not used and the EEPROM IP address is used.
# "Use DHCP first" means the EEPROM IP address is only used if DHCP fails.
# Power cycle the HL2 after changing this item.
Hware_Hl2_EepromIPUse = 'Ignore'
#Hware_Hl2_EepromIPUse = 'Use DHCP first'
#Hware_Hl2_EepromIPUse = 'Set address'
@@ -390,6 +408,7 @@ Hware_Hl2_EepromIPUse = 'Ignore'
# at power on instead of the factory supplied MAC address 0:1c:c0:a2:13:dd. It is only used at power on,
# and it is only used if it is marked valid by setting "Eeprom MAC usage" to "Set address".
# If you have two HL2's, you must have a unique MAC address for each.
# Power cycle the HL2 after changing this item.
Hware_Hl2_EepromMAC = '0xA1 0x6B'
#Hware_Hl2_EepromMAC = '0x4C 0x33'
@@ -398,6 +417,7 @@ Hware_Hl2_EepromMAC = '0xA1 0x6B'
# the HL2 will use the last two MAC bytes from "Eeprom MAC address"; otherwise the default MAC is used.
# If you change this item, the bit is written to the HL2.
# The HL2 only uses these bits at power on.
# Power cycle the HL2 after changing this item.
Hware_Hl2_EepromMACUse = 'Ignore'
#Hware_Hl2_EepromMACUse = 'Set address'
@@ -421,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).
@@ -474,13 +494,6 @@ hermes_iob_rxin = 'J10 available'
#hermes_iob_rxin = 'HL2 Rx to J9'
#hermes_iob_rxin = 'Use J9 and J10'
## hermes_lite2_enable Hermes Lite2 protocol, boolean
# This turns on some of the protocol extensions for the Hermes Lite2 that are not part
# of the official Hermes Protocol 1. Currently this only enables Tx Inhibit (CN8).
# Changes are immediate (no need to restart).
hermes_lite2_enable = True
#hermes_lite2_enable = False
# These are known power meter calibration tables. This table is not present in the JSON settings file.
power_meter_std_calibrations = {}
@@ -491,6 +504,7 @@ power_meter_std_calibrations['HL2FilterE3'] = [[ 0, 0.0 ], [ 25.865384615384617,
[ 4093.1785714285716, 7.28994845808807 ], [ 4502.496428571429, 8.820837634286566 ], [ 4952.746071428572, 10.673213537486745 ] ]
#power_meter_std_calibrations['HL2FilterE1'] = [[0, 0.0], [9.07, 0.002], [54.98, 0.014], [148.6, 0.057],
# [328.0, 0.208], [611.1, 0.646], [807.0, 1.098], [982.1, 1.6], [1223.3, 2.471], [1517.7, 3.738], [1758.7, 5.02]]
power_meter_std_calibrations['Show ADC'] = []
## Hermes_BandDict Rx IO Bus, dict
# The Hermes_BandDict sets the 7 bits on the J16 connector for Rx.
@@ -508,17 +522,17 @@ Hermes_BandDictEnTx = False
#Hermes_BandDictEnTx = True
################ Receivers Red Pitaya, The Red Pitaya Project by Pavel Demin. This uses the Hermes FPGA code.
################ Receivers Red Pitaya, The Red Pitaya project by Pavel Demin.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'hermes/quisk_hardware.py'
## widgets_file_name Widget file path, rfile
# This optional file adds additional controls for the radio.
#widgets_file_name = ''
#widgets_file_name = 'hermes/hpsdr_widgets.py'
## use_rx_udp Hardware type, integer choice
# This is the type of UDP hardware. Use 10 for the Hermes protocol.
# This is the type of UDP hardware. Use 10 for the Red Pitaya using the OpenHPSDR protocol.
#use_rx_udp = 10
## rx_udp_ip IP change, text
@@ -586,7 +600,123 @@ Hermes_BandDictEnTx = False
#Hermes_BandDictEnTx = True
################ Receivers SoapySDR, The SoapySDR interface to multiple hardware SDRs.
################ Receivers OpenHPSDR, Radios using the HPSDR Protocol 1.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'hermes/quisk_hardware.py'
## widgets_file_name Widget file path, rfile
# This optional file adds additional controls for the radio.
#widgets_file_name = 'hermes/hpsdr_widgets.py'
## use_rx_udp Hardware type, integer choice
# This is the type of UDP hardware. Use 10 for hardware using the OpenHPSDR protocol 1.
#use_rx_udp = 10
## rx_udp_ip IP change, text
# This item should be left blank. It is used to change the IP address of the hardware to a different
# IP once the hardware is found. Not all Hermes firmware supports changing the IP address.
#rx_udp_ip = ""
## rx_udp_port Hardware UDP port, integer
# This is the UDP port number of your hardware.
#rx_udp_port = 1024
## rx_udp_ip_netmask Network netmask, text
# This is the netmask for the network.
#rx_udp_ip_netmask = '255.255.255.0'
## tx_ip Transmit IP, text
# Leave this blank to use the same IP address as the receive hardware. Otherwise, enter "disable"
# to disable sending transmit I/Q samples, or enter the actual IP address. You must enter "disable"
# if you have multiple hardwares on the network, and only one should transmit. This item is normally blank.
tx_ip = ""
#tx_ip = "disable"
## tx_audio_port Tx audio UDP port, integer
# This is the UDP port for transmit audio I/Q samples. Enter zero to calculate this from the
# base hardware port. Otherwise enter the special custom port.
tx_audio_port = 0
## rx_udp_clock Clock frequency Hertz, integer
# This is the clock frequency of the hardware in Hertz.
#rx_udp_clock = 122880000
#rx_udp_clock = 76800000
#rx_udp_clock = 125000000
## tx_level Tx Level, dict
# This sets the transmit level 0 to 255 for each band.
# The config screen has a slider 0 to 100% so you can reduce the transmit power.
# The hardware only supports a limited adjustment range, so zero is still a small amount of power.
tx_level = {}
## digital_tx_level Digital Tx power %, integer
# Digital modes reduce power by the percentage on the config screen.
# This is the maximum value of the slider.
#digital_tx_level = 100
## hermes_code_version Hermes code version, integer
# There can be multiple Hermes devices on a network, but Quisk can only use one of these. If you have multiple
# hermes devices, you can use this to specify a unique device. Or use -1 to accept any board.
hermes_code_version = -1
## hermes_board_id Hermes board ID, integer
# There can be multiple Hermes devices on a network, but Quisk can only use one of these. If you have multiple
# hermes devices, you can use this to specify a unique device. Or use -1 to accept any board.
hermes_board_id = -1
## Hermes_BandDict Hermes Bus, dict
# The Hermes_BandDict sets the 7 bits on the J16 connector.
Hermes_BandDict = {
'160':0b0000001, '80':0b0000010, '60':0b0000100, '40':0b0001000, '30':0b0010000, '20':0b0100000, '15':0b1000000}
## Hermes_BandDictTx Tx IO Bus, dict
# The Hermes_BandDictTx sets the 7 bits on the J16 connector for Tx if enabled.
Hermes_BandDictTx = {'160':0, '80':0, '60':0, '40':0, '30':0, '20':0, '17':0, '15':0, '12':0, '10':0}
## Hermes_BandDictEnTx Enable Tx Filt, boolean
# Enable the separate Rx and Tx settings for the J16 connector.
Hermes_BandDictEnTx = False
#Hermes_BandDictEnTx = True
## calibrate_temp_20 PA temp ADC at 20\u2103, integer
# This is the ADC code returned for a temperature of 20C.
# To see the ADC code being returned, enter zero for both temperature codes.
# Linear interpolation is used to calculate the temperature.
# Changes are immediate (no need to restart).
calibrate_temp_20 = 0
## calibrate_temp_40 PA temp ADC at 40\u2103, integer
# This is the ADC code returned for a temperature of 40C.
# To see the ADC code being returned, enter zero for both temperature codes.
# Linear interpolation is used to calculate the temperature.
# Changes are immediate (no need to restart).
calibrate_temp_40 = 0
## calibrate_current_0 PA current ADC at zero, integer
# This is the ADC code returned for a PA current of zero.
# To see the ADC code being returned, enter zero for both current codes.
# Linear interpolation is used to calculate the current.
# Changes are immediate (no need to restart).
calibrate_current_0 = 0
## calibrate_current_1 PA current ADC at 1 amp, integer
# This is the ADC code returned for a PA current of one amp.
# To see the ADC code being returned, enter zero for both current codes.
# Linear interpolation is used to calculate the current.
# Changes are immediate (no need to restart).
calibrate_current_1 = 0
## power_meter_calib_name Power meter calibration, text choice
# This is the calibration table used to convert the power sensor voltage measured by the ADC to the transmit power display.
# It is a table of ADC codes and the corresponding measured power level. If you have a power meter, you can create your own
# table by selecting "New". Then enter ten or more power measurements from low to full power.
# For the Hermes Lite2 version E3 filter board, use the built-in table "HL2FilterE3".
# Changes are immediate (no need to restart).
power_meter_calib_name = 'HL2FilterE3'
################ Receivers SoapySDR, The SoapySDR interface to multiple SDRs.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'soapypkg/quisk_hardware.py'
@@ -602,7 +732,7 @@ Hermes_BandDictEnTx = False
# Further items are present in the radio dictionary with names soapy_*
################ Receivers SdrIQ, The SDR-IQ receiver by RfSpace
################ Receivers SdrIQ, The SDR-IQ receiver by RfSpace.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
hardware_file_name = 'quisk_hardware_sdriq.py'
@@ -633,7 +763,7 @@ sdriq_decimation = 1250
################ Receivers Odyssey, The Odyssey project using a UDP protocol similar to the HiQSDR
################ Receivers Odyssey, The Odyssey project.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'hiqsdr/quisk_hardware.py'
@@ -744,7 +874,7 @@ sndp_active = True
#radio_sound_mic_boost = True
################ Receivers Odyssey2, The Odyssey-2 project using the HPSDR Hermes protocol
################ Receivers Odyssey2, The Odyssey-2 project.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'hermes/quisk_hardware.py'
@@ -759,7 +889,7 @@ sndp_active = True
# You can set these options:
## use_rx_udp Hardware type, integer choice
# This is the type of UDP hardware. Use 10 for the Hermes protocol.
# This is the type of UDP hardware. Use 10 for the Odyssey-2.
#use_rx_udp = 10
## rx_udp_ip IP change, text
@@ -828,7 +958,7 @@ Hermes_BandDictEnTx = False
#Hermes_BandDictEnTx = True
################ Receivers Afedri, The Afedri SDR receiver with the Ethernet interface.
################ Receivers Afedri, The Afedri SDR receiver with Ethernet.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'afedrinet/quisk_hardware.py'
@@ -860,7 +990,7 @@ Hermes_BandDictEnTx = False
#default_rf_gain = 11
################ Receivers Control Head, Use Quisk to control a remote radio. No real hardware is used here.
################ Receivers Control Head, Use Quisk to control a remote Quisk radio.
## hardware_file_name Hardware file path, rfile
# Quisk can be used as a control head to control a real radio located remotely.
# This file contains the control logic for remote radios of type Hermes, SoftRock or HiQSDR.
@@ -1309,6 +1439,19 @@ fixed_tune_offset = 0
################ Remote
## tci_ip IP address for TCI, text
# Quisk can connect to WSJT-X and other digital programs using the TCI protocol.
# Enter the TCI server host name.
tci_ip = "127.0.0.1"
## tci_port IP port for TCI, integer
# Quisk can connect to WSJT-X and other digital programs using the TCI protocol.
# Enter the TCI port number. If the port number is zero, TCI is turned off.
# The preferred port is 40001.
tci_port = 0
#tci_port = 40001
#tci_port = 50001
## hamlib_ip IP address for Hamlib Rig 2, text
# You can control Quisk from Hamlib. Set the Hamlib rig to 2 and the device for rig 2 to
# localhost:4575. Or choose a different name and port here. Set the same name and port
@@ -1381,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 = ""
@@ -1797,23 +1953,6 @@ add_extern_demod = ""
add_fdx_button = 0
#add_fdx_button = 1
## add_freedv_button Add FreeDv button, integer choice
# These buttons add up to two additional mode buttons after CW, USB, etc.
# Set this to add the FDV mode button for digital voice:
add_freedv_button = 1
#add_freedv_button = 0
## freedv_tx_msg FreeDv Tx message, text
# For freedv, this is the text message to send.
freedv_tx_msg = ''
#freedv_tx_msg = 'N2XXX Jim, New Jersey, USA \n'
# This is the list of FreeDV modes and their index number. The starting mode is the first listed.
freedv_modes = (('Mode 1600', 0),
('Mode 2400A', 3), ('Mode 2400B', 4), ('Mode 800XA', 5),
('Mode 700C', 6), ('Mode 700D', 7), ('Mode 700E', 13), ('Mode 2020', 8))
# These are the filter bandwidths for each mode. Quisk has built-in optimized filters
# for these values, but you can change them if you want.
FilterBwCW = (200, 400, 600, 1000, 1500, 3000)
+23
View File
@@ -1,6 +1,29 @@
from __future__ import print_function
from __future__ import division
def wheel_step(rotation, delta, step):
"""Convert signed wheel rotation into configured frequency steps.
A measured downward event reported rotation=-180 with delta=120. Scaling
that by a 50 Hz step gives -75 Hz; flooring the resulting absolute frequency
made downward tuning jump -100 Hz while upward tuning moved only +50 Hz.
Round the wheel units symmetrically first: +/-180 now gives +/-50 Hz, while
+/-240 still gives +/-100 Hz rather than being collapsed to one step.
"""
if delta == 0:
return 0
rotation = int(rotation)
delta = int(delta)
step = int(step)
direction = -1 if rotation * delta < 0 else 1
whole_steps, remainder = divmod(abs(rotation), abs(delta))
if remainder * 2 > abs(delta):
whole_steps += 1
return direction * whole_steps * step
class SplineInterpolator: # From Numerical Recipes in C
"""Interpolate a table of [x, y] values."""
def __init__(self, table, Xmin=None, Xmax=None):
+4 -3
View File
@@ -26,6 +26,7 @@ import threading, webbrowser
import _quisk as QS
from quisk_widgets import *
import configure
import quisk_utils
DEBUG = 0
@@ -630,7 +631,7 @@ class GraphScreen(wx.Window):
mouse_x, mouse_y = self.GetMousePosition(event)
self.SetTxFreq(index=mouse_x - self.originX)
def OnWheel(self, event):
tune = self.display.tune_tx + event.GetWheelRotation() // event.GetWheelDelta()
tune = self.display.tune_tx + quisk_utils.wheel_step(event.GetWheelRotation(), event.GetWheelDelta(), 1)
self.SetTxFreq(index=tune)
class HelpScreen(wx.html.HtmlWindow):
@@ -640,8 +641,8 @@ class HelpScreen(wx.html.HtmlWindow):
if "gtk2" in wx.PlatformInfo:
self.SetStandardFonts()
self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22])
# read in text from file help.html in the directory of this module
self.LoadFile('help_vna.html')
if not self.LoadFile('help_vna.html'):
self.LoadFile('/usr/share/doc/quisk/help_vna.html')
def OnLinkClicked(self, link):
webbrowser.open(link.GetHref(), new=2)
+2
View File
@@ -1,7 +1,9 @@
// This module provides C access to the WDSP SDR library.
#include <Python.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <complex.h>
#define CLIP32 2147483647
+1
View File
@@ -0,0 +1 @@
#
+569
View File
@@ -0,0 +1,569 @@
# SDR-TRX Quisk Configuration File
# This file is to integrate the function of control of the SDR-TRX wint Quisk and with WSJT-X.
# This is for control of the SDR-TRX with using the UDP port port 12346. WSJT-X interfaces with this
# locally using UDP on port 2237. Capital letters begin commands to the SDR-TRX from Quisk,
# and lowercase letters are for commands coming from WSJT-X (through this Quisk interface).
# This is for usinng the SDR-TRX without a 3.5 mm audio card to the soundcard of the computer,
# and the WSJT-X software using UDP, and the data coming over UDP part 12345 too.
# It requires the libldl/ directory from weakmon to be in the directory you run this from.
# It also requires the WSJTXClass.py file from wsjtx_transceiver_interface to be in the lib directory
# and the version of that class. I have supplied a pruned down versions of ft8.py and ft4.45, because we don't need much
# other than FT8Send and FT4Send from those files. You need to edit transceiver_config.yml to have your callsign and grid, etc.
import socket
import time
import yaml
import struct
import datetime
import sys
import os
from ft8 import FT8Send
from ft4 import FT4Send
from quisk_hardware_model import Hardware as BaseHardware
import struct
import collections
import lib.WSJTXClass as WSJTXClass # To use encoders
sys.path.append(os.path.expandvars('$WEAKMON'))
DEBUG_ON = True
#
# Define the name of the hardware and the items on the hardware screen (see quisk_conf_defaults.py):
#
################ Receivers SdrTrx, The SdrTrx radio by Rob Frohne, KL7NA
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = 'sdrtrxpkg/quisk_hardware.py'
## widgets_file_name Widget file path, rfile
# This optional file adds additional controls for the radio.
#widgets_file_name = ''
## si570_xtal_freq Si570 crystal frequency, integer
# This is the Si570 startup frequency in Hz. 114.285MHz is the typical
# value from the data sheet; you can use 'usbsoftrock calibrate' to find
# the value for your device.
si570_xtal_freq = 114285000
## rx_max_amplitude_correct Max ampl correct, number
# If you get your I/Q samples from a sound card, you will need to correct the
# amplitude and phase for inaccuracies in the analog hardware. The correction is
# entered using the controls from the "Rx Phase" button on the config screen.
# You must enter a positive number. This controls the range of the control.
rx_max_amplitude_correct = 0.2
## rx_max_phase_correct Max phase correct, number
# If you get your I/Q samples from a sound card, you will need to correct the
# amplitude and phase for inaccuracies in the analog hardware. The correction is
# entered using the controls from the "Rx Phase" button on the config screen.
# You must enter a positive number. This controls the range of the control in degrees.
rx_max_phase_correct = 10.0
# SDR-TRX Hardware Control Class
#
class Hardware(BaseHardware):
# These are "static variable substitutes" since python doesn't have them.
# They are shared by all instances of the class and don't get redefinede each time a method is called.
# Move these into the Hardware class.
# Uncomment these lines if you wish to use Pulseaudio
name_of_sound_play = "pulse"
# Radio's Frequency limits.
radio_lower = 3500000
radio_upper = 30000000
PACKET_SIZE = 1444 # 4 bytes for packet number + 360 * 4 bytes for int32_t pairs
# Using 16 bit samples, so 2 bytes per sample.
# Set the number of Hz the signal is tuned to above the center frequency to avoid 1/f noise.
vfo_Center_Offset = 10000
tx_ready_wsjtx = False
tx_ready_wsjtx_sent = False
tx_now = False
# This is the open code for the WSJT server.
ft8_encoder = FT8Send()
ft4_encoder = FT4Send()
class Packet:
def __init__(self, data):
self.number = struct.unpack('<I', data[:4])[0]
self.pairs = [struct.unpack('<hh', data[i:i+4]) for i in range(4, len(data), 4)]
class PacketQueue:
def __init__(self):
self.queue = collections.deque()
def add_packet(self, packet):
self.queue.append(packet)
self.queue = collections.deque(sorted(self.queue, key=lambda p: p.number))
def check_missing_packets(self):
for i in range(1, len(self.queue)):
if self.queue[i].number != self.queue[i-1].number + 1:
avg_pairs = [((self.queue[i-1].pairs[j][0] + self.queue[i].pairs[j][0]) // 2,
(self.queue[i-1].pairs[j][1] + self.queue[i].pairs[j][1]) // 2)
for j in range(len(self.queue[i-1].pairs))]
self.queue.insert(i, Packet(struct.pack('<I', self.queue[i-1].number + 1) +
b''.join(struct.pack('<ii', pair[0], pair[1]) for pair in avg_pairs)))
def get_packets(self):
packets = list(self.queue)
self.queue.clear()
return packets
def open(self):
# Connection for WSJT-X and data
self.PICO_UDP_IP = None # Will be set to the IP of the machine sending data
self.COMMAND_UDP_PORT = 12346 # This is the port the Pico listens on for UDP commands coming from quisk.
self.DATA_UDP_PORT = 12345 # This is the port the quisk listens on for UDP IQ data coming from the Pico W.
self.BROADCAST_PORT = 12347 # This is the port the Pico gets our IP address from.-
self.broadcast_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.broadcast_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
self.broadcast_message = "Quisk"
self.broadcast_sock.setblocking(False)
self.isConnected = False
self.command_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.command_sock.setblocking(False)
self.data_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.data_sock.bind(('', self.DATA_UDP_PORT))
print("Data socket bound to IP:", self.data_sock.getsockname()[0])
self.data_sock.setblocking(False)
self.establish_connection()
self.InitSamples(2, 0) # Two bytes per sample, little endian.
self.queue = self.PacketQueue()
time.sleep(2)
# Poll for version. Should probably confirm the response on this.
version = str(self.get_parameter("VER")) # The way the firmware is now, this sets it up to use the UART or UDP.
# UART is used if get_parameter uses the UART, and UDP is used if it uses the UDP.
print(version)
# Return an informative message for the config screen
t = str(version) + ". Capture from sound card %s." % self.conf.name_of_sound_capt
configs_file = open('transceiver_config.yml', 'r')
configs = yaml.load(configs_file, Loader=yaml.BaseLoader)
# Global variables
self.callsign = configs['callsign']
self.grid = configs['grid']
self.current_msg = ''
self.rx_callsign = ''
self.mode = 'FT8'
self.tx_freq = 1200
# Connection for WSJT-X
WSJTX_UDP_IP = "127.0.0.1" # Assuming you are running WSJT-X on the same computer as Quisk...
WSJTX_UDP_PORT = 2237
self.wsjtx_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.wsjtx_sock.bind((WSJTX_UDP_IP, WSJTX_UDP_PORT))
self.wsjtx_sock.setblocking(False)
self.StartSamples()
return t
def close(self):
# Called once to close the Hardware
self.get_parameter("RX") # Make sure we are in RX mode.
time.sleep(1) # Wait for RX to be set.
self.StopSamples()
self.command_sock.close()
self.wsjtx_sock.close()
self.broadcast_sock.close()
def ChangeFrequency(self, tune, vfo, source='', band='', event=None):
# Called whenever quisk requests a frequency change.
# This sends the FREQ command dto set the centre frequency of the radio,
# and will also move the 'tune' frequency (the section within the RX passband
# which is to be demodulated) if it falls outside the passband (+/- sample_rate/2).
print("Setting VFO to %d, with tune to %d." % (vfo, tune))
print("Setting VFO to %d." % vfo)
if vfo < self.radio_lower:
vfo = self.radio_lower
print("Outside range! Setting to %d" % self.radio_lower)
if vfo > self.radio_upper:
vfo = self.radio_upper
print("Outside range! Setting to %d" % self.radio_upper)
# print("sample_rate =", sample_rate) # I believe sample_rate comes from quisk.
# If the tune frequency is outside the RX bandwidth, set it to somewhere within that bandwidth.
if tune > (vfo + sample_rate / 2) or tune < (vfo - sample_rate / 2):
tune = vfo + self.vfo_Center_Offset
print("Bringing tune frequency back into the RX bandwidth.")
# success = self.set_parameter("FREQ",str(vfo))
self.set_parameter("FREQ", str(vfo))
self.set_parameter("TX_FREQ", str(tune))
return tune, vfo
def OnButtonPTT(self, event):
btn = event.GetEventObject()
if btn.GetValue():
self.get_parameter("TX")
print("TX")
else:
self.get_parameter("RX")
print("RX")
return BaseHardware.OnButtonPTT(self, event)
def establish_connection(self):
while not self.isConnected:
print("isConnected is: ", self.isConnected)
# Check if a connection is established
# Wait for data to arrive and set PICO_UDP_IP to the sender's IP
while self.PICO_UDP_IP is None: # This needs to be fixed. It continues to send broadcast packets.
try:
print("Sending broadcast message...")
self.broadcast_sock.sendto(self.broadcast_message.encode(), ('<broadcast>', self.BROADCAST_PORT))
time.sleep(1) # Wait for 1 second before sending the next broadcast message
except Exception as e:
print("Broadcast message did not work: {0}".format(e))
break
try:
data, addr = self.data_sock.recvfrom(self.PACKET_SIZE) # Adjust the buffer size as needed
self.PICO_UDP_IP = addr[0]
print("PICO_UDP_IP set to:", self.PICO_UDP_IP)
# if data == self.broadcast_message:
self.isConnected = True
except socket.error:
if self.PICO_UDP_IP is not None:
print("Connection established with Pico W at IP:", self.PICO_UDP_IP, "but exception occurred.")
break # Exit the loop if self.PICO_UDP_IP is not None
pass # No data available yet
def GetRxSamples(self):
# Initialize packet counter and time marker
# This routine is made for FT8 and FT4, where we need to send a packet every 15 seconds.
# The Pico W sending the data has a sample rate of 48046 samples per second > 48 ks/s.
# We made the packets so they carry 180 samples per packet. That gives 4000 packets
# per 15 seconds. We discard any packets after that four times a minute at 59, 14, 29,
# and 44 seconds. This should make four ticks per minute, and we should be able to ignore
# # them if we are doing FT8 or FT4. Something else might be more optimum for other modes.
packet_counter = 0
next_time_marker = 0
send_packets = False
while True:
# Check if we've reached the next time marker
current_time = time.time()
current_seconds = current_time % 60
# Define the time markers
if 59 <= current_seconds < 60:
next_time_marker = current_time + 1 - current_seconds
send_packets = True
elif 14 <= current_seconds < 15:
next_time_marker = current_time + 1 - (current_seconds - 14)
send_packets = True
elif 29 <= current_seconds < 30:
next_time_marker = current_time + 1 - (current_seconds - 29)
send_packets = True
elif 44 <= current_seconds < 45:
next_time_marker = current_time + 1 - (current_seconds - 44)
send_packets = True
# If we've reached the next time marker, reset the packet counter
if current_time >= next_time_marker and send_packets:
packet_counter = 0
send_packets = False
try:
data, addr = self.data_sock.recvfrom(self.PACKET_SIZE)
self.no_data_repeat = 0
except socket.error:
# Send a broadcast message to the Pico W to get the IP address in case it has reset.
self.broadcast_sock.sendto(self.broadcast_message.encode(), ('<broadcast>', self.BROADCAST_PORT))
break # No more packets available
packet = self.Packet(data)
self.queue.add_packet(packet)
# Check for missing packets and fill them in
self.queue.check_missing_packets()
# Get the packets and pass them to the processing thread
packets = self.queue.get_packets()
for packet in packets:
data = b''.join(struct.pack('<hh', pair[0], pair[1]) for pair in packet.pairs)
# if len(data) % 4 != 0: # Each I/Q pair is now 4 bytes (2 * 2 bytes)
# self.GotReadError(self, DEBUG_ON, "Error: Mismatched number of I and Q samples")
# continue
# If we've already sent 4000 packets since the last time marker, discard the rest
if packet_counter >= 4000:
continue
self.AddRxSamples(data)
packet_counter += 1
#
# UDP comms functions, to communicate with the Raspberry Pi Pico W board used in the SDR-TRX.
#
def get_parameter(self, string):
string = string + "\n"
self.command_sock.sendto(string.encode(), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
return self.get_argument()
def set_parameter(self, string, arg):
string = string + "," + arg + "\r" + "\n"
self.command_sock.sendto(string.encode(), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
print('arg is: ', arg)
temp_arg = self.get_argument()
print('temp_arg is: ', temp_arg)
return True
def get_argument(self):
self.command_sock.setblocking(True)
try:
data1 = self.command_sock.recv(1024)
print('Received on line 178: ', data1)
except Exception as e:
print("Exception occurred: {0}".format(e))
return -1
self.command_sock.setblocking(False)
if not data1.startswith(b'OK'):
print('Received on line 185: ', data1)
# Maybe we didn't catch an OK line?
else:
while True:
try:
data1 = self.command_sock.recv(1024)
print('Received on line 190: ', data1)
break
except BlockingIOError:
pass
# Check to see if we have a comma in the string. If not, there is no argument.
if data1.find(b',') == -1:
return -1
data1 = data1.split(b',')[1].rstrip(b'\r\n')
print("data1 =")
print(data1)
# Check for the OK string. Should we wait for the OK string?
try:
data2 = self.command_sock.recv(1024)
if data2.startswith(b'OK'):
return data2
except BlockingIOError:
return -1
# We might want to integrate the UDP reads below into the stuff above.
# The code below is to interface WSJT-X with SDR-TRX.
def encode_ft8(self, msg):
try:
a77 = self.ft8_encoder.pack(msg, 1)
symbols = self.ft8_encoder.make_symbols(a77)
except Exception as e:
print("FT8 encoder error, check message!")
print(f"Exception type: {type(e).__name__}")
print(f"Exception message: {str(e)}")
symbols = None
time.sleep(3)
return symbols
def encode_ft4(self, msg):
try:
a77 = self.ft4_encoder.pack(msg, 1)
symbols = self.ft4_encoder.make_symbols(a77)
except:
print("FT4 encoder error, check message!")
symbols = None
time.sleep(3)
return symbols
def load_symbols(self, symbols):
print("Load symbols into transmitter..")
self.command_sock.sendto(b'm', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
count = 0
for symbol in symbols:
self.command_sock.sendto(struct.pack('>B', symbol), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
count += 1
# Wait to avoid Arduino serial buffer overflow. This may not be needed with the Pico.
if count % 50 == 0:
time.sleep(0.05)
self.command_sock.sendto(b'\0', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
resp = self.command_sock.recv(1)
if resp == b'm':
print("Load OK")
else:
print(resp)
def change_freq(self, new_freq):
#global self.tx_freq
self.command_sock.setblocking(True)
print("Change TX frequency to:", new_freq)
self.command_sock.sendto(b'o', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
self.command_sock.sendto(struct.pack('<H', new_freq), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
time.sleep(0.05)
resp = self.command_sock.recv(1)
print("resp after changing offset is: ", resp)
self.command_sock.setblocking(False)
if resp == b'o':
print("New freq OK")
self.tx_freq = new_freq
def set_mode(self, new_mode): # New more robust protocol.
#global mode
if new_mode == 'FT8':
self.command_sock.sendto(b'e', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
time.sleep(.05)
resp = self.command_sock.read(1)
print("resp = ", resp)
if resp == b'e':
self.mode = new_mode
print("Switched to: {0}".format(new_mode))
self.current_msg = ''
return True
elif new_mode == 'FT4':
self.command_sock.sendto(b'f', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
time.sleep(0.05)
resp = self.command_sock.recv(1)
print("resp = ", resp)
if resp == b'f':
self.mode = new_mode
self.current_msg = ''
print("Switched to: {0}".format(new_mode))
return True
else:
return False
def new_msg(self, msg):
#global self.current_msg
#global self.mode
if msg != self.current_msg:
print("Message: {0}".format(msg))
if 'FT8' in self.mode:
symbols = self.encode_ft8(msg)
else:
symbols = self.encode_ft4(msg)
if symbols.any():
# symbols = [kk for kk in range(79)]
self.load_symbols(symbols)
self.current_msg = msg
else:
return
else:
time.sleep(0.005) # Do we need this?
def transmit(self):
if False: # not current_msg:
print("No previous message!")
time.sleep(1)
else:
print("TX!")
self.command_sock.sendto(b't', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
# self.tx_now = False
def check_time_window(self, utc_time):
time_window = 15 if 'FT8' in self.mode else 7
print("Time window: ", time_window) # Is correct for FT8 and FT4.
rm = utc_time.second % time_window
if rm > 1 and rm < time_window - 1: # Within one second of exact time for FT8.
return False
else:
return True
# See if HeartBeat will work for this:
def HeartBeat(self): # Should be called at 10 Hz from the main.
# Check transmitter is initialized
#raise Exception("In HeartBeat")
# print("\n\nWait for transmitter ready...")
if self.tx_ready_wsjtx == False:
if self.tx_ready_wsjtx_sent == False:
self.command_sock.sendto(b'r', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
time.sleep(0.05)
self.tx_ready_wsjtx_sent = True
while True:
try:
x = self.command_sock.recv(1)
if x == b'r':
print("Transmitter ready!")
self.tx_ready_wsjtx = True
break
except BlockingIOError:
pass
if self.tx_ready_wsjtx == True:
try:
fileContent = self.wsjtx_sock.recv(1024)
except Exception as e:
if e == BlockingIOError: # This is our way of pollig the socket.
# Maybe use select to stop the errors printing after we quit.
return BaseHardware.HeartBeat(self)
else:
# print("Error in HeartBeat")
# print(f"Exception type: {type(e).__name__}")
# print(f"Exception message: {str(e)}")
pass
try:
NewPacket = WSJTXClass.WSJTX_Packet(fileContent, 0)
NewPacket.Decode()
if NewPacket.PacketType == 1:
# print('New Packet is type 1')
StatusPacket = WSJTXClass.WSJTX_Status(fileContent, NewPacket.index)
StatusPacket.Decode()
# Check TX frequency and update transceiver
new_freq = StatusPacket.TxDF
new_mode = StatusPacket.TxMode.strip()
if new_freq != self.tx_freq:
# time.sleep(0.03)
print('Changing frequency from {0} to {1}'.format(self.tx_freq, new_freq))
try:
self.change_freq(new_freq)
except Exception as e:
print(f"Exception occurred: {e}")
print ('It is now ', self.tx_freq)
# print('new_mode', new_mode, ' self.mode is: ', self.mode)
if new_mode != self.mode:
print("Mode before: {0}".format(self.mode))
self.set_mode(new_mode)
print("New mode after: {0}".format(self.mode))
# Check if TX is enabled
if StatusPacket.Transmitting == 1:
# Check time, avoid transmitting out of the time slot
current_time = datetime.datetime.now()
utc_time = current_time.astimezone(datetime.timezone.utc)
self.tx_now = self.check_time_window(utc_time)
if self.tx_now:
self.command_sock.sendto(b'p', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
message = StatusPacket.TxMessage
message = message.replace('<', '')
message = message.replace('>', '')
print('Message is: ', message)
self.new_msg(message.strip())
print("Encoded message is: ", self.current_msg)
# print("tx_now is: ", self.tx_now)
if self.tx_now:
print('Transmitting')
self.transmit()
print("Time: {0}:{1}:{2}".format(utc_time.hour, utc_time.minute, utc_time.second))
else:
pass
# print('Not Transmitting')
return BaseHardware.HeartBeat(self)
except Exception as e:
# print("Error in HeartBeat")
# print(f"Exception type: {type(e).__name__}")
# print(f"Exception message: {str(e)}")
# traceback.print_exc()
return BaseHardware.HeartBeat(self)
+5 -10
View File
@@ -4,19 +4,14 @@ 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.44'
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',
'sound_alsa.c', 'sound_pulseaudio.c', 'sound_portaudio.c', 'sound_directx.c', 'sound_wasapi.c',
'filter.c', 'extdemod.c', 'freedv.c', 'quisk_wdsp.c', 'ac2yd/remote.c']
'filter.c', 'extdemod.c', 'freedv.c', 'quisk_wdsp.c', 'ac2yd/remote.c',
'tci.c', 'base64.c', 'handshake.c', 'sha1.c', 'utf8.c', 'ws.c']
# Afedri hardware support added by Alex, Alex@gmail.com
mAfedri = Extension ('quisk.afedrinet.afedrinet_io',
@@ -87,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'],
+389
View File
@@ -0,0 +1,389 @@
/*
* sha1.c
*
* Description:
* This file implements the Secure Hashing Algorithm 1 as
* defined in FIPS PUB 180-1 published April 17, 1995.
*
* The SHA-1, produces a 160-bit message digest for a given
* data stream. It should take about 2**n steps to find a
* message with the same digest as a given message and
* 2**(n/2) to find any two messages with the same digest,
* when n is the digest size in bits. Therefore, this
* algorithm can serve as a means of providing a
* "fingerprint" for a message.
*
* Portability Issues:
* SHA-1 is defined in terms of 32-bit "words". This code
* uses <stdint.h> (included via "sha1.h" to define 32 and 8
* bit unsigned integer types. If your C compiler does not
* support 32 bit unsigned integers, this code is not
* appropriate.
*
* Caveats:
* SHA-1 is designed to work with messages less than 2^64 bits
* long. Although SHA-1 allows a message digest to be generated
* for messages of any number of bits less than 2^64, this
* implementation only works with messages with a length that is
* a multiple of the size of an 8-bit character.
*
*/
#include "sha1.h"
/*
* Define the SHA1 circular left shift macro
*/
#define SHA1CircularShift(bits,word) \
(((word) << (bits)) | ((word) >> (32-(bits))))
/* Local Function Prototyptes */
void SHA1PadMessage(SHA1Context *);
void SHA1ProcessMessageBlock(SHA1Context *);
/*
* SHA1Reset
*
* Description:
* This function will initialize the SHA1Context in preparation
* for computing a new SHA1 message digest.
*
* Parameters:
* context: [in/out]
* The context to reset.
*
* Returns:
* sha Error Code.
*
*/
int SHA1Reset(SHA1Context *context)
{
if (!context)
{
return shaNull;
}
context->Length_Low = 0;
context->Length_High = 0;
context->Message_Block_Index = 0;
context->Intermediate_Hash[0] = 0x67452301;
context->Intermediate_Hash[1] = 0xEFCDAB89;
context->Intermediate_Hash[2] = 0x98BADCFE;
context->Intermediate_Hash[3] = 0x10325476;
context->Intermediate_Hash[4] = 0xC3D2E1F0;
context->Computed = 0;
context->Corrupted = 0;
return shaSuccess;
}
/*
* SHA1Result
*
* Description:
* This function will return the 160-bit message digest into the
* Message_Digest array provided by the caller.
* NOTE: The first octet of hash is stored in the 0th element,
* the last octet of hash in the 19th element.
*
* Parameters:
* context: [in/out]
* The context to use to calculate the SHA-1 hash.
* Message_Digest: [out]
* Where the digest is returned.
*
* Returns:
* sha Error Code.
*
*/
int SHA1Result( SHA1Context *context,
uint8_t Message_Digest[SHA1HashSize])
{
int i;
if (!context || !Message_Digest)
{
return shaNull;
}
if (context->Corrupted)
{
return context->Corrupted;
}
if (!context->Computed)
{
SHA1PadMessage(context);
for(i=0; i<64; ++i)
{
/* message may be sensitive, clear it out */
context->Message_Block[i] = 0;
}
context->Length_Low = 0; /* and clear length */
context->Length_High = 0;
context->Computed = 1;
}
for(i = 0; i < SHA1HashSize; ++i)
{
Message_Digest[i] = context->Intermediate_Hash[i>>2]
>> 8 * ( 3 - ( i & 0x03 ) );
}
return shaSuccess;
}
/*
* SHA1Input
*
* Description:
* This function accepts an array of octets as the next portion
* of the message.
*
* Parameters:
* context: [in/out]
* The SHA context to update
* message_array: [in]
* An array of characters representing the next portion of
* the message.
* length: [in]
* The length of the message in message_array
*
* Returns:
* sha Error Code.
*
*/
int SHA1Input( SHA1Context *context,
const uint8_t *message_array,
unsigned length)
{
if (!length)
{
return shaSuccess;
}
if (!context || !message_array)
{
return shaNull;
}
if (context->Computed)
{
context->Corrupted = shaStateError;
return shaStateError;
}
if (context->Corrupted)
{
return context->Corrupted;
}
while(length-- && !context->Corrupted)
{
context->Message_Block[context->Message_Block_Index++] =
(*message_array & 0xFF);
context->Length_Low += 8;
if (context->Length_Low == 0)
{
context->Length_High++;
if (context->Length_High == 0)
{
/* Message is too long */
context->Corrupted = 1;
}
}
if (context->Message_Block_Index == 64)
{
SHA1ProcessMessageBlock(context);
}
message_array++;
}
return shaSuccess;
}
/*
* SHA1ProcessMessageBlock
*
* Description:
* This function will process the next 512 bits of the message
* stored in the Message_Block array.
*
* Parameters:
* None.
*
* Returns:
* Nothing.
*
* Comments:
* Many of the variable names in this code, especially the
* single character names, were used because those were the
* names used in the publication.
*
*
*/
void SHA1ProcessMessageBlock(SHA1Context *context)
{
const uint32_t K[] = { /* Constants defined in SHA-1 */
0x5A827999,
0x6ED9EBA1,
0x8F1BBCDC,
0xCA62C1D6
};
int t; /* Loop counter */
uint32_t temp; /* Temporary word value */
uint32_t W[80]; /* Word sequence */
uint32_t A, B, C, D, E; /* Word buffers */
/*
* Initialize the first 16 words in the array W
*/
for(t = 0; t < 16; t++)
{
W[t] = (uint32_t)context->Message_Block[t * 4] << 24;
W[t] |= (uint32_t)context->Message_Block[t * 4 + 1] << 16;
W[t] |= (uint32_t)context->Message_Block[t * 4 + 2] << 8;
W[t] |= (uint32_t)context->Message_Block[t * 4 + 3];
}
for(t = 16; t < 80; t++)
{
W[t] = SHA1CircularShift(1,W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16]);
}
A = context->Intermediate_Hash[0];
B = context->Intermediate_Hash[1];
C = context->Intermediate_Hash[2];
D = context->Intermediate_Hash[3];
E = context->Intermediate_Hash[4];
for(t = 0; t < 20; t++)
{
temp = SHA1CircularShift(5,A) +
((B & C) | ((~B) & D)) + E + W[t] + K[0];
E = D;
D = C;
C = SHA1CircularShift(30,B);
B = A;
A = temp;
}
for(t = 20; t < 40; t++)
{
temp = SHA1CircularShift(5,A) + (B ^ C ^ D) + E + W[t] + K[1];
E = D;
D = C;
C = SHA1CircularShift(30,B);
B = A;
A = temp;
}
for(t = 40; t < 60; t++)
{
temp = SHA1CircularShift(5,A) +
((B & C) | (B & D) | (C & D)) + E + W[t] + K[2];
E = D;
D = C;
C = SHA1CircularShift(30,B);
B = A;
A = temp;
}
for(t = 60; t < 80; t++)
{
temp = SHA1CircularShift(5,A) + (B ^ C ^ D) + E + W[t] + K[3];
E = D;
D = C;
C = SHA1CircularShift(30,B);
B = A;
A = temp;
}
context->Intermediate_Hash[0] += A;
context->Intermediate_Hash[1] += B;
context->Intermediate_Hash[2] += C;
context->Intermediate_Hash[3] += D;
context->Intermediate_Hash[4] += E;
context->Message_Block_Index = 0;
}
/*
* SHA1PadMessage
*
* Description:
* According to the standard, the message must be padded to an even
* 512 bits. The first padding bit must be a '1'. The last 64
* bits represent the length of the original message. All bits in
* between should be 0. This function will pad the message
* according to those rules by filling the Message_Block array
* accordingly. It will also call the ProcessMessageBlock function
* provided appropriately. When it returns, it can be assumed that
* the message digest has been computed.
*
* Parameters:
* context: [in/out]
* The context to pad
* ProcessMessageBlock: [in]
* The appropriate SHA*ProcessMessageBlock function
* Returns:
* Nothing.
*
*/
void SHA1PadMessage(SHA1Context *context)
{
/*
* Check to see if the current message block is too small to hold
* the initial padding bits and length. If so, we will pad the
* block, process it, and then continue padding into a second
* block.
*/
if (context->Message_Block_Index > 55)
{
context->Message_Block[context->Message_Block_Index++] = 0x80;
while(context->Message_Block_Index < 64)
{
context->Message_Block[context->Message_Block_Index++] = 0;
}
SHA1ProcessMessageBlock(context);
while(context->Message_Block_Index < 56)
{
context->Message_Block[context->Message_Block_Index++] = 0;
}
}
else
{
context->Message_Block[context->Message_Block_Index++] = 0x80;
while(context->Message_Block_Index < 56)
{
context->Message_Block[context->Message_Block_Index++] = 0;
}
}
/*
* Store the message length as the last 8 octets
*/
context->Message_Block[56] = context->Length_High >> 24;
context->Message_Block[57] = context->Length_High >> 16;
context->Message_Block[58] = context->Length_High >> 8;
context->Message_Block[59] = context->Length_High;
context->Message_Block[60] = context->Length_Low >> 24;
context->Message_Block[61] = context->Length_Low >> 16;
context->Message_Block[62] = context->Length_Low >> 8;
context->Message_Block[63] = context->Length_Low;
SHA1ProcessMessageBlock(context);
}
+73
View File
@@ -0,0 +1,73 @@
/*
* sha1.h
*
* Description:
* This is the header file for code which implements the Secure
* Hashing Algorithm 1 as defined in FIPS PUB 180-1 published
* April 17, 1995.
*
* Many of the variable names in this code, especially the
* single character names, were used because those were the names
* used in the publication.
*
* Please read the file sha1.c for more information.
*
*/
#ifndef _SHA1_H_
#define _SHA1_H_
#include <stdint.h>
/*
* If you do not have the ISO standard stdint.h header file, then you
* must typdef the following:
* name meaning
* uint32_t unsigned 32 bit integer
* uint8_t unsigned 8 bit integer (i.e., unsigned char)
* int_least16_t integer of >= 16 bits
*
*/
#ifndef _SHA_enum_
#define _SHA_enum_
enum
{
shaSuccess = 0,
shaNull, /* Null pointer parameter */
shaInputTooLong, /* input data too long */
shaStateError /* called Input after Result */
};
#endif
#define SHA1HashSize 20
/*
* This structure will hold context information for the SHA-1
* hashing operation
*/
typedef struct SHA1Context
{
uint32_t Intermediate_Hash[SHA1HashSize/4]; /* Message Digest */
uint32_t Length_Low; /* Message length in bits */
uint32_t Length_High; /* Message length in bits */
/* Index into message block array */
int_least16_t Message_Block_Index;
uint8_t Message_Block[64]; /* 512-bit message blocks */
int Computed; /* Is the digest computed? */
int Corrupted; /* Is the message digest corrupted? */
} SHA1Context;
/*
* Function Prototypes
*/
int SHA1Reset( SHA1Context *);
int SHA1Input( SHA1Context *,
const uint8_t *,
unsigned int);
int SHA1Result( SHA1Context *,
uint8_t Message_Digest[SHA1HashSize]);
#endif
+21 -4
View File
@@ -1,7 +1,24 @@
MODULE_NAME := soapy
OBJDIR := ../build/local
soapy2:
python2 setup.py build_ext --force --inplace
CFLAGS := $(shell python3 ../makefil.py --cflags)
LDFLAGS := $(shell python3 ../makefil.py --ldflags)
EXTENSION_SUFFIX := $(shell python3 ../makefil.py --suffix)
TARGET := $(MODULE_NAME)$(EXTENSION_SUFFIX)
soapy3:
# Default target: Link the shared library object
$(TARGET): soapy.o $(OBJDIR)/import_quisk_api.o
$(CC) -o $@ soapy.o $(LDFLAGS) -lSoapySDR $(OBJDIR)/import_quisk_api.o
# Compile the C source files into object files
soapy.o: soapy.c ../quisk.h ../filter.h
$(CC) -o $@ -c $< $(CFLAGS) -I..
.PHONY: clean
clean:
-rm -f *.o
-rm -f $(MODULE_NAME)*.so
.PHONY: soapy-old
soapy-old:
python3 setup.py build_ext --force --inplace
+8 -7
View File
@@ -1,5 +1,6 @@
#include <Python.h>
#include <complex.h>
#include <string.h>
#include <SoapySDR/Device.h>
#include <SoapySDR/Formats.h>
#include <SoapySDR/Version.h>
@@ -223,7 +224,7 @@ static PyObject * open_device(PyObject * self, PyObject * args)
else {
snprintf(buf128, 128, "SoapySDRDevice_make fail: %s", SoapySDRDevice_lastError());
}
return PyString_FromString(buf128);
return PyUnicode_FromString(buf128);
}
static void get_direc_len(const char * name, int * direction, int * length)
@@ -254,7 +255,7 @@ static PyObject * get_device_list(PyObject * self, PyObject * args) // Called fr
for (i = 0; i < length; i++) {
dict = PyDict_New();
for (j = 0; j < results[i].size; j++)
PyDict_SetItemString(dict, results[i].keys[j], PyString_FromString(results[i].vals[j]));
PyDict_SetItemString(dict, results[i].keys[j], PyUnicode_FromString(results[i].vals[j]));
PyList_Append(devices, dict);
Py_DECREF(dict);
}
@@ -329,7 +330,7 @@ static PyObject * set_parameter(PyObject * self, PyObject * args) // Called from
}
}
if (msg200[0])
return PyString_FromString(msg200);
return PyUnicode_FromString(msg200);
Py_INCREF (Py_None);
return Py_None;
}
@@ -377,7 +378,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from
pylist = PyList_New(0);
names = SoapySDRDevice_listAntennas(sdev, direction, 0, &len_list);
for (i = 0; i < len_list; i++) {
pyobj = PyString_FromString(names[i]);
pyobj = PyUnicode_FromString(names[i]);
PyList_Append(pylist, pyobj);
Py_DECREF(pyobj);
}
@@ -440,7 +441,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from
pylist = PyList_New(0);
names = SoapySDRDevice_listGains(sdev, direction, 0, &len_list);
for (i = 0; i < len_list; i++) {
pyobj = PyString_FromString(names[i]);
pyobj = PyUnicode_FromString(names[i]);
PyList_Append(pylist, pyobj);
Py_DECREF(pyobj);
}
@@ -450,7 +451,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from
else if ( ! strncmp(name, "soapy_listGainsValues", length)) {
pylist = PyList_New(0);
pylst2 = PyList_New(0); // First element is the total gain
pyobj = PyString_FromString("total");
pyobj = PyUnicode_FromString("total");
PyList_Append(pylst2, pyobj);
Py_DECREF(pyobj);
range = SoapySDRDevice_getGainRange(sdev, direction, 0);
@@ -460,7 +461,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from
names = SoapySDRDevice_listGains(sdev, direction, 0, &len_list);
for (i = 0; i < len_list; i++) {
pylst2 = PyList_New(0);
pyobj = PyString_FromString(names[i]);
pyobj = PyUnicode_FromString(names[i]);
PyList_Append(pylst2, pyobj);
Py_DECREF(pyobj);
range = SoapySDRDevice_getGainElementRange(sdev, direction, 0, names[i]);
+19 -7
View File
@@ -4,6 +4,7 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <complex.h>
#include <string.h>
#include <math.h>
#include <sys/time.h>
#include <time.h>
@@ -113,7 +114,7 @@ void quisk_sample_level(const char * msg, complex double * cSamples, int nSample
level = d;
}
if (QuiskTimeSec() - time0 > 0.1) {
printf ("sample_level %s: %10.6lf count %8d\n", msg, level / scale, count);
printf ("peak sample level %s: %14.6lf count %8d\n", msg, level / scale, count);
level = 0;
count = 0;
time0 = QuiskTimeSec();
@@ -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
@@ -1000,22 +1003,28 @@ int quisk_read_sound(void) // Called from sound thread
if (remote_control_slave)
send_remote_radio_sound_socket(cSamples, nSamples);
is_DGT = rxMode == DGT_U || rxMode == DGT_L || rxMode == DGT_IQ || rxMode == DGT_FM;
is_DGT = (rxMode == DGT_U || rxMode == DGT_L || rxMode == DGT_IQ || rxMode == DGT_FM || rxMode == FDV_U || rxMode == FDV_L);
if (quisk_record_state == TMP_PLAY_SPKR_MIC)
quisk_tmp_playback(cSamples, nSamples, 1.0); // replace radio sound
else if (quisk_record_state == FILE_PLAY_SPKR_MIC)
quisk_file_playback(cSamples, nSamples, file_play_level); // replace radio sound
// Play the demodulated audio
#if DEBUG_MIC != 2
if ( ! quisk_play_sidetone(&quisk_Playback)) // play sidetone
play_sound_interface(&quisk_Playback, nSamples, cSamples, 1, quisk_audioVolume); // else play radio sound
if ( ! quisk_play_sidetone(&quisk_Playback)) { // play sidetone
if (rxMode == FDV_U || rxMode == FDV_L)
play_sound_interface(&quisk_Playback, nSamples, cSamples, 1, 0); // play silence
else
play_sound_interface(&quisk_Playback, nSamples, cSamples, 1, quisk_audioVolume); // play radio sound
}
#endif
if (radio_sound_socket != INVALID_SOCKET)
send_radio_sound_socket(cSamples, nSamples, quisk_audioVolume);
// Play digital if required
if (is_DGT)
play_sound_interface(&DigitalOutput, nSamples, cSamples, 1, digital_output_level);
//if (is_DGT)
play_sound_interface(&DigitalOutput, nSamples, cSamples, 1, digital_output_level);
// Send radio sound to TCI
tci_send_audio(cSamples, nSamples);
// Perhaps record the speaker audio to a file
if ( ! key_state && file_rec_audio.fp)
@@ -1061,7 +1070,10 @@ int quisk_read_sound(void) // Called from sound thread
for (i = 0; i < mic_count; i++)
cSamples[i] = 0;
}
//quisk_sample_level("read mic or DGT", cSamples, mic_count, CLIP16);
// Receive microphone sound from TCI
//quisk_sample_level("read mic or DGT", cSamples, mic_count, CLIP32);
mic_count = tci_get_mic(cSamples, mic_count);
//quisk_sample_level("quisk_tci", cSamples, mic_count, CLIP32);
// Perhaps record the microphone audio to the speaker audio file
if (key_state && file_rec_audio.fp)
quisk_record_audio(&file_rec_audio, cSamples, mic_count);
+5 -4
View File
@@ -2,6 +2,7 @@
* This modue provides sound access for QUISK using the ALSA
* library for Linux.
*/
#include <string.h>
#ifdef QUISK_HAVE_ALSA
#include <Python.h>
#include <complex.h>
@@ -454,7 +455,7 @@ static int device_list(PyObject * py, snd_pcm_stream_t stream, char * name)
}
snprintf(buf100, 100, "%s %s (hw:%d,%d)", card_text, pcm_text, card, dev);
if (py) { // add to list of devices
PyList_Append(py, PyString_FromString(buf100));
PyList_Append(py, PyUnicode_FromString(buf100));
}
if (name) { // return the "hw:" name
if (strstr(buf100, name)) {
@@ -1129,10 +1130,10 @@ static void midi_in_devices(PyObject * pylist, int just_names)
}
else {
pytup = PyTuple_New(2);
PyList_Append(pylist, pytup);
PyTuple_SET_ITEM(pytup, 0, PyUnicode_DecodeUTF8(friendly_name, strlen(friendly_name), "replace"));
PyTuple_SetItem(pytup, 0, PyUnicode_DecodeUTF8(friendly_name, strlen(friendly_name), "replace"));
snprintf(card_name, 32, "hw:%d,%d,%d", card, device, sub);
PyTuple_SET_ITEM(pytup, 1, PyUnicode_DecodeUTF8(card_name, strlen(card_name), "replace"));
PyTuple_SetItem(pytup, 1, PyUnicode_DecodeUTF8(card_name, strlen(card_name), "replace"));
PyList_Append(pylist, pytup);
}
}
}
+11 -16
View File
@@ -1,3 +1,5 @@
#include <string.h>
#ifdef QUISK_HAVE_DIRECTX
#include <Python.h>
@@ -55,14 +57,7 @@ static int match_name(LPCTSTR lpszDesc, const char * name)
Py_DECREF(py_unicode);
return 0;
}
#if PY_MAJOR_VERSION >= 3
if (PyUnicode_READY(py_unicode) == 0)
length = PyUnicode_GET_LENGTH(py_unicode);
else
length = 0;
#else
length = PyUnicode_GET_SIZE(py_unicode);
#endif
length = PyUnicode_GetLength(py_unicode);
if (length <= 0) {
Py_DECREF(py_unicode);
Py_DECREF(py_substring);
@@ -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) {
+3 -1
View File
@@ -1,6 +1,8 @@
/*
* This module provides sound access for QUISK using the portaudio library.
*/
#include <string.h>
#ifdef QUISK_HAVE_PORTAUDIO
#include <Python.h>
@@ -475,7 +477,7 @@ static int device_list(PyObject * py, int input)
// found one
if (py) {
snprintf(buf100, 100, "%s", info->name);
PyList_Append(py, PyString_FromString(buf100));
PyList_Append(py, PyUnicode_FromString(buf100));
}
}
}
+55 -56
View File
@@ -34,6 +34,8 @@
* 2020 Apr by N2ADR Changes in printf's
*/
#include <string.h>
#ifdef QUISK_HAVE_PULSEAUDIO
#include <Python.h>
@@ -64,12 +66,20 @@ volatile int streams_to_start;
// remember all open devices for easy cleanup on exit
static pa_stream *OpenPulseDevices[PA_LIST_SIZE * 2] = {NULL};
static int have_QuiskDigitalInput; // Do we need to create this null sink?
static int have_QuiskDigitalOutput; // Do we need to create this null sink?
static int underrunPlayback;
static int tlength_bytes;
// list of additional sink devices to add
struct _NewSinks {
char * name; // name of the new sink
int create; // create the sink if it doesn't exist
};
#define NUM_NEW_SINKS 2
static struct _NewSinks sink_list[NUM_NEW_SINKS] = {
{"QuiskDigitalInput", 1},
{"QuiskDigitalOutput", 1}
};
/* This callback happens any time a stream changes state. Here, it's primary used to
* tell the quisk thread when streams are ready.
*/
@@ -1022,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);
}
@@ -1043,26 +1053,30 @@ static void source_sink(const char * name, const char * descr, pa_proplist * pro
// pa_mainloop will call this function when it's ready to tell us about a sink.
static void pa_sinklist_cb(pa_context *c, const pa_sink_info *l, int eol, void *userdata) {
if (eol > 0) // If eol is set to a positive number, you're at the end of the list
int i;
if (eol > 0) // If eol is set to a positive number, you're at the end of the list
return;
source_sink(l->name, l->description, l->proplist, (PyObject *)userdata);
if ( ! strcmp(l->name, "QuiskDigitalInput"))
have_QuiskDigitalInput = 1;
if ( ! strcmp(l->name, "QuiskDigitalOutput"))
have_QuiskDigitalOutput = 1;
source_sink(l->name, l->description, l->proplist, (PyObject *)userdata);
for (i = 0; i < NUM_NEW_SINKS; i++) {
if ( ! strcmp(l->name, sink_list[i].name)) {
sink_list[i].create = 0;
}
}
}
static void pa_sourcelist_cb(pa_context *c, const pa_source_info *l, int eol, void *userdata) {
if (eol > 0)
static void pa_sourcelist_cb(pa_context *c, const pa_source_info *l, int eol, void *userdata)
{
if (eol > 0)
return;
source_sink(l->name, l->description, l->proplist, (PyObject *)userdata);
source_sink(l->name, l->description, l->proplist, (PyObject *)userdata);
}
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;
@@ -1070,6 +1084,9 @@ PyObject * quisk_pulseaudio_sound_devices(PyObject * self, PyObject * args)
pa_operation *pa_op=NULL;
pa_context *pa_names_ctx;
int state = 0;
int sink_index = 0;
char * sink_name;
char buf256[256];
if (!PyArg_ParseTuple (args, ""))
return NULL;
@@ -1113,59 +1130,41 @@ PyObject * quisk_pulseaudio_sound_devices(PyObject * self, PyObject * args)
pa_mainloop_iterate(pa_names_ml, 1, NULL);
break;
case 2:
// Now we wait for our operation to complete. When it's
// complete our pa_output_devicelist is filled out, and we move
// along to the next state
if (pa_operation_get_state(pa_op) == PA_OPERATION_DONE) {
pa_operation_unref(pa_op);
// Now we perform another operation to get the source
// (input device) list just like before.
pa_op = pa_context_get_source_info_list(pa_names_ctx, pa_sourcelist_cb, pycapt);
// Update the state so we know what to do next
state++;
}
pa_mainloop_iterate(pa_names_ml, 1, NULL);
else
pa_mainloop_iterate(pa_names_ml, 1, NULL);
break;
// The following loads null sinks for digital input and output if they are not present.
case 3:
if (sink_index >= NUM_NEW_SINKS) {
state = 5;
continue;
}
if (sink_list[sink_index].create) {
sink_name = sink_list[sink_index].name;
snprintf(buf256, 256, "sink_name=%s sink_properties=device.description=%s", sink_name, sink_name);
pa_op = pa_context_load_module(pa_names_ctx, "module-null-sink", buf256, index_callback, NULL);
state++;
}
sink_index++;
break;
case 4:
if (pa_operation_get_state(pa_op) == PA_OPERATION_DONE) {
pa_operation_unref(pa_op);
state = 4;
state = 3;
}
else
pa_mainloop_iterate(pa_names_ml, 1, NULL);
break;
// The following loads modules for digital input and output if they are not present.
case 4:
if ( ! have_QuiskDigitalInput) {
pa_op = pa_context_load_module(pa_names_ctx, "module-null-sink",
"sink_name=QuiskDigitalInput sink_properties=device.description=QuiskDigitalInput",
index_callback, NULL);
state = 5;
pa_mainloop_iterate(pa_names_ml, 1, NULL);
}
else
state = 6;
break;
case 5:
if (pa_operation_get_state(pa_op) == PA_OPERATION_DONE) {
pa_operation_unref(pa_op);
state = 6;
}
else
pa_mainloop_iterate(pa_names_ml, 1, NULL);
pa_op = pa_context_get_source_info_list(pa_names_ctx, pa_sourcelist_cb, pycapt);
state++;
pa_mainloop_iterate(pa_names_ml, 1, NULL);
break;
case 6:
if ( ! have_QuiskDigitalOutput) {
pa_op = pa_context_load_module(pa_names_ctx, "module-null-sink",
"sink_name=QuiskDigitalOutput sink_properties=device.description=QuiskDigitalOutput",
index_callback, NULL);
state = 7;
pa_mainloop_iterate(pa_names_ml, 1, NULL);
}
else
state = 9;
break;
case 7:
if (pa_operation_get_state(pa_op) == PA_OPERATION_DONE) {
pa_operation_unref(pa_op);
state = 9;
+9 -6
View File
@@ -7,6 +7,9 @@
* Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!!
*/
#include <string.h>
#ifdef QUISK_HAVE_WASAPI
#define UNICODE
@@ -1125,10 +1128,10 @@ PyObject * quisk_wasapi_sound_devices(PyObject * self, PyObject * args) // Call
EXIT_ON_ERROR(hr)
// data items are (name, id, is_raw)
pytup = PyTuple_New(3);
PyTuple_SetItem(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1));
PyTuple_SetItem(pytup, 1, PyUnicode_FromWideChar(pwszID, -1));
PyTuple_SetItem(pytup, 2, PyInt_FromLong(1));
PyList_Append(pyplay, pytup);
PyTuple_SET_ITEM(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1));
PyTuple_SET_ITEM(pytup, 1, PyUnicode_FromWideChar(pwszID, -1));
PyTuple_SET_ITEM(pytup, 2, PyInt_FromLong(1));
CoTaskMemFree(pwszID);
pwszID = NULL;
PropVariantClear(&varName);
@@ -1155,10 +1158,10 @@ PyObject * quisk_wasapi_sound_devices(PyObject * self, PyObject * args) // Call
EXIT_ON_ERROR(hr)
// data items are (name, id, is_raw)
pytup = PyTuple_New(3);
PyTuple_SetItem(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1));
PyTuple_SetItem(pytup, 1, PyUnicode_FromWideChar(pwszID, -1));
PyTuple_SetItem(pytup, 2, PyInt_FromLong(1));
PyList_Append(pycapt, pytup);
PyTuple_SET_ITEM(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1));
PyTuple_SET_ITEM(pytup, 1, PyUnicode_FromWideChar(pwszID, -1));
PyTuple_SET_ITEM(pytup, 2, PyInt_FromLong(1));
CoTaskMemFree(pwszID);
pwszID = NULL;
PropVariantClear(&varName);
+970
View File
@@ -0,0 +1,970 @@
#include <Python.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include "ws.h"
#include <pthread.h>
#include <complex.h>
#include <time.h>
#include "quisk.h"
#include "filter.h"
#define TCI_STREAM_DATA_BYTES 16384
#define TCI_RX_BUF_SIZE 1024
#define TCI_COMMAND_SIZE 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 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.
// These are TCI parameters:
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;
static int client_split_enable=-1;
static char quisk_modulation[TCI_COMMAND_SIZE];
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;
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;
enum StreamType // ExpertSDR3
{
IQ_STREAM = 0, // Receiver IQ signal stream
RX_AUDIO_STREAM = 1, // Receiver audio stream
TX_AUDIO_STREAM = 2, // Audio stream for transmitter
TX_CHRONO = 3, // Time marker flow for audio signal transmission
LINEOUT_STREAM = 4 // Linear audio output audio stream
} ;
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
} ;
struct ClientData { // data for each client
char msg_buf[TCI_RX_BUF_SIZE];
int msg_length;
int send_Rx_audio_stream;
int audio_stream_samplerate;
int audio_stream_sample_type;
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 {
uint32_t receiver; // receiver number
uint32_t sample_rate; // sampling rate
uint32_t format; // sample type determined in SampleType
uint32_t codec; // compression algorithm (not implemented), always 0
uint32_t crc; // checksum (not implemented), always 0
int32_t length; // number of samples
uint32_t type; // stream type determined in StreamType
uint32_t channels; // number of channels for ExpertSDR3
uint32_t reserv[8]; // reserved
uint8_t data[TCI_STREAM_DATA_BYTES]; // samples
} ;
struct _TxChrono {
uint32_t receiver; // receiver number
uint32_t sample_rate; // sampling rate
uint32_t format; // sample type determined in SampleType
uint32_t codec; // compression algorithm (not implemented), always 0
uint32_t crc; // checksum (not implemented), always 0
int32_t length; // number of samples
uint32_t type; // stream type determined in StreamType
uint32_t channels; // number of channels for ExpertSDR3
uint32_t reserv[8]; // reserved
} ;
static struct _TxChrono TxChrono = {
.receiver = 0,
.sample_rate = 48000,
.format = TCI_FLOAT32,
.codec = 0,
.crc = 0,
.length = 0,
.type = TX_CHRONO,
.channels = 2,
.reserv = {0, 0, 0, 0, 0, 0, 0, 0}
} ;
struct _StreamTypes {
char * name;
time_t when;
} ;
#define NUM_STREAM_TYPES 5
struct _StreamTypes StreamTypes[NUM_STREAM_TYPES] = {
{"iq ", 0},
{"rx_audio ", 0},
{"tx_audio ", 0},
{"tx_chrono", 0},
{"lineout ", 0}
} ;
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 (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);
else
QuiskPrintf("TCI Sending %s length %5d:", StreamTypes[pt->type].name, (int)size);
QuiskPrintf("%d %5d %d %d %d %5d %d %d\n", (int)(pt->receiver), (int)(pt->sample_rate), (int)(pt->format), (int)(pt->codec),
(int)(pt->crc), (int)(pt->length), (int)(pt->type), (int)(pt->channels));
}
}
static int sendframe_txt(ws_cli_conn_t client, const char * msg)
{
if (verbose)
QuiskPrintf("TCI Send text %s\n", msg);
return ws_sendframe_txt(client, msg);
}
static int sendframe_txt_bcast(uint16_t port, const char * msg)
{
if (verbose)
QuiskPrintf("TCI Broadcast text %s\n", msg);
return ws_sendframe_txt_bcast(port, msg);
}
static size_t sendframe_bin(ws_cli_conn_t client, const char * msg, size_t size)
{
if (verbose)
PrintStream(msg, size, 0);
return ws_sendframe_bin(client, msg, size);
}
static int text_message(ws_cli_conn_t client, struct ClientData * ctx)
{ // This function alters the message ctx->msg_buf.
int i;
char char_buf[TCI_COMMAND_SIZE];
char * saveptr, * command;
char * arg1, * arg2, * arg3;
if (verbose)
QuiskPrintf("TCI Received text %s\n", ctx->msg_buf);
//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;
arg1 = strtok_r(NULL, ",;", &saveptr);
arg2 = strtok_r(NULL, ",;", &saveptr);
arg3 = strtok_r(NULL, ",;", &saveptr);
// The command will be broadcast to all clients unless the return value is zero.
switch (command[0]) {
case 'a':
if (strcmp(command, "audio_start") == 0) {
pthread_mutex_lock(&clients_list_mutex);
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);
for (i = 0; i < tci_clients_count; i++) {
if (tci_clients_list[i] == client) {
ctx->send_Rx_audio_stream = 0;
break;
}
}
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);
}
return 0;
}
else if (strcmp(command, "audio_samplerate") == 0) {
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)
ctx->audio_stream_channels = 1;
else if (strcmp(arg1, "2") == 0)
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) {
pthread_mutex_lock(&clients_list_mutex);
for (i = 0; i < tci_clients_count; i++) {
if (tci_clients_list[i] == client) {
ctx->send_IQ_stream = 1;
break;
}
}
pthread_mutex_unlock(&clients_list_mutex);
sendframe_txt(client, char_buf);
return 0;
}
else if (strcmp(command, "iq_stop") == 0) {
pthread_mutex_lock(&clients_list_mutex);
for (i = 0; i < tci_clients_count; i++) {
if (tci_clients_list[i] == client) {
ctx->send_IQ_stream = 0;
break;
}
}
pthread_mutex_unlock(&clients_list_mutex);
sendframe_txt(client, char_buf);
return 0;
}
else if (strcmp(command, "iq_samplerate") == 0) {
ctx->IQ_stream_samplerate = quisk_sound_state.sample_rate;
snprintf(char_buf, TCI_COMMAND_SIZE, "iq_samplerate:%d;", quisk_sound_state.sample_rate);
sendframe_txt(client, char_buf);
return 0;
}
else if (strcmp(command, "if") == 0) {
if (arg3) {
client_if = atoi(arg3);
// Echo comes from quisk_tci_set_params() UNIMPLEMENTED
}
else {
snprintf(char_buf, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if);
sendframe_txt(client, char_buf);
}
return 0;
}
break;
case 'm':
if (strcmp(command, "modulation") == 0) {
if (arg2) {
strncpy(client_modulation, arg2, TCI_COMMAND_SIZE - 1);
// Echo comes from quisk_tci_set_params()
}
else {
snprintf(char_buf, TCI_COMMAND_SIZE, "modulation:0,%.9s;", quisk_modulation);
sendframe_txt(client, char_buf);
}
return 0;
}
break;
case 's':
if (strcmp(command, "split_enable") == 0) {
if (arg2) {
if (strcmp(arg2, "true") == 0)
client_split_enable = 1;
else
client_split_enable = 0;
// Echo comes from quisk_tci_set_params()
}
else {
if (quisk_split_enable)
sendframe_txt(client, "split_enable:0,true;");
else
sendframe_txt(client, "split_enable:0,false;");
}
return 0;
}
break;
case 't':
if (strcmp(command, "trx") == 0) {
if (arg2) {
if (strcmp(arg2, "true") == 0) {
if (quisk_trx == 0) {
pthread_mutex_lock(&tx_buffer_mutex);
client_trx = 1;
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)
sendframe_txt(client, "trx:0,true;");
else
sendframe_txt(client, "trx:0,false;");
}
return 0;
}
else if (strcmp(command, "tx_stream_audio_buffering") == 0) {
return 0; // do not send
}
break;
case 'v':
if (strcmp(command, "vfo") == 0) {
if (arg3) {
client_vfo = atoll(arg3);
// Echo comes from quisk_tci_set_params()
}
else {
snprintf(char_buf, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo);
sendframe_txt(client, char_buf);
}
return 0;
}
break;
}
return 1; // send to all clients
}
static void onopen(ws_cli_conn_t client)
{
char *cli;
char command[TCI_COMMAND_SIZE];
struct ClientData * ctx;
// Make parameters for each 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;
ctx->audio_stream_bytes_per_sample = 4;
ws_set_connection_context(client, ctx);
pthread_mutex_lock(&clients_list_mutex);
tci_clients_list[tci_clients_count++] = client;
pthread_mutex_unlock(&clients_list_mutex);
if (verbose) {
cli = ws_getaddress(client);
QuiskPrintf("TCI *Connection opened, addr: %s\n", cli);
}
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, "ready;");
sendframe_txt(client, "start;");
sendframe_txt(client, "tx_enable:0,true;");
snprintf(command, TCI_COMMAND_SIZE, "dds:0,%lld;", quisk_dds);
sendframe_txt(client, command);
snprintf(command, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if);
sendframe_txt(client, command);
snprintf(command, TCI_COMMAND_SIZE, "modulation:0,%.9s;", quisk_modulation);
sendframe_txt(client, command);
snprintf(command, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo);
sendframe_txt(client, command);
if (quisk_trx)
sendframe_txt(client, "trx:0,true;");
else
sendframe_txt(client, "trx:0,false;");
if (quisk_split_enable)
sendframe_txt(client, "split_enable:0,true;");
else
sendframe_txt(client, "split_enable:0,false;");
}
static void onclose(ws_cli_conn_t client)
{
int i, count;
pthread_mutex_lock(&clients_list_mutex);
count = tci_clients_count;
tci_clients_count = 0;
for (i = 0; i < count; i++)
if (tci_clients_list[i] != client)
tci_clients_list[tci_clients_count++] = tci_clients_list[i];
pthread_mutex_unlock(&clients_list_mutex);
free(ws_get_connection_context(client));
if (verbose) {
char * cli = ws_getaddress(client);
QuiskPrintf("TCI *Connection closed, addr: %s\n", cli);
}
}
static void onmessage(ws_cli_conn_t client, const unsigned char *msg, uint64_t size, int type)
{
char echo_copy[TCI_RX_BUF_SIZE];
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
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
ctx->msg_length += size;
ctx->msg_buf[ctx->msg_length] = '\0';
char * semicolon;
char save;
while ((semicolon = strchr(ctx->msg_buf, ';')) != NULL) {
save = *(semicolon + 1);
*(semicolon + 1) = '\0';
strncpy(echo_copy, ctx->msg_buf, TCI_RX_BUF_SIZE);
if (text_message(client, ctx)) // should we send to all clients?
sendframe_txt_bcast(tci_port, echo_copy);
*(semicolon + 1) = save;
size_t processed_len = (semicolon - ctx->msg_buf) + 1;
size_t remaining_len = ctx->msg_length - processed_len;
memmove(ctx->msg_buf, ctx->msg_buf + processed_len, remaining_len);
ctx->msg_length = remaining_len;
ctx->msg_buf[ctx->msg_length] = '\0';
}
}
else if (type == WS_FR_OP_BIN) {
struct _Stream * pt = (struct _Stream *)msg;
int count = 0;
float re, im;
int channels = TxChrono.channels;
int two_channels = channels == 2;
int header = 16 * sizeof(uint32_t);
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) {
n = 0;
switch (TxChrono.format) {
case TCI_INT16:
break;
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;
}
cCopy[n++] = (re + I * im) * (CLIP32 / 2);
}
if (verbose && count != pt->length)
QuiskPrintf("TCI *TX count %ld %d %d\n", size, count, pt->length);
n = interpolate_tx_audio_stream(cCopy, n, ctx);
pthread_mutex_lock(&tx_buffer_mutex);
CircularBuffer(0, cCopy, 0, n);
pthread_mutex_unlock(&tx_buffer_mutex);
break;
}
}
}
if (verbose)
PrintStream((const char *)msg, size, 0);
}
}
static void tci_startup(void)
{
struct ws_server tci = {
.host = NULL,
.port = 0,
.thread_loop = 1,
.timeout_ms = 1000,
.evs.onopen = &onopen,
.evs.onclose = &onclose,
.evs.onmessage = &onmessage
};
char host32[32];
tci_clients_count = 0;
tci_port = QuiskGetConfigInt("tci_port", 40001);
strncpy(host32, QuiskGetConfigString("tci_ip", "127.0.0.1"), 31);
tci.host = host32;
tci.port = tci_port;
ws_socket(&tci);
if (verbose)
QuiskPrintf("TCI Start server on host %s port %d\n", tci.host, tci.port);
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;
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_Rx_audio_stream == 0)
continue;
memset(&stream, 0, 16 * sizeof(uint32_t));
stream.sample_rate = ctx->audio_stream_samplerate;
stream.format = ctx->audio_stream_sample_type;
stream.type = RX_AUDIO_STREAM;
stream.channels = ctx->audio_stream_channels;
int two_channels = 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;
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(cAudio[i]) * (1.0 / 2147483648.0 / 2));
vpt += bytes_per_sample;
stream.length++;
if (two_channels) {
*(float *)vpt = (float)(cimag(cAudio[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));
stream.length = 0;
vpt = &stream.data;
}
}
break;
}
}
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) {
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;
}
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", "close", "verbose", "tci_dds", "tci_if", "tci_vfo", "tci_trx", "tci_split_enable",
"tci_modulation", NULL} ;
long long new_vfo=-1;
long long new_dds=-1;
int start=-1, 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, "|iiiLiLiis", kwlist,
&start, &close, &verbose, &new_dds, &new_if, &new_vfo, &new_trx, &new_split_enable,
&mode))
return NULL;
if (start == 1) {
tci_started = 1;
tci_startup(); // start the TCP server for TCI
}
if (tci_started == 0) {
Py_INCREF (Py_None);
return Py_None;
}
if (close != -1) {
sendframe_txt_bcast(tci_port, "stop;");
}
if (new_dds != -1) {
quisk_dds = new_dds;
snprintf(char_buf, TCI_COMMAND_SIZE, "dds:0,%lld;", quisk_dds);
sendframe_txt_bcast(tci_port, char_buf);
}
if (new_if != BAD_TCI_IF) {
quisk_if = new_if;
snprintf(char_buf, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if);
sendframe_txt_bcast(tci_port, char_buf);
}
if (new_vfo != -1) {
quisk_vfo = new_vfo;
snprintf(char_buf, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo);
sendframe_txt_bcast(tci_port, char_buf);
}
if (new_split_enable != -1) {
quisk_split_enable = new_split_enable;
if (quisk_split_enable)
sendframe_txt_bcast(tci_port, "split_enable:0,true;");
else
sendframe_txt_bcast(tci_port, "split_enable:0,false;");
}
if (new_trx != -1) {
quisk_trx = new_trx;
if (quisk_trx) {
sendframe_txt_bcast(tci_port, "trx:0,true;");
}
else {
tci_tx_audio_client = 0;
sendframe_txt_bcast(tci_port, "trx:0,false;");
}
}
if (mode) {
if (strcmp(mode, "DGT-U") == 0)
strcpy(quisk_modulation, "digu");
else if (strcmp(mode, "DGT-L") == 0)
strcpy(quisk_modulation, "digl");
else if (strcmp(mode, "CWL") == 0 || strcmp(mode, "CWU") == 0)
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
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);
return Py_None;
}
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 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) {
ll = client_vfo;
client_vfo = -1;
return PyLong_FromLongLong(ll);
}
else if (strcmp(name, "tci_dds") == 0 && client_dds >= 0) {
ll = client_dds;
client_dds = -1;
return PyLong_FromLongLong(ll);
}
else if (strcmp(name, "tci_if") == 0 && client_if != BAD_TCI_IF) {
ii = client_if;
client_if = BAD_TCI_IF;
return PyLong_FromLong(ii);
}
else if (strcmp(name, "tci_split_enable") == 0 && client_split_enable >= 0) {
ii = client_split_enable;
client_split_enable = -1;
return PyLong_FromLong(ii);
}
else if (strcmp(name, "tci_trx") == 0 && client_trx >= 0) {
ii = client_trx;
client_trx = -1;
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)
strcpy(char_buf, "DGT-U");
else if (strcmp(client_modulation, "digl") == 0)
strcpy(char_buf, "DGT-L");
else if (strcmp(client_modulation, "cw") == 0)
strcpy(char_buf, "CWU");
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 PyUnicode_FromString(char_buf);
}
Py_INCREF (Py_None);
return Py_None;
}
+2
View File
@@ -0,0 +1,2 @@
This directory has files for the SDR-1000 clone synthesizer by
UT3MK and US5NCJ
+1
View File
@@ -0,0 +1 @@
#
+523
View File
@@ -0,0 +1,523 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import serial, time, logging
import re
import _quisk as QS
import quisk_conf_defaults as conf
import sys, wx, traceback
# ── ПОСЛЕДОВАТЕЛЬНЫЙ ПОРТ ДЛЯ CAT ───────────────────────────────────
sdr1000_serial_port = "/dev/ttyUSB0" # Windows: "COM3" / Linux: "/dev/ttyUSB0"
sdr1000_serial_rate = 57600 # PowerSDR CAT: до 115200
# ── ЛОГ ────────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-5s %(message)s",
datefmt="%H:%M:%S",
)
log = logging.getLogger("SDR1000")
Mode2Index = {'CWL':0, 'CWU':1, 'LSB':2, 'USB':3, 'AM':4,
'FM':5, 'EXT':6, 'DGT-U':7, 'DGT-L':8, 'DGT-IQ':9,
'IMD':10, 'FDV-U':11, 'FDV-L':12, 'DGT-FM':13}
# ── КЛАСС УПРАВЛЕНИЯ ЖЕЛЕЗОМ ────────────────────────────────────────
from quisk_hardware_model import Hardware as BaseHardware
class Hardware(BaseHardware):
"""
Quisk Hardware-класс для FlexRadio SDR-1000.
Использует CAT-команды протокола PowerSDR (ZZ + Kenwood).
"""
# Буфер для приема данных с com-порта
serialbuf = '';
# Буфер для отправки команд на com-порт
cmdbuf = '';
# Буфер отправки IF
statusbuf ='';
statussending = False;
# Сопоставление размера шагов
STEP_MAP = {
'1': '0000', '10': '0001', '50': '1000', '100': '0010',
'250': '1001', '500': '1010', '1k': '0011', '5k': '1011',
'9k': '1100', '10k': '0100', '100k':'0101', '1M': '0110',
'10M': '0111',
}
STEP_VAL = {
'1': 1,
'10': 10,
'50': 50,
'100': 100,
'250': 250,
'500': 500,
'1k': 1000,
'5k': 5000,
'9k': 9000,
'10k': 10000,
'100k': 100000,
'1M': 1000000,
'10M': 10000000,
}
# STEP_ENA = ['1', '10', '50', '100', '250', '500', '1k', '5k', '10k', '100k' ]
STEP_ENA = [ '10', '50', '100', '250', '500', '1k', '5k', '10k', '100k' ]
BAND_ENA = ['80', '40', '30', '20', '17', '15', '12', '10']
watchdog = 0
watchdog_limit = 100
# Сопоставление режимов Quisk → код MD (PowerSDR/Kenwood)
# TODO
MODE_MAP = {
'LSB': '1', 'USB': '2', 'CWU': '3', 'FM': '4',
'AM': '5', 'DGT-L': '6', 'CWL': '7',
# Quisk-специфичные → стандартные
'DGT-U':'2', 'FMN': '4', 'AMN': '5',
'DGT-FM': '4', 'FDV-U':'2', 'FDV-L':'1',
'FSK' : '9', 'FSKR' : '2',
}
def __init__(self, app, conf):
BaseHardware.__init__(self, app, conf)
self.ser = None
self.current_freq = 0
self.current_vfo = 0
self.current_mode = 'USB'
self.current_band = '80'
self.current_step = '50'
self.rf_gain_labels = ('RF -20', 'RF -10', 'RF 0', 'RF +10' )
self.current_pre = 0
self.current_ritfreq = 0
self.rit_active = False
self.ptt_active = False
# ── Открытие/закрытие ────────────────────────────────────────────
def open(self):
"""Вызывается один раз при старте Quisk."""
try:
self.ser = serial.Serial(
sdr1000_serial_port,
sdr1000_serial_rate,
timeout=0, xonxoff=True,
)
log.debug("SDR-1000: COM-порт открыт %s @ %d",
sdr1000_serial_port, sdr1000_serial_rate)
except serial.SerialException as e:
log.error("SDR-1000: не удалось открыть порт: %s", e)
self.ser = None
return "SDR-1000: ошибка порта %s" % sdr1000_serial_port
msg = "SDR-1000 подключен (%s, %s)" % (
sdr1000_serial_port, "PowerSDR")
return msg
def close(self):
"""Вызывается один раз при завершении Quisk."""
if self.ptt_active:
self.ser.cmdbuf += 'RX;' # снять передачу
if self.ser and self.ser.is_open:
self.cmdbuf = ''
self.ser.flush
self.ser.close()
log.debug("SDR-1000: порт закрыт")
# ── Низкоуровневый CAT-обмен ────────────────────────────────────
def _cat(self):
"""
Вызываться будет периодически для чтения порта
"""
if not self.ser or not self.ser.is_open:
return ""
try:
#self.ser.write(frame)
if self.ser.in_waiting > 0:
sdata = self.ser.read(size=self.ser.in_waiting)
self.serialbuf += sdata.decode('ascii')
else:
self.watchdog += 1
if self.watchdog > self.watchdog_limit:
log.debug("Watchdog! Resetting arduino")
self.ser.dtr = 0
time.sleep(0.1)
self.ser.dtr = 1
self.ser.flush
self.ser.close()
time.sleep(0.1)
try:
self.ser = serial.Serial(
sdr1000_serial_port,
sdr1000_serial_rate,
timeout=0, xonxoff=True,
)
log.debug("SDR-1000: COM-порт открыт %s @ %d",
sdr1000_serial_port, sdr1000_serial_rate)
except serial.SerialException as e:
log.error("SDR-1000: не удалось открыть порт: %s", e)
self.ser = None
return "SDR-1000: ошибка порта %s" % sdr1000_serial_port
self.ser.flushInput()
self.ser.flushOutput()
self.watchdog = 0
ans = ''
while ';' in self.serialbuf:
cmds = self.serialbuf.partition(';')
cmd = cmds[0]
self.serialbuf = cmds[-1]
# log.info("Splitted to %s and %s", cmd, self.serialbuf)
if len(cmd) > 0:
answer = ''
cmddone = 0
match cmd:
case 'IF':
# Запрос полного статуса
# Возврат в statusbuf
self.statusbuf = self.ReturnInfo()
log.debug("Status refresh, buf: %s", self.serialbuf)
cmddone = 1
case 'ZZSM0':
# Запрос показаний S-метра
ans = self.ReturnSmeterA()
log.debug("S-meter refresh, buf: %s", ans)
cmddone = 1
case 'PS1':
# Включение
ans = 'PS1;'
log.debug("PowerON, buf: %s", ans)
cmddone = 1
case 'ZZPA':
# Запрос показаний PRE ! TODO смена режима
ans = self.ReturnPre()
log.debug("PRE answer, buf: %s", ans)
cmddone = 1
case 'UP':
# Запрос на увеличение частоты
self.application.ChangeHwFrequency(9000,
self.current_vfo+self.STEP_VAL[self.current_step])
cmddone = 1
case 'DN':
# Запрос на уменьшение частоты
self.application.ChangeHwFrequency(9000,
self.current_vfo-self.STEP_VAL[self.current_step])
cmddone = 1
case 'ZZSU':
# Запрос на увеличение шага
self.StepInc()
cmddone = 1
case 'ZZSD':
# Запрос на уменьшение шага
self.StepDec()
cmddone = 1
case 'BU':
# Запрос смены диапазона
self.BandUp()
cmddone = 1
case 'BD':
# Запрос смены диапазона
self.BandDn()
cmddone = 1
case 'TX':
# Transmit
QS.set_PTT(1)
self.ptt_active = True
log.info("PTT ON")
cmddone = 1
case 'RX':
# Recieve
QS.set_PTT(0)
self.ptt_active = False
log.info("PTT OFF")
cmddone = 1
case 'ZZQR':
# Запрос восстановление QuickSave
cmddone = 1
if len(cmd) > 4 and not cmddone:
log.debug("Try to match cmd: %s, buf: %s", cmd[0:4], self.serialbuf)
match cmd[0:4]:
case 'ZZPA':
# Запрос изменения PRE
self.SetPre(cmd)
ans = self.ReturnPre()
log.debug("PRE set, buf: %s", ans)
cmddone = 1
case 'ZZMD':
# Запрос изменения режима
self.SetMode(cmd)
log.debug("Mode set, buf: %s", cmd)
cmddone = 1
case 'FA00':
# Запрос изменения частоты
self.SetFreq(cmd)
log.debug("Freq set, buf: %s", cmd)
cmddone = 1
if not cmddone:
log.info("unknown cmd: %s, buf: %s", cmd, self.serialbuf)
ans = '?;'
if len(ans) > 0:
log.debug("Sending answer, buf: %s", ans)
self.ser.write(ans.encode('ascii'))
if len(self.serialbuf) == 0:
if len(self.cmdbuf) > 0:
cmds = self.cmdbuf.partition(';')
cmd = cmds[0]
self.cmdbuf = cmds[-1]
log.debug("Sending cmd %s, buf: %s", cmd, self.cmdbuf)
self.ser.write(cmd.encode('ascii'))
if (len(self.statusbuf) > 0) and not self.statussending:
self.statussending = True
log.debug("Sending status, buf: %s", self.statusbuf)
self.ser.write(self.statusbuf.encode('ascii'))
self.statusbuf = ''
self.statussending = False
# End of CAT
except Exception as e:
log.error("SDR-1000 CAT error: %s", e)
return ""
@staticmethod
def _fmt_freq(hz):
"""Частота → 11 цифр: 14200000 → '00014200000'."""
return "%011d" % int(hz)
# ── Quisk API: частота ──────────────────────────────────────────
def ChangeFrequency(self, tune, vfo, source='', band='', event=None):
"""Quisk вызывает при смене частоты. Возвращает (tune, vfo)."""
if tune and tune != self.current_freq:
self.current_freq = tune
self.current_vfo = tune-9000
if re.search('FA\\d{11};', self.cmdbuf,0):
log.debug("--> RE MATCH")
re.sub('FA\\d{11};', "FA" + self._fmt_freq(tune)+";", self.cmdbuf, 1)
# self.cmdbuf += ("FA" + self._fmt_freq(tune)+";")
else:
self.cmdbuf += ("FA" + self._fmt_freq(tune)+";")
log.debug("VFO A → %d Hz (%.6f MHz)", self.current_vfo, self.current_freq / 1e6)
return self.current_freq, self.current_vfo
def ReturnInfo(self):
ritstr = self.application.ritFreq
if ritstr >= 0:
ritsign = "+"
else:
ritsign = ""
answer = "IF%011d%4s%s%05d%01d0000%01d%1s0000000;" % (
self.current_freq, self.STEP_MAP[self.current_step],
ritsign,ritstr,self.application.ritButton.GetValue(),
self.ptt_active,self.MODE_MAP[self.current_mode])
# log.info(answer)
return answer
def ReturnSmeterA(self):
i = round((self.application.smeter_db + 67) * 2)
if i < 0:
i = 0
elif i > 260:
i = 260
answer = "ZZSM0%03d;" % ( i )
# log.info(answer)
return answer
def ReturnPre(self):
answer = "ZZPA%01d;" % ( self.current_pre )
return answer
def SetPre(self, precmd):
if len(precmd) == 5:
match precmd:
case 'ZZPA0': i = 0
case 'ZZPA1': i = 1
case 'ZZPA2': i = 2
case 'ZZPA3': i = 3
case _: i = 0
self.current_pre = i
def SetFreq(self, freqcmd):
if len(freqcmd) == 13:
newfreq = int(freqcmd[2:])
log.info("Request to change freq: %d", newfreq)
self.current_vfo=newfreq-9000
self.application.ChangeHwFrequency(9000, self.current_vfo)
def OnButtonRfGain(self, event):
btn = event.GetEventObject()
n = btn.index
self.current_pre = n
self.cmdbuf += ("ZZPA%1d;" % self.current_pre)
def SetMode(self, modecmd):
if len(modecmd) == 6:
log.info("Request to change mode: %s", modecmd[4:])
match modecmd[4:]:
# 00 = LSB => LSB
# 01 = USB => USB
# 02 = DSB => USB
# 03 = CWL => CW
# 04 = CWU => CW
# 05 = FMN => FMN
# 06 = AM => AM
# 07 = DIGU => FSKR
# 08 = SPEC => AMN
# 09 = DIGL => FSK
# 10 = SAM => USB
# 11 = DRM => USB
case '00': newmode = 'LSB'
case '01': newmode = 'USB'
case '02': newmode = 'USB'
case '03': newmode = 'CWL'
case '04': newmode = 'CWU'
case '05': newmode = 'FM'
case '06': newmode = 'AM'
case '07': newmode = 'DGT-U'
case '08': newmode = 'AM'
case '09': newmode = 'DGT-L'
case '10': newmode = 'USB'
case '11': newmode = 'USB'
case _: newmode = 'USB'
self.current_mode = newmode
self.application.mode = self.current_mode
self.application.MakeFilterButtons(self.application.Mode2Filters(self.current_mode))
QS.set_rx_mode(Mode2Index.get(self.current_mode, 3))
if self.current_mode == 'CWL':
self.application.SetRit(conf.cwTone)
elif self.current_mode == 'CWU':
self.application.SetRit(-conf.cwTone)
else:
self.application.SetRit(0)
if self.current_mode in ('CWL', 'CWU'):
self.application.SetFilterByMode('CW')
elif self.current_mode in ('LSB', 'USB'):
self.application.SetFilterByMode('SSB')
elif self.current_mode == 'AM':
self.application.SetFilterByMode('AM')
elif self.current_mode[0:2] == 'FM':
self.application.SetFilterByMode('FM')
elif self.current_mode[0:4] == 'DGT-':
self.application.SetFilterByMode('DGT')
self.application.sliderSquelch.DeleteSliderWindow()
if self.current_mode == 'FM':
self.application.sliderSquelch.SetSlider(self.application.levelSquelch)
else:
self.application.sliderSquelch.SetSlider(self.application.levelSquelchSSB)
self.application.OnBtnSquelch()
if self.current_mode not in ('FDV-L', 'FDV-U'):
self.application.graph.SetDisplayMsg()
self.application.waterfall.SetDisplayMsg()
self.application.SetTxAudio()
def StepInc(self):
i = self.STEP_ENA.index(self.current_step)
log.debug("Found index of %s, idx: %d", self.current_step, i)
if i < (len(self.STEP_ENA)-1):
self.current_step = self.STEP_ENA[i + 1]
log.debug("Step changed to %s, idx: %d", self.current_step, i+1)
else:
log.debug("Step not changed, remain %s", self.current_step)
def StepDec(self):
i = self.STEP_ENA.index(self.current_step)
log.info("Found index of %s, idx: %d", self.current_step, i)
if i > 0:
self.current_step = self.STEP_ENA[i - 1]
log.debug("Step changed to %s, idx: %d", self.current_step, i-1)
else:
log.debug("Step not changed, remain %s", self.current_step)
def BandUp(self):
i = self.BAND_ENA.index(self.current_band)
log.debug("Found index of %s, idx: %d", self.current_band, i)
if i < (len(self.BAND_ENA)-1):
self.current_band = self.BAND_ENA[i + 1]
else:
self.current_band = self.BAND_ENA[0]
log.info("Band changed to %s, idx: %d", self.current_band, i)
vfo, tune, mode = self.application.bandState[self.current_band]
self.application.ChangeHwFrequency(9000, vfo+tune)
def BandDn(self):
i = self.BAND_ENA.index(self.current_band)
log.debug("Found index of %s, idx: %d", self.current_band, i)
if i > 0:
self.current_band = self.BAND_ENA[i - 1]
else:
self.current_band = self.BAND_ENA[len(self.BAND_ENA)-1]
log.info("Band changed to %s, idx: %d", self.current_band, i)
vfo, tune, mode = self.application.bandState[self.current_band]
self.application.ChangeHwFrequency(9000, vfo+tune)
def ReturnFrequency(self):
"""Опрос ~10 Гц. Возвращает (tune, vfo) или (None, None)."""
# SDR-1000 не сообщает частоту сам, поэтому возвращаем None
return None, None
# ── Quisk API: режим ─────────────────────────────────────────────
def ChangeMode(self, mode):
"""Quisk вызывает при смене режима. mode: 'USB', 'CW', и т.д."""
# Нормализуем DGT-* → USB
# if mode.startswith('DGT'):
# mode = 'USB'
code = self.MODE_MAP.get(mode)
if not code:
log.warning("Неизвестный режим Quisk: %s → по умолчанию USB", mode)
code = '2'
self.cmdbuf += ("MD" + code+';')
self.current_mode = mode
log.debug("Режим → %s (MD%s)", mode, code)
# ── Quisk API: диапазон ─────────────────────────────────────────
def ChangeBand(self, band):
"""Quisk вызывает при смене диапазона. band: '160', '80', …"""
BaseHardware.ChangeBand(self, band)
log.info("Диапазон → %s м", band)
# ── Quisk API: PTT ──────────────────────────────────────────────
def OnButtonPTT(self, event=None):
"""Quisk вызывает при нажатии/отпускании PTT."""
if event:
self.ptt_active = bool(event.GetEventObject().GetValue())
#self._cat("ZZTX%d" % (1 if self.ptt_active else 0))
log.debug("PTT %s", "ON" if self.ptt_active else "OFF")
# ── Quisk API: HeartBeat (~10 Гц) ───────────────────────────────
def HeartBeat(self):
"""Периодический опрос — можно использовать для синхронизации."""
self._cat()
def FastHeartBeat(self): # Called frequently by the GUI thread
pass
# ── Quisk API: TUNE ─────────────────────────────────────────────
def OnBtnFDX(self, is_fdx):
"""Кнопка FDX (полный дуплекс)."""
log.debug("FDX → %s", is_fdx)
+89
View File
@@ -0,0 +1,89 @@
/**
* Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/*
* Amazing utf8 decoder & validator grabbed from:
* http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
*
* All rights goes to the original author.
*/
#include "utf8.h"
static const uint8_t utf8d[] = {
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8
};
static
uint32_t decode(uint32_t* state, uint32_t* codep, uint32_t byte) {
uint32_t type = utf8d[byte];
*codep = (*state != UTF8_ACCEPT) ?
(byte & 0x3fu) | (*codep << 6) :
(0xff >> type) & (byte);
*state = utf8d[256 + *state*16 + type];
return *state;
}
int is_utf8(uint8_t *s) {
uint32_t codepoint, state = 0;
while (*s)
decode(&state, &codepoint, *s++);
return state == UTF8_ACCEPT;
}
int is_utf8_len(uint8_t *s, size_t len) {
uint32_t codepoint, state = 0;
size_t i;
for (i = 0; i < len; i++)
decode(&state, &codepoint, *s++);
return state == UTF8_ACCEPT;
}
uint32_t is_utf8_len_state(uint8_t *s, size_t len, uint32_t state) {
uint32_t codepoint;
size_t i;
codepoint = 0;
for (i = 0; i < len; i++)
decode(&state, &codepoint, *s++);
return state;
}
+37
View File
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef UTF8_DECODE_H
#define UTF8_DECODE_H
#include <inttypes.h>
#include <stddef.h>
/* UTF8 return state. */
#define UTF8_ACCEPT 0
#define UTF8_REJECT 1
extern int is_utf8(uint8_t* s);
extern int is_utf8_len(uint8_t *s, size_t len);
extern uint32_t is_utf8_len_state(uint8_t *s, size_t len, uint32_t state);
#endif
+84 -33
View File
@@ -6,6 +6,7 @@
#include <sys/time.h>
#endif
#include <complex.h>
#include <string.h>
#include "quisk.h"
// Access to config file attributes.
@@ -71,40 +72,27 @@ double QuiskGetConfigDouble(const char * name, double deflt)
char * QuiskGetConfigString(const char * name, char * deflt)
{ // Return the UTF-8 configuration string. Return deflt for failure.
char * res;
PyObject * attr;
#if PY_MAJOR_VERSION < 3
static char retbuf[QUISK_SC_SIZE];
#endif
Py_ssize_t size;
if (!quisk_pyConfig || PyErr_Occurred())
return deflt;
attr = PyObject_GetAttrString(quisk_pyConfig, name);
if (attr) {
#if PY_MAJOR_VERSION >= 3
res = (char *)PyUnicode_AsUTF8(attr);
#else
if (PyUnicode_Check(attr)) {
PyObject * pystr = PyUnicode_AsUTF8String(attr);
strMcpy(retbuf, PyString_AsString(pystr), QUISK_SC_SIZE);
retbuf[QUISK_SC_SIZE - 1] = 0;
res = retbuf;
Py_DECREF(pystr);
}
else {
res = PyString_AsString(attr);
}
#endif
Py_DECREF(attr);
if (res)
return res; // success
else
PyErr_Clear();
}
else {
PyErr_Clear();
}
return deflt; // failure
if (!quisk_pyConfig || PyErr_Occurred())
return deflt; // failure
PyObject * attr = PyObject_GetAttrString(quisk_pyConfig, name);
if ( ! attr) {
PyErr_Clear();
return deflt; // failure
}
if ( ! PyUnicode_Check(attr)) {
Py_DECREF(attr);
return deflt; // failure
}
const char * utf8_str = PyUnicode_AsUTF8AndSize(attr, &size);
if ( ! utf8_str) {
Py_DECREF(attr);
PyErr_Clear();
return deflt; // failure
}
Py_DECREF(attr);
return (char *)utf8_str;
}
double QuiskTimeSec(void)
@@ -321,3 +309,66 @@ PyObject * QuiskPrintf(char * format, ...)
return NULL;
}
#endif
#define MAX_CHANNELS 8
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 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
int sizeBuf; // current size of cBuf in complex samples
int nDelay; // number of samples needed to start reading the buffer
int nBuf; // current number of complex samples in cBuf
int filling; // if true, we are filling the buffer and not returning samples
int Windex; // index of write
int Rindex; // index of read
} Channels[MAX_CHANNELS] = {0};
struct _channel * ptChannel = Channels + channel;
int i, num;
if ( ! cSamples) { // initialize
ptChannel->nDelay = nRead;
ptChannel->sizeBuf = nRead * 16;
ptChannel->cBuf = (complex double *)realloc(ptChannel->cBuf, ptChannel->sizeBuf * sizeof(complex double));
memset(ptChannel->cBuf, 0, ptChannel->sizeBuf * sizeof(complex double));
ptChannel->nBuf = 0;
ptChannel->Windex = 0;
ptChannel->Rindex = 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++) {
ptChannel->cBuf[ptChannel->Windex++] = cSamples[i];
if (ptChannel->Windex >= ptChannel->sizeBuf)
ptChannel->Windex = 0;
ptChannel->nBuf++;
num++;
}
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) { // 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)
ptChannel->Rindex = 0;
ptChannel->nBuf--;
num++;
}
if (ptChannel->nBuf <= 0)
ptChannel->filling = 1;
}
}
return num;
}
+2101
View File
File diff suppressed because it is too large Load Diff
+337
View File
@@ -0,0 +1,337 @@
/*
* Copyright (C) 2016-2022 Davidson Francis <davidsondfgl@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
/**
* @dir include/
* @brief wsServer include directory
*
* @file ws.h
* @brief wsServer constants and functions.
*/
#ifndef WS_H
#define WS_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include <inttypes.h>
/**
* @name Global configurations
*/
/**@{*/
/**
* @brief Max clients connected simultaneously.
*/
#ifndef MAX_CLIENTS
#define MAX_CLIENTS 8
#endif
/**
* @name Key and message configurations.
*/
/**@{*/
/**
* @brief Message buffer length.
*/
#define MESSAGE_LENGTH 2048
/**
* @brief Maximum frame/message length.
*/
#define MAX_FRAME_LENGTH (16*1024*1024)
/**
* @brief WebSocket key length.
*/
#define WS_KEY_LEN 24
/**
* @brief Magic string length.
*/
#define WS_MS_LEN 36
/**
* @brief Accept message response length.
*/
#define WS_KEYMS_LEN (WS_KEY_LEN + WS_MS_LEN)
/**
* @brief Magic string.
*/
#define MAGIC_STRING "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
/**@}*/
/**
* @name Handshake constants.
*/
/**@{*/
/**
* @brief Alias for 'Sec-WebSocket-Key'.
*/
#define WS_HS_REQ "Sec-WebSocket-Key"
/**
* @brief Handshake accept message length.
*/
#define WS_HS_ACCLEN 130
/**
* @brief Handshake accept message.
*/
#define WS_HS_ACCEPT \
"HTTP/1.1 101 Switching Protocols\r\n" \
"Upgrade: websocket\r\n" \
"Connection: Upgrade\r\n" \
"Sec-WebSocket-Accept: "
/**@}*/
/**
* @name Frame types.
*/
/**@{*/
/**
* @brief Frame FIN.
*/
#define WS_FIN 128
/**
* @brief Frame FIN shift
*/
#define WS_FIN_SHIFT 7
/**
* @brief Continuation frame.
*/
#define WS_FR_OP_CONT 0
/**
* @brief Text frame.
*/
#define WS_FR_OP_TXT 1
/**
* @brief Binary frame.
*/
#define WS_FR_OP_BIN 2
/**
* @brief Close frame.
*/
#define WS_FR_OP_CLSE 8
/**
* @brief Ping frame.
*/
#define WS_FR_OP_PING 0x9
/**
* @brief Pong frame.
*/
#define WS_FR_OP_PONG 0xA
/**
* @brief Unsupported frame.
*/
#define WS_FR_OP_UNSUPPORTED 0xF
/**@}*/
/**
* @name Close codes
*/
/**@{*/
/**
* @brief Normal close
*/
#define WS_CLSE_NORMAL 1000
/**
* @brief Protocol error
*/
#define WS_CLSE_PROTERR 1002
/**@}*/
/**
* @brief Inconsistent message (invalid utf-8)
*/
#define WS_CLSE_INVUTF8 1007
/**
* @name Connection states
*/
/**@{*/
/**
* @brief Connection not established yet.
*/
#define WS_STATE_CONNECTING 0
/**
* @brief Communicating.
*/
#define WS_STATE_OPEN 1
/**
* @brief Closing state.
*/
#define WS_STATE_CLOSING 2
/**
* @brief Closed.
*/
#define WS_STATE_CLOSED 3
/**@}*/
/**
* @name Timeout util
*/
/**@{*/
/**
* @brief Nanoseconds macro converter
*/
#define MS_TO_NS(x) ((x)*1000000)
/**
* @brief Timeout in milliseconds.
*/
#define TIMEOUT_MS (500)
/**@}*/
/**
* @name Handshake constants.
*/
/**@{*/
/**
* @brief Debug
*/
#ifdef VERBOSE_MODE
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)
#else
#define DEBUG(...)
#endif
/**@}*/
#ifndef AFL_FUZZ
#define SEND(client,buf,len) send_all((client), (buf), (len), MSG_NOSIGNAL)
#define RECV(fd,buf,len) recv((fd)->client_sock, (buf), (len), 0)
#else
#define SEND(client,buf,len) write(fileno(stdout), (buf), (len))
#define RECV(fd,buf,len) read((fd)->client_sock, (buf), (len))
#endif
/* Opaque client connection type. */
typedef uint64_t ws_cli_conn_t;
/* Opaque server instance type. */
typedef struct ws_server ws_server_t;
/**
* @brief Get server context.
* Set when initializing `.context` in `struct ws_server`.
*/
void *ws_get_server_context(ws_cli_conn_t client);
/**
* @brief Set connection context.
*/
void ws_set_connection_context(ws_cli_conn_t client, void *ptr);
/**
* @brief Get connection context.
*/
void *ws_get_connection_context(ws_cli_conn_t client);
/**
* @brief events Web Socket events types.
*/
struct ws_events
{
/**
* @brief On open event, called when a new client connects.
*/
void (*onopen)(ws_cli_conn_t client);
/**
* @brief On close event, called when a client disconnects.
*/
void (*onclose)(ws_cli_conn_t client);
/**
* @brief On message event, called when a client sends a text
* or binary message.
*/
void (*onmessage)(ws_cli_conn_t client,
const unsigned char *msg, uint64_t msg_size, int type);
};
/**
* @brief server Web Socket server parameters
*/
struct ws_server
{
/**
* @brief Required hostname that the wsServer will bind to
*/
const char *host;
/**
* @brief Listening port
*/
uint16_t port;
/**
* @brief Whether if the ws_socket() should create a new thread
* and be non-blocking (1) or not (0).
*/
int thread_loop;
/**
* @brief Ping timeout in milliseconds
*/
uint32_t timeout_ms;
/**
* @brief Server events.
*/
struct ws_events evs;
/**
* @brief Server context.
* Provided by the user, can be accessed via `ws_get_server_context` from `onopen`.
*/
void* context;
};
/* Forward declarations. */
/* Internal usage. */
extern int get_handshake_accept(char *wsKey, unsigned char **dest);
extern int get_handshake_response(char *hsrequest, char **hsresponse);
/* External usage. */
extern char *ws_getaddress(ws_cli_conn_t client);
extern char *ws_getport(ws_cli_conn_t client);
extern int ws_sendframe(
ws_cli_conn_t client, const char *msg, uint64_t size, int type);
extern int ws_sendframe_bcast(
uint16_t port, const char *msg, uint64_t size, int type);
extern int ws_sendframe_txt(ws_cli_conn_t client, const char *msg);
extern int ws_sendframe_txt_bcast(uint16_t port, const char *msg);
extern int ws_sendframe_bin(ws_cli_conn_t client, const char *msg,
uint64_t size);
extern int ws_sendframe_bin_bcast(uint16_t port, const char *msg,
uint64_t size);
extern int ws_get_state(ws_cli_conn_t client);
extern int ws_close_client(ws_cli_conn_t client);
extern int ws_socket(struct ws_server *ws_srv);
/* Ping routines. */
extern void ws_ping(ws_cli_conn_t cid, int threshold);
#ifdef AFL_FUZZ
extern int ws_file(struct ws_events *evs, const char *file);
#endif
#ifdef __cplusplus
}
#endif
#endif /* WS_H */