Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e219a5b975 | ||
|
|
2a530e5d11 | ||
|
|
ceab3bed26 |
@@ -1,9 +1,3 @@
|
||||
Quisk Version 4.2.55 October 2026
|
||||
==================================
|
||||
Karl Heinz, K5KHK, provided a patche to fix the AC2YD remote control head. He provided another patch
|
||||
to fix Hamlib control when using WSJT-X improved. He added a mic jitter buffer to the remote control
|
||||
head so it works over WiFi. Thanks Karl!
|
||||
|
||||
Quisk Version 4.2.54 August 2026
|
||||
=================================
|
||||
Quisk can now accept audio samples for Tx from TCI at rates 48000, 24000, 12000 and 8000. Please test.
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# Quisk Software Defined Radio
|
||||
|
||||
Quisk is a software defined radio transceiver that runs on a PC or on a single board computer such as a Raspberry Pi.
|
||||
It is written in Python and C.
|
||||
It works with various kinds of radio hardware such as SoftRock, Hermes Lite 2,
|
||||
the SDR-IQ by RfSpace, and with network UDP samples.
|
||||
Quisk is easily extensible by writing your own hardware file in Python.
|
||||
|
||||
I wrote Quisk back in 2008, and I have been maintaining it ever since.
|
||||
For questions and support, please visit the group https://groups.io/g/n2adr-sdr.
|
||||
|
||||
For more information see https://james.ahlstrom.name/quisk.
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
#Quisk version 4.2.55
|
||||
#Quisk version 4.2.54
|
||||
from .quisk import main
|
||||
|
||||
BIN
Binary file not shown.
+8
-101
@@ -53,41 +53,6 @@ static int control_head_data_width; // app.data_width of control head for gr
|
||||
static int packets_sent;
|
||||
static int packets_recd;
|
||||
|
||||
///// The jitter buffer is thanks to Karl Heinz - K5KHK
|
||||
// Jitter buffer for the microphone samples received from the control head. The network can hold
|
||||
// packets for 100 msec or more (Wi-Fi power saving and scanning) and then deliver them in a burst,
|
||||
// but the Hermes Tx buffer only has 50 msec of slack. Without a buffer here, each late burst is an
|
||||
// underflow (a gap on the air) followed by an overflow (discarded samples). So buffer mic_jb_target
|
||||
// frames before playing, and then release them at a steady MIC_JB_RATE.
|
||||
// The buffer adds its delay to the transmit audio, so it is off unless the configuration item
|
||||
// remote_radio_mic_jitter_ms is set. When it is off, samples are passed on as they arrive.
|
||||
#define MIC_JB_RATE 8000 // frames per second sent by the control head
|
||||
#define MIC_JB_SIZE 8000 // ring buffer size in stereo frames
|
||||
#define MIC_JB_MAX_MS 500 // largest configurable delay; MIC_JB_SIZE must hold this plus MIC_JB_EXTRA
|
||||
#define MIC_JB_EXTRA 2000 // trim back to mic_jb_target above mic_jb_target + this (clock drift): 250 msec
|
||||
#define MIC_JB_IDLE 0.5 // seconds without packets that end a transmission
|
||||
|
||||
static int16_t mic_jb[MIC_JB_SIZE * 2];
|
||||
static int mic_jb_target; // frames to buffer before playing, from remote_radio_mic_jitter_ms; zero for off
|
||||
static int mic_jb_read, mic_jb_count, mic_jb_playing, mic_jb_starved;
|
||||
static int mic_jb_underruns, mic_jb_overruns;
|
||||
static double mic_jb_time, mic_jb_frac, mic_jb_last_packet;
|
||||
|
||||
static void mic_jb_reset(void)
|
||||
{
|
||||
int msec;
|
||||
|
||||
msec = QuiskGetConfigInt("remote_radio_mic_jitter_ms", 0);
|
||||
if (msec < 0)
|
||||
msec = 0;
|
||||
else if (msec > MIC_JB_MAX_MS)
|
||||
msec = MIC_JB_MAX_MS;
|
||||
mic_jb_target = msec * MIC_JB_RATE / 1000;
|
||||
mic_jb_read = mic_jb_count = mic_jb_playing = mic_jb_starved = 0;
|
||||
mic_jb_underruns = mic_jb_overruns = 0;
|
||||
mic_jb_frac = 0;
|
||||
}
|
||||
|
||||
// Receive stereo 16-bit pcm radio speaker sound on the control head via UDP
|
||||
int read_remote_radio_sound_socket(complex double * cSamples)
|
||||
{
|
||||
@@ -152,9 +117,9 @@ int read_remote_radio_sound_socket(complex double * cSamples)
|
||||
// Receive stereo 16-bit pcm microphone samples at the remote radio via UDP
|
||||
int read_remote_mic_sound_socket(complex double * cSamples)
|
||||
{
|
||||
int i, j, bytes, nInt16, nSamples, nFrames;
|
||||
int i, bytes, nInt16, nSamples;
|
||||
int16_t buf[UDP_SEND_INT16];
|
||||
double now, due;
|
||||
double samp_r, samp_l;
|
||||
struct timeval tm_wait;
|
||||
fd_set fds;
|
||||
static struct quisk_cHB45Filter HalfBand;
|
||||
@@ -169,8 +134,8 @@ int read_remote_mic_sound_socket(complex double * cSamples)
|
||||
quisk_filt_cInit(&cFiltInterp3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double));
|
||||
}
|
||||
|
||||
// read all available packets into the jitter buffer, one per loop
|
||||
now = QuiskTimeSec();
|
||||
// read all available packets, one per loop
|
||||
nSamples = 0;
|
||||
while (1) {
|
||||
tm_wait.tv_sec = 0;
|
||||
tm_wait.tv_usec = 0;
|
||||
@@ -187,71 +152,15 @@ int read_remote_mic_sound_socket(complex double * cSamples)
|
||||
break;
|
||||
}
|
||||
if (bytes > 0) {
|
||||
if (mic_jb_starved) { // the buffer ran dry; count it unless the transmission had ended
|
||||
mic_jb_starved = 0;
|
||||
if (now - mic_jb_last_packet < MIC_JB_IDLE)
|
||||
mic_jb_underruns++;
|
||||
}
|
||||
mic_jb_last_packet = now;
|
||||
nInt16 = bytes / 2;
|
||||
for (i = 0; i + 1 < nInt16; i += 2) {
|
||||
if (mic_jb_count >= MIC_JB_SIZE) { // full: drop the oldest frame
|
||||
mic_jb_read = (mic_jb_read + 1) % MIC_JB_SIZE;
|
||||
mic_jb_count--;
|
||||
}
|
||||
j = (mic_jb_read + mic_jb_count) % MIC_JB_SIZE * 2;
|
||||
mic_jb[j] = buf[i];
|
||||
mic_jb[j + 1] = buf[i + 1];
|
||||
mic_jb_count++;
|
||||
for (i = 0; i < nInt16; i += 2) {
|
||||
samp_r = buf[i];
|
||||
samp_l = buf[i + 1];
|
||||
cSamples[nSamples++] = (samp_r + I * samp_l) / CLIP16 * CLIP32;
|
||||
}
|
||||
}
|
||||
} // while(1)
|
||||
|
||||
// release the frames that are due since the last call
|
||||
nSamples = 0;
|
||||
if (mic_jb_target == 0) { // the jitter buffer is off: pass on everything received
|
||||
nFrames = mic_jb_count;
|
||||
}
|
||||
else if ( ! mic_jb_playing) {
|
||||
nFrames = 0;
|
||||
if (mic_jb_count >= mic_jb_target) {
|
||||
mic_jb_playing = 1;
|
||||
mic_jb_time = now;
|
||||
mic_jb_frac = 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
due = (now - mic_jb_time) * MIC_JB_RATE + mic_jb_frac;
|
||||
if (due < 0) // the clock was set back
|
||||
due = 0;
|
||||
else if (due > MIC_JB_RATE / 2) // limit to 0.5 sec, well within SAMP_BUFFER_SIZE after interpolation
|
||||
due = MIC_JB_RATE / 2;
|
||||
mic_jb_time = now;
|
||||
nFrames = (int)due;
|
||||
mic_jb_frac = due - nFrames;
|
||||
if (nFrames > mic_jb_count) { // ran dry: play what is left, then buffer up again
|
||||
nFrames = mic_jb_count;
|
||||
mic_jb_playing = 0;
|
||||
mic_jb_starved = 1;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < nFrames; i++) {
|
||||
j = mic_jb_read * 2;
|
||||
cSamples[nSamples++] = ((double)mic_jb[j] + I * (double)mic_jb[j + 1]) / CLIP16 * CLIP32;
|
||||
mic_jb_read = (mic_jb_read + 1) % MIC_JB_SIZE;
|
||||
}
|
||||
mic_jb_count -= nFrames;
|
||||
if (mic_jb_target && mic_jb_count > mic_jb_target + MIC_JB_EXTRA) { // the control head's clock is faster than ours
|
||||
i = mic_jb_count - mic_jb_target;
|
||||
mic_jb_read = (mic_jb_read + i) % MIC_JB_SIZE;
|
||||
mic_jb_count -= i;
|
||||
mic_jb_overruns++;
|
||||
}
|
||||
if ((mic_jb_underruns || mic_jb_overruns) && now - mic_jb_last_packet > MIC_JB_IDLE) {
|
||||
QuiskPrintf("Remote mic jitter buffer: %d underruns, %d overruns\n", mic_jb_underruns, mic_jb_overruns);
|
||||
mic_jb_underruns = mic_jb_overruns = 0;
|
||||
}
|
||||
|
||||
nSamples = quisk_cInterpolate(cSamples, nSamples, &cFiltInterp3, 3);
|
||||
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand);
|
||||
|
||||
@@ -807,7 +716,6 @@ PyObject * quisk_start_remote_radio_remote_sound(PyObject * self, PyObject * arg
|
||||
|
||||
packets_sent = 0;
|
||||
packets_recd = 0;
|
||||
mic_jb_reset();
|
||||
|
||||
return Py_None;
|
||||
}
|
||||
@@ -834,7 +742,6 @@ PyObject * quisk_stop_remote_radio_remote_sound(PyObject * self, PyObject * args
|
||||
control_head_graph_socket_started = 0;
|
||||
remote_radio_graph_socket_started = 0;
|
||||
control_head_data_width = 0;
|
||||
mic_jb_reset();
|
||||
|
||||
QuiskPrintf("total packets sent = %i, recd = %i\n", packets_sent, packets_recd);
|
||||
|
||||
|
||||
+3
-22
@@ -190,20 +190,10 @@ class Remot: # Remote comtrol base class
|
||||
return
|
||||
try: # Read any data from the socket
|
||||
text = self.remote_ctl_connection.recv(1024)
|
||||
# Thanks to Karl Heinz - K5KHK
|
||||
except (BlockingIOError, InterruptedError, socket.timeout): # No data available now
|
||||
return
|
||||
except OSError: # For example ConnectionResetError: the Control Head is gone
|
||||
print('Closing Remote Control connection: recv() failed')
|
||||
self.RemoteCtlClose(False)
|
||||
except:
|
||||
#traceback.print_exc()
|
||||
return
|
||||
else: # We got some characters
|
||||
if not text: # recv() returns nothing only when the Control Head has closed the connection
|
||||
# Close now rather than waiting for the heartbeat timeout: a Control Head that
|
||||
# crashed or lost its network may have left the transmitter keyed.
|
||||
print('Closing Remote Control connection: closed by Control Head')
|
||||
self.RemoteCtlClose(False)
|
||||
return
|
||||
if not isinstance(text, str):
|
||||
text = text.decode('utf-8')
|
||||
self.received += text
|
||||
@@ -385,22 +375,13 @@ class Remot: # Remote comtrol base class
|
||||
self.cw_next_keydown = None
|
||||
|
||||
def StopTransmit(self):
|
||||
# PTT code added thanks to Karl Heinz - K5KHK
|
||||
# Called whenever the remote session ends (RemoteCtlClose). Release everything the
|
||||
# Control Head may have keyed: the CW key and, for voice and digital modes, PTT.
|
||||
# Without the PTT release, a Control Head that disappears while transmitting (a crash,
|
||||
# a lost network, a dead battery) leaves the transmitter keyed until someone unkeys it
|
||||
# at the radio.
|
||||
# TODO: Add code for modes other than CW
|
||||
while len(self.cw_event_queue):
|
||||
self.cw_event_queue.popleft()
|
||||
self.cw_next_event_ts = None
|
||||
self.cw_next_keydown = None
|
||||
self.cw_key_down = 0
|
||||
QS.set_remote_cwkey(0)
|
||||
ptt = getattr(self.app, 'pttButton', None)
|
||||
if ptt and ptt.GetValue():
|
||||
print('Remote Control connection closed while transmitting: releasing PTT')
|
||||
ptt.SetValue(0, True) # Same as releasing PTT at the radio; runs OnButtonPTT
|
||||
|
||||
def ProcessRadioBtn(self, command, cmd_text):
|
||||
# Large and Small format screens send different button events for radio buttons.
|
||||
|
||||
@@ -128,10 +128,10 @@ static PyObject * open_rx_udp(const char * ip, int port)
|
||||
shutdown(rx_udp_socket, QUISK_SHUT_BOTH);
|
||||
close(rx_udp_socket);
|
||||
rx_udp_socket = INVALID_SOCKET;
|
||||
snprintf(buf, sizeof(buf), "Failed to connect to UDP %s port %u", ip, port);
|
||||
sprintf(buf, "Failed to connect to UDP %s port %u", ip, port);
|
||||
}
|
||||
else {
|
||||
snprintf(buf, sizeof(buf), "Capture from UDP %s port %u", ip, port);
|
||||
sprintf(buf, "Capture from UDP %s port %u", ip, port);
|
||||
#if DEBUG_IO
|
||||
if (getsockopt(rx_udp_socket, SOL_SOCKET, SO_RCVBUF, (char *)&intbuf, &bufsize) == 0)
|
||||
{
|
||||
|
||||
+12
-2
@@ -727,6 +727,17 @@ class Configuration:
|
||||
count -= 1
|
||||
return count
|
||||
|
||||
class xxConfigHelp(wx.html.HtmlWindow): # The "Help with Radios" first-level page
|
||||
"""Create the help screen for the configuration tabs."""
|
||||
def __init__(self, parent):
|
||||
wx.html.HtmlWindow.__init__(self, parent, -1, size=(win_width, 100))
|
||||
if "gtk2" in wx.PlatformInfo:
|
||||
self.SetStandardFonts()
|
||||
self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22])
|
||||
self.SetBackgroundColour(parent.bg_color)
|
||||
# read in text from file help_conf.html in the directory of this module
|
||||
self.LoadFile('help_conf.html')
|
||||
|
||||
class QPowerMeterCalibration(wx.Frame):
|
||||
"""Create a window to enter the power output and corresponding ADC value AIN1/2"""
|
||||
def __init__(self, parent, local_names):
|
||||
@@ -2291,8 +2302,7 @@ class ControlMixin:
|
||||
if name in ('hot_key_ptt_toggle', 'hot_key_ptt_if_hidden', 'keyupDelay', 'cwTone', 'pulse_audio_verbose_output',
|
||||
'start_cw_delay', 'start_ssb_delay', 'maximum_tx_secs', 'quisk_serial_cts', 'quisk_serial_dsr',
|
||||
'hot_key_ptt1', 'hot_key_ptt2', 'midi_ptt_toggle', 'TxRxSilenceMsec',
|
||||
'fixed_tune_offset', 'calibrate_temp_20', 'calibrate_temp_40', 'calibrate_current_0', 'calibrate_current_1',
|
||||
'remote_radio_mic_jitter_ms'):
|
||||
'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_'):
|
||||
|
||||
@@ -307,7 +307,6 @@ These are part of your operating system. You only install them once unless it ch
|
||||
</p>
|
||||
<br>
|
||||
<pre>
|
||||
sudo apt-get install gcc
|
||||
sudo apt-get install libfftw3-dev
|
||||
sudo apt-get install libasound2-dev
|
||||
sudo apt-get install portaudio19-dev
|
||||
|
||||
@@ -56,7 +56,7 @@ $(OBJDIR):
|
||||
@mkdir -p $(OBJDIR)
|
||||
|
||||
# Link the shared library object
|
||||
$(TARGET): $(OBJS) ac2yd/remote.o
|
||||
$(TARGET): $(OBJS)
|
||||
$(CC) -o $@ $(OBJS) ac2yd/remote.o $(LDFLAGS)
|
||||
|
||||
# Define the location for dependency files
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
# This provides support for the SDR Micron TRX by David Fainitski, N7DDC
|
||||
|
||||
# On Windows this uses Python module ftd2xx. Install with pip.
|
||||
# On Linux it uses ftdi1. Install python3-ftdi1 with your package manager.
|
||||
|
||||
import sys, wx, traceback, time
|
||||
from quisk_hardware_model import Hardware as BaseHardware
|
||||
|
||||
DEBUG = 1 # print debug messages during operation
|
||||
WRITE_LOG = 1 # write the data received from the MicronTRX during self.log_time to the file self.log_name
|
||||
#WRITE_LOG = 2 # receive data from the file self.log_name instead of from USB
|
||||
|
||||
# https://github.com/Dfinitski/SDR-Micron
|
||||
|
||||
# Short Control Packet type "SCP" from PC to Micron TRX, 16 bytes, MSB first (big endian)
|
||||
#
|
||||
# (0 - 7) Preamble, seven 0x55 and 0xD5
|
||||
# (8, 9, 10) "SCP"
|
||||
#(11) RX0 enable, 0 - 1
|
||||
#(12) RX1 enable, 0 - 1
|
||||
#(13) BS0 enable, 0 - 1
|
||||
#(14) TX0 enable, 0 - 1
|
||||
#(15) CDC enable, 0 - 1
|
||||
|
||||
# Universal control packet type "UCP" from PC to TRX or from TRX to PC, 64 bytes, MSB first (big-endian)
|
||||
#
|
||||
# (0 - 7) Preamble, seven 0x55 and 0xD5
|
||||
# (8, 9, 10) "UCP"
|
||||
# (11) read only FW prefix MSB
|
||||
# (12) read only FW prefix
|
||||
# (13) read only FW prefix LSB
|
||||
# (14) read only char '-'
|
||||
# (15) Not used
|
||||
# (16) Not used
|
||||
# (17) Not used
|
||||
# (18, 19, 20, 21) - 4 bytes RX0 frequency
|
||||
# (22, 23, 24, 25) - 4 bytes RX1 frequency
|
||||
# (26, 27, 28, 29) - 4 bytes BS0 frequency
|
||||
# (30, 31, 32, 33) - 4 bytes TX0 frequency
|
||||
# (34) - Attenuator, 0 - 31 (dB value)
|
||||
# (35) - BS0 Rate
|
||||
# 0 - 48 kHz,
|
||||
# 1 - 96 kHz,
|
||||
# 2 - 192 kHz,
|
||||
# 3 - 384 kHz,
|
||||
# 4 - 768 kHz,
|
||||
# 5 - 1536 kHz
|
||||
# (36) LNA enable
|
||||
# (37) TX level 0 - 255
|
||||
# (38) RX BPF bypass 0 - 1
|
||||
# (39) BS0 mode
|
||||
# 0 - 4096 buffered samples
|
||||
# 1 - 8192 buffered samples
|
||||
# 2 - 16384 buffered samples
|
||||
# (40) Not used
|
||||
# (41) Not used
|
||||
# (42) read-only PTT state from device 0-1
|
||||
# (43) Mic Boost, 0 - 1
|
||||
# (44) TX LPF Bypass 0 - 1
|
||||
# (45) PTT to device 1 - 0
|
||||
# (46) CW mode on 0 - 1
|
||||
# (47) CW key manipulation from PC to device 1 - 0
|
||||
# (48) Read only PWR, Output power in Watts
|
||||
# (49) Read only SWR x.x, from 1.0 to 9.9 as a natural number without decimal point
|
||||
# (50) Key Speed 10 - 63 ppm (20 by default)
|
||||
# (51) Paddle swap 0, 1
|
||||
# (52) Key mode, 0 = straight, 01 = Mode A, 10 = Mode B
|
||||
# (53) Key weight, 33 to 66, (50 by default)
|
||||
# (54) Key auto spacing, 0, 1
|
||||
# (55) ST Level, 0 - 255 sight tone level
|
||||
# (56) ST Freq in tens Hertz, 40 - 100 (400 - 1000Hz), 70 by default
|
||||
# (57) Not used
|
||||
# (58) Delay ON, 8 - 255 ms (25 by default) - time for TX carrier delay is needed for transmitter preparing to TX
|
||||
# (59) Hang out time in tens ms 0 - 255 (0 to 2550 msec) (250 ms by default) needed to keep transmitter ready after PTT
|
||||
# (60) Read only CW key manipulation from device to PC
|
||||
# (61) Read only FW2 - firmware version MSB
|
||||
# (62) Read only FW1
|
||||
# (63) Read only FW0 - firmware version LSB
|
||||
|
||||
# Define the name of the hardware and the items on the hardware screen (see quisk_conf_defaults.py):
|
||||
################ Receivers SdrMicronTRX, The SDR Micron TRX project by David Fainitski.
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
#hardware_file_name = 'microntrxpkg/quisk_hardware.py'
|
||||
|
||||
|
||||
class Hardware(BaseHardware):
|
||||
sample_rates = [48] #[48, 96, 192, 384, 768, 1536]
|
||||
BS0_samples = {0:4096, 1:8192, 2:16384}
|
||||
BS0_bytes = {0:4096 * 2, 1:8192 * 2, 2:16384 * 2} # Two bytes per sample
|
||||
def __init__(self, app, conf):
|
||||
BaseHardware.__init__(self, app, conf)
|
||||
self.usb_windows = None
|
||||
self.usb_linux = None
|
||||
self.prefix = b'\x55\x55\x55\x55\x55\x55\x55\xD5'
|
||||
self.read_data = bytearray()
|
||||
self.tx_samples = b''
|
||||
self.rf_gain_labels = ('RF 0', 'RF -10', 'RF -20', 'RF -30')
|
||||
self.index = 1
|
||||
self.old_vfo = 0
|
||||
self.sdrmicron_clock = 76800000
|
||||
self.sdrmicron_decim = 1600
|
||||
self.bscope_data = bytearray()
|
||||
self.have_version = False
|
||||
self.frame_msg = ''
|
||||
self.remaining_BS0 = -1
|
||||
self.log_name = "microntrxpkg/microntrx_log.dat"
|
||||
self.log_time = 5.0 # seconds of log data
|
||||
self.log_time0 = None # starting time.time() to start log file
|
||||
self.log_fp = None
|
||||
self.pc_control = bytearray(64)
|
||||
self.pc_control[0:8] = self.prefix
|
||||
self.pc_control[8:11] = b'UCP'
|
||||
self.got_defaults = 0
|
||||
self.pc_enables = bytearray(16)
|
||||
self.pc_enables[0:8] = self.prefix
|
||||
self.pc_enables[8:11] = b'SCP'
|
||||
self.fw_version = bytearray
|
||||
|
||||
if conf.fft_size_multiplier == 0:
|
||||
conf.fft_size_multiplier = 3 # Set size needed by VarDecim
|
||||
|
||||
def open(self): # This method must return a string showing whether the open succeeded or failed.
|
||||
if WRITE_LOG == 1:
|
||||
self.log_fp = open(self.log_name, "wb")
|
||||
elif WRITE_LOG == 2:
|
||||
self.log_fp = open(self.log_name, "rb")
|
||||
rx_bytes = 3 # rx_bytes is the number of bytes in each I or Q sample: 1, 2, 3, or 4
|
||||
rx_endian = 1 # rx_endian is the order of bytes in the sample array: 0 == little endian; 1 == big endian
|
||||
self.InitSamples(rx_bytes, rx_endian) # Initialize: read samples from this hardware file and send them to Quisk
|
||||
self.InitTxSamples(2, 1, 48000) # Initialize Tx samples to send to the hardware
|
||||
bs_bytes = 2
|
||||
bs_endian = 1
|
||||
#self.InitBscope(bs_bytes, bs_endian, self.sdrmicron_clock, self.BS1_samples[self.pc_control[40]]) # Initialize bandscope
|
||||
if WRITE_LOG == 2:
|
||||
return "Read data from %s" % self.log_name
|
||||
elif sys.platform == 'win32':
|
||||
return self.open_win()
|
||||
else:
|
||||
return self.open_linux()
|
||||
|
||||
def open_win(self):
|
||||
try:
|
||||
import ftd2xx as d2xx
|
||||
except:
|
||||
return 'The ftd2xx module is missing'
|
||||
enum = d2xx.createDeviceInfoList() # quantity of FTDI devices
|
||||
if enum == 0:
|
||||
return 'There are no FTDI devices available'
|
||||
for i in range(enum): # Searching and opening needed device
|
||||
a = d2xx.getDeviceInfoDetail(i)
|
||||
if DEBUG:
|
||||
print ("%d: description %s, serial %s" % (i, a['description'], a['serial']))
|
||||
if a['description']==b'SDR-Micron':
|
||||
try:
|
||||
usb = d2xx.openEx(a['serial'])
|
||||
except:
|
||||
return 'Device SDR-Micron could not be opened'
|
||||
usb.setBitMode(255, 0) # reset
|
||||
time.sleep(0.1)
|
||||
usb.setBitMode(255, 64) # Configure FT2232H into Sync FIFO mode
|
||||
usb.setTimeouts(100, 100) # read, write
|
||||
usb.setLatencyTimer(2)
|
||||
usb.setUSBParameters(32, 32) # in_tx_size, out_tx_size
|
||||
time.sleep(1.5) # waiting for device initialisation
|
||||
usb.read(usb.getQueueStatus()) # clean the usb data buffer
|
||||
self.frame_msg = a['description'].decode('utf-8') + ' S/N - ' + a['serial'].decode('utf-8')
|
||||
self.usb_windows = usb
|
||||
return self.frame_msg
|
||||
return 'Device SDR-Micron was not found'
|
||||
|
||||
def open_linux(self): # Not finished
|
||||
try:
|
||||
import ftdi1
|
||||
except:
|
||||
return 'The ftdi1 module is missing'
|
||||
usb = ftdi1.new()
|
||||
# Open Channel A (interface 1 in libftdi)
|
||||
ftdi1.set_interface(usb, ftdi1.INTERFACE_A)
|
||||
rc = ftdi1.usb_open(usb, 0x0403, 0x6014) # 0x6014 is the standard FT232H PID
|
||||
if rc == 0:
|
||||
ftdi1.usb_reset(usb)
|
||||
ftdi1.set_usb_read_timout(usb, 1)
|
||||
ftdi1.set_usb_write_timout(usb, 5)
|
||||
ftdi1.set_latency_timer(usb, 2)
|
||||
# Enable Sync FIFO mode
|
||||
# 0xFF = pin mask, 0x40 = BITMODE_SYNCFF
|
||||
ftdi1.set_bitmode(usb, 0xFF, ftdi1.BITMODE_SYNCFF)
|
||||
if DEBUG:
|
||||
print("Sync FIFO active via ftdi1.")
|
||||
self.usb_linux = usb
|
||||
return "Device SDR-Micron opened"
|
||||
return 'Device SDR-Micron was not found'
|
||||
|
||||
def usb_read(self):
|
||||
if WRITE_LOG == 2:
|
||||
data = self.log_fp.read(120)
|
||||
return data
|
||||
elif self.usb_windows:
|
||||
count = self.usb_windows.getQueueStatus() # Read all available bytes
|
||||
if count > 0:
|
||||
return self.usb_windows.read(count) # return Python bytes
|
||||
elif self.usb_linux:
|
||||
count, data = ftdi1.read_data(self.usb_linux, 1024)
|
||||
if count > 0:
|
||||
return data # data is Python bytes
|
||||
elif count < 0 and DEBUG:
|
||||
print("Error %d in usb_read", count)
|
||||
return None
|
||||
|
||||
def usb_write(self, data):
|
||||
data = bytes(data) # Convert bytearray to bytes
|
||||
if self.usb_windows:
|
||||
try:
|
||||
self.usb_windows.write(data)
|
||||
except:
|
||||
if DEBUG:
|
||||
print('Error while usb_write')
|
||||
elif self.usb_linux:
|
||||
count = ftdi1.write_data(self.usb_linux, data)
|
||||
if count != len(data) and DEBUG:
|
||||
print("Failure in usb_write_data %d %d", count, len(data))
|
||||
|
||||
def close(self):
|
||||
if self.usb_windows:
|
||||
self.usb_windows.setBitMode(255, 0) # reset
|
||||
time.sleep(0.5)
|
||||
self.usb_windows.close()
|
||||
self.usb_windows = None
|
||||
elif self.usb_linux:
|
||||
ftdi1.usb_close(self.usb_linux)
|
||||
ftdi1.free(self.usb_linux)
|
||||
self.usb_linux = None
|
||||
|
||||
def OnButtonRfGain(self, event):
|
||||
btn = event.GetEventObject()
|
||||
n = btn.index
|
||||
if n == 0:
|
||||
self.pc_control[34] = 0
|
||||
elif n == 1:
|
||||
self.pc_control[34] = 10
|
||||
elif n == 2:
|
||||
self.pc_control[34] = 20
|
||||
elif n == 3:
|
||||
self.pc_control[34] = 30
|
||||
if DEBUG:
|
||||
print ('pc_control[34]', self.pc_control[34])
|
||||
self.rx_control_upd()
|
||||
|
||||
def ChangeFrequency(self, tx_freq, vfo, source='', band='', event=None):
|
||||
if DEBUG:
|
||||
print('ChangeFrequency tx_freq', tx_freq, 'vfo', vfo)
|
||||
if tx_freq and tx_freq > 0:
|
||||
tx_freq = int(tx_freq - self.transverter_offset)
|
||||
self.pc_control[30] = self.pc_control[18] = (tx_freq >> 24) & 0xFF # MSB
|
||||
self.pc_control[31] = self.pc_control[19] = (tx_freq >> 16) & 0xFF
|
||||
self.pc_control[32] = self.pc_control[20] = (tx_freq >> 8) & 0xFF
|
||||
self.pc_control[33] = self.pc_control[21] = (tx_freq ) & 0xFF #LSB
|
||||
self.rx_control_upd()
|
||||
if vfo:
|
||||
vfo = int(vfo - self.transverter_offset)
|
||||
if vfo != self.old_vfo:
|
||||
self.old_vfo = vfo
|
||||
self.pc_control[18] = (vfo >> 24) & 0xFF # MSB
|
||||
self.pc_control[19] = (vfo >> 16) & 0xFF
|
||||
self.pc_control[20] = (vfo >> 8) & 0xFF
|
||||
self.pc_control[21] = (vfo ) & 0xFF #LSB
|
||||
self.rx_control_upd()
|
||||
return tx_freq, vfo
|
||||
|
||||
def OnSpot(self, level):
|
||||
pass
|
||||
|
||||
def OnChangeRxTx(self, is_rx):
|
||||
self.pc_control[45] = is_rx
|
||||
self.rx_control_upd()
|
||||
|
||||
def VarDecimGetChoices(self): # Return a list/tuple of strings for the decimation control.
|
||||
return list(map(str, self.sample_rates)) # convert integer to string
|
||||
|
||||
def VarDecimGetLabel(self): # return a text label for the control
|
||||
return "Sample rate ksps"
|
||||
|
||||
def VarDecimGetIndex(self): # return the current index
|
||||
return self.index
|
||||
|
||||
def VarDecimSet(self, index=None): # return sample rate
|
||||
if index is None: # initial call to set the sample rate before the call to open()
|
||||
rate = self.application.vardecim_set
|
||||
try:
|
||||
self.index = self.sample_rates.index(rate // 1000)
|
||||
except:
|
||||
self.index = 0
|
||||
else:
|
||||
self.index = index
|
||||
self.pc_control[35] = self.index
|
||||
rate = self.sample_rates[self.index] * 1000
|
||||
if DEBUG:
|
||||
print("varDecimSet index", self.index, "rate", rate)
|
||||
self.rx_control_upd()
|
||||
return rate
|
||||
|
||||
def VarDecimRange(self): # Return the lowest and highest sample rate.
|
||||
#return (48000, 1536000)
|
||||
return (48000, 48000)
|
||||
|
||||
def StartSamples(self): # called by the sound thread
|
||||
self.pc_enables[11] = 1 # RX0 enable
|
||||
#self.pc_enables[13] = 1 # BS0 enable
|
||||
self.usb_write(self.pc_enables)
|
||||
if DEBUG:
|
||||
print ("Start samples")
|
||||
|
||||
def StopSamples(self): # called by the sound thread
|
||||
self.pc_enables[11] = 0
|
||||
self.pc_enables[13] = 0
|
||||
self.usb_write(self.pc_enables)
|
||||
if DEBUG:
|
||||
print ("Stop Samples")
|
||||
|
||||
def rx_control_upd(self):
|
||||
if self.got_defaults:
|
||||
self.usb_write(self.pc_control)
|
||||
|
||||
def GetRxSamples(self): # Read from the MicronTRX. Called frequently from the sound thread.
|
||||
# Send Tx samples to the hardware
|
||||
self.tx_samples += self.GetTxSamples()
|
||||
while len(self.tx_samples) >= 492:
|
||||
data = self.prefix + b'TX0\x00\x00\x00\x00\x00' + self.tx_samples[0:492]
|
||||
self.usb_write(data)
|
||||
#print(data[0:24], len(data))
|
||||
#i = data[16] << 8 | data[17]
|
||||
#print ("%8d" % i)
|
||||
self.tx_samples = self.tx_samples[492:]
|
||||
# Read messages from USB
|
||||
data = self.usb_read()
|
||||
if data:
|
||||
self.read_data += data
|
||||
if DEBUG > 1:
|
||||
print("Read %s bytes, buffer length %d" % (len(data), len(self.read_data)))
|
||||
if WRITE_LOG == 1:
|
||||
if self.log_fp:
|
||||
if self.log_time0 is None:
|
||||
self.log_time0 = time.time()
|
||||
if time.time() - self.log_time0 < self.log_time:
|
||||
self.log_fp.write(data)
|
||||
else:
|
||||
self.log_fp.close()
|
||||
self.log_fp = None
|
||||
while True:
|
||||
start = self.read_data.find(self.prefix)
|
||||
if start < 0: # No prefix
|
||||
return
|
||||
if start > 0:
|
||||
del self.read_data[0:start]
|
||||
if DEBUG:
|
||||
print ("USB blocks lost sync")
|
||||
if len(self.read_data) < 11:
|
||||
return
|
||||
block_type = self.read_data[8:11]
|
||||
if block_type == b'BS0':
|
||||
if len(self.read_data) < 508:
|
||||
return
|
||||
packet_number = self.read_data[11]
|
||||
if packet_number == 0: # start of a block of data
|
||||
self.remaining_BS0 = self.BS0_bytes[self.pc_control[39]]
|
||||
self.bscope_data = self.read_data[16:508] # 492 bytes
|
||||
self.remaining_BS0 -= 492
|
||||
elif self.remaining_BS0 >= 492:
|
||||
self.bscope_data += self.read_data[16:508] # 492 bytes
|
||||
self.remaining_BS0 -= 492
|
||||
elif self.remaining_BS0 <= 0:
|
||||
pass
|
||||
else:
|
||||
self.bscope_data += self.read_data[16:self.remaining_BS0 + 16]
|
||||
self.remaining_BS0 = -1
|
||||
# self.AddBscopeSamples(self.bscope_data)
|
||||
if DEBUG > 1:
|
||||
print("BS0 buffer length %d" % len(self.bscope_data))
|
||||
del self.read_data[0:508]
|
||||
elif block_type == b'CDM':
|
||||
if len(self.read_data) < 180:
|
||||
return
|
||||
if DEBUG > 1:
|
||||
print("CDM")
|
||||
del self.read_data[0:180]
|
||||
elif block_type == b'RX0':
|
||||
if len(self.read_data) < 508:
|
||||
return
|
||||
if self.read_data[12]:
|
||||
self.GotClip()
|
||||
self.AddRxSamples(self.read_data[16:508])
|
||||
if DEBUG > 1:
|
||||
print("RX0")
|
||||
del self.read_data[0:508]
|
||||
elif block_type == b'RX1':
|
||||
if len(self.read_data) < 508:
|
||||
return
|
||||
if DEBUG > 1:
|
||||
print("RX1")
|
||||
del self.read_data[0:508]
|
||||
elif block_type == b'UCP':
|
||||
if len(self.read_data) < 64:
|
||||
return
|
||||
if self.have_version is False:
|
||||
self.have_version = True
|
||||
self.fw_version = self.read_data[11:15] + self.read_data[61:64]
|
||||
self.frame_msg += ' F/W version - ' + self.fw_version.decode('utf-8')
|
||||
self.application.main_frame.SetConfigText(self.frame_msg)
|
||||
self.pc_control[11:64] = self.read_data[11:64] # save UCP content with defaults
|
||||
self.got_defaults = 1;
|
||||
if DEBUG > 1:
|
||||
print("UCP")
|
||||
del self.read_data[0:64]
|
||||
else:
|
||||
del self.read_data[0:11]
|
||||
if DEBUG:
|
||||
print("Unknown USB block type")
|
||||
+2
-2
@@ -73,7 +73,7 @@ static double modulation_index = 1.6; // For FM transmit, the modulation index
|
||||
static int is_vox = 0; // Is the VOX level exceeded?
|
||||
static int vox_level = CLIP16; // VOX trigger level as a number 0 to CLIP16
|
||||
static int timeVOX = 2000; // VOX hang time in milliseconds
|
||||
static int tx_sample_rate = 48000; // Used for SoapySDR
|
||||
static int tx_sample_rate = 48000; // Used for SoapySDR and Tx samples from Python
|
||||
static int reverse_tx_sideband;
|
||||
static int PsEnable = 0;
|
||||
static int PsCal = 0;
|
||||
@@ -1305,7 +1305,7 @@ int quisk_process_microphone(int mic_sample_rate, complex double * cSamples, int
|
||||
; // hermes handles this itself
|
||||
else if (quisk_play_state == SOFTWARE_CWKEY)
|
||||
serial_key_samples(cSamples, count);
|
||||
if(quisk_pt_sample_write) { // Used for SoapySDR
|
||||
if(quisk_pt_sample_write && key_down) { // Used for SoapySDR and samples from Python
|
||||
// Interpolate the mic samples to the Tx sample rate
|
||||
//printf("Tx sample rate %i\n", tx_sample_rate);
|
||||
switch (tx_sample_rate) {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "quisk"
|
||||
version = "4.2.55"
|
||||
version = "4.2.54"
|
||||
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"
|
||||
|
||||
@@ -115,6 +115,8 @@ int quiskTxHoldState; // hold Tx until the repeater frequency shift is complet
|
||||
int quisk_is_vna; // zero for normal program, one for the VNA program
|
||||
static int py_sample_rx_bytes=2; // number of bytes in each I or Q sample: 1, 2, 3, or 4
|
||||
static int py_sample_rx_endian; // order of sample array: 0 == little endian; 1 == big endian
|
||||
static int py_sample_tx_bytes; // number of bytes in each I or Q sample: 2, 3, or 4
|
||||
static int py_sample_tx_endian; // order of sample array: 0 == little endian; 1 == big endian
|
||||
static int py_bscope_bytes;
|
||||
static int py_bscope_endian;
|
||||
static int quisk_auto_notch; // auto notch control
|
||||
@@ -159,6 +161,8 @@ static int softrock_correct_active; // 0 No correction adjustment; 1 Manual adju
|
||||
|
||||
static complex double PySampleBuf[SAMP_BUFFER_SIZE]; // buffer for samples returned from Python
|
||||
static int PySampleCount; // count of samples in buffer
|
||||
static uint8_t PySampleTxBytes[SAMP_BUFFER_SIZE * 8]; // buffer for Tx sample bytes to send to the hardware
|
||||
static int PySampleTxCount;
|
||||
static bool quisk_tx_inhibit;
|
||||
|
||||
static int multirx_data_width; // width of graph data to return
|
||||
@@ -3018,6 +3022,17 @@ static PyObject * add_bscope_samples(PyObject * self, PyObject * args)
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
static PyObject * get_tx_samples(PyObject * self, PyObject * args)
|
||||
{
|
||||
PyObject * by;
|
||||
|
||||
if (!PyArg_ParseTuple (args, ""))
|
||||
return NULL;
|
||||
by = PyBytes_FromStringAndSize((const char *)PySampleTxBytes, PySampleTxCount);
|
||||
PySampleTxCount = 0;
|
||||
return by;
|
||||
}
|
||||
|
||||
static void py_sample_start(void)
|
||||
{
|
||||
}
|
||||
@@ -3040,6 +3055,83 @@ static int py_sample_read(complex double * cSamples)
|
||||
return n;
|
||||
}
|
||||
|
||||
static int py_sample_tx_save(complex double * cSamples, int nSamples)
|
||||
{ // These cSamples are 16-bit integers from the microphone
|
||||
int i;
|
||||
int16_t rr, ii;
|
||||
uint8_t rrH, rrL, iiH, iiL;
|
||||
|
||||
if (PySampleTxCount + nSamples * 8 >= SAMP_BUFFER_SIZE * 8) {
|
||||
QuiskPrintf ("py_sample_tx_save: buffer is too full\n");
|
||||
return 0;
|
||||
}
|
||||
for (i = 0; i < nSamples; i++) {
|
||||
rr = (int16_t)creal(cSamples[i]);
|
||||
ii = (int16_t)cimag(cSamples[i]);
|
||||
rrH = 0xFF & (rr >> 8);
|
||||
rrL = 0xFF & rr;
|
||||
iiH = 0xFF & (ii >> 8);
|
||||
iiL = 0xFF & ii;
|
||||
if (py_sample_tx_endian == 0) { // byte order of samples is little-endian
|
||||
switch(py_sample_tx_bytes) {
|
||||
case 2:
|
||||
PySampleTxBytes[PySampleTxCount++] = rrL;
|
||||
PySampleTxBytes[PySampleTxCount++] = rrH;
|
||||
PySampleTxBytes[PySampleTxCount++] = iiL;
|
||||
PySampleTxBytes[PySampleTxCount++] = iiH;
|
||||
break;
|
||||
case 3:
|
||||
PySampleTxBytes[PySampleTxCount++] = 0;
|
||||
PySampleTxBytes[PySampleTxCount++] = rrL;
|
||||
PySampleTxBytes[PySampleTxCount++] = rrH;
|
||||
PySampleTxBytes[PySampleTxCount++] = 0;
|
||||
PySampleTxBytes[PySampleTxCount++] = iiL;
|
||||
PySampleTxBytes[PySampleTxCount++] = iiH;
|
||||
break;
|
||||
case 4:
|
||||
PySampleTxBytes[PySampleTxCount++] = 0;
|
||||
PySampleTxBytes[PySampleTxCount++] = 0;
|
||||
PySampleTxBytes[PySampleTxCount++] = rrL;
|
||||
PySampleTxBytes[PySampleTxCount++] = rrH;
|
||||
PySampleTxBytes[PySampleTxCount++] = 0;
|
||||
PySampleTxBytes[PySampleTxCount++] = 0;
|
||||
PySampleTxBytes[PySampleTxCount++] = iiL;
|
||||
PySampleTxBytes[PySampleTxCount++] = iiH;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else { // byte order of samples is big-endian
|
||||
switch(py_sample_tx_bytes) {
|
||||
case 2:
|
||||
PySampleTxBytes[PySampleTxCount++] = rrH;
|
||||
PySampleTxBytes[PySampleTxCount++] = rrL;
|
||||
PySampleTxBytes[PySampleTxCount++] = iiH;
|
||||
PySampleTxBytes[PySampleTxCount++] = iiL;
|
||||
break;
|
||||
case 3:
|
||||
PySampleTxBytes[PySampleTxCount++] = rrH;
|
||||
PySampleTxBytes[PySampleTxCount++] = rrL;
|
||||
PySampleTxBytes[PySampleTxCount++] = 0;
|
||||
PySampleTxBytes[PySampleTxCount++] = iiH;
|
||||
PySampleTxBytes[PySampleTxCount++] = iiL;
|
||||
PySampleTxBytes[PySampleTxCount++] = 0;
|
||||
break;
|
||||
case 4:
|
||||
PySampleTxBytes[PySampleTxCount++] = rrH;
|
||||
PySampleTxBytes[PySampleTxCount++] = rrL;
|
||||
PySampleTxBytes[PySampleTxCount++] = 0;
|
||||
PySampleTxBytes[PySampleTxCount++] = 0;
|
||||
PySampleTxBytes[PySampleTxCount++] = iiH;
|
||||
PySampleTxBytes[PySampleTxCount++] = iiL;
|
||||
PySampleTxBytes[PySampleTxCount++] = 0;
|
||||
PySampleTxBytes[PySampleTxCount++] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static PyObject * get_params(PyObject * self, PyObject * args)
|
||||
{
|
||||
const char * name;
|
||||
@@ -3093,18 +3185,22 @@ static PyObject * set_params(PyObject * self, PyObject * args, PyObject * keywds
|
||||
{ /* Call with keyword arguments ONLY; change local parameters */
|
||||
static char * kwlist[] = {"quisk_is_vna", "rx_bytes", "rx_endian", "read_error", "clip",
|
||||
"bscope_bytes", "bscope_endian", "bscope_size", "bandscopeScale", "hermes_pause",
|
||||
"tx_bytes", "tx_endian",
|
||||
NULL} ;
|
||||
int i, nbytes, read_error, clip, bscope_size, hermes_pause;
|
||||
|
||||
nbytes = read_error = clip = bscope_size = hermes_pause = -1;
|
||||
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiiiiiidii", kwlist,
|
||||
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiiiiiidiii", kwlist,
|
||||
&quisk_is_vna, &nbytes, &py_sample_rx_endian, &read_error, &clip,
|
||||
&py_bscope_bytes, &py_bscope_endian, &bscope_size, &bandscopeScale, &hermes_pause))
|
||||
&py_bscope_bytes, &py_bscope_endian, &bscope_size, &bandscopeScale, &hermes_pause,
|
||||
&py_sample_tx_bytes, &py_sample_tx_endian))
|
||||
return NULL;
|
||||
if (nbytes != -1) {
|
||||
py_sample_rx_bytes = nbytes;
|
||||
quisk_sample_source4(py_sample_start, py_sample_stop, py_sample_read, NULL);
|
||||
quisk_sample_source(py_sample_start, py_sample_stop, py_sample_read);
|
||||
}
|
||||
if (py_sample_tx_bytes)
|
||||
quisk_pt_sample_write = py_sample_tx_save;
|
||||
if (read_error != -1)
|
||||
quisk_sound_state.read_error++;
|
||||
if (clip != -1)
|
||||
@@ -4830,7 +4926,7 @@ static PyObject * set_multirx_mode(PyObject * self, PyObject * args)
|
||||
|
||||
if (!PyArg_ParseTuple (args, "ii", &index, &mode))
|
||||
return NULL;
|
||||
if (index >= 0 && index < QUISK_MAX_SUB_RECEIVERS)
|
||||
if (index < QUISK_MAX_SUB_RECEIVERS)
|
||||
multirx_mode[index] = mode;
|
||||
Py_INCREF (Py_None);
|
||||
return Py_None;
|
||||
@@ -4842,7 +4938,7 @@ static PyObject * set_multirx_freq(PyObject * self, PyObject * args)
|
||||
|
||||
if (!PyArg_ParseTuple (args, "ii", &index, &freq))
|
||||
return NULL;
|
||||
if (index >= 0 && index < QUISK_MAX_SUB_RECEIVERS)
|
||||
if (index < QUISK_MAX_SUB_RECEIVERS)
|
||||
multirx_freq[index] = freq;
|
||||
Py_INCREF (Py_None);
|
||||
return Py_None;
|
||||
@@ -4860,7 +4956,7 @@ static PyObject * set_multirx_play_channel(PyObject * self, PyObject * args)
|
||||
{
|
||||
if (!PyArg_ParseTuple (args, "i", &multirx_play_channel))
|
||||
return NULL;
|
||||
if (multirx_play_channel < 0 || multirx_play_channel >= QUISK_MAX_SUB_RECEIVERS)
|
||||
if (multirx_play_channel >= QUISK_MAX_SUB_RECEIVERS)
|
||||
multirx_play_channel = -1;
|
||||
Py_INCREF (Py_None);
|
||||
return Py_None;
|
||||
@@ -5638,10 +5734,6 @@ static void measure_freq(complex double * cSamples, int nSamples, int srate)
|
||||
ipeak = i;
|
||||
}
|
||||
}
|
||||
if (ipeak < 1)
|
||||
ipeak = 1;
|
||||
else if (ipeak > fft_size - 2)
|
||||
ipeak = fft_size - 2;
|
||||
c3 = 1.36 * (fft_average[ipeak+1] - fft_average[ipeak - 1]) / (fft_average[ipeak-1] + fft_average[ipeak] + fft_average[ipeak+1]);
|
||||
freq = srate * (2 * (ipeak + c3) - fft_size) / 2 / fft_size;
|
||||
freq += rx_tune_freq;
|
||||
@@ -6010,8 +6102,6 @@ static PyObject * record_app(PyObject * self, PyObject * args)
|
||||
}
|
||||
// Initialize plan for multirx FFT
|
||||
multirx_fft_width = multirx_data_width * MULTIRX_FFT_MULT; // Use larger FFT than graph size
|
||||
if (multirx_fft_next_samples)
|
||||
free(multirx_fft_next_samples);
|
||||
multirx_fft_next_samples = (fftw_complex *)malloc(multirx_fft_width * sizeof(fftw_complex));
|
||||
multirx_fft_next_plan = quisk_create_or_cache_fftw_plan_dft_1d(multirx_fft_width, multirx_fft_next_samples, multirx_fft_next_samples, FFTW_FORWARD, FFTW_MEASURE);
|
||||
if (current_graph)
|
||||
@@ -6096,6 +6186,7 @@ static PyMethodDef QuiskMethods[] = {
|
||||
{"get_filter_rate", get_filter_rate, METH_VARARGS, "Return the sample rate used for the filters."},
|
||||
{"get_tx_filter", quisk_get_tx_filter, METH_VARARGS, "Return the frequency response of the transmit filter."},
|
||||
{"get_audio_graph", get_audio_graph, METH_VARARGS, "Return a tuple of the audio graph data."},
|
||||
{"get_tx_samples", get_tx_samples, METH_VARARGS, "Return Tx samples to send to the hardware."},
|
||||
{"softrock_corrections", softrock_corrections, METH_VARARGS, "Control and return SoftRock amplitude and phase corrections."},
|
||||
{"measure_frequency", measure_frequency, METH_VARARGS, "Set the method, return the measured frequency."},
|
||||
{"measure_audio", measure_audio, METH_VARARGS, "Set the method, return the measured audio voltage."},
|
||||
|
||||
@@ -39,7 +39,6 @@ from filters import Filters
|
||||
import dxcluster
|
||||
import configure
|
||||
import quisk_conf_defaults as conf
|
||||
import quisk_utils
|
||||
import quisk_wdsp
|
||||
|
||||
for name in configure.name2format: # Add defaults. These may be overwritten by a user config file.
|
||||
@@ -66,13 +65,15 @@ else:
|
||||
Q3StringTypes = (str, unicode)
|
||||
|
||||
class StdOutput(wx.Frame):
|
||||
def __init__(self, app):
|
||||
def __init__(self, app): # Called from the GUI thread
|
||||
self.app = app
|
||||
self.ctrl = None
|
||||
self.old_stdout = sys.stdout
|
||||
self.old_stderr = sys.stderr
|
||||
self.old_excepthook = sys.excepthook
|
||||
self.path = os.path.join(app.QuiskFilesDir, "quisk_logfile.txt")
|
||||
self.gui_thread = threading.get_native_id()
|
||||
self.text_buffer = ''
|
||||
try:
|
||||
size = os.path.getsize(self.path)
|
||||
except:
|
||||
@@ -117,6 +118,9 @@ class StdOutput(wx.Frame):
|
||||
self.fp.write(text)
|
||||
self.fp.write("\n")
|
||||
def write(self, text):
|
||||
if threading.get_native_id() != self.gui_thread:
|
||||
self.text_buffer += text # When called by non-GUI thread, save text
|
||||
return
|
||||
if self.fp:
|
||||
self.fp.write(text)
|
||||
if self.old_stdout:
|
||||
@@ -125,6 +129,10 @@ class StdOutput(wx.Frame):
|
||||
self.ctrl.write(text)
|
||||
def flush(self):
|
||||
pass
|
||||
def PrintBuffer(self): # Print any saved text from non-GUI thread
|
||||
if self.text_buffer:
|
||||
print(self.text_buffer)
|
||||
self.text_buffer = ''
|
||||
def ExceptHook(self, typ, value, traceb):
|
||||
self.Show(True)
|
||||
self.Raise()
|
||||
@@ -743,7 +751,6 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
|
||||
self.vfo = "Main"
|
||||
self.split_mode = 0
|
||||
self.split_vfo = 'VFO'
|
||||
self.lock_mode = 0
|
||||
h = self.Handlers = {}
|
||||
h[''] = self.ErrProtocol
|
||||
h['dump_state'] = self.DumpState
|
||||
@@ -767,8 +774,6 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
|
||||
h['set_split_freq'] = self.SetSplitFreq
|
||||
h['get_split_vfo'] = self.GetSplitVfo
|
||||
h['set_split_vfo'] = self.SetSplitVfo
|
||||
h['get_lock_mode'] = self.GetLockMode
|
||||
h['set_lock_mode'] = self.SetLockMode
|
||||
self.MakeDumpState()
|
||||
def MakeDumpState(self):
|
||||
dump_state = []
|
||||
@@ -1013,8 +1018,7 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
|
||||
self.ErrParam()
|
||||
else:
|
||||
freq = int(freq + 0.5)
|
||||
# Like a real radio, keep the mode. Hamlib caches the mode, so the client would not notice a mode change.
|
||||
self.app.ChangeRxTxFrequency(freq, None, keep_mode=True)
|
||||
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
|
||||
@@ -1027,7 +1031,7 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
|
||||
else:
|
||||
freq = int(freq + 0.5)
|
||||
# This always controls the Tx frequency
|
||||
self.app.ChangeRxTxFrequency(None, freq, keep_mode=True)
|
||||
self.app.ChangeRxTxFrequency(None, freq)
|
||||
def GetSplitVfo(self):
|
||||
self.Reply('Split', self.split_mode, 'TX VFO', self.split_vfo, 0)
|
||||
def SetSplitVfo(self):
|
||||
@@ -1112,22 +1116,6 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
|
||||
Lab = label
|
||||
diff = df
|
||||
self.app.filterButns.SetLabel(Lab, True)
|
||||
# The code for the lock_mode and keep_mode was supplied by Karl Heinz - K5KHK. Thanks Karl!
|
||||
def GetLockMode(self):
|
||||
# Hamlib netrigctl always reads two lines, so an error reply "RPRT -4" makes it wait for its 10 second timeout.
|
||||
# Reply like rigctld with the lock value and then "RPRT 0".
|
||||
if self.extended:
|
||||
self.Reply('Locked', self.lock_mode, 0)
|
||||
else:
|
||||
self.Send("%d\nRPRT 0\n" % self.lock_mode)
|
||||
def SetLockMode(self):
|
||||
lock = self.GetParamNumber()
|
||||
try:
|
||||
self.lock_mode = int(lock)
|
||||
except:
|
||||
self.ErrParam()
|
||||
else:
|
||||
self.Reply(0)
|
||||
def GetVfo(self):
|
||||
self.Reply('VFO', self.vfo, 0)
|
||||
def SetVfo(self):
|
||||
@@ -1523,9 +1511,7 @@ class SoundThread(threading.Thread):
|
||||
#print ("Quisk thread %12.3f" % ((tm - self.time0) * 1E3))
|
||||
#self.time0 = tm
|
||||
if self.samples_from_python:
|
||||
samples = Hardware.GetRxSamples()
|
||||
if samples:
|
||||
QS.set_params(rx_samples=samples)
|
||||
Hardware.GetRxSamples()
|
||||
if self.poll_cw:
|
||||
Hardware.PollCwKey()
|
||||
if conf.midi_cwkey_device:
|
||||
@@ -2585,7 +2571,7 @@ class GraphScreen(wx.Window):
|
||||
else:
|
||||
self.mouse_is_rx = False
|
||||
if self.mouse_is_rx:
|
||||
freq = application.rxFreq + self.VFO + quisk_utils.wheel_step(event.GetWheelRotation(), event.GetWheelDelta(), wm)
|
||||
freq = application.rxFreq + self.VFO + wm * event.GetWheelRotation() // event.GetWheelDelta()
|
||||
if conf.freq_spacing:
|
||||
freq = self.FreqRound(freq, 0)
|
||||
elif freq >= 0:
|
||||
@@ -2595,7 +2581,7 @@ class GraphScreen(wx.Window):
|
||||
tune = freq - self.VFO
|
||||
self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseWheel', event=event, rx_freq=tune)
|
||||
else:
|
||||
freq = self.txFreq + self.VFO + quisk_utils.wheel_step(event.GetWheelRotation(), event.GetWheelDelta(), wm)
|
||||
freq = self.txFreq + self.VFO + wm * event.GetWheelRotation() // event.GetWheelDelta()
|
||||
if conf.freq_spacing:
|
||||
freq = self.FreqRound(freq, 0)
|
||||
elif freq >= 0:
|
||||
@@ -3663,8 +3649,8 @@ class HelpScreen(wx.html.HtmlWindow):
|
||||
if "gtk2" in wx.PlatformInfo:
|
||||
self.SetStandardFonts()
|
||||
self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22])
|
||||
if not self.LoadFile('help.html'):
|
||||
self.LoadFile('/usr/share/doc/quisk/help.html')
|
||||
# read in text from file help.html in the directory of this module
|
||||
self.LoadFile('help.html')
|
||||
def OnGraphData(self, data):
|
||||
pass
|
||||
def ChangeYscale(self, y_scale):
|
||||
@@ -4715,6 +4701,7 @@ class App(wx.App):
|
||||
msg = QS.GetQuiskPrintf()
|
||||
if msg:
|
||||
print(msg, end='')
|
||||
self.std_out_err.PrintBuffer()
|
||||
QS.close_key()
|
||||
if QS.tci_get_params("tci_clients_count") > 0:
|
||||
QS.tci_set_params(close=1)
|
||||
@@ -4748,6 +4735,7 @@ class App(wx.App):
|
||||
msg = QS.GetQuiskPrintf()
|
||||
if msg:
|
||||
print(msg, end='')
|
||||
self.std_out_err.PrintBuffer()
|
||||
def OnBtnOnOff(self, event):
|
||||
if event.GetEventObject().GetValue(): # Start samples
|
||||
try:
|
||||
@@ -6157,7 +6145,7 @@ class App(wx.App):
|
||||
self.freqDisplay.Display(self.txFreq + self.VFO)
|
||||
self.fldigi_new_freq = self.txFreq + self.VFO
|
||||
return change
|
||||
def ChangeRxTxFrequency(self, rx_freq=None, tx_freq=None, keep_mode=False):
|
||||
def ChangeRxTxFrequency(self, rx_freq=None, tx_freq=None):
|
||||
if not self.split_rxtx and not tx_freq:
|
||||
tx_freq = rx_freq
|
||||
if tx_freq:
|
||||
@@ -6168,7 +6156,7 @@ class App(wx.App):
|
||||
else: # Change the VFO
|
||||
vfo = (tx_freq // 5000) * 5000 - 5000
|
||||
tune = tx_freq - vfo
|
||||
self.BandFromFreq(tx_freq, keep_mode)
|
||||
self.BandFromFreq(tx_freq)
|
||||
self.ChangeHwFrequency(tune, vfo, 'FreqEntry')
|
||||
if rx_freq and self.split_rxtx: # Frequency must be on-screen
|
||||
tune = rx_freq - self.VFO
|
||||
@@ -6358,8 +6346,7 @@ class App(wx.App):
|
||||
self.pttButton.Enable(False)
|
||||
else:
|
||||
self.pttButton.Enable(True)
|
||||
def BandFromFreq(self, frequency, keep_mode=False): # Change to a new band based on the frequency
|
||||
# If keep_mode is True, do not change to the mode saved for the new band.
|
||||
def BandFromFreq(self, frequency): # Change to a new band based on the frequency
|
||||
if self.screen == self.bandscope_screen:
|
||||
return
|
||||
try:
|
||||
@@ -6381,8 +6368,7 @@ class App(wx.App):
|
||||
vfo, tune, mode = self.bandState[band]
|
||||
except KeyError:
|
||||
vfo, tune, mode = (0, 0, 'LSB')
|
||||
if not keep_mode:
|
||||
self.modeButns.SetLabel(mode, True)
|
||||
self.modeButns.SetLabel(mode, True)
|
||||
self.ChangeBand(band)
|
||||
break
|
||||
def ChangeBand(self, band):
|
||||
@@ -6917,6 +6903,7 @@ class App(wx.App):
|
||||
msg = QS.GetQuiskPrintf()
|
||||
if msg:
|
||||
print(msg, end='')
|
||||
self.std_out_err.PrintBuffer()
|
||||
if self.screen == self.config_screen:
|
||||
self.screen.OnGraphData() # Send message to draw new data
|
||||
if self.add_version and Hardware.GetFirmwareVersion() is not None:
|
||||
|
||||
+1
-30
@@ -172,22 +172,6 @@ tx_level = {}
|
||||
# This is the maximum value of the slider.
|
||||
digital_tx_level = 100
|
||||
|
||||
################ Receivers SuperPuper SDR, by UT3MK and US5NCJ
|
||||
## hardware_file_name Hardware file path, rfile
|
||||
# This is the file that contains the control logic for each radio.
|
||||
#hardware_file_name = 'ut3mkpkg/quisk_hardware.py'
|
||||
|
||||
## widgets_file_name Widget file path, rfile
|
||||
# This optional file adds additional controls for the radio.
|
||||
#widgets_file_name = ''
|
||||
|
||||
## sdr1000_serial_port Serial port, text
|
||||
# This is serial Port for the synthesizer, usually /dev/ttyUSBn on Linux or COMn on Win
|
||||
sdr1000_serial_port = "/dev/ttyUSB0"
|
||||
|
||||
## sdr1000_serial_rate Serial baud rate, integer
|
||||
# This is baud rate for the synthesizer.
|
||||
sdr1000_serial_rate = 57600
|
||||
|
||||
|
||||
################ Receivers HiQSDR, The original N2ADR and HiQSDR radios.
|
||||
@@ -441,7 +425,7 @@ hermes_PTT_hang_time = 4
|
||||
# This option only applies to the Hermes Lite 2. Set this to None if you don't have a tuner.
|
||||
# Set this to "Tune" to control the Icom AH-4 compatible ATU attached to the Hermes Lite 2 end plate.
|
||||
# Note that this option only applies to the HL2 end plate, not the IO board.
|
||||
# A tuner attached to the IO board is controlled from a button on the last line of the screen.
|
||||
# A tuner attached to the IO board is controlled from a button on the last line of the screen.
|
||||
# Then when the Spot button is pressed with a positive power, a tune request is sent to the ATU.
|
||||
# If the Spot button is pressed with a zero power level, the tuner is set to bypass mode.
|
||||
# Changes are immediate (no need to restart).
|
||||
@@ -1524,19 +1508,6 @@ remote_radio_ip = ""
|
||||
# It is only necessary to enter it once on each computer.
|
||||
remote_radio_password = ""
|
||||
|
||||
## remote_radio_mic_jitter_ms Mic buffer msec for remote radio, integer
|
||||
# This is set on the remote radio, not the control head. The network can hold the control
|
||||
# head's microphone packets for 100 msec or more and then deliver them in a burst, which is
|
||||
# common over Wi-Fi. The transmit audio then has gaps. This buffers the microphone audio
|
||||
# for the given time before transmitting it, so the bursts are smoothed out, at the cost
|
||||
# of that much more transmit delay. Zero turns the buffer off, which is best on a wired
|
||||
# network. If needed, try 150 msec. The maximum is 500. A change takes effect when the
|
||||
# next control head connects.
|
||||
remote_radio_mic_jitter_ms = 0
|
||||
#remote_radio_mic_jitter_ms = 100
|
||||
#remote_radio_mic_jitter_ms = 150
|
||||
#remote_radio_mic_jitter_ms = 250
|
||||
|
||||
## k4_tcp_ip IP address for K4 TCP, text
|
||||
# This is the Quisk IP address for the TCP server implementing K4 commands.
|
||||
k4_tcp_ip = ""
|
||||
|
||||
+11
-2
@@ -118,17 +118,24 @@ class Hardware:
|
||||
# length is the number of samples in each block of ADC samples, and equals the FFT size.
|
||||
QS.set_params(bscope_bytes=int_size, bscope_endian=endian, bscope_size=length)
|
||||
self.application.bandscope_clock = clock
|
||||
def InitTxSamples(self, int_size, endian, rate): # Tx sample initialization; you must call this from your hardware __init__().
|
||||
# int_size is the number of bytes in each I or Q sample: 2, 3, or 4
|
||||
# endian is the order of bytes in the sample: 0 == little endian; 1 == big endian
|
||||
# This can be called again to change the format. For example, a different number of bytes for different sample rates.
|
||||
QS.set_tx_audio(tx_sample_rate=rate) # Available rates are 48, 50, 96, 100, 192 ksps
|
||||
QS.set_params(tx_bytes=int_size, tx_endian=endian)
|
||||
#def PollCwKey(self): # Optional. Called frequently by the sound thread to check the CW key status.
|
||||
# pass # Do not define if not needed.
|
||||
#def PollGuiControl(self): # Optional. Called frequently by the GUI thread to change GUI settings (PTT etc.)
|
||||
# pass # Do not define if not needed.
|
||||
def StartSamples(self): # Quisk calls this from the sound thread to start sending samples.
|
||||
def StartSamples(self): # Quisk calls this from the sound thread to start samples.
|
||||
# If you return a string, it replaces the string returned from hardware open()
|
||||
pass
|
||||
def StopSamples(self): # Quisk calls this from the sound thread to stop sending samples.
|
||||
def StopSamples(self): # Quisk calls this from the sound thread to stop samples.
|
||||
pass
|
||||
def GetRxSamples(self): # Quisk calls this frequently from the sound thread. Poll your hardware for samples.
|
||||
# Return any available samples by calling AddRxSamples() and perhaps AddBscopeSamples() from within this method.
|
||||
# Call GetTxSamples() from within this method to return any Tx samples that must be sent.
|
||||
pass
|
||||
def AddRxSamples(self, samples): # Call this from within GetRxSamples() to record the Rx samples.
|
||||
# "samples" is int_size of integer I data followed by int_size of integer Q data, repeated.
|
||||
@@ -142,6 +149,8 @@ class Hardware:
|
||||
# For Python 2, "samples" must be a byte array or bytes or a string.
|
||||
# The number of bytes in "samples" must equal the block length times the bytes per sample.
|
||||
QS.add_bscope_samples(samples)
|
||||
def GetTxSamples(self): # Call this from within GetRxSamples() to return the Tx samples to send.
|
||||
return QS.get_tx_samples() # Return a Python bytes object
|
||||
def GotClip(self): # Call this to indicate that samples were received with the clip (overrange) indicator true.
|
||||
QS.set_params(clip=1)
|
||||
def GotReadError(self, print_msg, msg): # Call this to indicate an error in receiving the Rx samples.
|
||||
|
||||
@@ -1,29 +1,6 @@
|
||||
from __future__ import print_function
|
||||
from __future__ import division
|
||||
|
||||
|
||||
def wheel_step(rotation, delta, step):
|
||||
"""Convert signed wheel rotation into configured frequency steps.
|
||||
|
||||
A measured downward event reported rotation=-180 with delta=120. Scaling
|
||||
that by a 50 Hz step gives -75 Hz; flooring the resulting absolute frequency
|
||||
made downward tuning jump -100 Hz while upward tuning moved only +50 Hz.
|
||||
Round the wheel units symmetrically first: +/-180 now gives +/-50 Hz, while
|
||||
+/-240 still gives +/-100 Hz rather than being collapsed to one step.
|
||||
"""
|
||||
if delta == 0:
|
||||
return 0
|
||||
rotation = int(rotation)
|
||||
delta = int(delta)
|
||||
step = int(step)
|
||||
direction = -1 if rotation * delta < 0 else 1
|
||||
whole_steps, remainder = divmod(abs(rotation), abs(delta))
|
||||
if remainder * 2 > abs(delta):
|
||||
whole_steps += 1
|
||||
|
||||
return direction * whole_steps * step
|
||||
|
||||
|
||||
class SplineInterpolator: # From Numerical Recipes in C
|
||||
"""Interpolate a table of [x, y] values."""
|
||||
def __init__(self, table, Xmin=None, Xmax=None):
|
||||
|
||||
+3
-4
@@ -26,7 +26,6 @@ import threading, webbrowser
|
||||
import _quisk as QS
|
||||
from quisk_widgets import *
|
||||
import configure
|
||||
import quisk_utils
|
||||
|
||||
DEBUG = 0
|
||||
|
||||
@@ -631,7 +630,7 @@ class GraphScreen(wx.Window):
|
||||
mouse_x, mouse_y = self.GetMousePosition(event)
|
||||
self.SetTxFreq(index=mouse_x - self.originX)
|
||||
def OnWheel(self, event):
|
||||
tune = self.display.tune_tx + quisk_utils.wheel_step(event.GetWheelRotation(), event.GetWheelDelta(), 1)
|
||||
tune = self.display.tune_tx + event.GetWheelRotation() // event.GetWheelDelta()
|
||||
self.SetTxFreq(index=tune)
|
||||
|
||||
class HelpScreen(wx.html.HtmlWindow):
|
||||
@@ -641,8 +640,8 @@ class HelpScreen(wx.html.HtmlWindow):
|
||||
if "gtk2" in wx.PlatformInfo:
|
||||
self.SetStandardFonts()
|
||||
self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22])
|
||||
if not self.LoadFile('help_vna.html'):
|
||||
self.LoadFile('/usr/share/doc/quisk/help_vna.html')
|
||||
# read in text from file help.html in the directory of this module
|
||||
self.LoadFile('help_vna.html')
|
||||
def OnLinkClicked(self, link):
|
||||
webbrowser.open(link.GetHref(), new=2)
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
This directory has files for the SDR-1000 clone synthesizer by
|
||||
UT3MK and US5NCJ
|
||||
@@ -1,523 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from __future__ import print_function
|
||||
import serial, time, logging
|
||||
import re
|
||||
import _quisk as QS
|
||||
import quisk_conf_defaults as conf
|
||||
import sys, wx, traceback
|
||||
|
||||
|
||||
# ── ПОСЛЕДОВАТЕЛЬНЫЙ ПОРТ ДЛЯ CAT ───────────────────────────────────
|
||||
sdr1000_serial_port = "/dev/ttyUSB0" # Windows: "COM3" / Linux: "/dev/ttyUSB0"
|
||||
sdr1000_serial_rate = 57600 # PowerSDR CAT: до 115200
|
||||
|
||||
# ── ЛОГ ────────────────────────────────────────────────────────────
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)-5s %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
log = logging.getLogger("SDR1000")
|
||||
|
||||
Mode2Index = {'CWL':0, 'CWU':1, 'LSB':2, 'USB':3, 'AM':4,
|
||||
'FM':5, 'EXT':6, 'DGT-U':7, 'DGT-L':8, 'DGT-IQ':9,
|
||||
'IMD':10, 'FDV-U':11, 'FDV-L':12, 'DGT-FM':13}
|
||||
|
||||
# ── КЛАСС УПРАВЛЕНИЯ ЖЕЛЕЗОМ ────────────────────────────────────────
|
||||
from quisk_hardware_model import Hardware as BaseHardware
|
||||
|
||||
class Hardware(BaseHardware):
|
||||
"""
|
||||
Quisk Hardware-класс для FlexRadio SDR-1000.
|
||||
Использует CAT-команды протокола PowerSDR (ZZ + Kenwood).
|
||||
"""
|
||||
# Буфер для приема данных с com-порта
|
||||
serialbuf = '';
|
||||
|
||||
# Буфер для отправки команд на com-порт
|
||||
cmdbuf = '';
|
||||
|
||||
# Буфер отправки IF
|
||||
statusbuf ='';
|
||||
statussending = False;
|
||||
|
||||
# Сопоставление размера шагов
|
||||
STEP_MAP = {
|
||||
'1': '0000', '10': '0001', '50': '1000', '100': '0010',
|
||||
'250': '1001', '500': '1010', '1k': '0011', '5k': '1011',
|
||||
'9k': '1100', '10k': '0100', '100k':'0101', '1M': '0110',
|
||||
'10M': '0111',
|
||||
}
|
||||
|
||||
STEP_VAL = {
|
||||
'1': 1,
|
||||
'10': 10,
|
||||
'50': 50,
|
||||
'100': 100,
|
||||
'250': 250,
|
||||
'500': 500,
|
||||
'1k': 1000,
|
||||
'5k': 5000,
|
||||
'9k': 9000,
|
||||
'10k': 10000,
|
||||
'100k': 100000,
|
||||
'1M': 1000000,
|
||||
'10M': 10000000,
|
||||
}
|
||||
|
||||
# STEP_ENA = ['1', '10', '50', '100', '250', '500', '1k', '5k', '10k', '100k' ]
|
||||
STEP_ENA = [ '10', '50', '100', '250', '500', '1k', '5k', '10k', '100k' ]
|
||||
|
||||
BAND_ENA = ['80', '40', '30', '20', '17', '15', '12', '10']
|
||||
|
||||
watchdog = 0
|
||||
watchdog_limit = 100
|
||||
|
||||
# Сопоставление режимов Quisk → код MD (PowerSDR/Kenwood)
|
||||
# TODO
|
||||
|
||||
MODE_MAP = {
|
||||
'LSB': '1', 'USB': '2', 'CWU': '3', 'FM': '4',
|
||||
'AM': '5', 'DGT-L': '6', 'CWL': '7',
|
||||
# Quisk-специфичные → стандартные
|
||||
'DGT-U':'2', 'FMN': '4', 'AMN': '5',
|
||||
'DGT-FM': '4', 'FDV-U':'2', 'FDV-L':'1',
|
||||
'FSK' : '9', 'FSKR' : '2',
|
||||
}
|
||||
|
||||
def __init__(self, app, conf):
|
||||
BaseHardware.__init__(self, app, conf)
|
||||
self.ser = None
|
||||
self.current_freq = 0
|
||||
self.current_vfo = 0
|
||||
self.current_mode = 'USB'
|
||||
self.current_band = '80'
|
||||
self.current_step = '50'
|
||||
self.rf_gain_labels = ('RF -20', 'RF -10', 'RF 0', 'RF +10' )
|
||||
self.current_pre = 0
|
||||
self.current_ritfreq = 0
|
||||
self.rit_active = False
|
||||
self.ptt_active = False
|
||||
|
||||
# ── Открытие/закрытие ────────────────────────────────────────────
|
||||
|
||||
def open(self):
|
||||
"""Вызывается один раз при старте Quisk."""
|
||||
|
||||
try:
|
||||
self.ser = serial.Serial(
|
||||
sdr1000_serial_port,
|
||||
sdr1000_serial_rate,
|
||||
timeout=0, xonxoff=True,
|
||||
)
|
||||
log.debug("SDR-1000: COM-порт открыт %s @ %d",
|
||||
sdr1000_serial_port, sdr1000_serial_rate)
|
||||
except serial.SerialException as e:
|
||||
log.error("SDR-1000: не удалось открыть порт: %s", e)
|
||||
self.ser = None
|
||||
return "SDR-1000: ошибка порта %s" % sdr1000_serial_port
|
||||
|
||||
msg = "SDR-1000 подключен (%s, %s)" % (
|
||||
sdr1000_serial_port, "PowerSDR")
|
||||
return msg
|
||||
|
||||
def close(self):
|
||||
"""Вызывается один раз при завершении Quisk."""
|
||||
if self.ptt_active:
|
||||
self.ser.cmdbuf += 'RX;' # снять передачу
|
||||
|
||||
if self.ser and self.ser.is_open:
|
||||
self.cmdbuf = ''
|
||||
self.ser.flush
|
||||
self.ser.close()
|
||||
log.debug("SDR-1000: порт закрыт")
|
||||
|
||||
# ── Низкоуровневый CAT-обмен ────────────────────────────────────
|
||||
|
||||
def _cat(self):
|
||||
"""
|
||||
Вызываться будет периодически для чтения порта
|
||||
"""
|
||||
if not self.ser or not self.ser.is_open:
|
||||
return ""
|
||||
|
||||
try:
|
||||
#self.ser.write(frame)
|
||||
if self.ser.in_waiting > 0:
|
||||
sdata = self.ser.read(size=self.ser.in_waiting)
|
||||
self.serialbuf += sdata.decode('ascii')
|
||||
else:
|
||||
self.watchdog += 1
|
||||
if self.watchdog > self.watchdog_limit:
|
||||
log.debug("Watchdog! Resetting arduino")
|
||||
self.ser.dtr = 0
|
||||
time.sleep(0.1)
|
||||
self.ser.dtr = 1
|
||||
self.ser.flush
|
||||
self.ser.close()
|
||||
time.sleep(0.1)
|
||||
try:
|
||||
self.ser = serial.Serial(
|
||||
sdr1000_serial_port,
|
||||
sdr1000_serial_rate,
|
||||
timeout=0, xonxoff=True,
|
||||
)
|
||||
log.debug("SDR-1000: COM-порт открыт %s @ %d",
|
||||
sdr1000_serial_port, sdr1000_serial_rate)
|
||||
except serial.SerialException as e:
|
||||
log.error("SDR-1000: не удалось открыть порт: %s", e)
|
||||
self.ser = None
|
||||
return "SDR-1000: ошибка порта %s" % sdr1000_serial_port
|
||||
self.ser.flushInput()
|
||||
self.ser.flushOutput()
|
||||
self.watchdog = 0
|
||||
|
||||
|
||||
ans = ''
|
||||
while ';' in self.serialbuf:
|
||||
cmds = self.serialbuf.partition(';')
|
||||
|
||||
cmd = cmds[0]
|
||||
self.serialbuf = cmds[-1]
|
||||
# log.info("Splitted to %s and %s", cmd, self.serialbuf)
|
||||
if len(cmd) > 0:
|
||||
answer = ''
|
||||
cmddone = 0
|
||||
match cmd:
|
||||
case 'IF':
|
||||
# Запрос полного статуса
|
||||
# Возврат в statusbuf
|
||||
self.statusbuf = self.ReturnInfo()
|
||||
log.debug("Status refresh, buf: %s", self.serialbuf)
|
||||
cmddone = 1
|
||||
case 'ZZSM0':
|
||||
# Запрос показаний S-метра
|
||||
ans = self.ReturnSmeterA()
|
||||
log.debug("S-meter refresh, buf: %s", ans)
|
||||
cmddone = 1
|
||||
case 'PS1':
|
||||
# Включение
|
||||
ans = 'PS1;'
|
||||
log.debug("PowerON, buf: %s", ans)
|
||||
cmddone = 1
|
||||
case 'ZZPA':
|
||||
# Запрос показаний PRE ! TODO смена режима
|
||||
ans = self.ReturnPre()
|
||||
log.debug("PRE answer, buf: %s", ans)
|
||||
cmddone = 1
|
||||
case 'UP':
|
||||
# Запрос на увеличение частоты
|
||||
self.application.ChangeHwFrequency(9000,
|
||||
self.current_vfo+self.STEP_VAL[self.current_step])
|
||||
cmddone = 1
|
||||
case 'DN':
|
||||
# Запрос на уменьшение частоты
|
||||
self.application.ChangeHwFrequency(9000,
|
||||
self.current_vfo-self.STEP_VAL[self.current_step])
|
||||
cmddone = 1
|
||||
case 'ZZSU':
|
||||
# Запрос на увеличение шага
|
||||
self.StepInc()
|
||||
cmddone = 1
|
||||
case 'ZZSD':
|
||||
# Запрос на уменьшение шага
|
||||
self.StepDec()
|
||||
cmddone = 1
|
||||
case 'BU':
|
||||
# Запрос смены диапазона
|
||||
self.BandUp()
|
||||
cmddone = 1
|
||||
case 'BD':
|
||||
# Запрос смены диапазона
|
||||
self.BandDn()
|
||||
cmddone = 1
|
||||
case 'TX':
|
||||
# Transmit
|
||||
QS.set_PTT(1)
|
||||
self.ptt_active = True
|
||||
log.info("PTT ON")
|
||||
cmddone = 1
|
||||
case 'RX':
|
||||
# Recieve
|
||||
QS.set_PTT(0)
|
||||
self.ptt_active = False
|
||||
log.info("PTT OFF")
|
||||
cmddone = 1
|
||||
|
||||
case 'ZZQR':
|
||||
# Запрос восстановление QuickSave
|
||||
cmddone = 1
|
||||
|
||||
if len(cmd) > 4 and not cmddone:
|
||||
log.debug("Try to match cmd: %s, buf: %s", cmd[0:4], self.serialbuf)
|
||||
match cmd[0:4]:
|
||||
case 'ZZPA':
|
||||
# Запрос изменения PRE
|
||||
self.SetPre(cmd)
|
||||
ans = self.ReturnPre()
|
||||
log.debug("PRE set, buf: %s", ans)
|
||||
cmddone = 1
|
||||
case 'ZZMD':
|
||||
# Запрос изменения режима
|
||||
self.SetMode(cmd)
|
||||
log.debug("Mode set, buf: %s", cmd)
|
||||
cmddone = 1
|
||||
case 'FA00':
|
||||
# Запрос изменения частоты
|
||||
self.SetFreq(cmd)
|
||||
log.debug("Freq set, buf: %s", cmd)
|
||||
cmddone = 1
|
||||
if not cmddone:
|
||||
log.info("unknown cmd: %s, buf: %s", cmd, self.serialbuf)
|
||||
ans = '?;'
|
||||
if len(ans) > 0:
|
||||
log.debug("Sending answer, buf: %s", ans)
|
||||
self.ser.write(ans.encode('ascii'))
|
||||
|
||||
if len(self.serialbuf) == 0:
|
||||
if len(self.cmdbuf) > 0:
|
||||
cmds = self.cmdbuf.partition(';')
|
||||
cmd = cmds[0]
|
||||
self.cmdbuf = cmds[-1]
|
||||
log.debug("Sending cmd %s, buf: %s", cmd, self.cmdbuf)
|
||||
self.ser.write(cmd.encode('ascii'))
|
||||
if (len(self.statusbuf) > 0) and not self.statussending:
|
||||
self.statussending = True
|
||||
log.debug("Sending status, buf: %s", self.statusbuf)
|
||||
self.ser.write(self.statusbuf.encode('ascii'))
|
||||
self.statusbuf = ''
|
||||
self.statussending = False
|
||||
|
||||
# End of CAT
|
||||
except Exception as e:
|
||||
log.error("SDR-1000 CAT error: %s", e)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _fmt_freq(hz):
|
||||
"""Частота → 11 цифр: 14200000 → '00014200000'."""
|
||||
return "%011d" % int(hz)
|
||||
|
||||
# ── Quisk API: частота ──────────────────────────────────────────
|
||||
|
||||
def ChangeFrequency(self, tune, vfo, source='', band='', event=None):
|
||||
"""Quisk вызывает при смене частоты. Возвращает (tune, vfo)."""
|
||||
if tune and tune != self.current_freq:
|
||||
self.current_freq = tune
|
||||
self.current_vfo = tune-9000
|
||||
if re.search('FA\\d{11};', self.cmdbuf,0):
|
||||
log.debug("--> RE MATCH")
|
||||
re.sub('FA\\d{11};', "FA" + self._fmt_freq(tune)+";", self.cmdbuf, 1)
|
||||
# self.cmdbuf += ("FA" + self._fmt_freq(tune)+";")
|
||||
else:
|
||||
self.cmdbuf += ("FA" + self._fmt_freq(tune)+";")
|
||||
log.debug("VFO A → %d Hz (%.6f MHz)", self.current_vfo, self.current_freq / 1e6)
|
||||
return self.current_freq, self.current_vfo
|
||||
|
||||
def ReturnInfo(self):
|
||||
ritstr = self.application.ritFreq
|
||||
if ritstr >= 0:
|
||||
ritsign = "+"
|
||||
else:
|
||||
ritsign = ""
|
||||
answer = "IF%011d%4s%s%05d%01d0000%01d%1s0000000;" % (
|
||||
self.current_freq, self.STEP_MAP[self.current_step],
|
||||
ritsign,ritstr,self.application.ritButton.GetValue(),
|
||||
self.ptt_active,self.MODE_MAP[self.current_mode])
|
||||
# log.info(answer)
|
||||
return answer
|
||||
|
||||
def ReturnSmeterA(self):
|
||||
i = round((self.application.smeter_db + 67) * 2)
|
||||
if i < 0:
|
||||
i = 0
|
||||
elif i > 260:
|
||||
i = 260
|
||||
answer = "ZZSM0%03d;" % ( i )
|
||||
# log.info(answer)
|
||||
return answer
|
||||
|
||||
def ReturnPre(self):
|
||||
answer = "ZZPA%01d;" % ( self.current_pre )
|
||||
return answer
|
||||
|
||||
def SetPre(self, precmd):
|
||||
if len(precmd) == 5:
|
||||
match precmd:
|
||||
case 'ZZPA0': i = 0
|
||||
case 'ZZPA1': i = 1
|
||||
case 'ZZPA2': i = 2
|
||||
case 'ZZPA3': i = 3
|
||||
case _: i = 0
|
||||
self.current_pre = i
|
||||
|
||||
def SetFreq(self, freqcmd):
|
||||
if len(freqcmd) == 13:
|
||||
newfreq = int(freqcmd[2:])
|
||||
log.info("Request to change freq: %d", newfreq)
|
||||
self.current_vfo=newfreq-9000
|
||||
self.application.ChangeHwFrequency(9000, self.current_vfo)
|
||||
|
||||
def OnButtonRfGain(self, event):
|
||||
btn = event.GetEventObject()
|
||||
n = btn.index
|
||||
self.current_pre = n
|
||||
self.cmdbuf += ("ZZPA%1d;" % self.current_pre)
|
||||
|
||||
def SetMode(self, modecmd):
|
||||
if len(modecmd) == 6:
|
||||
log.info("Request to change mode: %s", modecmd[4:])
|
||||
match modecmd[4:]:
|
||||
|
||||
# 00 = LSB => LSB
|
||||
# 01 = USB => USB
|
||||
# 02 = DSB => USB
|
||||
# 03 = CWL => CW
|
||||
# 04 = CWU => CW
|
||||
# 05 = FMN => FMN
|
||||
# 06 = AM => AM
|
||||
# 07 = DIGU => FSKR
|
||||
# 08 = SPEC => AMN
|
||||
# 09 = DIGL => FSK
|
||||
# 10 = SAM => USB
|
||||
# 11 = DRM => USB
|
||||
|
||||
case '00': newmode = 'LSB'
|
||||
case '01': newmode = 'USB'
|
||||
case '02': newmode = 'USB'
|
||||
case '03': newmode = 'CWL'
|
||||
case '04': newmode = 'CWU'
|
||||
case '05': newmode = 'FM'
|
||||
case '06': newmode = 'AM'
|
||||
case '07': newmode = 'DGT-U'
|
||||
case '08': newmode = 'AM'
|
||||
case '09': newmode = 'DGT-L'
|
||||
case '10': newmode = 'USB'
|
||||
case '11': newmode = 'USB'
|
||||
case _: newmode = 'USB'
|
||||
self.current_mode = newmode
|
||||
self.application.mode = self.current_mode
|
||||
self.application.MakeFilterButtons(self.application.Mode2Filters(self.current_mode))
|
||||
QS.set_rx_mode(Mode2Index.get(self.current_mode, 3))
|
||||
if self.current_mode == 'CWL':
|
||||
self.application.SetRit(conf.cwTone)
|
||||
elif self.current_mode == 'CWU':
|
||||
self.application.SetRit(-conf.cwTone)
|
||||
else:
|
||||
self.application.SetRit(0)
|
||||
if self.current_mode in ('CWL', 'CWU'):
|
||||
self.application.SetFilterByMode('CW')
|
||||
elif self.current_mode in ('LSB', 'USB'):
|
||||
self.application.SetFilterByMode('SSB')
|
||||
elif self.current_mode == 'AM':
|
||||
self.application.SetFilterByMode('AM')
|
||||
elif self.current_mode[0:2] == 'FM':
|
||||
self.application.SetFilterByMode('FM')
|
||||
elif self.current_mode[0:4] == 'DGT-':
|
||||
self.application.SetFilterByMode('DGT')
|
||||
self.application.sliderSquelch.DeleteSliderWindow()
|
||||
|
||||
if self.current_mode == 'FM':
|
||||
self.application.sliderSquelch.SetSlider(self.application.levelSquelch)
|
||||
else:
|
||||
self.application.sliderSquelch.SetSlider(self.application.levelSquelchSSB)
|
||||
self.application.OnBtnSquelch()
|
||||
if self.current_mode not in ('FDV-L', 'FDV-U'):
|
||||
self.application.graph.SetDisplayMsg()
|
||||
self.application.waterfall.SetDisplayMsg()
|
||||
self.application.SetTxAudio()
|
||||
|
||||
def StepInc(self):
|
||||
i = self.STEP_ENA.index(self.current_step)
|
||||
log.debug("Found index of %s, idx: %d", self.current_step, i)
|
||||
if i < (len(self.STEP_ENA)-1):
|
||||
self.current_step = self.STEP_ENA[i + 1]
|
||||
log.debug("Step changed to %s, idx: %d", self.current_step, i+1)
|
||||
else:
|
||||
log.debug("Step not changed, remain %s", self.current_step)
|
||||
|
||||
|
||||
def StepDec(self):
|
||||
i = self.STEP_ENA.index(self.current_step)
|
||||
log.info("Found index of %s, idx: %d", self.current_step, i)
|
||||
if i > 0:
|
||||
self.current_step = self.STEP_ENA[i - 1]
|
||||
log.debug("Step changed to %s, idx: %d", self.current_step, i-1)
|
||||
else:
|
||||
log.debug("Step not changed, remain %s", self.current_step)
|
||||
|
||||
def BandUp(self):
|
||||
i = self.BAND_ENA.index(self.current_band)
|
||||
log.debug("Found index of %s, idx: %d", self.current_band, i)
|
||||
if i < (len(self.BAND_ENA)-1):
|
||||
self.current_band = self.BAND_ENA[i + 1]
|
||||
else:
|
||||
self.current_band = self.BAND_ENA[0]
|
||||
log.info("Band changed to %s, idx: %d", self.current_band, i)
|
||||
vfo, tune, mode = self.application.bandState[self.current_band]
|
||||
self.application.ChangeHwFrequency(9000, vfo+tune)
|
||||
|
||||
def BandDn(self):
|
||||
i = self.BAND_ENA.index(self.current_band)
|
||||
log.debug("Found index of %s, idx: %d", self.current_band, i)
|
||||
if i > 0:
|
||||
self.current_band = self.BAND_ENA[i - 1]
|
||||
else:
|
||||
self.current_band = self.BAND_ENA[len(self.BAND_ENA)-1]
|
||||
log.info("Band changed to %s, idx: %d", self.current_band, i)
|
||||
vfo, tune, mode = self.application.bandState[self.current_band]
|
||||
self.application.ChangeHwFrequency(9000, vfo+tune)
|
||||
|
||||
def ReturnFrequency(self):
|
||||
"""Опрос ~10 Гц. Возвращает (tune, vfo) или (None, None)."""
|
||||
# SDR-1000 не сообщает частоту сам, поэтому возвращаем None
|
||||
return None, None
|
||||
|
||||
# ── Quisk API: режим ─────────────────────────────────────────────
|
||||
|
||||
def ChangeMode(self, mode):
|
||||
"""Quisk вызывает при смене режима. mode: 'USB', 'CW', и т.д."""
|
||||
# Нормализуем DGT-* → USB
|
||||
# if mode.startswith('DGT'):
|
||||
# mode = 'USB'
|
||||
code = self.MODE_MAP.get(mode)
|
||||
if not code:
|
||||
log.warning("Неизвестный режим Quisk: %s → по умолчанию USB", mode)
|
||||
code = '2'
|
||||
self.cmdbuf += ("MD" + code+';')
|
||||
self.current_mode = mode
|
||||
log.debug("Режим → %s (MD%s)", mode, code)
|
||||
|
||||
# ── Quisk API: диапазон ─────────────────────────────────────────
|
||||
|
||||
def ChangeBand(self, band):
|
||||
"""Quisk вызывает при смене диапазона. band: '160', '80', …"""
|
||||
BaseHardware.ChangeBand(self, band)
|
||||
log.info("Диапазон → %s м", band)
|
||||
|
||||
# ── Quisk API: PTT ──────────────────────────────────────────────
|
||||
|
||||
def OnButtonPTT(self, event=None):
|
||||
"""Quisk вызывает при нажатии/отпускании PTT."""
|
||||
if event:
|
||||
self.ptt_active = bool(event.GetEventObject().GetValue())
|
||||
#self._cat("ZZTX%d" % (1 if self.ptt_active else 0))
|
||||
log.debug("PTT %s", "ON" if self.ptt_active else "OFF")
|
||||
|
||||
# ── Quisk API: HeartBeat (~10 Гц) ───────────────────────────────
|
||||
|
||||
def HeartBeat(self):
|
||||
"""Периодический опрос — можно использовать для синхронизации."""
|
||||
self._cat()
|
||||
|
||||
def FastHeartBeat(self): # Called frequently by the GUI thread
|
||||
pass
|
||||
|
||||
# ── Quisk API: TUNE ─────────────────────────────────────────────
|
||||
|
||||
def OnBtnFDX(self, is_fdx):
|
||||
"""Кнопка FDX (полный дуплекс)."""
|
||||
log.debug("FDX → %s", is_fdx)
|
||||
|
||||
Reference in New Issue
Block a user