4 Commits
Author SHA1 Message Date
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
48 changed files with 5460 additions and 171 deletions
+23
View File
@@ -1,3 +1,26 @@
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.
-21
View File
@@ -1,21 +0,0 @@
# Quisk Software Defined Radio by N2ADR
**December 27, 2023**
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.
The web page for this project is [https://groups.io/g/n2adr-sdr](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).
+2 -2
View File
@@ -1,2 +1,2 @@
#Quisk version 4.2.49
from .quisk import main
#Quisk version 4.2.53
from .quisk import main
+1
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>
+4 -3
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;
@@ -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 */
+3
View File
@@ -4203,6 +4203,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.')
+33 -6
View File
@@ -84,7 +84,7 @@ h3 {font-size:1.6rem;}
<div class="contents">
<h1>
<a id="Welcome"></a>
Welcome to QUISK (September 2025)
Welcome to QUISK (June 2026)
</h1>
<p>
@@ -197,7 +197,7 @@ Quisk is free open source software written in Python and C. It is hosted on the
<br>
<p>
If you have Python 3 installed on your computer, just continue to use that version.
<b>On Windows Quisk requires Python 3.8, 3.9, 3.10, 3.11, 3.12 or 3.13.</b>
On Windows Quisk requires Python 3.8 or higher.
If you want to install a more recent version of Python, uninstall the old version first.
Running multiple versions of Python is possible but can be confusing and is not necessary.
<b>A Quisk installation is needed for each version of Python you have. If you upgrade to a newer Python
@@ -208,8 +208,7 @@ you must re-install Quisk. But your Quisk settings will be preserved.</b>
<p>
Windows does not include Python, so you must first install a recent version of 64-bit Python 3.
If you already have 64-bit Python 3 installed, just keep using it.
<b>On Windows Quisk requires Python 3.8, 3.9, 3.10, 3.11, 3.12 or 3.13.</b>
There may be newer versions of Python, but until support is added, use the versions listed above.
On Windows Quisk requires Python 3.8 or higher.
Python is available from <a href="http://www.python.org/">http://www.python.org</a>.
There is an option to <b>Add Python to PATH</b>. You MUST select
this option so that you can start python by just typing "python" instead of the whole path
@@ -377,12 +376,14 @@ 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>
@@ -1433,9 +1434,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"
+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);
}
+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)
$(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)
+5
View File
@@ -3,6 +3,7 @@
#include <math.h>
#include <sys/time.h>
#include <complex.h>
#include <string.h>
#include <fftw3.h>
#include "quisk.h"
#include <sys/types.h>
@@ -1219,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
+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);
}
+41
View File
@@ -0,0 +1,41 @@
[build-system]
requires = ["setuptools>=61.0.0"]
build-backend = "setuptools.build_meta"
[project]
name = "quisk"
version = "4.2.53"
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',
]
[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.packages.find]
where = ["."]
+21 -22
View File
@@ -3,6 +3,7 @@
#include <stdlib.h>
#include <math.h>
#include <complex.h> // Use native C99 complex type for fftw3
#include <string.h>
#include <fftw3.h>
#include <sys/types.h>
#include <stdbool.h>
@@ -4027,7 +4028,7 @@ static PyObject * open_rx_udp(PyObject * self, PyObject * args)
wVersionRequested = MAKEWORD(2, 2);
if (WSAStartup(wVersionRequested, &wsaData) != 0) {
sprintf(buf, "Failed to initialize Winsock (WSAStartup)");
return PyString_FromString(buf);
return PyUnicode_FromString(buf);
}
else {
cleanupWSA = 1;
@@ -4076,7 +4077,7 @@ static PyObject * open_rx_udp(PyObject * self, PyObject * args)
else {
sprintf(buf, "Failed to open socket");
}
return PyString_FromString(buf);
return PyUnicode_FromString(buf);
}
static PyObject * open_sound(PyObject * self, PyObject * args)
@@ -4290,7 +4291,7 @@ static PyObject * mixer_set(PyObject * self, PyObject * args)
return NULL;
quisk_alsa_mixer_set(card_name, numid, value, err_msg, QUISK_SC_SIZE);
return PyString_FromString(err_msg);
return PyUnicode_FromString(err_msg);
}
static PyObject * pc_to_hermes(PyObject * self, PyObject * args)
@@ -4493,12 +4494,12 @@ static PyObject * ip_interfaces(PyObject * self, PyObject * args)
baddr |= ~netmask;
tup = PyTuple_New(4);
if (name == NULL)
PyTuple_SetItem(tup, 0, PyString_FromString("unnamed"));
PyTuple_SetItem(tup, 0, PyUnicode_FromString("unnamed"));
else
PyTuple_SetItem(tup, 0, PyString_FromString(name));
PyTuple_SetItem(tup, 1, PyString_FromString(Win_NtoA(ipAddr)));
PyTuple_SetItem(tup, 2, PyString_FromString(Win_NtoA(netmask)));
PyTuple_SetItem(tup, 3, PyString_FromString(Win_NtoA(baddr)));
PyTuple_SetItem(tup, 0, PyUnicode_FromString(name));
PyTuple_SetItem(tup, 1, PyUnicode_FromString(Win_NtoA(ipAddr)));
PyTuple_SetItem(tup, 2, PyUnicode_FromString(Win_NtoA(netmask)));
PyTuple_SetItem(tup, 3, PyUnicode_FromString(Win_NtoA(baddr)));
PyList_Append(pylist, tup);
Py_DECREF(tup);
}
@@ -4517,10 +4518,10 @@ static PyObject * ip_interfaces(PyObject * self, PyObject * args)
while(p) {
if ((p->ifa_addr) && p->ifa_addr->sa_family == AF_INET) {
tup = PyTuple_New(4);
PyTuple_SetItem(tup, 0, PyString_FromString(p->ifa_name));
PyTuple_SetItem(tup, 1, PyString_FromString(Lin_NtoA(p->ifa_addr)));
PyTuple_SetItem(tup, 2, PyString_FromString(Lin_NtoA(p->ifa_netmask)));
PyTuple_SetItem(tup, 3, PyString_FromString(Lin_NtoA(p->ifa_broadaddr)));
PyTuple_SetItem(tup, 0, PyUnicode_FromString(p->ifa_name));
PyTuple_SetItem(tup, 1, PyUnicode_FromString(Lin_NtoA(p->ifa_addr)));
PyTuple_SetItem(tup, 2, PyUnicode_FromString(Lin_NtoA(p->ifa_netmask)));
PyTuple_SetItem(tup, 3, PyUnicode_FromString(Lin_NtoA(p->ifa_broadaddr)));
PyList_Append(pylist, tup);
Py_DECREF(tup);
}
@@ -5660,7 +5661,7 @@ static PyObject * Xdft(PyObject * pyseq, int inverse, int window)
static fftw_complex * samples; // complex data for fft
static fftw_plan planF, planB; // fft plan for fftW
static double * fft_window; // window function
Py_complex pycx; // Python C complex value
complex double pycx;
if (PySequence_Check(pyseq) != 1) {
PyErr_SetString (QuiskError, "DFT input data is not a sequence");
@@ -5695,22 +5696,20 @@ static PyObject * Xdft(PyObject * pyseq, int inverse, int window)
for (i = 0; i < size; i++) {
obj = PySequence_GetItem(pyseq, j);
if (PyComplex_Check(obj)) {
pycx = PyComplex_AsCComplex(obj);
pycx = PyComplex_RealAsDouble(obj) + I * PyComplex_ImagAsDouble(obj);
}
else if (PyFloat_Check(obj)) {
pycx.real = PyFloat_AsDouble(obj);
pycx.imag = 0;
pycx = PyFloat_AsDouble(obj);
}
else if (PyInt_Check(obj)) {
pycx.real = PyInt_AsLong(obj);
pycx.imag = 0;
pycx = PyInt_AsLong(obj);
}
else {
Py_XDECREF(obj);
PyErr_SetString (QuiskError, "DFT input data is not a complex/float/int number");
return NULL;
}
samples[i] = pycx.real + I * pycx.imag;
samples[i] = pycx;
if (++j >= size)
j = 0;
Py_XDECREF(obj);
@@ -5736,9 +5735,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;
}
@@ -6188,6 +6185,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."},
+11
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);
@@ -394,6 +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);
int CircularBuffer(int channel, complex double * cSamples, int nRead, int nWrite);
// Driver function definitions=================================================
int quisk_read_alsa(struct sound_dev *, complex double *);
+85 -3
View File
@@ -3938,6 +3938,7 @@ class App(wx.App):
self.hot_key_ptt_active = False
self.serial_ptt_active = False
self.vox_ptt_active = False
self.tci_ptt_active = False
self.hermes_LNA_dB = 20
self.hermes_atten_dB = 0
if hasattr(Hardware, "OnChangeRxTx"):
@@ -4028,6 +4029,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)
@@ -4275,6 +4280,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:
@@ -4349,6 +4358,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()
@@ -4673,6 +4695,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:
@@ -4758,6 +4782,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:
@@ -5774,7 +5799,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
@@ -6673,11 +6698,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 ":
@@ -6732,6 +6759,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:
@@ -6796,10 +6831,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 Hardware.is_HermesLite2:
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
+13
View File
@@ -1423,6 +1423,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
+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)
+4 -9
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.49'
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',
+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]);
+11 -3
View File
@@ -4,6 +4,7 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <complex.h>
#include <string.h>
#include <math.h>
#include <sys/time.h>
#include <time.h>
@@ -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
@@ -1019,7 +1022,9 @@ int quisk_read_sound(void) // Called from sound thread
// Play digital if required
//if (is_DGT)
play_sound_interface(&DigitalOutput, nSamples, cSamples, 1, digital_output_level);
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)
@@ -1065,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));
}
}
}
+8 -6
View File
@@ -34,6 +34,8 @@
* 2020 Apr by N2ADR Changes in printf's
*/
#include <string.h>
#ifdef QUISK_HAVE_PULSEAUDIO
#include <Python.h>
@@ -1030,20 +1032,20 @@ static void source_sink(const char * name, const char * descr, pa_proplist * pro
PyObject * pytup;
pytup = PyTuple_New(3);
PyList_Append(pylist, pytup);
PyTuple_SET_ITEM(pytup, 0, PyString_FromString(name));
PyTuple_SET_ITEM(pytup, 1, PyString_FromString(descr));
PyTuple_SetItem(pytup, 0, PyUnicode_FromString(name));
PyTuple_SetItem(pytup, 1, PyUnicode_FromString(descr));
value = pa_proplist_gets(props, "device.api");
if (value && ! strcmp(value, "alsa")) {
snprintf(buf300, 300, "%s %s (hw:%s,%s)", pa_proplist_gets(props, "alsa.card_name"), pa_proplist_gets(props, "alsa.name"),
pa_proplist_gets(props, "alsa.card"), pa_proplist_gets(props, "alsa.device"));
PyTuple_SET_ITEM(pytup, 2, PyString_FromString(buf300));
PyTuple_SetItem(pytup, 2, PyUnicode_FromString(buf300));
}
else {
PyTuple_SET_ITEM(pytup, 2, PyString_FromString(""));
PyTuple_SetItem(pytup, 2, PyUnicode_FromString(""));
}
PyList_Append(pylist, pytup);
}
@@ -1074,7 +1076,7 @@ static void index_callback(pa_context *c, uint32_t idx, void *userdata) {
//printf("%u\n", idx);
}
PyObject * quisk_pulseaudio_sound_devices(PyObject * self, PyObject * args)
PyObject * quisk_pulseaudio_sound_devices(PyObject * self, PyObject * args) // Called from the GUI thread
{ // Return a list of PulseAudio device names [pycapt, pyplay]
PyObject * pylist, * pycapt, * pyplay;
pa_mainloop *pa_names_ml;
+9 -6
View File
@@ -7,6 +7,9 @@
* Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!!
*/
#include <string.h>
#ifdef QUISK_HAVE_WASAPI
#define UNICODE
@@ -1125,10 +1128,10 @@ PyObject * quisk_wasapi_sound_devices(PyObject * self, PyObject * args) // Call
EXIT_ON_ERROR(hr)
// data items are (name, id, is_raw)
pytup = PyTuple_New(3);
PyTuple_SetItem(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1));
PyTuple_SetItem(pytup, 1, PyUnicode_FromWideChar(pwszID, -1));
PyTuple_SetItem(pytup, 2, PyInt_FromLong(1));
PyList_Append(pyplay, pytup);
PyTuple_SET_ITEM(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1));
PyTuple_SET_ITEM(pytup, 1, PyUnicode_FromWideChar(pwszID, -1));
PyTuple_SET_ITEM(pytup, 2, PyInt_FromLong(1));
CoTaskMemFree(pwszID);
pwszID = NULL;
PropVariantClear(&varName);
@@ -1155,10 +1158,10 @@ PyObject * quisk_wasapi_sound_devices(PyObject * self, PyObject * args) // Call
EXIT_ON_ERROR(hr)
// data items are (name, id, is_raw)
pytup = PyTuple_New(3);
PyTuple_SetItem(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1));
PyTuple_SetItem(pytup, 1, PyUnicode_FromWideChar(pwszID, -1));
PyTuple_SetItem(pytup, 2, PyInt_FromLong(1));
PyList_Append(pycapt, pytup);
PyTuple_SET_ITEM(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1));
PyTuple_SET_ITEM(pytup, 1, PyUnicode_FromWideChar(pwszID, -1));
PyTuple_SET_ITEM(pytup, 2, PyInt_FromLong(1));
CoTaskMemFree(pwszID);
pwszID = NULL;
PropVariantClear(&varName);
+928
View File
@@ -0,0 +1,928 @@
#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;
struct quisk_cHB45Filter HalfBand1;
struct quisk_cFilter filtDecim3;
struct quisk_cFilter filtDecim48to24;
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 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;
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);
}
}
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:ESDR,1.4;");
//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];
complex double sample;
if (type == WS_FR_OP_TXT) {
if (size > TCI_RX_BUF_SIZE / 2)
return; // prevent overflow
struct ClientData * ctx = ws_get_connection_context(client);
if (ctx->msg_length + size >= TCI_RX_BUF_SIZE - 2)
ctx->msg_length = 0; // prevent overflow
memcpy(ctx->msg_buf + ctx->msg_length, msg, size); // append msg to buffer
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;
int buf_start=0, buf_end;
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) {
if (pt->length > 0 && client == tci_tx_audio_client) {
pthread_mutex_lock(&tx_buffer_mutex);
if (verbose >= 2)
buf_start = CircularBuffer(0, &sample, 0, 0);
switch (TxChrono.format) {
case TCI_INT16:
break;
case TCI_FLOAT32:
// Version 1.4 does not return the number of channels. We assume two channels.
// pt->length is the number of floats
while (vpt < vpt_end && count < pt->length) {
re = *(float *)vpt;
vpt += sizeof(float);
count++;
if (two_channels) {
im = *(float *)vpt;
vpt += sizeof(float);
count++;
}
else {
im = re;
}
sample = (re + I * im) * (CLIP32 / 2);
CircularBuffer(0, &sample, 0, 1);
}
if (verbose && count != pt->length)
QuiskPrintf("TCI *TX count %ld %d %d\n", size, count, pt->length);
break;
}
if (verbose >= 2) {
buf_end = CircularBuffer(0, &sample, 0, 0);
QuiskPrintf("TCI Tx buffer add %4d start %5d end %5d\n", count, buf_start, buf_end);
}
pthread_mutex_unlock(&tx_buffer_mutex);
}
}
if (verbose)
PrintStream((const char *)msg, size, 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;
}
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;
}
+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 */