Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84e194c644 | ||
|
|
b45a71b017 | ||
|
|
e6b9d9dd05 | ||
|
|
1f68d05cd5 | ||
|
|
018fc0e871 | ||
|
|
1111d24333 | ||
|
|
d8aee9284d | ||
|
|
7121dacbbd |
@@ -1,3 +1,70 @@
|
||||
Quisk Version 4.2.53 June 2026
|
||||
===============================
|
||||
I added TCI support for Software Defined Connectors CW skimmer. Please test.
|
||||
|
||||
This version of Quisk now works on Python 3.14 and higher. I changed to a new build system because
|
||||
the old one has been deprecated by Python. They no longer support running setup.py.
|
||||
The Linux version has a new makefile. The Windows version is installed by pip as usual.
|
||||
Please test.
|
||||
|
||||
Quisk Version 4.2.52 May 2026
|
||||
==============================
|
||||
This is a fix for various TCI bugs. Please test with WSJT-X and let me know if all problems are solved.
|
||||
It is unlikely that other digital software will connect without problems, but please test and let me know.
|
||||
|
||||
Quisk Version 4.2.51 May 2026
|
||||
==============================
|
||||
This version can use the TCI protocol to connect to WSJT-X. Please see Digital Modes in the documentation.
|
||||
|
||||
Quisk Version 4.2.50 December 2025
|
||||
===================================
|
||||
This is a quick fix for a bug in 4.2.49 that results in the repeated message
|
||||
AttributeError: 'Hardware' object has no attribute 'is_HermesLite2'
|
||||
|
||||
Quisk Version 4.2.49 December 2025
|
||||
===================================
|
||||
This version adds more features for the Anan 10E and other OpenHPSDR radios. Please test.
|
||||
|
||||
I added the forward power, SWR, temperature and PA current display to the OpenHPSDR radio. But
|
||||
I don't have that radio so I need some help. I think the forward and reverse power connections
|
||||
are correct, but it is still necessary to create a table to convert analog input codes to power.
|
||||
Go to the Hardware screen and check the "Power meter calibration" setting. You will need to enter
|
||||
"New" and create a suitable table. To see the ADC values, choose table "Show ADC".
|
||||
|
||||
There is no protocol standard to specify which analog input is used for temperature and PA current.
|
||||
And AFAIK the Anan 10E has no such sensors. I am hoping to use the board ID to find the right input.
|
||||
The ADC conversion is linear and the two points are specified on the Hardware screen.
|
||||
|
||||
I currently assigned the analog inputs as follows:
|
||||
AIN1 Forward power
|
||||
AIN2 Reverse power
|
||||
AIN3 PA current
|
||||
AIN5 Temperature
|
||||
If you need to see the ADC codes 0-4095 for each input, go to hermes/quisk_hardware.py and
|
||||
change DEBUG_SENSORS to one.
|
||||
|
||||
Quisk Version 4.2.48 November 2025
|
||||
===================================
|
||||
This is a quisk fix for a bug in the new OpenHPSDR radios. It fixes use_rx_udp that was reset to zero.
|
||||
|
||||
Quisk Version 4.2.47 November 2025
|
||||
===================================
|
||||
To improve the performance of Quisk with the Anan 10-E and other OpenHPSDR radios, I added
|
||||
a new type of radio "OpenHPSDR". Use this type for the Anan 10-E and similar radios. It removes
|
||||
all the protocol extensions added by the Hermes-Lite2 project. I removed the configuration
|
||||
option "Hermes Lite2 protocol enable". This is now automatic.
|
||||
|
||||
I added buttons for antennas 0, 1, 2 and 3 for OpenHPSDR radios that support more antennas.
|
||||
I added controls for the OpenHPSDR preamplifier and attenuator.
|
||||
|
||||
I do not have any Apache Labs radios, so I must depend on my users to report problems and solutions.
|
||||
|
||||
Quisk Version 4.2.46 October 2025
|
||||
==================================
|
||||
I fixed a bug in Hamlib rig 2 which happens with the new Pure Signal code. The Hamlib "f" command
|
||||
now always returns the main receiver frequency, not one of the added receiver frequencies. This is
|
||||
consistent with the Flex ZZ commands.
|
||||
|
||||
Quisk Version 4.2.45 October 2025
|
||||
==================================
|
||||
The new FreeDV including RADE mode is now a built-in mode of Quisk. Just press "FDV" to talk FreeDV.
|
||||
|
||||
@@ -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
@@ -1,2 +1,2 @@
|
||||
#Quisk version 4.2.45
|
||||
from .quisk import main
|
||||
#Quisk version 4.2.53
|
||||
from .quisk import main
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <Python.h>
|
||||
#include <complex.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
+21
-25
@@ -88,6 +88,7 @@ class Configuration:
|
||||
platform_ignore = 'win_'
|
||||
self.sections = []
|
||||
self.receiver_data = []
|
||||
self.receiver_explain = []
|
||||
self.StatePath = conf.settings_file_path
|
||||
if not self.StatePath:
|
||||
self.StatePath = os.path.join(app.QuiskFilesDir, "quisk_settings.json")
|
||||
@@ -158,9 +159,9 @@ class Configuration:
|
||||
dlg.Destroy()
|
||||
else:
|
||||
radio_dict["use_soapy"] = '0'
|
||||
if radio_type not in ("HiQSDR", "Hermes", "Red Pitaya", "Odyssey", "Odyssey2"):
|
||||
if radio_type not in ("HiQSDR", "Hermes", "Red Pitaya", "Odyssey", "Odyssey2", "OpenHPSDR"):
|
||||
radio_dict["use_rx_udp"] = '0'
|
||||
if radio_type in ("Hermes", "Red Pitaya", "Odyssey2"):
|
||||
if radio_type in ("Hermes", "Red Pitaya", "Odyssey2", "OpenHPSDR"):
|
||||
if "Hermes_BandDict" not in radio_dict:
|
||||
radio_dict["Hermes_BandDict"] = {}
|
||||
if "Hermes_BandDictTx" not in radio_dict:
|
||||
@@ -601,6 +602,7 @@ class Configuration:
|
||||
# Receiver sections start with 16 #, "Receivers ", receiver name, explain
|
||||
# self.receiver_data is a list of [receiver_name, receiver_data]
|
||||
# receiver_data is a list of [data_name, text, fmt, help_text, values]
|
||||
# self.receiver_explain is a list of the text in Receivers: the name, a comma and the explanation.
|
||||
|
||||
# Variable names start with ## variable_name variable_text, format
|
||||
# The format is integer, number, text, boolean, integer choice, text choice, rfile
|
||||
@@ -638,6 +640,7 @@ class Configuration:
|
||||
continue
|
||||
if line[0:27] == '################ Receivers ':
|
||||
section = 'Receivers'
|
||||
self.receiver_explain.append(line[27:].strip())
|
||||
args = line[27:].split(',', 1)
|
||||
rxname = args[0].strip()
|
||||
section_data = []
|
||||
@@ -2298,8 +2301,8 @@ class ControlMixin:
|
||||
if self.radio_name == Settings[1]: # changed for current radio
|
||||
if name in ('hot_key_ptt_toggle', 'hot_key_ptt_if_hidden', 'keyupDelay', 'cwTone', 'pulse_audio_verbose_output',
|
||||
'start_cw_delay', 'start_ssb_delay', 'maximum_tx_secs', 'quisk_serial_cts', 'quisk_serial_dsr',
|
||||
'hot_key_ptt1', 'hot_key_ptt2', 'midi_ptt_toggle', 'TxRxSilenceMsec', 'hermes_lite2_enable',
|
||||
'fixed_tune_offset'):
|
||||
'hot_key_ptt1', 'hot_key_ptt2', 'midi_ptt_toggle', 'TxRxSilenceMsec',
|
||||
'fixed_tune_offset', 'calibrate_temp_20', 'calibrate_temp_40', 'calibrate_current_0', 'calibrate_current_1'):
|
||||
setattr(conf, name, x)
|
||||
application.ImmediateChange(name)
|
||||
elif name[0:4] in ('lin_', 'win_'):
|
||||
@@ -2400,6 +2403,7 @@ class BaseWindow(wx.ScrolledWindow, ControlMixin):
|
||||
def __init__(self, parent):
|
||||
wx.ScrolledWindow.__init__(self, parent)
|
||||
ControlMixin.__init__(self, parent)
|
||||
self.SetScrollRate(8, 8)
|
||||
|
||||
class ConfigConfig(BaseWindow):
|
||||
def __init__(self, parent, width):
|
||||
@@ -2510,7 +2514,6 @@ The CW message will then repeat. A time of zero means no repeat.'
|
||||
sl, btn = self.AddTextSliderHelp(4, " Repeat secs %.1f ", 0, 0, 100, self.OnPlayFileRepeat, help_text, span=2, scale=0.1)
|
||||
self.NextRow()
|
||||
self.FitInside()
|
||||
self.SetScrollRate(1, 1)
|
||||
def MakeFileButton(self, text, path, index, name, help_text):
|
||||
if index < 10: # record buttons
|
||||
cb = self.AddCheckBox(4, text, self.OnCheckRecPlay)
|
||||
@@ -2688,7 +2691,6 @@ class ConfigTxAudio(BaseWindow):
|
||||
application.CtrlTxAudioPreemph = sld
|
||||
self.NextRow()
|
||||
self.FitInside()
|
||||
self.SetScrollRate(1, 1)
|
||||
def OnTimer(self, event):
|
||||
if not self.tmp_playing:
|
||||
self.timer.Stop()
|
||||
@@ -2737,8 +2739,8 @@ class Radios(BaseWindow): # The "Radios" first-level page
|
||||
self.NextRow()
|
||||
rb = self.AddRadioButton(1, "Add a new radio with the general type", self.NewTop)
|
||||
choices = []
|
||||
for name, data in local_conf.receiver_data:
|
||||
choices.append(name)
|
||||
for explain in local_conf.receiver_explain:
|
||||
choices.append(explain)
|
||||
choices.sort()
|
||||
self.add_type = self.AddComboCtrl(2, '', choices=choices, no_edit=True)
|
||||
self.add_type.SetSelection(0)
|
||||
@@ -2766,7 +2768,6 @@ class Radios(BaseWindow): # The "Radios" first-level page
|
||||
self.apply_btn = self.AddPushButton(3, "Apply", self.OnBtnApply)
|
||||
self.NewRadioNames()
|
||||
self.NewTop()
|
||||
self.SetScrollRate(1, 1)
|
||||
def NewTop(self, event=None):
|
||||
if event:
|
||||
btn = event.GetEventObject()
|
||||
@@ -2810,7 +2811,7 @@ class Radios(BaseWindow): # The "Radios" first-level page
|
||||
if not name or self.DuplicateName(name):
|
||||
return
|
||||
self.add_name.SetValue('')
|
||||
typ = self.add_type.GetValue().strip()
|
||||
typ = self.add_type.GetValue().split(',')[0].strip()
|
||||
if local_conf.AddRadio(name, typ):
|
||||
self.NewRadioNames()
|
||||
local_conf.settings_changed = True
|
||||
@@ -3003,7 +3004,6 @@ class RadioSection(BaseWindow): # The pages for each section in the second-leve
|
||||
self.AddColSpacer(2, 20)
|
||||
self.AddColSpacer(5, 20)
|
||||
self.FitInside()
|
||||
self.SetScrollRate(1, 1)
|
||||
def OnButtonChangeFavorites(self, event):
|
||||
if isinstance(event, (ChoiceCombo, ComboCtrl)):
|
||||
path = event.GetValue()
|
||||
@@ -3128,7 +3128,6 @@ class RadioHardwareBase(BaseWindow): # The Hardware page in the second-level no
|
||||
self.NextRow(7)
|
||||
self.AddColSpacer(2, 20)
|
||||
self.AddColSpacer(5, 20)
|
||||
self.SetScrollRate(1, 1)
|
||||
def OnButtonChangeHardware(self, event):
|
||||
if isinstance(event, (ChoiceCombo, ComboCtrl)):
|
||||
path = event.GetValue()
|
||||
@@ -3175,25 +3174,24 @@ class RadioHardware(RadioHardwareBase): # The Hardware page in the second-level
|
||||
data_names = local_conf.GetReceiverData(radio_type)
|
||||
col = 1
|
||||
border = 2
|
||||
hermes_board_id = 0
|
||||
if radio_type == "Hermes":
|
||||
try:
|
||||
hermes_board_id = application.Hardware.hermes_board_id
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
is_HermesLite2 = application.Hardware.is_HermesLite2
|
||||
except:
|
||||
is_HermesLite2 = False
|
||||
if radio_name == Settings[1] and hasattr(application.Hardware, "ProgramGateware"):
|
||||
help_text = "Choose an RBF file and program the Gateware (FPGA software) over Ethernet."
|
||||
self.AddTextButtonHelp(1, "Gateware Update", "Program from RBF file..", application.Hardware.ProgramGateware, help_text)
|
||||
col = 1
|
||||
self.NextRow(self.row + 2)
|
||||
for name, text, fmt, help_text, values in data_names:
|
||||
text = text.replace("\\u2103", "\u2103")
|
||||
if name in ('hardware_file_name', 'widgets_file_name'):
|
||||
pass
|
||||
elif name[0:4] == platform_ignore:
|
||||
pass
|
||||
elif name in ('Hermes_BandDictEnTx', ):
|
||||
pass
|
||||
elif 'Hl2_' in name and hermes_board_id != 6:
|
||||
elif 'Hl2_' in name and not is_HermesLite2:
|
||||
pass
|
||||
elif fmt[0:4] in ('dict', 'list'):
|
||||
pass
|
||||
@@ -3222,7 +3220,7 @@ class RadioHardware(RadioHardwareBase): # The Hardware page in the second-level
|
||||
col = 1
|
||||
border = 2
|
||||
self.NextRow()
|
||||
if hermes_board_id == 6:
|
||||
if is_HermesLite2:
|
||||
if col == 4:
|
||||
self.NextRow()
|
||||
help_text = ('This controls the bias level for transistors in the final power amplifier. Enter a level from 0 to 255.'
|
||||
@@ -3238,7 +3236,6 @@ class RadioHardware(RadioHardwareBase): # The Hardware page in the second-level
|
||||
self.HermesWriteBiasButton = self.AddPushButton(7, "Write", self.OnButtonHermesWriteBias, border=0)
|
||||
self.HermesWriteBiasButton.Enable(enbl)
|
||||
self.FitInside()
|
||||
self.SetScrollRate(1, 1)
|
||||
def OnHermesChangeBias0(self, event):
|
||||
value = self.HermesBias0.GetValue()
|
||||
application.Hardware.ChangeBias0(value)
|
||||
@@ -3852,7 +3849,6 @@ class RadioSound(BaseWindow): # The Sound page in the second-level notebook for
|
||||
break
|
||||
self.gbs = gbs
|
||||
self.FitInside()
|
||||
self.SetScrollRate(1, 1)
|
||||
def ItemValue(self, row, col):
|
||||
data_name = self.sound_names[row][col]
|
||||
if col == 4: # Device names
|
||||
@@ -4080,7 +4076,6 @@ The Tx bits are sent as the "Alex" filters and are sent to the J16 interface.'
|
||||
self.AddBitField(col + 1, 7, 'Hermes_BandDictTx', band, bus, self.ChangeIO)
|
||||
self.NextRow()
|
||||
self.FitInside()
|
||||
self.SetScrollRate(1, 1)
|
||||
def SortCmp(self, item1):
|
||||
# Numerical conversion of band name to megahertz
|
||||
try:
|
||||
@@ -4208,6 +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.')
|
||||
@@ -4255,7 +4253,6 @@ class RadioRemote(BaseWindow): # The Remote page in the second-level notebook f
|
||||
self.AddColSpacer(3, 20)
|
||||
self.AddColSpacer(6, 20)
|
||||
self.FitInside()
|
||||
self.SetScrollRate(1, 1)
|
||||
def OnChangePassword(self, event):
|
||||
text = local_conf.globals.get('remote_radio_password', '')
|
||||
dlg = wx.TextEntryDialog(self, "Please enter a password for remote access",
|
||||
@@ -4337,7 +4334,6 @@ class xxRadioFilters(BaseWindow): # The Filters page in the second-level notebo
|
||||
self.NextRow()
|
||||
index += 1
|
||||
self.FitInside()
|
||||
self.SetScrollRate(1, 1)
|
||||
def OnChangeFreq(self, event):
|
||||
freq = event.GetString()
|
||||
radio_dict = local_conf.GetRadioDict(self.radio_name)
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
#include <Python.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <complex.h>
|
||||
#include "quisk.h"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <Python.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
#include <complex.h> // Use native C99 complex type for fftw3
|
||||
#include "quisk.h"
|
||||
#include "filter.h"
|
||||
|
||||
+144
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
# Please do not change this widgets module for Quisk. Instead copy
|
||||
# it to your own quisk_widgets.py and make changes there.
|
||||
#
|
||||
# This module is used to add extra widgets to the QUISK screen.
|
||||
|
||||
import wx, math
|
||||
|
||||
class BottomWidgets: # Add extra widgets to the bottom of the screen
|
||||
def __init__(self, app, hardware, conf, frame, gbs, vertBox):
|
||||
self.config = conf
|
||||
self.hardware = hardware
|
||||
self.application = app
|
||||
self.start_row = app.widget_row # The first available row
|
||||
self.start_col = app.button_start_col # The start of the button columns
|
||||
self.num_rows_added = 1
|
||||
self.btnPreamp = app.QuiskCheckbutton(frame, self.OnBtnPreamp, text='Preamp')
|
||||
gbs.Add(self.btnPreamp, (self.start_row, self.start_col), flag=wx.EXPAND)
|
||||
bw, bh = self.btnPreamp.GetMinSize()
|
||||
init = app.hermes_atten_dB
|
||||
self.sliderAtten = app.SliderBoxHH(frame, 'Atten %d dB', init, 0, 31, self.OnAtten, True)
|
||||
self.sliderAtten.idName = "RfAtten"
|
||||
app.midiControls["RfAtten"] = (self.sliderAtten, self.OnAtten)
|
||||
hardware.ChangeAtten(init)
|
||||
gbs.Add(self.sliderAtten, (self.start_row, self.start_col + 2), (1, 6), flag=wx.EXPAND)
|
||||
if conf.button_layout == "Small screen":
|
||||
# Display four data items in a single window
|
||||
self.text_temperature = app.QuiskText1(frame, '', bh)
|
||||
self.text_pa_current = app.QuiskText1(frame, '', bh)
|
||||
self.text_fwd_power = app.QuiskText1(frame, '', bh)
|
||||
self.text_swr = app.QuiskText1(frame, '', bh)
|
||||
self.text_data = self.text_temperature
|
||||
self.text_pa_current.Hide()
|
||||
self.text_fwd_power.Hide()
|
||||
self.text_swr.Hide()
|
||||
b = app.QuiskPushbutton(frame, self.OnTextDataMenu, '..')
|
||||
szr = self.data_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
szr.Add(self.text_data, 1, flag=wx.ALIGN_CENTER_VERTICAL)
|
||||
szr.Add(b, 0, flag=wx.ALIGN_CENTER_VERTICAL)
|
||||
gbs.Add(szr, (self.start_row, self.start_col + 10), (1, 2), flag=wx.EXPAND)
|
||||
# Make a popup menu for the data window
|
||||
self.text_data_menu = wx.Menu()
|
||||
item = self.text_data_menu.Append(-1, 'Temperature')
|
||||
app.Bind(wx.EVT_MENU, self.OnDataTemperature, item)
|
||||
item = self.text_data_menu.Append(-1, 'PA Current')
|
||||
app.Bind(wx.EVT_MENU, self.OnDataPaCurrent, item)
|
||||
item = self.text_data_menu.Append(-1, 'Fwd Power')
|
||||
app.Bind(wx.EVT_MENU, self.OnDataFwdPower, item)
|
||||
item = self.text_data_menu.Append(-1, 'SWR')
|
||||
app.Bind(wx.EVT_MENU, self.OnDataSwr, item)
|
||||
else:
|
||||
szr = wx.BoxSizer(wx.HORIZONTAL)
|
||||
gbs.Add(szr, (self.start_row, self.start_col + 10), (1, 18), flag=wx.EXPAND)
|
||||
text_temperature = wx.StaticText(frame, -1, ' Temp 100DC XX', style=wx.ST_NO_AUTORESIZE)
|
||||
size = text_temperature.GetBestSize()
|
||||
text_temperature.Destroy()
|
||||
self.text_temperature = wx.StaticText(frame, -1, '', size=size, style=wx.ST_NO_AUTORESIZE)
|
||||
self.text_pa_current = wx.StaticText(frame, -1, '', size=size, style=wx.ST_NO_AUTORESIZE)
|
||||
self.text_fwd_power = wx.StaticText(frame, -1, '', size=size, style=wx.ST_NO_AUTORESIZE)
|
||||
self.text_swr = wx.StaticText(frame, -1, '', size=size, style=wx.ST_NO_AUTORESIZE)
|
||||
flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL
|
||||
szr.Add(self.text_temperature, 0, flag=flag)
|
||||
szr.Add(self.text_pa_current, 0, flag=flag)
|
||||
szr.Add(self.text_fwd_power, 0, flag=flag)
|
||||
szr.Add(self.text_swr, 0, flag=flag)
|
||||
def OnBtnPreamp(self, event):
|
||||
self.hardware.ChangePreamp(self.btnPreamp.GetValue())
|
||||
def OnAtten(self, event):
|
||||
value = self.sliderAtten.GetValue()
|
||||
self.hardware.ChangeAtten(value)
|
||||
self.application.hermes_atten_dB = value
|
||||
def Code2FwdRevWatts(self, fwd, rev): # Convert the fwd/rev power code to watts forward and reverse
|
||||
#print (self.hardware.hermes_rev_power, self.hardware.hermes_fwd_power)
|
||||
fwd = self.hardware.InterpolatePower(fwd)
|
||||
rev = self.hardware.InterpolatePower(rev)
|
||||
# Which voltage is forward and reverse depends on the polarity of the current sense transformer
|
||||
if fwd >= rev:
|
||||
return fwd, rev
|
||||
else:
|
||||
return rev, fwd
|
||||
def UpdateText(self): # Called at intervals
|
||||
conf = self.config
|
||||
# Temperature
|
||||
temp = self.hardware.hermes_temperature
|
||||
if conf.calibrate_temp_20 == conf.calibrate_temp_40:
|
||||
temp = " Temp ADC %4d" % temp
|
||||
else:
|
||||
m = 20.0 / (conf.calibrate_temp_40 - conf.calibrate_temp_20)
|
||||
b = 20.0 - m * conf.calibrate_temp_20
|
||||
temp = m * temp + b
|
||||
temp = (" Temp %3.0f" % temp) + u'\u2103'
|
||||
self.text_temperature.SetLabel(temp)
|
||||
# power amp current
|
||||
current = self.hardware.hermes_pa_current
|
||||
if conf.calibrate_current_0 == conf.calibrate_current_1:
|
||||
current = " Amps ADC %4d" % current
|
||||
else:
|
||||
m = 1.0 / (conf.calibrate_current_1 - conf.calibrate_current_0)
|
||||
b = - m * conf.calibrate_current_0
|
||||
current = m * current + b
|
||||
current = " PA %4.0f ma" % (1000*current)
|
||||
self.text_pa_current.SetLabel(current)
|
||||
# forward and reverse peak power
|
||||
if not self.hardware.power_interpolator: # For no interpolator, show ADC
|
||||
fwd = self.hardware.hermes_fwd_power
|
||||
rev = self.hardware.hermes_rev_power
|
||||
self.text_fwd_power.SetLabel("Fwd ADC %4d" % fwd)
|
||||
self.text_swr.SetLabel("Rev ADC %4d" % rev)
|
||||
return
|
||||
fwd, rev = self.Code2FwdRevWatts(self.hardware.hermes_fwd_peak, self.hardware.hermes_rev_peak)
|
||||
# forward less reverse power
|
||||
power = fwd - rev
|
||||
if power < 0.0:
|
||||
power = 0.0
|
||||
text = " PEP %3.1f watts" % power
|
||||
self.text_fwd_power.SetLabel(text)
|
||||
# SWR based on average power
|
||||
fwd, rev = self.Code2FwdRevWatts(self.hardware.hermes_fwd_power, self.hardware.hermes_rev_power)
|
||||
if fwd >= 0.05:
|
||||
gamma = math.sqrt(rev / fwd)
|
||||
if gamma < 0.98:
|
||||
swr = (1.0 + gamma) / (1.0 - gamma)
|
||||
else:
|
||||
swr = 99.0
|
||||
if swr < 9.95:
|
||||
text = " SWR %4.2f" % swr
|
||||
else:
|
||||
text = " SWR %4.0f" % swr
|
||||
else:
|
||||
text = " SWR ---"
|
||||
self.text_swr.SetLabel(text)
|
||||
def OnTextDataMenu(self, event):
|
||||
btn = event.GetEventObject()
|
||||
btn.PopupMenu(self.text_data_menu, (0,0))
|
||||
def OnDataTemperature(self, event):
|
||||
self.data_sizer.Replace(self.text_data, self.text_temperature)
|
||||
self.text_data.Hide()
|
||||
self.text_data = self.text_temperature
|
||||
self.text_data.Show()
|
||||
self.data_sizer.Layout()
|
||||
def OnDataPaCurrent(self, event):
|
||||
self.data_sizer.Replace(self.text_data, self.text_pa_current)
|
||||
self.text_data.Hide()
|
||||
self.text_data = self.text_pa_current
|
||||
self.text_data.Show()
|
||||
self.data_sizer.Layout()
|
||||
def OnDataFwdPower(self, event):
|
||||
self.data_sizer.Replace(self.text_data, self.text_fwd_power)
|
||||
self.text_data.Hide()
|
||||
self.text_data = self.text_fwd_power
|
||||
self.text_data.Show()
|
||||
self.data_sizer.Layout()
|
||||
def OnDataSwr(self, event):
|
||||
self.data_sizer.Replace(self.text_data, self.text_swr)
|
||||
self.text_data.Hide()
|
||||
self.text_data = self.text_swr
|
||||
self.text_data.Show()
|
||||
self.data_sizer.Layout()
|
||||
+43
-15
@@ -15,6 +15,7 @@ from quisk_hardware_model import Hardware as BaseHardware
|
||||
|
||||
DEBUG = 0
|
||||
DEBUG_I2C = 0
|
||||
DEBUG_SENSORS = 0
|
||||
|
||||
class IOBoard:
|
||||
"This class controls the N2ADR IO Board for the HermesLite 2"
|
||||
@@ -46,6 +47,8 @@ class IOBoard:
|
||||
self.old_receive = None
|
||||
self.slow = 0
|
||||
def HeartBeat(self): # Called at 10 Hz for housekeeping tasks
|
||||
if not self.hardware.is_HermesLite2:
|
||||
return
|
||||
if not QS.get_params('rx_udp_started'):
|
||||
return
|
||||
if self.have_IO_Board is None:
|
||||
@@ -154,6 +157,7 @@ class Hardware(BaseHardware):
|
||||
self.hermes_ip = ""
|
||||
self.hermes_code_version = -1
|
||||
self.hermes_board_id = -1
|
||||
self.is_HermesLite2 = False
|
||||
self.hermes_temperature = 0.0
|
||||
self.hermes_fwd_power = 0.0
|
||||
self.hermes_rev_power = 0.0
|
||||
@@ -166,7 +170,10 @@ class Hardware(BaseHardware):
|
||||
self.vna_count = 0
|
||||
self.vna_started = False
|
||||
self.repeater_freq = None # original repeater output frequency
|
||||
self.antenna_labels = ('Ant 0', 'Ant 1') # labels for the antenna button
|
||||
if conf.hardware_file_type == "Red Pitaya":
|
||||
self.antenna_labels = ('Ant 0', 'Ant 1') # labels for the antenna button
|
||||
else:
|
||||
self.antenna_labels = ('Ant 0', 'Ant 1', 'Ant 2', 'Ant 3')
|
||||
self.antenna_index = 0 # index of antenna in use
|
||||
self.delay_config = True # Delay sending message to HL2 until after sound starts
|
||||
self.TFRC_counter = 0 # Call for power etc. at intervals
|
||||
@@ -271,6 +278,7 @@ class Hardware(BaseHardware):
|
||||
self.hermes_ip = addr[0]
|
||||
self.hermes_code_version = data[9]
|
||||
self.hermes_board_id = data[10]
|
||||
self.is_HermesLite2 = self.hermes_board_id == 6 and self.conf.hardware_file_type == "Hermes"
|
||||
QS.set_hermes_id(data[9], data[10])
|
||||
if data[0x16] >> 6 == 0:
|
||||
QS.set_params(bandscopeScale = 2048)
|
||||
@@ -317,6 +325,7 @@ class Hardware(BaseHardware):
|
||||
bid = 6
|
||||
self.hermes_code_version = code
|
||||
self.hermes_board_id = bid
|
||||
self.is_HermesLite2 = self.hermes_board_id == 6 and self.conf.hardware_file_type == "Hermes"
|
||||
QS.set_hermes_id(code, bid)
|
||||
st = 'Capture from Hermes device at specified IP %s' % self.hermes_ip
|
||||
found = True
|
||||
@@ -331,9 +340,13 @@ class Hardware(BaseHardware):
|
||||
def open(self):
|
||||
self.delay_config = True # Delay sending message to HL2 until after sound starts
|
||||
# This list only changes control bits; no use of WriteQueue()
|
||||
for name in ('keyupDelay',
|
||||
'hermes_lowpwr_tr_enable', 'hermes_PWM', 'hermes_disable_sync', 'hermes_power_amp', 'Hermes_BandDictEnTx'):
|
||||
self.ImmediateChange(name)
|
||||
self.ImmediateChange('keyupDelay')
|
||||
self.ImmediateChange('Hermes_BandDictEnTx')
|
||||
if self.is_HermesLite2:
|
||||
for name in ('hermes_lowpwr_tr_enable', 'hermes_PWM', 'hermes_disable_sync', 'hermes_power_amp'):
|
||||
self.ImmediateChange(name)
|
||||
#else: # DEBUG: Turn on power amp for testing OpenHPSDR radio with my Hermes Lite2
|
||||
# self.SetControlBit(0x09, 19, 1)
|
||||
return self.config_text
|
||||
def GetValue(self, name): # return values stored in the hardware
|
||||
if name == 'Hware_Hl2_EepromIP':
|
||||
@@ -492,7 +505,15 @@ class Hardware(BaseHardware):
|
||||
self.key_was_down = key_down
|
||||
if self.TFRC_counter >= 3:
|
||||
self.TFRC_counter = 0
|
||||
self.hermes_temperature, self.hermes_fwd_power, self.hermes_rev_power, self.hermes_pa_current, self.hermes_fwd_peak, self.hermes_rev_peak = QS.get_hermes_TFRC()
|
||||
AIN1_avg, AIN1_peak, AIN2_avg, AIN2_peak, AIN3, AIN4, AIN5, AIN6 = QS.get_hermes_TFRC()
|
||||
if self.hermes_board_id == 6:
|
||||
(self.hermes_fwd_power, self.hermes_fwd_peak, self.hermes_rev_power, self.hermes_rev_peak,
|
||||
self.hermes_pa_current, self.hermes_temperature) = AIN1_avg, AIN1_peak, AIN2_avg, AIN2_peak, AIN3, AIN5
|
||||
else:
|
||||
(self.hermes_fwd_power, self.hermes_fwd_peak, self.hermes_rev_power, self.hermes_rev_peak,
|
||||
self.hermes_pa_current, self.hermes_temperature) = AIN1_avg, AIN1_peak, AIN2_avg, AIN2_peak, AIN3, AIN5
|
||||
if DEBUG_SENSORS:
|
||||
print("AIN 1-6 %4d %4d %4d %4d %4d %4d" % (AIN1_avg, AIN2_avg, AIN3, AIN4, AIN5, AIN6))
|
||||
if self.application.bottom_widgets:
|
||||
self.application.bottom_widgets.UpdateText()
|
||||
if self.delay_config and QS.get_params('rx_udp_started'):
|
||||
@@ -529,7 +550,9 @@ class Hardware(BaseHardware):
|
||||
def ChangeBandFilters(self):
|
||||
if not hasattr(self.application, "multi_rx_screen"):
|
||||
return # Needed for the VNA program
|
||||
self.SetControlBit(0x00, 13, self.antenna_index)
|
||||
self.SetControlBit(0x00, 13, self.antenna_index & 0x01)
|
||||
self.SetControlBit(0x00, 14, self.antenna_index & 0x02)
|
||||
self.SetControlBit(0x00, 15, self.antenna_index > 0)
|
||||
highest = self.band
|
||||
freq = self.conf.BandEdge.get(highest, (0, 0))[0]
|
||||
for pane in self.application.multi_rx_screen.receiver_list:
|
||||
@@ -606,12 +629,7 @@ class Hardware(BaseHardware):
|
||||
return (48000, 384000)
|
||||
## Hardware AGC is no longer supported in HL2 identifying as version >=40
|
||||
def ChangeAGC(self, value):
|
||||
if value:
|
||||
self.pc2hermes[2] |= 0x10 # C0 index == 0, C3[4]: AGC enable
|
||||
else:
|
||||
self.pc2hermes[2] &= ~0x10
|
||||
QS.pc_to_hermes(self.pc2hermes)
|
||||
if DEBUG: print ("Change AGC to", value)
|
||||
pass
|
||||
## Simpler LNA setting for HL2 identifying as version >=40, see HL2 wiki for details
|
||||
def ChangeLNA(self, value): # LNA for Rx
|
||||
# value is -12 to +48
|
||||
@@ -624,7 +642,7 @@ class Hardware(BaseHardware):
|
||||
value = 51 - value
|
||||
else:
|
||||
value = ((value+12) & 0x3f) | 0x40
|
||||
self.pc2hermes[4 * 10 + 3] = value # C0 index == 0x1010, C4[4:0] LNA 0-32 dB gain
|
||||
self.pc2hermes[4 * 10 + 3] = value # C0 index == 0b1010, C4[4:0] LNA 0-32 dB gain
|
||||
QS.pc_to_hermes(self.pc2hermes)
|
||||
if DEBUG: print ("Change LNA to", value)
|
||||
def ChangeTxLNA(self, value): # LNA for Tx
|
||||
@@ -634,9 +652,17 @@ class Hardware(BaseHardware):
|
||||
elif value > 48:
|
||||
value = 48
|
||||
value = ((value+12) & 0x3f) | 0x40 | 0x80
|
||||
self.SetControlByte(0x0e, 3, value, False) # C0 index == 0x0e, C3
|
||||
self.SetControlByte(0x0e, 3, value, False) # C0 index == 0b1110 == 0x0e, C3
|
||||
QS.pc_to_hermes(self.pc2hermes)
|
||||
if DEBUG: print ("Change Tx LNA to", value)
|
||||
def ChangeAtten(self, value):
|
||||
# value is 0 to 31
|
||||
self.pc2hermes[4 * 10 + 3] = value & 0x1F # C0 index == 0b1010, C4[4:0] attenuator, 0 to 31 dB
|
||||
self.SetControlBit(0b1010, 5, value) # enable attenuator
|
||||
QS.pc_to_hermes(self.pc2hermes)
|
||||
if DEBUG: print ("Change attenuator to", value)
|
||||
def ChangePreamp(self, value):
|
||||
self.SetControlBit(0b0000, 10, value) # enable preamp
|
||||
def SetTxLevel(self):
|
||||
try:
|
||||
tx_level = self.conf.tx_level[self.band]
|
||||
@@ -653,7 +679,7 @@ class Hardware(BaseHardware):
|
||||
tx_level = 0
|
||||
elif tx_level > 255:
|
||||
tx_level = 255
|
||||
self.pc2hermes[4 * 9] = tx_level # C0 index == 0x1001, C1[7:0] Tx level
|
||||
self.pc2hermes[4 * 9] = tx_level # C0 index == 0b1001, C1[7:0] Tx level
|
||||
QS.pc_to_hermes(self.pc2hermes)
|
||||
if DEBUG: print("Change tx_level to", tx_level)
|
||||
def MultiRxCount(self, count): # count == number of additional receivers besides the Tx/Rx receiver: 1, 2, 3
|
||||
@@ -876,6 +902,8 @@ class Hardware(BaseHardware):
|
||||
QS.set_hermeslite_writepointer(0)
|
||||
return False
|
||||
def WriteQueue(self, wait=False):
|
||||
if not self.is_HermesLite2:
|
||||
return False
|
||||
self._wait_queue()
|
||||
# Send next write
|
||||
self.old_writequeue = self.pc2hermeslitewritequeue[:]
|
||||
|
||||
@@ -154,6 +154,12 @@ class BottomWidgets: # Add extra widgets to the bottom of the screen
|
||||
current = " PA %4.0f ma" % (1000*current)
|
||||
self.text_pa_current.SetLabel(current)
|
||||
# forward and reverse peak power
|
||||
if not self.hardware.power_interpolator: # For no interpolator, show ADC
|
||||
fwd = self.hardware.hermes_fwd_power
|
||||
rev = self.hardware.hermes_rev_power
|
||||
self.text_fwd_power.SetLabel("Fwd ADC %4d" % fwd)
|
||||
self.text_swr.SetLabel("Rev ADC %4d" % rev)
|
||||
return
|
||||
fwd, rev = self.Code2FwdRevWatts(self.hardware.hermes_fwd_peak, self.hardware.hermes_rev_peak)
|
||||
# forward less reverse power
|
||||
power = fwd - rev
|
||||
|
||||
@@ -32,6 +32,8 @@ with this:
|
||||
*/
|
||||
|
||||
#include <Python.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
void ** Quisk_API; // array of pointers to functions and variables from module _quisk
|
||||
struct sound_conf * pt_quisk_sound_state; // pointer to quisk_sound_state
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <Python.h> // used by quisk.h
|
||||
#include <complex.h> // Used by quisk.h
|
||||
#include <string.h>
|
||||
#include "quisk.h"
|
||||
|
||||
// This module provides methods to access the state of the key.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+70
@@ -0,0 +1,70 @@
|
||||
# This provides helper functions for our makefile.
|
||||
|
||||
# Changes for MacOS support thanks to Mario, DL3LSM.
|
||||
# Changes for building from macports provided by Eric, KM4DSJ
|
||||
# Updated code for a Mac build contributed by Christoph, DL1YCF, December 2020.
|
||||
# Converted from setup.py to makefile by Jim Ahlstrom, June 2026
|
||||
|
||||
import sys, os, subprocess, sysconfig
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit(2)
|
||||
|
||||
job = sys.argv[1]
|
||||
|
||||
def flags ():
|
||||
# Fetch compiler flags and link flags from Python
|
||||
result = subprocess.run(["python3-config", "--cflags"], capture_output=True, text=True, check=True)
|
||||
cflags = result.stdout.strip()
|
||||
cflags += " -fPIC"
|
||||
result = subprocess.run(["python3-config", "--ldflags"], capture_output=True, text=True, check=True)
|
||||
ldflags = result.stdout.strip()
|
||||
ldflags += " -shared -lm -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-z,relro -g -fwrapv -O2 -lfftw3"
|
||||
if sys.platform == "darwin": # Build for Macintosh
|
||||
cflags += " -DQUISK_HAVE_PORTAUDIO "
|
||||
ldflags += " -lportaudio "
|
||||
if os.path.isdir('/opt/local/include'): # MacPorts
|
||||
base_dir = '/opt/local'
|
||||
elif os.path.isdir('/usr/local/include'): # HomeBrew on macOS Intel
|
||||
base_dir = '/usr/local'
|
||||
elif os.path.isdir('/opt/homebrew/include'): # HomeBrew on Apple Silicon
|
||||
base_dir = '/opt/homebrew'
|
||||
else: # Regular build?
|
||||
base_dir = '/usr'
|
||||
if os.path.isfile(base_dir + "/include/pulse/pulseaudio.h"):
|
||||
cflags += " -DQUISK_HAVE_PULSEAUDIO "
|
||||
ldflags += " -lpulse "
|
||||
cflags += " -I. -I%s/include " % base_dir
|
||||
ldflags += " -L. -L%s/lib " % base_dir
|
||||
elif "freebsd" in sys.platform: #Build for FreeBSD
|
||||
cflags += " -DQUISK_HAVE_PULSEAUDIO -I. -I/usr/local/include"
|
||||
ldflags += " -lpulse -L. -L/usr/local/lib"
|
||||
else: # Linux
|
||||
cflags += " -DQUISK_HAVE_ALSA -DQUISK_HAVE_PULSEAUDIO"
|
||||
ldflags += " -lasound -lpulse"
|
||||
if os.path.isfile("/usr/include/portaudio.h"):
|
||||
cflags += " -DQUISK_HAVE_PORTAUDIO"
|
||||
ldflags += " -lportaudio"
|
||||
return cflags, ldflags
|
||||
|
||||
if job == "--soapy":
|
||||
# See if soapy is available
|
||||
if os.path.isdir("/usr/include/SoapySDR") or os.path.isdir("/usr/local/include/SoapySDR"):
|
||||
print('1')
|
||||
else:
|
||||
print('0')
|
||||
|
||||
elif job == "--suffix":
|
||||
# Determine the platform-specific extension suffix (e.g., .cpython-311-x86_64-linux-gnu.so)
|
||||
print(sysconfig.get_config_var('EXT_SUFFIX'))
|
||||
|
||||
elif job == "--cflags":
|
||||
cc, ld = flags()
|
||||
print(cc)
|
||||
|
||||
elif job == "--ldflags":
|
||||
cc, ld = flags()
|
||||
print(ld)
|
||||
|
||||
else:
|
||||
sys.exit(1)
|
||||
@@ -1,18 +1,93 @@
|
||||
.PHONY: quisk
|
||||
# This makefile replaces the old Python "setup.py build_ext" which has been deprecated.
|
||||
# See "quisk-old" below to make Quisk using the old setup.py.
|
||||
|
||||
quisk:
|
||||
MODULE_NAME := _quisk
|
||||
OBJDIR := build/local
|
||||
SUBDIRS := afedrinet
|
||||
|
||||
ifeq ($(shell python3 makefil.py --soapy), 1)
|
||||
SUBDIRS := $(SUBDIRS) soapypkg
|
||||
endif
|
||||
|
||||
SRCS := \
|
||||
base64.c \
|
||||
extdemod.c \
|
||||
filter.c \
|
||||
freedv.c \
|
||||
handshake.c \
|
||||
is_key_down.c \
|
||||
microphone.c \
|
||||
quisk.c \
|
||||
quisk_wdsp.c \
|
||||
sha1.c \
|
||||
sound.c\
|
||||
sound_alsa.c \
|
||||
sound_directx.c \
|
||||
sound_portaudio.c \
|
||||
sound_pulseaudio.c \
|
||||
sound_wasapi.c \
|
||||
tci.c \
|
||||
utf8.c \
|
||||
utility.c \
|
||||
ws.c
|
||||
|
||||
CFLAGS := $(shell python3 makefil.py --cflags)
|
||||
LDFLAGS := $(shell python3 makefil.py --ldflags)
|
||||
EXTENSION_SUFFIX := $(shell python3 makefil.py --suffix)
|
||||
TARGET := $(MODULE_NAME)$(EXTENSION_SUFFIX)
|
||||
|
||||
#$(info Info CFLAGS $(CFLAGS))
|
||||
#$(info Info LDFLAGS $(LDFLAGS))
|
||||
#$(info Info TARGET $(TARGET))
|
||||
#$(info Info SUBDIRS $(SUBDIRS))
|
||||
|
||||
OBJS := $(patsubst %.c, $(OBJDIR)/%.o, $(SRCS))
|
||||
|
||||
.PHONY: all clean test
|
||||
.PHONY: $(SUBDIRS)
|
||||
|
||||
# Default target
|
||||
all: $(OBJDIR) $(OBJDIR)/import_quisk_api.o ac2yd/remote.o $(TARGET) $(SUBDIRS)
|
||||
|
||||
$(SUBDIRS):
|
||||
$(MAKE) -C $@
|
||||
|
||||
$(OBJDIR):
|
||||
@mkdir -p $(OBJDIR)
|
||||
|
||||
# Link the shared library object
|
||||
$(TARGET): $(OBJS)
|
||||
$(CC) -o $@ $(OBJS) ac2yd/remote.o $(LDFLAGS)
|
||||
|
||||
# Define the location for dependency files
|
||||
DEPS = $(patsubst %.o,$(OBJDIR)/%.d,$(notdir $(OBJS)))
|
||||
DEPFLAGS = -MMD -MP -MF $(OBJDIR)/$(notdir $*).d
|
||||
-include $(DEPS)
|
||||
|
||||
# Compile the C source files into object files
|
||||
ac2yd/remote.o: ac2yd/remote.c quisk.h filter.h
|
||||
$(CC) -o $@ -c $< $(CFLAGS) $(DEPFLAGS)
|
||||
|
||||
$(OBJDIR)/import_quisk_api.o: import_quisk_api.c
|
||||
$(CC) -o $@ -c $< $(CFLAGS) $(DEPFLAGS)
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) -o $@ -c $< $(CFLAGS) $(DEPFLAGS)
|
||||
|
||||
clean:
|
||||
-rm -f $(OBJDIR)/*.o $(OBJDIR)/*.d
|
||||
-rm -f ac2yd/*.o
|
||||
-rm -f $(MODULE_NAME)*.so
|
||||
for dir in $(SUBDIRS); do \
|
||||
$(MAKE) -C $$dir clean; \
|
||||
done
|
||||
|
||||
# Run a test script to verify that the module can be imported
|
||||
test: $(TARGET)
|
||||
@python3 -c "import $(MODULE_NAME); print('Successfully imported:', $(MODULE_NAME).__file__)"
|
||||
|
||||
|
||||
|
||||
.PHONY: quisk-old
|
||||
quisk-old:
|
||||
python3 setup.py build_ext --force --inplace
|
||||
@echo
|
||||
|
||||
quisk3:
|
||||
python3 setup.py build_ext --force --inplace
|
||||
@echo
|
||||
|
||||
soapy3:
|
||||
(cd soapypkg; make soapy3)
|
||||
|
||||
afedrinet3:
|
||||
(cd afedrinet; make afedrinet3)
|
||||
|
||||
perseus3:
|
||||
(cd perseuspkg; make perseus3)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <math.h>
|
||||
#include <sys/time.h>
|
||||
#include <complex.h>
|
||||
#include <string.h>
|
||||
#include <fftw3.h>
|
||||
#include "quisk.h"
|
||||
#include <sys/types.h>
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# This is the hardware control file for my shack.
|
||||
# It is for the Hermes-Lite2 5 watt output which uses only the antenna tuner.
|
||||
from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
# It is for the Hermes-Lite2 RF1 output and Alt Rx input.
|
||||
|
||||
from hermes.quisk_hardware import Hardware as BaseHw
|
||||
from n2adr import station_hardware
|
||||
@@ -17,7 +14,7 @@ class Hardware(BaseHw):
|
||||
self.controlbox = station_hardware.ControlBox(app, conf) # Control my Station Control Box
|
||||
self.v2filter = station_hardware.FilterBoxV2(app, conf) # Control V2 filter box
|
||||
def open(self):
|
||||
if False:
|
||||
if False: # Start the station control GUI
|
||||
from n2adr.station_hardware import StationControlGUI
|
||||
self.GUI = StationControlGUI(self.application.main_frame, self, self.application, self.conf)
|
||||
self.GUI.Show()
|
||||
@@ -29,12 +26,11 @@ class Hardware(BaseHw):
|
||||
return BaseHw.close(self)
|
||||
def ChangeFilterFrequency(self, tx_freq):
|
||||
if tx_freq and tx_freq > 0:
|
||||
self.v2filter.SetTxFreq(tx_freq)
|
||||
if self.GUI:
|
||||
self.GUI.SetTxFreq(tx_freq)
|
||||
self.GUI.freq_entry.ChangeValue("%.3f" % (tx_freq * 1E-6))
|
||||
else:
|
||||
self.anttuner.SetTxFreq(tx_freq)
|
||||
self.v2filter.SetTxFreq(tx_freq)
|
||||
def ChangeFrequency(self, tx_freq, vfo_freq, source='', band='', event=None):
|
||||
self.ChangeFilterFrequency(tx_freq)
|
||||
return BaseHw.ChangeFrequency(self, tx_freq, vfo_freq, source, band, event)
|
||||
@@ -42,8 +38,6 @@ class Hardware(BaseHw):
|
||||
# band is a string: "60", "40", "WWV", etc.
|
||||
ret = BaseHw.ChangeBand(self, band)
|
||||
self.anttuner.ChangeBand(band)
|
||||
#self.lpfilter.ChangeBand(band)
|
||||
#self.hpfilter.ChangeBand(band)
|
||||
self.v2filter.ChangeBand(band)
|
||||
self.CorrectSmeter()
|
||||
return ret
|
||||
|
||||
+4
-50
@@ -1,56 +1,10 @@
|
||||
# Please do not change this widgets module for Quisk. Instead copy
|
||||
# it to your own quisk_widgets.py and make changes there.
|
||||
#
|
||||
# This module is used to add extra widgets to the QUISK screen.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
|
||||
import wx, time
|
||||
import _quisk as QS
|
||||
|
||||
class BottomWidgets: # Add extra widgets to the bottom of the screen
|
||||
from hermes.quisk_widgets import BottomWidgets as BaseWidgets
|
||||
|
||||
class BottomWidgets(BaseWidgets): # Add extra widgets to the bottom of the screen
|
||||
def __init__(self, app, hardware, conf, frame, gbs, vertBox):
|
||||
#self.config = conf
|
||||
#self.hardware = hardware
|
||||
#self.application = app
|
||||
#start_row = app.widget_row # The first available row
|
||||
#start_col = app.button_start_col # The start of the button columns
|
||||
#b = app.QuiskCycleCheckbutton(frame, self.OnAntTuner, ('Antenna', 'Ant 0', 'Ant 1'))
|
||||
#bw, bh = b.GetMinSize()
|
||||
#gbs.Add(b, (start_row, start_col), (1, 2), flag=wx.EXPAND)
|
||||
#b = app.QuiskPushbutton(frame, self.OnAntTuner, 'L+')
|
||||
#b.Enable(0)
|
||||
#gbs.Add(b, (start_row, start_col + 2), (1, 2), flag=wx.EXPAND)
|
||||
#b = app.QuiskPushbutton(frame, self.OnAntTuner, 'L-')
|
||||
#b.Enable(0)
|
||||
#gbs.Add(b, (start_row, start_col + 4), (1, 2), flag=wx.EXPAND)
|
||||
#b = app.QuiskPushbutton(frame, self.OnAntTuner, 'C+')
|
||||
#b.Enable(0)
|
||||
#gbs.Add(b, (start_row, start_col + 6), (1, 2), flag=wx.EXPAND)
|
||||
#b = app.QuiskPushbutton(frame, self.OnAntTuner, 'C-')
|
||||
#b.Enable(0)
|
||||
#gbs.Add(b, (start_row, start_col + 8), (1, 2), flag=wx.EXPAND)
|
||||
#b = app.QuiskPushbutton(frame, self.OnAntTuner, 'Save')
|
||||
#b.Enable(0)
|
||||
#gbs.Add(b, (start_row, start_col + 10), (1, 2), flag=wx.EXPAND)
|
||||
#self.swr_label = app.QuiskText(frame, 'Watts 000 SWR 10.1 Zh Ind 22 Cap 33 Freq 28100 (7777)', bh)
|
||||
#gbs.Add(self.swr_label, (start_row, start_col + 2), (1, 10), flag=wx.EXPAND)
|
||||
#b = app.QuiskCheckbutton(frame, None, text='')
|
||||
#gbs.Add(b, (start_row, start_col + 25), (1, 2), flag=wx.EXPAND)
|
||||
# Example of a horizontal slider:
|
||||
# lab = wx.StaticText(frame, -1, 'Preamp', style=wx.ALIGN_CENTER)
|
||||
# gbs.Add(lab, (5,0), flag=wx.EXPAND)
|
||||
# sl = wx.Slider(frame, -1, 1024, 0, 2048) # parent, -1, initial, min, max
|
||||
# gbs.Add(sl, (5,1), (1, 5), flag=wx.EXPAND)
|
||||
# sl.Bind(wx.EVT_SCROLL, self.OnPreamp)
|
||||
# def OnPreamp(self, event):
|
||||
# print event.GetPosition()
|
||||
self.num_rows_added = 0
|
||||
#def OnAntTuner(self, event):
|
||||
# btn = event.GetEventObject()
|
||||
# text = btn.GetLabel()
|
||||
# self.hardware.OnAntTuner(text)
|
||||
#def UpdateText(self, text):
|
||||
# self.swr_label.SetLabel(text)
|
||||
BaseWidgets.__init__(self, app, hardware, conf, frame, gbs, vertBox)
|
||||
|
||||
+85
-123
@@ -1,9 +1,5 @@
|
||||
# This file supports various hardware boxes at my shack
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
|
||||
import sys, struct, math, socket, select, time, traceback, os
|
||||
import wx, wx.lib.buttons
|
||||
|
||||
@@ -411,18 +407,15 @@ class AntennaTuner: # Control my homebrew antenna tuner and my KI8BV dipole
|
||||
self.set_HighZ = 0
|
||||
self.antnum = 0 # Antenna number 0 or 1
|
||||
self.dipole2 = AntennaControl(app, conf) # Control the KI8BV dipole
|
||||
if False and conf.use_rx_udp == 10: # Hermes UDP protocol for Hermes-Lite2
|
||||
path = 'TunerLCZ_HL2.txt'
|
||||
else:
|
||||
path = 'TunerLCZ.txt'
|
||||
path = 'TunerLCZ.txt'
|
||||
if sys.platform == "win32":
|
||||
path = 'C:/pub/' + path
|
||||
self.path = 'C:/pub/' + path
|
||||
else:
|
||||
path = '/home/jim/pub/' + path
|
||||
fp = open(path, "r")
|
||||
self.path = '/home/jim/pub/' + path
|
||||
fp = open(self.path, "r")
|
||||
lines = fp.readlines()
|
||||
fp.close()
|
||||
self.TunerLCZ = [(0, 0, 0, 0), (999111000, 0, 0, 0)] # Add dummy first and last entry.
|
||||
self.TunerLCZ = []
|
||||
for line in lines:
|
||||
freq, antL, antC, hilo = line.split()
|
||||
freq = int(freq)
|
||||
@@ -451,7 +444,12 @@ class AntennaTuner: # Control my homebrew antenna tuner and my KI8BV dipole
|
||||
pass
|
||||
def ChangeBand(self, band):
|
||||
pass
|
||||
def FindLCZ(self, tx_freq):
|
||||
def SaveLCZ(self):
|
||||
fp = open(self.path, 'w')
|
||||
for tup in self.TunerLCZ:
|
||||
fp.write("%12d %2d %2d %d\n" % tup)
|
||||
fp.close()
|
||||
def FindIndexTup(self, tx_freq):
|
||||
i1 = 0
|
||||
i2 = len(self.TunerLCZ) - 1
|
||||
while 1: # binary partition
|
||||
@@ -462,20 +460,14 @@ class AntennaTuner: # Control my homebrew antenna tuner and my KI8BV dipole
|
||||
i2 = i
|
||||
if i2 - i1 <= 1:
|
||||
break
|
||||
# The correct setting is between i1 and i2.
|
||||
# The correct setting is between i1 and i2, or below i1 or above i2.
|
||||
# Choose the index that is closest in frequency.
|
||||
delta1 = tx_freq - self.TunerLCZ[i1][0]
|
||||
delta2 = self.TunerLCZ[i2][0] - tx_freq
|
||||
if i1 == 0: # below the first frequency
|
||||
if abs(tx_freq - self.TunerLCZ[i1][0]) < abs(tx_freq - self.TunerLCZ[i2][0]):
|
||||
index = i1
|
||||
elif i2 == len(self.TunerLCZ) - 1: # above the last frequency
|
||||
else:
|
||||
index = i2
|
||||
elif delta2 < delta1: # i2 is closer
|
||||
index = i2
|
||||
else: # i1 is closer
|
||||
index = i1
|
||||
if DEBUG: print ("AntennaTuner FindLCZ", tx_freq, i1, i2, len(self.TunerLCZ), self.TunerLCZ[index])
|
||||
return self.TunerLCZ[index]
|
||||
if DEBUG: print ("StatuinControlGUI FindIndexTup", tx_freq, i1, i2, len(self.TunerLCZ), index, self.TunerLCZ[index])
|
||||
return index, self.TunerLCZ[index]
|
||||
def SetTxFreq(self, tx_freq, no_tune=False):
|
||||
#if DEBUG: print("AntennaTuner SetTxFreq", tx_freq)
|
||||
self.tx_freq = tx_freq
|
||||
@@ -484,10 +476,10 @@ class AntennaTuner: # Control my homebrew antenna tuner and my KI8BV dipole
|
||||
if tx_freq < 17000000:
|
||||
if DEBUG and self.antnum == 1: print ("antnum 0")
|
||||
self.antnum = 0
|
||||
else:
|
||||
elif self.dipole2:
|
||||
if DEBUG and self.antnum == 0: print ("antnum 1")
|
||||
self.antnum = 1
|
||||
if self.antnum == 1:
|
||||
if self.antnum == 1 and self.dipole2:
|
||||
self.dipole2.SetTxFreq(tx_freq)
|
||||
if not self.socket:
|
||||
return
|
||||
@@ -496,7 +488,7 @@ class AntennaTuner: # Control my homebrew antenna tuner and my KI8BV dipole
|
||||
if abs(self.tune_freq - tx_freq) < 5000: # ignore small changes
|
||||
return
|
||||
self.tune_freq = tx_freq
|
||||
f, newL, newC, newH = self.FindLCZ(tx_freq)
|
||||
index, (f, newL, newC, newH) = self.FindIndexTup(tx_freq)
|
||||
if newH != self.set_HighZ or self.set_C != newC or self.set_L != newL:
|
||||
self.set_HighZ = newH
|
||||
self.set_C = newC
|
||||
@@ -521,7 +513,8 @@ class AntennaTuner: # Control my homebrew antenna tuner and my KI8BV dipole
|
||||
self.timer = 0
|
||||
else:
|
||||
self.timer = 0
|
||||
self.dipole2.HeartBeat()
|
||||
if self.dipole2:
|
||||
self.dipole2.HeartBeat()
|
||||
def Send(self):
|
||||
try:
|
||||
self.socket.send(self.want_data)
|
||||
@@ -539,26 +532,7 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
|
||||
self.conf = conf
|
||||
self.data_saving = False
|
||||
self.tx_freq = 0
|
||||
if sys.platform == "win32":
|
||||
self.filename = 'C:/pub/TunerLCZ.tmp'
|
||||
else:
|
||||
self.filename = '/home/jim/pub/TunerLCZ.tmp'
|
||||
try:
|
||||
fp = open(self.filename, "r")
|
||||
except FileNotFoundError:
|
||||
lines = ()
|
||||
else:
|
||||
lines = fp.readlines()
|
||||
fp.close()
|
||||
self.TunerLCZ = [(0, 0, 0, 0), (999111000, 0, 0, 0)] # Add dummy first and last entry.
|
||||
for line in lines:
|
||||
freq, antL, antC, hilo = line.split()
|
||||
freq = int(freq)
|
||||
antL = int(antL)
|
||||
antC = int(antC)
|
||||
hilo = int(hilo)
|
||||
self.TunerLCZ.append((freq, antL, antC, hilo))
|
||||
self.TunerLCZ.sort()
|
||||
self.anttuner = self.hware.anttuner
|
||||
self.SetBackgroundColour('light steel blue')
|
||||
self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyDown)
|
||||
sizer = wx.GridBagSizer(hgap=5, vgap=3)
|
||||
@@ -568,6 +542,7 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
|
||||
self.bands = []
|
||||
for band in ('160', '80', '60', '40', '30', '20', '17', '15', '12', '10'):
|
||||
b = wx.lib.buttons.GenToggleButton(self, -1, band)
|
||||
b.Enable(False)
|
||||
b.SetUseFocusIndicator(False)
|
||||
b.SetBezelWidth(4)
|
||||
self.bands.append(b)
|
||||
@@ -604,6 +579,7 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
|
||||
szr = wx.BoxSizer(wx.HORIZONTAL)
|
||||
szr.AddSpacer(20)
|
||||
b = wx.lib.buttons.GenToggleButton(self, -1, "Key Down")
|
||||
b.Enable(False)
|
||||
b.SetFont(font)
|
||||
b.SetUseFocusIndicator(False)
|
||||
b.SetBezelWidth(4)
|
||||
@@ -611,6 +587,7 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
|
||||
szr.Add(b)
|
||||
szr.AddSpacer(10)
|
||||
b = wx.lib.buttons.GenToggleButton(self, -1, "FilterV2 Preamp")
|
||||
b.Enable(False)
|
||||
b.SetFont(font)
|
||||
b.SetUseFocusIndicator(False)
|
||||
b.SetBezelWidth(4)
|
||||
@@ -622,16 +599,31 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
|
||||
b.SetFont(font)
|
||||
szr.Add(b, flag=wx.ALIGN_CENTER_VERTICAL)
|
||||
szr.AddSpacer(10)
|
||||
self.freq_entry = wx.TextCtrl(self, value='0000.000', style=wx.TE_PROCESS_ENTER)
|
||||
self.freq_entry = wx.StaticText(self, -1, "0000.000")
|
||||
#self.freq_entry = wx.TextCtrl(self, value='0000.000', style=wx.TE_PROCESS_ENTER)
|
||||
self.freq_entry.SetFont(font)
|
||||
self.Bind(wx.EVT_TEXT_ENTER, self.OnFreqEntry, self.freq_entry)
|
||||
szr.Add(self.freq_entry)
|
||||
#self.Bind(wx.EVT_TEXT_ENTER, self.OnFreqEntry, self.freq_entry)
|
||||
szr.Add(self.freq_entry, flag=wx.ALIGN_CENTER_VERTICAL)
|
||||
szr.AddSpacer(10)
|
||||
b = wx.lib.buttons.GenButton(self, -1, "Search Freq")
|
||||
b = wx.lib.buttons.GenButton(self, -1, "Freq-")
|
||||
b.SetUseFocusIndicator(False)
|
||||
b.SetBezelWidth(4)
|
||||
b.SetFont(font)
|
||||
self.Bind(wx.EVT_BUTTON, self.OnButtonSearch, b)
|
||||
self.Bind(wx.EVT_BUTTON, self.OnButtonDown, b)
|
||||
szr.Add(b)
|
||||
szr.AddSpacer(10)
|
||||
b = wx.lib.buttons.GenButton(self, -1, "Freq+")
|
||||
b.SetUseFocusIndicator(False)
|
||||
b.SetBezelWidth(4)
|
||||
b.SetFont(font)
|
||||
self.Bind(wx.EVT_BUTTON, self.OnButtonUp, b)
|
||||
szr.Add(b)
|
||||
szr.AddSpacer(10)
|
||||
b = wx.lib.buttons.GenButton(self, -1, "Delete")
|
||||
b.SetUseFocusIndicator(False)
|
||||
b.SetBezelWidth(4)
|
||||
b.SetFont(font)
|
||||
self.Bind(wx.EVT_BUTTON, self.OnButtonDelete, b)
|
||||
szr.Add(b)
|
||||
sizer.Add(szr, pos=(row, 0), span=(1, 3))
|
||||
if frame is None:
|
||||
@@ -647,46 +639,22 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
|
||||
self.Bind(wx.EVT_TIMER, self.HeartBeat)
|
||||
self.timer.Start(100)
|
||||
def SetTxFreq(self, tx_freq):
|
||||
#print("StationControlGUI SetTxFreq", tx_freq)
|
||||
tx_freq = ((tx_freq + 500) // 1000) * 1000
|
||||
self.tx_freq = tx_freq
|
||||
index, tup = self.FindIndexTup(tx_freq)
|
||||
anttuner = self.anttuner
|
||||
index, tup = anttuner.FindIndexTup(tx_freq)
|
||||
self.sliderL.SetValue(tup[1])
|
||||
self.sliderC.SetValue(tup[2])
|
||||
self.btnHiLo.SetValue(tup[3])
|
||||
anttuner = self.hware.anttuner
|
||||
fff, anttuner.set_L, anttuner.set_C, anttuner.set_HighZ = tup
|
||||
if tx_freq < 17000000:
|
||||
anttuner.antnum = 0
|
||||
else:
|
||||
elif anttuner.dipole2:
|
||||
anttuner.antnum = 1
|
||||
if anttuner.antnum == 1:
|
||||
if anttuner.antnum == 1 and anttuner.dipole2:
|
||||
anttuner.dipole2.SetTxFreq(tx_freq)
|
||||
anttuner.WantData()
|
||||
def FindIndexTup(self, tx_freq):
|
||||
i1 = 0
|
||||
i2 = len(self.TunerLCZ) - 1
|
||||
while 1: # binary partition
|
||||
i = (i1 + i2) // 2
|
||||
if self.TunerLCZ[i][0] < tx_freq:
|
||||
i1 = i
|
||||
else:
|
||||
i2 = i
|
||||
if i2 - i1 <= 1:
|
||||
break
|
||||
# The correct setting is between i1 and i2.
|
||||
# Choose the index that is closest in frequency.
|
||||
delta1 = tx_freq - self.TunerLCZ[i1][0]
|
||||
delta2 = self.TunerLCZ[i2][0] - tx_freq
|
||||
if i1 == 0: # below the first frequency
|
||||
index = i1
|
||||
elif i2 == len(self.TunerLCZ) - 1: # above the last frequency
|
||||
index = i2
|
||||
elif delta2 < delta1: # i2 is closer
|
||||
index = i2
|
||||
else: # i1 is closer
|
||||
index = i1
|
||||
if DEBUG: print ("StatuinControlGUI FindLCZ", tx_freq, i1, i2, len(self.TunerLCZ), index, self.TunerLCZ[index])
|
||||
return index, self.TunerLCZ[index]
|
||||
self.freq_entry.SetLabel("%.3f" % (tx_freq * 1E-6))
|
||||
def OnBtnClose(self, event):
|
||||
self.hware.anttuner.close()
|
||||
self.hware.controlbox.close()
|
||||
@@ -730,16 +698,15 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
|
||||
f = ((f + 500) // 1000) * 1000
|
||||
if self.hware.v2filter:
|
||||
self.hware.v2filter.SetTxFreq(f)
|
||||
self.hware.anttuner.SetTxFreq(f, no_tune=True)
|
||||
self.anttuner.SetTxFreq(f, no_tune=True)
|
||||
self.freq_entry.ChangeValue("%.3f" % (f * 1E-6))
|
||||
self.SetTxFreq(f)
|
||||
def OnButtonHiLo(self, event):
|
||||
anttuner = self.hware.anttuner
|
||||
if self.btnHiLo.GetValue():
|
||||
anttuner.set_HighZ = 1
|
||||
self.anttuner.set_HighZ = 1
|
||||
else:
|
||||
anttuner.set_HighZ = 0
|
||||
anttuner.WantData()
|
||||
self.anttuner.set_HighZ = 0
|
||||
self.anttuner.WantData()
|
||||
def OnFreqEntry(self, event=None):
|
||||
freq = self.freq_entry.GetValue()
|
||||
freq = float(freq) * 1E6
|
||||
@@ -751,11 +718,27 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
|
||||
freq = int(freq + 0.1)
|
||||
self.tx_freq = freq
|
||||
self.SetTxFreq(self.tx_freq)
|
||||
def OnButtonDown(self, event):
|
||||
index, tup = self.anttuner.FindIndexTup(self.tx_freq)
|
||||
index -= 1
|
||||
if index >= 0:
|
||||
self.SetTxFreq(self.anttuner.TunerLCZ[index][0])
|
||||
def OnButtonUp(self, event):
|
||||
index, tup = self.anttuner.FindIndexTup(self.tx_freq)
|
||||
index += 1
|
||||
if index < len(self.anttuner.TunerLCZ):
|
||||
self.SetTxFreq(self.anttuner.TunerLCZ[index][0])
|
||||
def OnButtonDelete(self, event):
|
||||
index, tup = self.anttuner.FindIndexTup(self.tx_freq)
|
||||
if self.tx_freq == self.anttuner.TunerLCZ[index][0]:
|
||||
del self.anttuner.TunerLCZ[index]
|
||||
if index > 0:
|
||||
self.SetTxFreq(self.anttuner.TunerLCZ[index - 1][0])
|
||||
elif len(self.anttuner.TunerLCZ) > 0:
|
||||
self.SetTxFreq(self.anttuner.TunerLCZ[0][0])
|
||||
else:
|
||||
self.SetTxFreq(0)
|
||||
def OnButtonSave(self, event):
|
||||
freq = self.freq_entry.GetValue()
|
||||
freq = float(freq) * 1E6
|
||||
freq = int(freq + 0.1)
|
||||
self.tx_freq = freq
|
||||
L = self.sliderL.GetValue()
|
||||
C = self.sliderC.GetValue()
|
||||
if self.btnHiLo.GetValue():
|
||||
@@ -763,22 +746,19 @@ class StationControlGUI(wx.Frame): # Display a stand-alone control window for
|
||||
else:
|
||||
hilo = 0
|
||||
tup = (self.tx_freq, L, C, hilo)
|
||||
index, t = self.FindIndexTup(self.tx_freq)
|
||||
if self.tx_freq == self.TunerLCZ[index][0]:
|
||||
self.TunerLCZ[index] = tup
|
||||
index, t = self.anttuner.FindIndexTup(self.tx_freq)
|
||||
if self.tx_freq == self.anttuner.TunerLCZ[index][0]:
|
||||
self.anttuner.TunerLCZ[index] = tup
|
||||
else:
|
||||
self.TunerLCZ.append(tup)
|
||||
self.TunerLCZ.sort()
|
||||
fp = open(self.filename, 'w')
|
||||
for tup in self.TunerLCZ[1:-1]: # Throw away dummy entries
|
||||
fp.write("%12d %2d %2d %d\n" % tup)
|
||||
fp.close()
|
||||
self.anttuner.TunerLCZ.append(tup)
|
||||
self.anttuner.TunerLCZ.sort()
|
||||
self.anttuner.SaveLCZ()
|
||||
def OnSliderL(self, event=None):
|
||||
self.hware.anttuner.set_L = self.sliderL.GetValue()
|
||||
self.hware.anttuner.WantData()
|
||||
self.anttuner.set_L = self.sliderL.GetValue()
|
||||
self.anttuner.WantData()
|
||||
def OnSliderC(self, event=None):
|
||||
self.hware.anttuner.set_C = self.sliderC.GetValue()
|
||||
self.hware.anttuner.WantData()
|
||||
self.anttuner.set_C = self.sliderC.GetValue()
|
||||
self.anttuner.WantData()
|
||||
def OnBtnV2Preamp(self, event):
|
||||
if self.hware.v2filter:
|
||||
self.hware.v2filter.SetPreamp(event.GetEventObject().GetValue())
|
||||
@@ -1072,21 +1052,3 @@ class AT200PC: # Control an AT-200PC autotuner made by LDG
|
||||
self.set_C += 1
|
||||
elif text == 'C-':
|
||||
self.set_C -= 1
|
||||
|
||||
class App(wx.App):
|
||||
def OnInit(self):
|
||||
if sys.path[0] != "'.'": # Make sure the current working directory is on path
|
||||
sys.path.insert(0, '.')
|
||||
import quisk_conf_defaults as conf
|
||||
import quisk_hardware_model as hardware
|
||||
self.bottom_widgets = None
|
||||
hardware.anttuner = AntennaTuner(self, conf) # Control the antenna tuner
|
||||
hardware.v2filter = FilterBoxV2(self, conf) # Control V2 filter box
|
||||
hardware.controlbox = ControlBox(self, conf) # Control my Station Control Box
|
||||
self.main_frame = frame = StationControlGUI (None, hardware, self, conf)
|
||||
frame.Open()
|
||||
frame.Show()
|
||||
return True
|
||||
|
||||
if __name__ == '__main__':
|
||||
App().MainLoop()
|
||||
|
||||
+21
-3
@@ -1,7 +1,25 @@
|
||||
MODULE_NAME := perseus
|
||||
OBJDIR := ../build/local
|
||||
|
||||
perseus2:
|
||||
python2 setup.py build_ext --force --inplace
|
||||
CFLAGS := $(shell python3 ../makefil.py --cflags)
|
||||
LDFLAGS := $(shell python3 ../makefil.py --ldflags)
|
||||
EXTENSION_SUFFIX := $(shell python3 ../makefil.py --suffix)
|
||||
TARGET := $(MODULE_NAME)$(EXTENSION_SUFFIX)
|
||||
|
||||
perseus3:
|
||||
# Default target: Link the shared library object
|
||||
$(TARGET): perseus.o $(OBJDIR)/import_quisk_api.o
|
||||
$(CC) -o $@ perseus.o $(LDFLAGS) $(OBJDIR)/import_quisk_api.o
|
||||
|
||||
# Compile the C source files into object files
|
||||
perseus.o: perseus.c ../quisk.h ../filter.h
|
||||
$(CC) -o $@ -c $< $(CFLAGS) -I..
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-rm -f *.o
|
||||
-rm -f $(TARGET)
|
||||
|
||||
.PHONY: perseus-old
|
||||
perseus-old:
|
||||
python3 setup.py build_ext --force --inplace
|
||||
|
||||
|
||||
@@ -297,11 +297,11 @@ static PyObject * open_device(PyObject * self, PyObject * args)
|
||||
|
||||
|
||||
main_cleanup:
|
||||
return PyString_FromString("ERROR");
|
||||
return PyUnicode_FromString("ERROR");
|
||||
|
||||
exit_success:
|
||||
|
||||
return PyString_FromString(buf128);
|
||||
return PyUnicode_FromString(buf128);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ from quisk_hardware_model import Hardware as BaseHardware
|
||||
DEBUG = 1
|
||||
|
||||
# Define the name of the hardware and the items on the hardware screen (see quisk_conf_defaults.py):
|
||||
################ Receivers PerseusSDR, The PerseusSDR interface to multiple hardware SDRs.
|
||||
################ Receivers PerseusSDR, The PerseusSDR interface to multiple SDRs.
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
#hardware_file_name = 'perseuspkg/quisk_hardware.py'
|
||||
|
||||
@@ -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 = ["."]
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -218,14 +219,17 @@ static unsigned char quisk_hermes_to_pc[5 * 4]; // data received from the Herme
|
||||
static unsigned char quisk_hermeslite_response[5]; // response from Hermes-Lite commands
|
||||
unsigned int quisk_hermes_code_version = -1; // code version returned by the Hermes hardware
|
||||
unsigned int quisk_hermes_board_id = -1; // board ID returned by the Hermes hardware
|
||||
static double hermes_temperature; // average temperature
|
||||
static double hermes_fwd_power; // average forward power
|
||||
static double hermes_rev_power; // average reverse power
|
||||
static double hermes_fwd_peak; // peak forward power
|
||||
static double hermes_rev_peak; // peak reverse power
|
||||
static double hermes_pa_current; // average power amp current
|
||||
static int hermes_count_temperature; // number of temperature samples
|
||||
static int hermes_count_current; // number of current samples
|
||||
static double hpsdr_AIN1_avg; // average forward power
|
||||
static double hpsdr_AIN1_peak; // peak forward power
|
||||
static double hpsdr_AIN2_avg; // average reverse power
|
||||
static double hpsdr_AIN2_peak; // peak reverse power
|
||||
static double hpsdr_AIN3; // average power amp current
|
||||
static double hpsdr_AIN4;
|
||||
static double hpsdr_AIN5; // average temperature
|
||||
static double hpsdr_AIN6;
|
||||
static int hpsdr_AIN3_count; // number of current samples
|
||||
static int hpsdr_AIN4_count; // number of current samples
|
||||
static int hpsdr_AIN5_count; // number of current samples
|
||||
static int hardware_ptt; // hardware PTT switch
|
||||
|
||||
int quisk_hardware_cwkey; // hardware CW key from UDP or USB
|
||||
@@ -3136,31 +3140,43 @@ static PyObject * get_hermes_TFRC(PyObject * self, PyObject * args)
|
||||
|
||||
if (!PyArg_ParseTuple (args, ""))
|
||||
return NULL;
|
||||
if (hermes_count_temperature > 0) {
|
||||
hermes_temperature /= hermes_count_temperature;
|
||||
hermes_fwd_power /= hermes_count_temperature;
|
||||
if (hpsdr_AIN5_count > 0) {
|
||||
hpsdr_AIN5 /= hpsdr_AIN5_count;
|
||||
hpsdr_AIN1_avg /= hpsdr_AIN5_count;
|
||||
}
|
||||
else {
|
||||
hermes_temperature = 0.0;
|
||||
hermes_fwd_power = 0.0;
|
||||
hpsdr_AIN5 = 0.0;
|
||||
hpsdr_AIN1_avg = 0.0;
|
||||
}
|
||||
if (hermes_count_current > 0) {
|
||||
hermes_rev_power /= hermes_count_current;
|
||||
hermes_pa_current /= hermes_count_current;
|
||||
if (hpsdr_AIN3_count > 0) {
|
||||
hpsdr_AIN2_avg /= hpsdr_AIN3_count;
|
||||
hpsdr_AIN3 /= hpsdr_AIN3_count;
|
||||
}
|
||||
else {
|
||||
hermes_rev_power = 0.0;
|
||||
hermes_pa_current = 0.0;
|
||||
hpsdr_AIN2_avg = 0.0;
|
||||
hpsdr_AIN3 = 0.0;
|
||||
}
|
||||
ret = Py_BuildValue("dddddd", hermes_temperature, hermes_fwd_power, hermes_rev_power, hermes_pa_current, hermes_fwd_peak, hermes_rev_peak);
|
||||
hermes_temperature = 0;
|
||||
hermes_fwd_power = 0;
|
||||
hermes_rev_power = 0;
|
||||
hermes_fwd_peak = 0;
|
||||
hermes_rev_peak = 0;
|
||||
hermes_pa_current = 0;
|
||||
hermes_count_temperature = 0;
|
||||
hermes_count_current = 0;
|
||||
if (hpsdr_AIN4_count > 0) {
|
||||
hpsdr_AIN4 /= hpsdr_AIN4_count;
|
||||
hpsdr_AIN6 /= hpsdr_AIN4_count;
|
||||
}
|
||||
else {
|
||||
hpsdr_AIN4 = 0.0;
|
||||
hpsdr_AIN6 = 0.0;
|
||||
}
|
||||
ret = Py_BuildValue("dddddddd", hpsdr_AIN1_avg, hpsdr_AIN1_peak, hpsdr_AIN2_avg, hpsdr_AIN2_peak,
|
||||
hpsdr_AIN3, hpsdr_AIN4, hpsdr_AIN5, hpsdr_AIN6);
|
||||
hpsdr_AIN1_avg = 0;
|
||||
hpsdr_AIN1_peak = 0;
|
||||
hpsdr_AIN2_avg = 0;
|
||||
hpsdr_AIN2_peak = 0;
|
||||
hpsdr_AIN3 = 0;
|
||||
hpsdr_AIN4 = 0;
|
||||
hpsdr_AIN5 = 0;
|
||||
hpsdr_AIN6 = 0;
|
||||
hpsdr_AIN3_count = 0;
|
||||
hpsdr_AIN4_count = 0;
|
||||
hpsdr_AIN5_count = 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -3708,19 +3724,24 @@ static int read_rx_udp10(complex double * samp) // Read samples from UDP using t
|
||||
quisk_set_play_state();
|
||||
}
|
||||
}
|
||||
else if(dindex == 1) { // temperature and forward power
|
||||
hermes_temperature += quisk_hermes_to_pc[4] << 8 | quisk_hermes_to_pc[5];
|
||||
else if(dindex == 1) {
|
||||
hpsdr_AIN5 += quisk_hermes_to_pc[4] << 8 | quisk_hermes_to_pc[5];
|
||||
power = quisk_hermes_to_pc[6] << 8 | quisk_hermes_to_pc[7];
|
||||
hermes_fwd_power += power;
|
||||
hermes_fwd_peak = fmax(hermes_fwd_peak, (double)power);
|
||||
hermes_count_temperature++;
|
||||
hpsdr_AIN1_avg += power;
|
||||
hpsdr_AIN1_peak = fmax(hpsdr_AIN1_peak, (double)power);
|
||||
hpsdr_AIN5_count++;
|
||||
}
|
||||
else if (dindex == 2) { // reverse power and current
|
||||
else if (dindex == 2) {
|
||||
power = quisk_hermes_to_pc[8] << 8 | quisk_hermes_to_pc[9];
|
||||
hermes_rev_power += power;
|
||||
hermes_rev_peak = fmax(hermes_rev_peak, (double)power);
|
||||
hermes_pa_current += quisk_hermes_to_pc[10] << 8 | quisk_hermes_to_pc[11];
|
||||
hermes_count_current++;
|
||||
hpsdr_AIN2_avg += power;
|
||||
hpsdr_AIN2_peak = fmax(hpsdr_AIN2_peak, (double)power);
|
||||
hpsdr_AIN3 += quisk_hermes_to_pc[10] << 8 | quisk_hermes_to_pc[11];
|
||||
hpsdr_AIN3_count++;
|
||||
}
|
||||
else if (dindex == 3) {
|
||||
hpsdr_AIN4 += quisk_hermes_to_pc[12] << 8 | quisk_hermes_to_pc[13];
|
||||
hpsdr_AIN6 += quisk_hermes_to_pc[14] << 8 | quisk_hermes_to_pc[15];
|
||||
hpsdr_AIN4_count++;
|
||||
}
|
||||
// convert 24-bit samples to 32-bit samples; int must be 32 bits.
|
||||
index = start + 5;
|
||||
@@ -4007,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;
|
||||
@@ -4056,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)
|
||||
@@ -4270,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)
|
||||
@@ -4473,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);
|
||||
}
|
||||
@@ -4497,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);
|
||||
}
|
||||
@@ -5640,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");
|
||||
@@ -5675,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);
|
||||
@@ -5716,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;
|
||||
}
|
||||
@@ -6168,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."},
|
||||
|
||||
@@ -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 *);
|
||||
|
||||
@@ -995,17 +995,12 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
|
||||
else:
|
||||
self.Send('0\n')
|
||||
def GetFreq(self): # The Rx frequency
|
||||
rx = self.app.multi_rx_screen.receiver_list
|
||||
if rx:
|
||||
rx = rx[0]
|
||||
self.Reply('Frequency', rx.txFreq + rx.VFO, 0)
|
||||
if HAMLIB_DEBUG:
|
||||
print ("GetFreq rx", rx.txFreq, rx.VFO)
|
||||
else:
|
||||
self.Reply('Frequency', self.app.rxFreq + self.app.VFO, 0)
|
||||
if HAMLIB_DEBUG:
|
||||
print ("GetFreq app", self.app.rxFreq, self.app.txFreq, self.app.VFO)
|
||||
# This is always the main Rx frequency without reference to added receivers.
|
||||
self.Reply('Frequency', self.app.rxFreq + self.app.VFO, 0)
|
||||
if HAMLIB_DEBUG:
|
||||
print ("GetFreq app", self.app.rxFreq, self.app.txFreq, self.app.VFO)
|
||||
def SetFreq(self): # The Rx frequency
|
||||
# This is always the main Rx frequency without reference to added receivers.
|
||||
freq = self.GetParamNumber()
|
||||
try:
|
||||
freq = float(freq)
|
||||
@@ -1014,12 +1009,7 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
|
||||
self.ErrParam()
|
||||
else:
|
||||
freq = int(freq + 0.5)
|
||||
# For multiple receivers this controls the first added receiver frequency; else it controls split Rx/Tx frequency.
|
||||
rx = self.app.multi_rx_screen.receiver_list
|
||||
if rx:
|
||||
rx[0].ChangeRxTxFrequency(freq)
|
||||
else:
|
||||
self.app.ChangeRxTxFrequency(freq, None)
|
||||
self.app.ChangeRxTxFrequency(freq, None)
|
||||
def GetSplitFreq(self): # The Tx Frequency
|
||||
self.Reply('TX Frequency', self.app.txFreq + self.app.VFO, 0)
|
||||
def SetSplitFreq(self): # The Tx Frequency
|
||||
@@ -3728,7 +3718,7 @@ class App(wx.App):
|
||||
'wfallScaleZ', 'wfallGrScaleZ', 'graphScaleZ', 'split_rxtx_play', 'modeFilter',
|
||||
'file_name_rec_audio', 'file_name_rec_samples', 'file_name_rec_mic', 'file_name_rec_tmp',
|
||||
'file_name_play_audio', 'file_name_play_samples', 'file_name_play_cq',
|
||||
'file_play_source', 'hermes_LNA_dB']
|
||||
'file_play_source', 'hermes_LNA_dB', 'hermes_atten_dB']
|
||||
def __init__(self):
|
||||
global application
|
||||
QS.AppStatus(1)
|
||||
@@ -3948,7 +3938,9 @@ 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"):
|
||||
self.want_RxTx = True
|
||||
else:
|
||||
@@ -4037,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)
|
||||
@@ -4284,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:
|
||||
@@ -4358,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()
|
||||
@@ -4682,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:
|
||||
@@ -4767,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:
|
||||
@@ -5783,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
|
||||
@@ -6682,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 ":
|
||||
@@ -6741,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:
|
||||
@@ -6805,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 conf.hermes_lite2_enable:
|
||||
if QS.tci_get_params("tci_clients_count") > 0: # limit the speed of frequency updates
|
||||
# these are changes made in Quisk that must be broadcast to all clients.
|
||||
# modulation
|
||||
if self.tci_modulation != self.mode:
|
||||
self.tci_modulation = self.mode
|
||||
QS.tci_set_params(tci_modulation=self.tci_modulation)
|
||||
# split
|
||||
if self.tci_split_enable != self.split_rxtx:
|
||||
self.tci_split_enable = self.split_rxtx
|
||||
QS.tci_set_params(tci_split_enable=self.tci_split_enable)
|
||||
# tci dds == self.VFO
|
||||
if self.tci_dds != self.VFO:
|
||||
self.tci_dds = self.VFO
|
||||
QS.tci_set_params(tci_dds=self.tci_dds)
|
||||
# tci_vfo == self.VFO + self.rxFreq
|
||||
tci_vfo = self.VFO + self.rxFreq
|
||||
if self.tci_vfo != tci_vfo:
|
||||
self.tci_vfo = tci_vfo
|
||||
QS.tci_set_params(tci_vfo=self.tci_vfo)
|
||||
# tci_trx handled elsewhere
|
||||
if self.is_HermesLite2:
|
||||
self.tx_inhibit = QS.get_params('quisk_tx_inhibit')
|
||||
else:
|
||||
self.tx_inhibit = 0
|
||||
|
||||
+149
-22
@@ -48,7 +48,7 @@ quisk_widgets = None
|
||||
|
||||
|
||||
|
||||
################ Receivers SoftRock USB, Devices controlled by USB that capture samples from a sound card, and (for Tx) play samples to a sound card
|
||||
################ Receivers SoftRock USB, Sound card radios controlled by USB.
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
#hardware_file_name = 'softrock/hardware_usb.py'
|
||||
@@ -133,7 +133,7 @@ digital_tx_level = 100
|
||||
|
||||
|
||||
|
||||
################ Receivers SoftRock Fixed, Fixed frequency devices that capture samples from a sound card, and (for Tx) play samples to a sound card
|
||||
################ Receivers SoftRock Fixed, Fixed frequency sound card radios.
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
#hardware_file_name = 'quisk_hardware_fixed.py'
|
||||
@@ -174,7 +174,7 @@ digital_tx_level = 100
|
||||
|
||||
|
||||
|
||||
################ Receivers HiQSDR, The original N2ADR hardware and the improved HiQSDR using UDP
|
||||
################ Receivers HiQSDR, The original N2ADR and HiQSDR radios.
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
#hardware_file_name = 'hiqsdr/quisk_hardware.py'
|
||||
@@ -258,7 +258,7 @@ sndp_active = True
|
||||
|
||||
|
||||
|
||||
################ Receivers Hermes, The Hermes-Lite Project and possibly other hardware with the Hermes FPGA code.
|
||||
################ Receivers Hermes, The Hermes-Lite project by Steve Haynal.
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
#hardware_file_name = 'hermes/quisk_hardware.py'
|
||||
@@ -275,7 +275,7 @@ sndp_active = True
|
||||
# You can set these options:
|
||||
|
||||
## use_rx_udp Hardware type, integer choice
|
||||
# This is the type of UDP hardware. Use 10 for the Hermes protocol.
|
||||
# This is the type of UDP hardware. Use 10 for the Hermes Lite2 protocol.
|
||||
#use_rx_udp = 10
|
||||
|
||||
## udp_rx_ip Hermes known IP, text
|
||||
@@ -373,6 +373,7 @@ hermes_disable_sync = False
|
||||
# be sure to write it down. To use this address you must set "Eeprom IP Usage".
|
||||
# And you may want to set "Hermes known IP" too. Make sure the address does not conflict
|
||||
# with your DHCP server.
|
||||
# Power cycle the HL2 after changing this item.
|
||||
Hware_Hl2_EepromIP = '192.168.1.6'
|
||||
#Hware_Hl2_EepromIP = '192.168.1.241'
|
||||
|
||||
@@ -380,6 +381,7 @@ Hware_Hl2_EepromIP = '192.168.1.6'
|
||||
# This is the way the EEPROM IP address is used at power on.
|
||||
# "Ignore" means it is not used at all. "Set address" means DHCP is not used and the EEPROM IP address is used.
|
||||
# "Use DHCP first" means the EEPROM IP address is only used if DHCP fails.
|
||||
# Power cycle the HL2 after changing this item.
|
||||
Hware_Hl2_EepromIPUse = 'Ignore'
|
||||
#Hware_Hl2_EepromIPUse = 'Use DHCP first'
|
||||
#Hware_Hl2_EepromIPUse = 'Set address'
|
||||
@@ -390,6 +392,7 @@ Hware_Hl2_EepromIPUse = 'Ignore'
|
||||
# at power on instead of the factory supplied MAC address 0:1c:c0:a2:13:dd. It is only used at power on,
|
||||
# and it is only used if it is marked valid by setting "Eeprom MAC usage" to "Set address".
|
||||
# If you have two HL2's, you must have a unique MAC address for each.
|
||||
# Power cycle the HL2 after changing this item.
|
||||
Hware_Hl2_EepromMAC = '0xA1 0x6B'
|
||||
#Hware_Hl2_EepromMAC = '0x4C 0x33'
|
||||
|
||||
@@ -398,6 +401,7 @@ Hware_Hl2_EepromMAC = '0xA1 0x6B'
|
||||
# the HL2 will use the last two MAC bytes from "Eeprom MAC address"; otherwise the default MAC is used.
|
||||
# If you change this item, the bit is written to the HL2.
|
||||
# The HL2 only uses these bits at power on.
|
||||
# Power cycle the HL2 after changing this item.
|
||||
Hware_Hl2_EepromMACUse = 'Ignore'
|
||||
#Hware_Hl2_EepromMACUse = 'Set address'
|
||||
|
||||
@@ -474,13 +478,6 @@ hermes_iob_rxin = 'J10 available'
|
||||
#hermes_iob_rxin = 'HL2 Rx to J9'
|
||||
#hermes_iob_rxin = 'Use J9 and J10'
|
||||
|
||||
## hermes_lite2_enable Hermes Lite2 protocol, boolean
|
||||
# This turns on some of the protocol extensions for the Hermes Lite2 that are not part
|
||||
# of the official Hermes Protocol 1. Currently this only enables Tx Inhibit (CN8).
|
||||
# Changes are immediate (no need to restart).
|
||||
hermes_lite2_enable = True
|
||||
#hermes_lite2_enable = False
|
||||
|
||||
|
||||
# These are known power meter calibration tables. This table is not present in the JSON settings file.
|
||||
power_meter_std_calibrations = {}
|
||||
@@ -491,6 +488,7 @@ power_meter_std_calibrations['HL2FilterE3'] = [[ 0, 0.0 ], [ 25.865384615384617,
|
||||
[ 4093.1785714285716, 7.28994845808807 ], [ 4502.496428571429, 8.820837634286566 ], [ 4952.746071428572, 10.673213537486745 ] ]
|
||||
#power_meter_std_calibrations['HL2FilterE1'] = [[0, 0.0], [9.07, 0.002], [54.98, 0.014], [148.6, 0.057],
|
||||
# [328.0, 0.208], [611.1, 0.646], [807.0, 1.098], [982.1, 1.6], [1223.3, 2.471], [1517.7, 3.738], [1758.7, 5.02]]
|
||||
power_meter_std_calibrations['Show ADC'] = []
|
||||
|
||||
## Hermes_BandDict Rx IO Bus, dict
|
||||
# The Hermes_BandDict sets the 7 bits on the J16 connector for Rx.
|
||||
@@ -508,17 +506,17 @@ Hermes_BandDictEnTx = False
|
||||
#Hermes_BandDictEnTx = True
|
||||
|
||||
|
||||
################ Receivers Red Pitaya, The Red Pitaya Project by Pavel Demin. This uses the Hermes FPGA code.
|
||||
################ Receivers Red Pitaya, The Red Pitaya project by Pavel Demin.
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
#hardware_file_name = 'hermes/quisk_hardware.py'
|
||||
|
||||
## widgets_file_name Widget file path, rfile
|
||||
# This optional file adds additional controls for the radio.
|
||||
#widgets_file_name = ''
|
||||
#widgets_file_name = 'hermes/hpsdr_widgets.py'
|
||||
|
||||
## use_rx_udp Hardware type, integer choice
|
||||
# This is the type of UDP hardware. Use 10 for the Hermes protocol.
|
||||
# This is the type of UDP hardware. Use 10 for the Red Pitaya using the OpenHPSDR protocol.
|
||||
#use_rx_udp = 10
|
||||
|
||||
## rx_udp_ip IP change, text
|
||||
@@ -586,7 +584,123 @@ Hermes_BandDictEnTx = False
|
||||
#Hermes_BandDictEnTx = True
|
||||
|
||||
|
||||
################ Receivers SoapySDR, The SoapySDR interface to multiple hardware SDRs.
|
||||
################ Receivers OpenHPSDR, Radios using the HPSDR Protocol 1.
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
#hardware_file_name = 'hermes/quisk_hardware.py'
|
||||
|
||||
## widgets_file_name Widget file path, rfile
|
||||
# This optional file adds additional controls for the radio.
|
||||
#widgets_file_name = 'hermes/hpsdr_widgets.py'
|
||||
|
||||
## use_rx_udp Hardware type, integer choice
|
||||
# This is the type of UDP hardware. Use 10 for hardware using the OpenHPSDR protocol 1.
|
||||
#use_rx_udp = 10
|
||||
|
||||
## rx_udp_ip IP change, text
|
||||
# This item should be left blank. It is used to change the IP address of the hardware to a different
|
||||
# IP once the hardware is found. Not all Hermes firmware supports changing the IP address.
|
||||
#rx_udp_ip = ""
|
||||
|
||||
## rx_udp_port Hardware UDP port, integer
|
||||
# This is the UDP port number of your hardware.
|
||||
#rx_udp_port = 1024
|
||||
|
||||
## rx_udp_ip_netmask Network netmask, text
|
||||
# This is the netmask for the network.
|
||||
#rx_udp_ip_netmask = '255.255.255.0'
|
||||
|
||||
## tx_ip Transmit IP, text
|
||||
# Leave this blank to use the same IP address as the receive hardware. Otherwise, enter "disable"
|
||||
# to disable sending transmit I/Q samples, or enter the actual IP address. You must enter "disable"
|
||||
# if you have multiple hardwares on the network, and only one should transmit. This item is normally blank.
|
||||
tx_ip = ""
|
||||
#tx_ip = "disable"
|
||||
|
||||
## tx_audio_port Tx audio UDP port, integer
|
||||
# This is the UDP port for transmit audio I/Q samples. Enter zero to calculate this from the
|
||||
# base hardware port. Otherwise enter the special custom port.
|
||||
tx_audio_port = 0
|
||||
|
||||
## rx_udp_clock Clock frequency Hertz, integer
|
||||
# This is the clock frequency of the hardware in Hertz.
|
||||
#rx_udp_clock = 122880000
|
||||
#rx_udp_clock = 76800000
|
||||
#rx_udp_clock = 125000000
|
||||
|
||||
## tx_level Tx Level, dict
|
||||
# This sets the transmit level 0 to 255 for each band.
|
||||
# The config screen has a slider 0 to 100% so you can reduce the transmit power.
|
||||
# The hardware only supports a limited adjustment range, so zero is still a small amount of power.
|
||||
tx_level = {}
|
||||
|
||||
## digital_tx_level Digital Tx power %, integer
|
||||
# Digital modes reduce power by the percentage on the config screen.
|
||||
# This is the maximum value of the slider.
|
||||
#digital_tx_level = 100
|
||||
|
||||
## hermes_code_version Hermes code version, integer
|
||||
# There can be multiple Hermes devices on a network, but Quisk can only use one of these. If you have multiple
|
||||
# hermes devices, you can use this to specify a unique device. Or use -1 to accept any board.
|
||||
hermes_code_version = -1
|
||||
|
||||
## hermes_board_id Hermes board ID, integer
|
||||
# There can be multiple Hermes devices on a network, but Quisk can only use one of these. If you have multiple
|
||||
# hermes devices, you can use this to specify a unique device. Or use -1 to accept any board.
|
||||
hermes_board_id = -1
|
||||
|
||||
## Hermes_BandDict Hermes Bus, dict
|
||||
# The Hermes_BandDict sets the 7 bits on the J16 connector.
|
||||
Hermes_BandDict = {
|
||||
'160':0b0000001, '80':0b0000010, '60':0b0000100, '40':0b0001000, '30':0b0010000, '20':0b0100000, '15':0b1000000}
|
||||
|
||||
## Hermes_BandDictTx Tx IO Bus, dict
|
||||
# The Hermes_BandDictTx sets the 7 bits on the J16 connector for Tx if enabled.
|
||||
Hermes_BandDictTx = {'160':0, '80':0, '60':0, '40':0, '30':0, '20':0, '17':0, '15':0, '12':0, '10':0}
|
||||
|
||||
## Hermes_BandDictEnTx Enable Tx Filt, boolean
|
||||
# Enable the separate Rx and Tx settings for the J16 connector.
|
||||
Hermes_BandDictEnTx = False
|
||||
#Hermes_BandDictEnTx = True
|
||||
|
||||
## calibrate_temp_20 PA temp ADC at 20\u2103, integer
|
||||
# This is the ADC code returned for a temperature of 20C.
|
||||
# To see the ADC code being returned, enter zero for both temperature codes.
|
||||
# Linear interpolation is used to calculate the temperature.
|
||||
# Changes are immediate (no need to restart).
|
||||
calibrate_temp_20 = 0
|
||||
|
||||
## calibrate_temp_40 PA temp ADC at 40\u2103, integer
|
||||
# This is the ADC code returned for a temperature of 40C.
|
||||
# To see the ADC code being returned, enter zero for both temperature codes.
|
||||
# Linear interpolation is used to calculate the temperature.
|
||||
# Changes are immediate (no need to restart).
|
||||
calibrate_temp_40 = 0
|
||||
|
||||
## calibrate_current_0 PA current ADC at zero, integer
|
||||
# This is the ADC code returned for a PA current of zero.
|
||||
# To see the ADC code being returned, enter zero for both current codes.
|
||||
# Linear interpolation is used to calculate the current.
|
||||
# Changes are immediate (no need to restart).
|
||||
calibrate_current_0 = 0
|
||||
|
||||
## calibrate_current_1 PA current ADC at 1 amp, integer
|
||||
# This is the ADC code returned for a PA current of one amp.
|
||||
# To see the ADC code being returned, enter zero for both current codes.
|
||||
# Linear interpolation is used to calculate the current.
|
||||
# Changes are immediate (no need to restart).
|
||||
calibrate_current_1 = 0
|
||||
|
||||
## power_meter_calib_name Power meter calibration, text choice
|
||||
# This is the calibration table used to convert the power sensor voltage measured by the ADC to the transmit power display.
|
||||
# It is a table of ADC codes and the corresponding measured power level. If you have a power meter, you can create your own
|
||||
# table by selecting "New". Then enter ten or more power measurements from low to full power.
|
||||
# For the Hermes Lite2 version E3 filter board, use the built-in table "HL2FilterE3".
|
||||
# Changes are immediate (no need to restart).
|
||||
power_meter_calib_name = 'HL2FilterE3'
|
||||
|
||||
|
||||
################ Receivers SoapySDR, The SoapySDR interface to multiple SDRs.
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
#hardware_file_name = 'soapypkg/quisk_hardware.py'
|
||||
@@ -602,7 +716,7 @@ Hermes_BandDictEnTx = False
|
||||
# Further items are present in the radio dictionary with names soapy_*
|
||||
|
||||
|
||||
################ Receivers SdrIQ, The SDR-IQ receiver by RfSpace
|
||||
################ Receivers SdrIQ, The SDR-IQ receiver by RfSpace.
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
hardware_file_name = 'quisk_hardware_sdriq.py'
|
||||
@@ -633,7 +747,7 @@ sdriq_decimation = 1250
|
||||
|
||||
|
||||
|
||||
################ Receivers Odyssey, The Odyssey project using a UDP protocol similar to the HiQSDR
|
||||
################ Receivers Odyssey, The Odyssey project.
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
#hardware_file_name = 'hiqsdr/quisk_hardware.py'
|
||||
@@ -744,7 +858,7 @@ sndp_active = True
|
||||
#radio_sound_mic_boost = True
|
||||
|
||||
|
||||
################ Receivers Odyssey2, The Odyssey-2 project using the HPSDR Hermes protocol
|
||||
################ Receivers Odyssey2, The Odyssey-2 project.
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
#hardware_file_name = 'hermes/quisk_hardware.py'
|
||||
@@ -759,7 +873,7 @@ sndp_active = True
|
||||
# You can set these options:
|
||||
|
||||
## use_rx_udp Hardware type, integer choice
|
||||
# This is the type of UDP hardware. Use 10 for the Hermes protocol.
|
||||
# This is the type of UDP hardware. Use 10 for the Odyssey-2.
|
||||
#use_rx_udp = 10
|
||||
|
||||
## rx_udp_ip IP change, text
|
||||
@@ -828,7 +942,7 @@ Hermes_BandDictEnTx = False
|
||||
#Hermes_BandDictEnTx = True
|
||||
|
||||
|
||||
################ Receivers Afedri, The Afedri SDR receiver with the Ethernet interface.
|
||||
################ Receivers Afedri, The Afedri SDR receiver with Ethernet.
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
#hardware_file_name = 'afedrinet/quisk_hardware.py'
|
||||
@@ -860,7 +974,7 @@ Hermes_BandDictEnTx = False
|
||||
#default_rf_gain = 11
|
||||
|
||||
|
||||
################ Receivers Control Head, Use Quisk to control a remote radio. No real hardware is used here.
|
||||
################ Receivers Control Head, Use Quisk to control a remote Quisk radio.
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# Quisk can be used as a control head to control a real radio located remotely.
|
||||
# This file contains the control logic for remote radios of type Hermes, SoftRock or HiQSDR.
|
||||
@@ -1309,6 +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
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// This module provides C access to the WDSP SDR library.
|
||||
|
||||
#include <Python.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <complex.h>
|
||||
|
||||
#define CLIP32 2147483647
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
#
|
||||
@@ -0,0 +1,569 @@
|
||||
# SDR-TRX Quisk Configuration File
|
||||
# This file is to integrate the function of control of the SDR-TRX wint Quisk and with WSJT-X.
|
||||
# This is for control of the SDR-TRX with using the UDP port port 12346. WSJT-X interfaces with this
|
||||
# locally using UDP on port 2237. Capital letters begin commands to the SDR-TRX from Quisk,
|
||||
# and lowercase letters are for commands coming from WSJT-X (through this Quisk interface).
|
||||
# This is for usinng the SDR-TRX without a 3.5 mm audio card to the soundcard of the computer,
|
||||
# and the WSJT-X software using UDP, and the data coming over UDP part 12345 too.
|
||||
# It requires the libldl/ directory from weakmon to be in the directory you run this from.
|
||||
# It also requires the WSJTXClass.py file from wsjtx_transceiver_interface to be in the lib directory
|
||||
# and the version of that class. I have supplied a pruned down versions of ft8.py and ft4.45, because we don't need much
|
||||
# other than FT8Send and FT4Send from those files. You need to edit transceiver_config.yml to have your callsign and grid, etc.
|
||||
|
||||
import socket
|
||||
import time
|
||||
import yaml
|
||||
import struct
|
||||
import datetime
|
||||
import sys
|
||||
import os
|
||||
from ft8 import FT8Send
|
||||
from ft4 import FT4Send
|
||||
from quisk_hardware_model import Hardware as BaseHardware
|
||||
import struct
|
||||
import collections
|
||||
import lib.WSJTXClass as WSJTXClass # To use encoders
|
||||
|
||||
sys.path.append(os.path.expandvars('$WEAKMON'))
|
||||
|
||||
DEBUG_ON = True
|
||||
|
||||
#
|
||||
# Define the name of the hardware and the items on the hardware screen (see quisk_conf_defaults.py):
|
||||
#
|
||||
|
||||
################ Receivers SdrTrx, The SdrTrx radio by Rob Frohne, KL7NA
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
#hardware_file_name = 'sdrtrxpkg/quisk_hardware.py'
|
||||
|
||||
## widgets_file_name Widget file path, rfile
|
||||
# This optional file adds additional controls for the radio.
|
||||
#widgets_file_name = ''
|
||||
|
||||
## si570_xtal_freq Si570 crystal frequency, integer
|
||||
# This is the Si570 startup frequency in Hz. 114.285MHz is the typical
|
||||
# value from the data sheet; you can use 'usbsoftrock calibrate' to find
|
||||
# the value for your device.
|
||||
si570_xtal_freq = 114285000
|
||||
|
||||
## rx_max_amplitude_correct Max ampl correct, number
|
||||
# If you get your I/Q samples from a sound card, you will need to correct the
|
||||
# amplitude and phase for inaccuracies in the analog hardware. The correction is
|
||||
# entered using the controls from the "Rx Phase" button on the config screen.
|
||||
# You must enter a positive number. This controls the range of the control.
|
||||
rx_max_amplitude_correct = 0.2
|
||||
|
||||
## rx_max_phase_correct Max phase correct, number
|
||||
# If you get your I/Q samples from a sound card, you will need to correct the
|
||||
# amplitude and phase for inaccuracies in the analog hardware. The correction is
|
||||
# entered using the controls from the "Rx Phase" button on the config screen.
|
||||
# You must enter a positive number. This controls the range of the control in degrees.
|
||||
rx_max_phase_correct = 10.0
|
||||
|
||||
|
||||
# SDR-TRX Hardware Control Class
|
||||
#
|
||||
|
||||
class Hardware(BaseHardware):
|
||||
# These are "static variable substitutes" since python doesn't have them.
|
||||
# They are shared by all instances of the class and don't get redefinede each time a method is called.
|
||||
# Move these into the Hardware class.
|
||||
# Uncomment these lines if you wish to use Pulseaudio
|
||||
name_of_sound_play = "pulse"
|
||||
|
||||
# Radio's Frequency limits.
|
||||
radio_lower = 3500000
|
||||
radio_upper = 30000000
|
||||
PACKET_SIZE = 1444 # 4 bytes for packet number + 360 * 4 bytes for int32_t pairs
|
||||
# Using 16 bit samples, so 2 bytes per sample.
|
||||
|
||||
# Set the number of Hz the signal is tuned to above the center frequency to avoid 1/f noise.
|
||||
vfo_Center_Offset = 10000
|
||||
|
||||
tx_ready_wsjtx = False
|
||||
tx_ready_wsjtx_sent = False
|
||||
tx_now = False
|
||||
|
||||
# This is the open code for the WSJT server.
|
||||
ft8_encoder = FT8Send()
|
||||
ft4_encoder = FT4Send()
|
||||
|
||||
class Packet:
|
||||
def __init__(self, data):
|
||||
self.number = struct.unpack('<I', data[:4])[0]
|
||||
self.pairs = [struct.unpack('<hh', data[i:i+4]) for i in range(4, len(data), 4)]
|
||||
|
||||
class PacketQueue:
|
||||
def __init__(self):
|
||||
self.queue = collections.deque()
|
||||
|
||||
def add_packet(self, packet):
|
||||
self.queue.append(packet)
|
||||
self.queue = collections.deque(sorted(self.queue, key=lambda p: p.number))
|
||||
|
||||
def check_missing_packets(self):
|
||||
for i in range(1, len(self.queue)):
|
||||
if self.queue[i].number != self.queue[i-1].number + 1:
|
||||
avg_pairs = [((self.queue[i-1].pairs[j][0] + self.queue[i].pairs[j][0]) // 2,
|
||||
(self.queue[i-1].pairs[j][1] + self.queue[i].pairs[j][1]) // 2)
|
||||
for j in range(len(self.queue[i-1].pairs))]
|
||||
self.queue.insert(i, Packet(struct.pack('<I', self.queue[i-1].number + 1) +
|
||||
b''.join(struct.pack('<ii', pair[0], pair[1]) for pair in avg_pairs)))
|
||||
|
||||
def get_packets(self):
|
||||
packets = list(self.queue)
|
||||
self.queue.clear()
|
||||
return packets
|
||||
|
||||
def open(self):
|
||||
# Connection for WSJT-X and data
|
||||
self.PICO_UDP_IP = None # Will be set to the IP of the machine sending data
|
||||
self.COMMAND_UDP_PORT = 12346 # This is the port the Pico listens on for UDP commands coming from quisk.
|
||||
self.DATA_UDP_PORT = 12345 # This is the port the quisk listens on for UDP IQ data coming from the Pico W.
|
||||
self.BROADCAST_PORT = 12347 # This is the port the Pico gets our IP address from.-
|
||||
self.broadcast_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.broadcast_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
self.broadcast_message = "Quisk"
|
||||
self.broadcast_sock.setblocking(False)
|
||||
self.isConnected = False
|
||||
self.command_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.command_sock.setblocking(False)
|
||||
self.data_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.data_sock.bind(('', self.DATA_UDP_PORT))
|
||||
print("Data socket bound to IP:", self.data_sock.getsockname()[0])
|
||||
self.data_sock.setblocking(False)
|
||||
self.establish_connection()
|
||||
|
||||
self.InitSamples(2, 0) # Two bytes per sample, little endian.
|
||||
self.queue = self.PacketQueue()
|
||||
time.sleep(2)
|
||||
# Poll for version. Should probably confirm the response on this.
|
||||
version = str(self.get_parameter("VER")) # The way the firmware is now, this sets it up to use the UART or UDP.
|
||||
# UART is used if get_parameter uses the UART, and UDP is used if it uses the UDP.
|
||||
print(version)
|
||||
# Return an informative message for the config screen
|
||||
t = str(version) + ". Capture from sound card %s." % self.conf.name_of_sound_capt
|
||||
configs_file = open('transceiver_config.yml', 'r')
|
||||
configs = yaml.load(configs_file, Loader=yaml.BaseLoader)
|
||||
|
||||
# Global variables
|
||||
self.callsign = configs['callsign']
|
||||
self.grid = configs['grid']
|
||||
self.current_msg = ''
|
||||
self.rx_callsign = ''
|
||||
self.mode = 'FT8'
|
||||
self.tx_freq = 1200
|
||||
|
||||
# Connection for WSJT-X
|
||||
WSJTX_UDP_IP = "127.0.0.1" # Assuming you are running WSJT-X on the same computer as Quisk...
|
||||
WSJTX_UDP_PORT = 2237
|
||||
self.wsjtx_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.wsjtx_sock.bind((WSJTX_UDP_IP, WSJTX_UDP_PORT))
|
||||
self.wsjtx_sock.setblocking(False)
|
||||
|
||||
self.StartSamples()
|
||||
return t
|
||||
|
||||
def close(self):
|
||||
# Called once to close the Hardware
|
||||
self.get_parameter("RX") # Make sure we are in RX mode.
|
||||
time.sleep(1) # Wait for RX to be set.
|
||||
self.StopSamples()
|
||||
self.command_sock.close()
|
||||
self.wsjtx_sock.close()
|
||||
self.broadcast_sock.close()
|
||||
|
||||
def ChangeFrequency(self, tune, vfo, source='', band='', event=None):
|
||||
# Called whenever quisk requests a frequency change.
|
||||
# This sends the FREQ command dto set the centre frequency of the radio,
|
||||
# and will also move the 'tune' frequency (the section within the RX passband
|
||||
# which is to be demodulated) if it falls outside the passband (+/- sample_rate/2).
|
||||
print("Setting VFO to %d, with tune to %d." % (vfo, tune))
|
||||
print("Setting VFO to %d." % vfo)
|
||||
if vfo < self.radio_lower:
|
||||
vfo = self.radio_lower
|
||||
print("Outside range! Setting to %d" % self.radio_lower)
|
||||
|
||||
if vfo > self.radio_upper:
|
||||
vfo = self.radio_upper
|
||||
print("Outside range! Setting to %d" % self.radio_upper)
|
||||
|
||||
# print("sample_rate =", sample_rate) # I believe sample_rate comes from quisk.
|
||||
# If the tune frequency is outside the RX bandwidth, set it to somewhere within that bandwidth.
|
||||
if tune > (vfo + sample_rate / 2) or tune < (vfo - sample_rate / 2):
|
||||
tune = vfo + self.vfo_Center_Offset
|
||||
print("Bringing tune frequency back into the RX bandwidth.")
|
||||
|
||||
# success = self.set_parameter("FREQ",str(vfo))
|
||||
self.set_parameter("FREQ", str(vfo))
|
||||
self.set_parameter("TX_FREQ", str(tune))
|
||||
|
||||
return tune, vfo
|
||||
|
||||
def OnButtonPTT(self, event):
|
||||
btn = event.GetEventObject()
|
||||
if btn.GetValue():
|
||||
self.get_parameter("TX")
|
||||
print("TX")
|
||||
else:
|
||||
self.get_parameter("RX")
|
||||
print("RX")
|
||||
return BaseHardware.OnButtonPTT(self, event)
|
||||
|
||||
|
||||
|
||||
def establish_connection(self):
|
||||
while not self.isConnected:
|
||||
print("isConnected is: ", self.isConnected)
|
||||
# Check if a connection is established
|
||||
# Wait for data to arrive and set PICO_UDP_IP to the sender's IP
|
||||
while self.PICO_UDP_IP is None: # This needs to be fixed. It continues to send broadcast packets.
|
||||
try:
|
||||
print("Sending broadcast message...")
|
||||
self.broadcast_sock.sendto(self.broadcast_message.encode(), ('<broadcast>', self.BROADCAST_PORT))
|
||||
time.sleep(1) # Wait for 1 second before sending the next broadcast message
|
||||
except Exception as e:
|
||||
print("Broadcast message did not work: {0}".format(e))
|
||||
break
|
||||
try:
|
||||
data, addr = self.data_sock.recvfrom(self.PACKET_SIZE) # Adjust the buffer size as needed
|
||||
self.PICO_UDP_IP = addr[0]
|
||||
print("PICO_UDP_IP set to:", self.PICO_UDP_IP)
|
||||
# if data == self.broadcast_message:
|
||||
self.isConnected = True
|
||||
except socket.error:
|
||||
if self.PICO_UDP_IP is not None:
|
||||
print("Connection established with Pico W at IP:", self.PICO_UDP_IP, "but exception occurred.")
|
||||
break # Exit the loop if self.PICO_UDP_IP is not None
|
||||
pass # No data available yet
|
||||
|
||||
def GetRxSamples(self):
|
||||
# Initialize packet counter and time marker
|
||||
# This routine is made for FT8 and FT4, where we need to send a packet every 15 seconds.
|
||||
# The Pico W sending the data has a sample rate of 48046 samples per second > 48 ks/s.
|
||||
# We made the packets so they carry 180 samples per packet. That gives 4000 packets
|
||||
# per 15 seconds. We discard any packets after that four times a minute at 59, 14, 29,
|
||||
# and 44 seconds. This should make four ticks per minute, and we should be able to ignore
|
||||
# # them if we are doing FT8 or FT4. Something else might be more optimum for other modes.
|
||||
|
||||
packet_counter = 0
|
||||
next_time_marker = 0
|
||||
send_packets = False
|
||||
|
||||
while True:
|
||||
# Check if we've reached the next time marker
|
||||
current_time = time.time()
|
||||
current_seconds = current_time % 60
|
||||
|
||||
# Define the time markers
|
||||
if 59 <= current_seconds < 60:
|
||||
next_time_marker = current_time + 1 - current_seconds
|
||||
send_packets = True
|
||||
elif 14 <= current_seconds < 15:
|
||||
next_time_marker = current_time + 1 - (current_seconds - 14)
|
||||
send_packets = True
|
||||
elif 29 <= current_seconds < 30:
|
||||
next_time_marker = current_time + 1 - (current_seconds - 29)
|
||||
send_packets = True
|
||||
elif 44 <= current_seconds < 45:
|
||||
next_time_marker = current_time + 1 - (current_seconds - 44)
|
||||
send_packets = True
|
||||
|
||||
# If we've reached the next time marker, reset the packet counter
|
||||
if current_time >= next_time_marker and send_packets:
|
||||
packet_counter = 0
|
||||
send_packets = False
|
||||
|
||||
try:
|
||||
data, addr = self.data_sock.recvfrom(self.PACKET_SIZE)
|
||||
self.no_data_repeat = 0
|
||||
except socket.error:
|
||||
# Send a broadcast message to the Pico W to get the IP address in case it has reset.
|
||||
self.broadcast_sock.sendto(self.broadcast_message.encode(), ('<broadcast>', self.BROADCAST_PORT))
|
||||
break # No more packets available
|
||||
|
||||
packet = self.Packet(data)
|
||||
self.queue.add_packet(packet)
|
||||
|
||||
# Check for missing packets and fill them in
|
||||
self.queue.check_missing_packets()
|
||||
|
||||
# Get the packets and pass them to the processing thread
|
||||
packets = self.queue.get_packets()
|
||||
|
||||
for packet in packets:
|
||||
data = b''.join(struct.pack('<hh', pair[0], pair[1]) for pair in packet.pairs)
|
||||
# if len(data) % 4 != 0: # Each I/Q pair is now 4 bytes (2 * 2 bytes)
|
||||
# self.GotReadError(self, DEBUG_ON, "Error: Mismatched number of I and Q samples")
|
||||
# continue
|
||||
# If we've already sent 4000 packets since the last time marker, discard the rest
|
||||
if packet_counter >= 4000:
|
||||
continue
|
||||
|
||||
self.AddRxSamples(data)
|
||||
packet_counter += 1
|
||||
|
||||
#
|
||||
# UDP comms functions, to communicate with the Raspberry Pi Pico W board used in the SDR-TRX.
|
||||
#
|
||||
|
||||
def get_parameter(self, string):
|
||||
string = string + "\n"
|
||||
self.command_sock.sendto(string.encode(), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
|
||||
return self.get_argument()
|
||||
|
||||
def set_parameter(self, string, arg):
|
||||
string = string + "," + arg + "\r" + "\n"
|
||||
self.command_sock.sendto(string.encode(), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
|
||||
print('arg is: ', arg)
|
||||
temp_arg = self.get_argument()
|
||||
print('temp_arg is: ', temp_arg)
|
||||
return True
|
||||
|
||||
def get_argument(self):
|
||||
self.command_sock.setblocking(True)
|
||||
try:
|
||||
data1 = self.command_sock.recv(1024)
|
||||
print('Received on line 178: ', data1)
|
||||
except Exception as e:
|
||||
print("Exception occurred: {0}".format(e))
|
||||
return -1
|
||||
self.command_sock.setblocking(False)
|
||||
if not data1.startswith(b'OK'):
|
||||
print('Received on line 185: ', data1)
|
||||
# Maybe we didn't catch an OK line?
|
||||
else:
|
||||
while True:
|
||||
try:
|
||||
data1 = self.command_sock.recv(1024)
|
||||
print('Received on line 190: ', data1)
|
||||
break
|
||||
except BlockingIOError:
|
||||
pass
|
||||
# Check to see if we have a comma in the string. If not, there is no argument.
|
||||
if data1.find(b',') == -1:
|
||||
return -1
|
||||
|
||||
data1 = data1.split(b',')[1].rstrip(b'\r\n')
|
||||
print("data1 =")
|
||||
print(data1)
|
||||
|
||||
# Check for the OK string. Should we wait for the OK string?
|
||||
try:
|
||||
data2 = self.command_sock.recv(1024)
|
||||
if data2.startswith(b'OK'):
|
||||
return data2
|
||||
except BlockingIOError:
|
||||
return -1
|
||||
|
||||
# We might want to integrate the UDP reads below into the stuff above.
|
||||
|
||||
# The code below is to interface WSJT-X with SDR-TRX.
|
||||
|
||||
def encode_ft8(self, msg):
|
||||
try:
|
||||
a77 = self.ft8_encoder.pack(msg, 1)
|
||||
symbols = self.ft8_encoder.make_symbols(a77)
|
||||
except Exception as e:
|
||||
print("FT8 encoder error, check message!")
|
||||
print(f"Exception type: {type(e).__name__}")
|
||||
print(f"Exception message: {str(e)}")
|
||||
symbols = None
|
||||
time.sleep(3)
|
||||
return symbols
|
||||
|
||||
def encode_ft4(self, msg):
|
||||
try:
|
||||
a77 = self.ft4_encoder.pack(msg, 1)
|
||||
symbols = self.ft4_encoder.make_symbols(a77)
|
||||
except:
|
||||
print("FT4 encoder error, check message!")
|
||||
symbols = None
|
||||
time.sleep(3)
|
||||
return symbols
|
||||
|
||||
def load_symbols(self, symbols):
|
||||
print("Load symbols into transmitter..")
|
||||
self.command_sock.sendto(b'm', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
|
||||
count = 0
|
||||
for symbol in symbols:
|
||||
self.command_sock.sendto(struct.pack('>B', symbol), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
|
||||
count += 1
|
||||
# Wait to avoid Arduino serial buffer overflow. This may not be needed with the Pico.
|
||||
if count % 50 == 0:
|
||||
time.sleep(0.05)
|
||||
self.command_sock.sendto(b'\0', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
|
||||
resp = self.command_sock.recv(1)
|
||||
if resp == b'm':
|
||||
print("Load OK")
|
||||
else:
|
||||
print(resp)
|
||||
|
||||
def change_freq(self, new_freq):
|
||||
#global self.tx_freq
|
||||
self.command_sock.setblocking(True)
|
||||
print("Change TX frequency to:", new_freq)
|
||||
self.command_sock.sendto(b'o', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
|
||||
self.command_sock.sendto(struct.pack('<H', new_freq), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
|
||||
time.sleep(0.05)
|
||||
resp = self.command_sock.recv(1)
|
||||
print("resp after changing offset is: ", resp)
|
||||
self.command_sock.setblocking(False)
|
||||
if resp == b'o':
|
||||
print("New freq OK")
|
||||
self.tx_freq = new_freq
|
||||
|
||||
def set_mode(self, new_mode): # New more robust protocol.
|
||||
#global mode
|
||||
if new_mode == 'FT8':
|
||||
self.command_sock.sendto(b'e', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
|
||||
time.sleep(.05)
|
||||
resp = self.command_sock.read(1)
|
||||
print("resp = ", resp)
|
||||
if resp == b'e':
|
||||
self.mode = new_mode
|
||||
print("Switched to: {0}".format(new_mode))
|
||||
self.current_msg = ''
|
||||
return True
|
||||
elif new_mode == 'FT4':
|
||||
self.command_sock.sendto(b'f', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
|
||||
time.sleep(0.05)
|
||||
resp = self.command_sock.recv(1)
|
||||
print("resp = ", resp)
|
||||
if resp == b'f':
|
||||
self.mode = new_mode
|
||||
self.current_msg = ''
|
||||
print("Switched to: {0}".format(new_mode))
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def new_msg(self, msg):
|
||||
#global self.current_msg
|
||||
#global self.mode
|
||||
if msg != self.current_msg:
|
||||
print("Message: {0}".format(msg))
|
||||
if 'FT8' in self.mode:
|
||||
symbols = self.encode_ft8(msg)
|
||||
else:
|
||||
symbols = self.encode_ft4(msg)
|
||||
if symbols.any():
|
||||
# symbols = [kk for kk in range(79)]
|
||||
self.load_symbols(symbols)
|
||||
self.current_msg = msg
|
||||
else:
|
||||
return
|
||||
else:
|
||||
time.sleep(0.005) # Do we need this?
|
||||
|
||||
def transmit(self):
|
||||
if False: # not current_msg:
|
||||
print("No previous message!")
|
||||
time.sleep(1)
|
||||
else:
|
||||
print("TX!")
|
||||
self.command_sock.sendto(b't', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
|
||||
# self.tx_now = False
|
||||
|
||||
def check_time_window(self, utc_time):
|
||||
time_window = 15 if 'FT8' in self.mode else 7
|
||||
print("Time window: ", time_window) # Is correct for FT8 and FT4.
|
||||
rm = utc_time.second % time_window
|
||||
if rm > 1 and rm < time_window - 1: # Within one second of exact time for FT8.
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
# See if HeartBeat will work for this:
|
||||
def HeartBeat(self): # Should be called at 10 Hz from the main.
|
||||
# Check transmitter is initialized
|
||||
#raise Exception("In HeartBeat")
|
||||
|
||||
# print("\n\nWait for transmitter ready...")
|
||||
if self.tx_ready_wsjtx == False:
|
||||
if self.tx_ready_wsjtx_sent == False:
|
||||
self.command_sock.sendto(b'r', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
|
||||
time.sleep(0.05)
|
||||
self.tx_ready_wsjtx_sent = True
|
||||
while True:
|
||||
try:
|
||||
x = self.command_sock.recv(1)
|
||||
if x == b'r':
|
||||
print("Transmitter ready!")
|
||||
self.tx_ready_wsjtx = True
|
||||
break
|
||||
except BlockingIOError:
|
||||
pass
|
||||
if self.tx_ready_wsjtx == True:
|
||||
try:
|
||||
fileContent = self.wsjtx_sock.recv(1024)
|
||||
except Exception as e:
|
||||
if e == BlockingIOError: # This is our way of pollig the socket.
|
||||
# Maybe use select to stop the errors printing after we quit.
|
||||
return BaseHardware.HeartBeat(self)
|
||||
else:
|
||||
# print("Error in HeartBeat")
|
||||
# print(f"Exception type: {type(e).__name__}")
|
||||
# print(f"Exception message: {str(e)}")
|
||||
pass
|
||||
try:
|
||||
NewPacket = WSJTXClass.WSJTX_Packet(fileContent, 0)
|
||||
NewPacket.Decode()
|
||||
|
||||
if NewPacket.PacketType == 1:
|
||||
# print('New Packet is type 1')
|
||||
StatusPacket = WSJTXClass.WSJTX_Status(fileContent, NewPacket.index)
|
||||
StatusPacket.Decode()
|
||||
|
||||
# Check TX frequency and update transceiver
|
||||
new_freq = StatusPacket.TxDF
|
||||
new_mode = StatusPacket.TxMode.strip()
|
||||
|
||||
|
||||
if new_freq != self.tx_freq:
|
||||
# time.sleep(0.03)
|
||||
print('Changing frequency from {0} to {1}'.format(self.tx_freq, new_freq))
|
||||
try:
|
||||
self.change_freq(new_freq)
|
||||
except Exception as e:
|
||||
print(f"Exception occurred: {e}")
|
||||
print ('It is now ', self.tx_freq)
|
||||
|
||||
# print('new_mode', new_mode, ' self.mode is: ', self.mode)
|
||||
if new_mode != self.mode:
|
||||
print("Mode before: {0}".format(self.mode))
|
||||
self.set_mode(new_mode)
|
||||
print("New mode after: {0}".format(self.mode))
|
||||
|
||||
# Check if TX is enabled
|
||||
if StatusPacket.Transmitting == 1:
|
||||
# Check time, avoid transmitting out of the time slot
|
||||
current_time = datetime.datetime.now()
|
||||
utc_time = current_time.astimezone(datetime.timezone.utc)
|
||||
self.tx_now = self.check_time_window(utc_time)
|
||||
if self.tx_now:
|
||||
self.command_sock.sendto(b'p', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT))
|
||||
message = StatusPacket.TxMessage
|
||||
message = message.replace('<', '')
|
||||
message = message.replace('>', '')
|
||||
print('Message is: ', message)
|
||||
self.new_msg(message.strip())
|
||||
print("Encoded message is: ", self.current_msg)
|
||||
# print("tx_now is: ", self.tx_now)
|
||||
if self.tx_now:
|
||||
print('Transmitting')
|
||||
self.transmit()
|
||||
print("Time: {0}:{1}:{2}".format(utc_time.hour, utc_time.minute, utc_time.second))
|
||||
else:
|
||||
pass
|
||||
# print('Not Transmitting')
|
||||
return BaseHardware.HeartBeat(self)
|
||||
|
||||
except Exception as e:
|
||||
# print("Error in HeartBeat")
|
||||
# print(f"Exception type: {type(e).__name__}")
|
||||
# print(f"Exception message: {str(e)}")
|
||||
# traceback.print_exc()
|
||||
return BaseHardware.HeartBeat(self)
|
||||
@@ -4,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.45'
|
||||
|
||||
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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -1,5 +1,6 @@
|
||||
#include <Python.h>
|
||||
#include <complex.h>
|
||||
#include <string.h>
|
||||
#include <SoapySDR/Device.h>
|
||||
#include <SoapySDR/Formats.h>
|
||||
#include <SoapySDR/Version.h>
|
||||
@@ -223,7 +224,7 @@ static PyObject * open_device(PyObject * self, PyObject * args)
|
||||
else {
|
||||
snprintf(buf128, 128, "SoapySDRDevice_make fail: %s", SoapySDRDevice_lastError());
|
||||
}
|
||||
return PyString_FromString(buf128);
|
||||
return PyUnicode_FromString(buf128);
|
||||
}
|
||||
|
||||
static void get_direc_len(const char * name, int * direction, int * length)
|
||||
@@ -254,7 +255,7 @@ static PyObject * get_device_list(PyObject * self, PyObject * args) // Called fr
|
||||
for (i = 0; i < length; i++) {
|
||||
dict = PyDict_New();
|
||||
for (j = 0; j < results[i].size; j++)
|
||||
PyDict_SetItemString(dict, results[i].keys[j], PyString_FromString(results[i].vals[j]));
|
||||
PyDict_SetItemString(dict, results[i].keys[j], PyUnicode_FromString(results[i].vals[j]));
|
||||
PyList_Append(devices, dict);
|
||||
Py_DECREF(dict);
|
||||
}
|
||||
@@ -329,7 +330,7 @@ static PyObject * set_parameter(PyObject * self, PyObject * args) // Called from
|
||||
}
|
||||
}
|
||||
if (msg200[0])
|
||||
return PyString_FromString(msg200);
|
||||
return PyUnicode_FromString(msg200);
|
||||
Py_INCREF (Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
@@ -377,7 +378,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from
|
||||
pylist = PyList_New(0);
|
||||
names = SoapySDRDevice_listAntennas(sdev, direction, 0, &len_list);
|
||||
for (i = 0; i < len_list; i++) {
|
||||
pyobj = PyString_FromString(names[i]);
|
||||
pyobj = PyUnicode_FromString(names[i]);
|
||||
PyList_Append(pylist, pyobj);
|
||||
Py_DECREF(pyobj);
|
||||
}
|
||||
@@ -440,7 +441,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from
|
||||
pylist = PyList_New(0);
|
||||
names = SoapySDRDevice_listGains(sdev, direction, 0, &len_list);
|
||||
for (i = 0; i < len_list; i++) {
|
||||
pyobj = PyString_FromString(names[i]);
|
||||
pyobj = PyUnicode_FromString(names[i]);
|
||||
PyList_Append(pylist, pyobj);
|
||||
Py_DECREF(pyobj);
|
||||
}
|
||||
@@ -450,7 +451,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from
|
||||
else if ( ! strncmp(name, "soapy_listGainsValues", length)) {
|
||||
pylist = PyList_New(0);
|
||||
pylst2 = PyList_New(0); // First element is the total gain
|
||||
pyobj = PyString_FromString("total");
|
||||
pyobj = PyUnicode_FromString("total");
|
||||
PyList_Append(pylst2, pyobj);
|
||||
Py_DECREF(pyobj);
|
||||
range = SoapySDRDevice_getGainRange(sdev, direction, 0);
|
||||
@@ -460,7 +461,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from
|
||||
names = SoapySDRDevice_listGains(sdev, direction, 0, &len_list);
|
||||
for (i = 0; i < len_list; i++) {
|
||||
pylst2 = PyList_New(0);
|
||||
pyobj = PyString_FromString(names[i]);
|
||||
pyobj = PyUnicode_FromString(names[i]);
|
||||
PyList_Append(pylst2, pyobj);
|
||||
Py_DECREF(pyobj);
|
||||
range = SoapySDRDevice_getGainElementRange(sdev, direction, 0, names[i]);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
#include <complex.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
Reference in New Issue
Block a user