4 Commits
Author SHA1 Message Date
James C. Ahlstrom c97ffa155e Quisk version 4.2.55 2026-10-12 13:37:47 -04:00
jimahlstrom 745882a506 Add README for Quisk Software Defined Radio
Added introductory information about Quisk software defined radio, including its features and support links.
2026-09-27 16:19:11 -04:00
James C. Ahlstrom 674bbad1ab fix non-ascii char in setup.py 2026-08-20 14:08:21 -04:00
James C. Ahlstrom 67c6fbcad9 Quisk version 4.2.54 2026-08-04 12:01:45 -04:00
22 changed files with 489 additions and 42 deletions
+15
View File
@@ -1,3 +1,18 @@
Quisk Version 4.2.55 October 2026
==================================
Karl Heinz, K5KHK, provided a patche to fix the AC2YD remote control head. He provided another patch
to fix Hamlib control when using WSJT-X improved. He added a mic jitter buffer to the remote control
head so it works over WiFi. Thanks Karl!
Quisk Version 4.2.54 August 2026
=================================
Quisk can now accept audio samples for Tx from TCI at rates 48000, 24000, 12000 and 8000. Please test.
Quisk now includes program icons in the directory "quisk/icons". The Windows icon is quisk/icons/quisk.ico.
On Windows, right-click the desktop icon and change the icon to <path-to-quisk>/quisk/icons/quisk.ico.
The Linux icon is quisk/icons/quisk.svg. On Linux, change the quisk.desktop file to
Icon=<path-to-quisk>/quisk/icons/quisk.svg.
Quisk Version 4.2.53 June 2026
===============================
I added TCI support for Software Defined Connectors CW skimmer. Please test.
+12
View File
@@ -0,0 +1,12 @@
# 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
View File
@@ -1,2 +1,2 @@
#Quisk version 4.2.53
#Quisk version 4.2.55
from .quisk import main
+101 -8
View File
@@ -53,6 +53,41 @@ static int control_head_data_width; // app.data_width of control head for gr
static int packets_sent;
static int packets_recd;
///// The jitter buffer is thanks to Karl Heinz - K5KHK
// Jitter buffer for the microphone samples received from the control head. The network can hold
// packets for 100 msec or more (Wi-Fi power saving and scanning) and then deliver them in a burst,
// but the Hermes Tx buffer only has 50 msec of slack. Without a buffer here, each late burst is an
// underflow (a gap on the air) followed by an overflow (discarded samples). So buffer mic_jb_target
// frames before playing, and then release them at a steady MIC_JB_RATE.
// The buffer adds its delay to the transmit audio, so it is off unless the configuration item
// remote_radio_mic_jitter_ms is set. When it is off, samples are passed on as they arrive.
#define MIC_JB_RATE 8000 // frames per second sent by the control head
#define MIC_JB_SIZE 8000 // ring buffer size in stereo frames
#define MIC_JB_MAX_MS 500 // largest configurable delay; MIC_JB_SIZE must hold this plus MIC_JB_EXTRA
#define MIC_JB_EXTRA 2000 // trim back to mic_jb_target above mic_jb_target + this (clock drift): 250 msec
#define MIC_JB_IDLE 0.5 // seconds without packets that end a transmission
static int16_t mic_jb[MIC_JB_SIZE * 2];
static int mic_jb_target; // frames to buffer before playing, from remote_radio_mic_jitter_ms; zero for off
static int mic_jb_read, mic_jb_count, mic_jb_playing, mic_jb_starved;
static int mic_jb_underruns, mic_jb_overruns;
static double mic_jb_time, mic_jb_frac, mic_jb_last_packet;
static void mic_jb_reset(void)
{
int msec;
msec = QuiskGetConfigInt("remote_radio_mic_jitter_ms", 0);
if (msec < 0)
msec = 0;
else if (msec > MIC_JB_MAX_MS)
msec = MIC_JB_MAX_MS;
mic_jb_target = msec * MIC_JB_RATE / 1000;
mic_jb_read = mic_jb_count = mic_jb_playing = mic_jb_starved = 0;
mic_jb_underruns = mic_jb_overruns = 0;
mic_jb_frac = 0;
}
// Receive stereo 16-bit pcm radio speaker sound on the control head via UDP
int read_remote_radio_sound_socket(complex double * cSamples)
{
@@ -117,9 +152,9 @@ int read_remote_radio_sound_socket(complex double * cSamples)
// Receive stereo 16-bit pcm microphone samples at the remote radio via UDP
int read_remote_mic_sound_socket(complex double * cSamples)
{
int i, bytes, nInt16, nSamples;
int i, j, bytes, nInt16, nSamples, nFrames;
int16_t buf[UDP_SEND_INT16];
double samp_r, samp_l;
double now, due;
struct timeval tm_wait;
fd_set fds;
static struct quisk_cHB45Filter HalfBand;
@@ -134,8 +169,8 @@ int read_remote_mic_sound_socket(complex double * cSamples)
quisk_filt_cInit(&cFiltInterp3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double));
}
// read all available packets, one per loop
nSamples = 0;
// read all available packets into the jitter buffer, one per loop
now = QuiskTimeSec();
while (1) {
tm_wait.tv_sec = 0;
tm_wait.tv_usec = 0;
@@ -152,15 +187,71 @@ int read_remote_mic_sound_socket(complex double * cSamples)
break;
}
if (bytes > 0) {
if (mic_jb_starved) { // the buffer ran dry; count it unless the transmission had ended
mic_jb_starved = 0;
if (now - mic_jb_last_packet < MIC_JB_IDLE)
mic_jb_underruns++;
}
mic_jb_last_packet = now;
nInt16 = bytes / 2;
for (i = 0; i < nInt16; i += 2) {
samp_r = buf[i];
samp_l = buf[i + 1];
cSamples[nSamples++] = (samp_r + I * samp_l) / CLIP16 * CLIP32;
for (i = 0; i + 1 < nInt16; i += 2) {
if (mic_jb_count >= MIC_JB_SIZE) { // full: drop the oldest frame
mic_jb_read = (mic_jb_read + 1) % MIC_JB_SIZE;
mic_jb_count--;
}
j = (mic_jb_read + mic_jb_count) % MIC_JB_SIZE * 2;
mic_jb[j] = buf[i];
mic_jb[j + 1] = buf[i + 1];
mic_jb_count++;
}
}
} // while(1)
// release the frames that are due since the last call
nSamples = 0;
if (mic_jb_target == 0) { // the jitter buffer is off: pass on everything received
nFrames = mic_jb_count;
}
else if ( ! mic_jb_playing) {
nFrames = 0;
if (mic_jb_count >= mic_jb_target) {
mic_jb_playing = 1;
mic_jb_time = now;
mic_jb_frac = 0;
}
}
else {
due = (now - mic_jb_time) * MIC_JB_RATE + mic_jb_frac;
if (due < 0) // the clock was set back
due = 0;
else if (due > MIC_JB_RATE / 2) // limit to 0.5 sec, well within SAMP_BUFFER_SIZE after interpolation
due = MIC_JB_RATE / 2;
mic_jb_time = now;
nFrames = (int)due;
mic_jb_frac = due - nFrames;
if (nFrames > mic_jb_count) { // ran dry: play what is left, then buffer up again
nFrames = mic_jb_count;
mic_jb_playing = 0;
mic_jb_starved = 1;
}
}
for (i = 0; i < nFrames; i++) {
j = mic_jb_read * 2;
cSamples[nSamples++] = ((double)mic_jb[j] + I * (double)mic_jb[j + 1]) / CLIP16 * CLIP32;
mic_jb_read = (mic_jb_read + 1) % MIC_JB_SIZE;
}
mic_jb_count -= nFrames;
if (mic_jb_target && mic_jb_count > mic_jb_target + MIC_JB_EXTRA) { // the control head's clock is faster than ours
i = mic_jb_count - mic_jb_target;
mic_jb_read = (mic_jb_read + i) % MIC_JB_SIZE;
mic_jb_count -= i;
mic_jb_overruns++;
}
if ((mic_jb_underruns || mic_jb_overruns) && now - mic_jb_last_packet > MIC_JB_IDLE) {
QuiskPrintf("Remote mic jitter buffer: %d underruns, %d overruns\n", mic_jb_underruns, mic_jb_overruns);
mic_jb_underruns = mic_jb_overruns = 0;
}
nSamples = quisk_cInterpolate(cSamples, nSamples, &cFiltInterp3, 3);
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand);
@@ -716,6 +807,7 @@ PyObject * quisk_start_remote_radio_remote_sound(PyObject * self, PyObject * arg
packets_sent = 0;
packets_recd = 0;
mic_jb_reset();
return Py_None;
}
@@ -742,6 +834,7 @@ PyObject * quisk_stop_remote_radio_remote_sound(PyObject * self, PyObject * args
control_head_graph_socket_started = 0;
remote_radio_graph_socket_started = 0;
control_head_data_width = 0;
mic_jb_reset();
QuiskPrintf("total packets sent = %i, recd = %i\n", packets_sent, packets_recd);
+22 -3
View File
@@ -190,10 +190,20 @@ class Remot: # Remote comtrol base class
return
try: # Read any data from the socket
text = self.remote_ctl_connection.recv(1024)
except:
#traceback.print_exc()
# Thanks to Karl Heinz - K5KHK
except (BlockingIOError, InterruptedError, socket.timeout): # No data available now
return
except OSError: # For example ConnectionResetError: the Control Head is gone
print('Closing Remote Control connection: recv() failed')
self.RemoteCtlClose(False)
return
else: # We got some characters
if not text: # recv() returns nothing only when the Control Head has closed the connection
# Close now rather than waiting for the heartbeat timeout: a Control Head that
# crashed or lost its network may have left the transmitter keyed.
print('Closing Remote Control connection: closed by Control Head')
self.RemoteCtlClose(False)
return
if not isinstance(text, str):
text = text.decode('utf-8')
self.received += text
@@ -375,13 +385,22 @@ class Remot: # Remote comtrol base class
self.cw_next_keydown = None
def StopTransmit(self):
# TODO: Add code for modes other than CW
# PTT code added thanks to Karl Heinz - K5KHK
# Called whenever the remote session ends (RemoteCtlClose). Release everything the
# Control Head may have keyed: the CW key and, for voice and digital modes, PTT.
# Without the PTT release, a Control Head that disappears while transmitting (a crash,
# a lost network, a dead battery) leaves the transmitter keyed until someone unkeys it
# at the radio.
while len(self.cw_event_queue):
self.cw_event_queue.popleft()
self.cw_next_event_ts = None
self.cw_next_keydown = None
self.cw_key_down = 0
QS.set_remote_cwkey(0)
ptt = getattr(self.app, 'pttButton', None)
if ptt and ptt.GetValue():
print('Remote Control connection closed while transmitting: releasing PTT')
ptt.SetValue(0, True) # Same as releasing PTT at the radio; runs OnButtonPTT
def ProcessRadioBtn(self, command, cmd_text):
# Large and Small format screens send different button events for radio buttons.
+2 -1
View File
@@ -2302,7 +2302,8 @@ class ControlMixin:
if name in ('hot_key_ptt_toggle', 'hot_key_ptt_if_hidden', 'keyupDelay', 'cwTone', 'pulse_audio_verbose_output',
'start_cw_delay', 'start_ssb_delay', 'maximum_tx_secs', 'quisk_serial_cts', 'quisk_serial_dsr',
'hot_key_ptt1', 'hot_key_ptt2', 'midi_ptt_toggle', 'TxRxSilenceMsec',
'fixed_tune_offset', 'calibrate_temp_20', 'calibrate_temp_40', 'calibrate_current_0', 'calibrate_current_1'):
'fixed_tune_offset', 'calibrate_temp_20', 'calibrate_temp_40', 'calibrate_current_0', 'calibrate_current_1',
'remote_radio_mic_jitter_ms'):
setattr(conf, name, x)
application.ImmediateChange(name)
elif name[0:4] in ('lin_', 'win_'):
+3
View File
@@ -255,6 +255,8 @@ and then name the shortcut, perhaps "Quisk".
If you check the properties you will see that Windows expanded the location and start directory to the full path.
If you are curious, Quisk and its Python source files are installed in "<b>your_install_directory</b>\Lib\site-packages\quisk".
If you change to that directory you can run Quisk with "pythonw quisk.py".
Quisk has its icon file in quisk\icons. Right-click your desktop shortcut and change the Icon to
"<b>your_install_directory</b>\Lib\site-packages\quisk\icons\quisk.ico".
</p>
<br>
<p>
@@ -389,6 +391,7 @@ make
<p>
You run Quisk by running the file quisk.py with python3. For example, "python3 /home/jim/quisk/quisk.py".
You can create a desktop icon to do that, but the method depends on your version of Linux.
The quisk icon is the file quisk/icons/quisk.svg.
<b>Never run Quisk with root privelege by using sudo. Fix the permission problem instead.</b>
To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios.
Add a radio from the list of hardware radios available. Or add the best match.
+4 -4
View File
@@ -414,7 +414,7 @@ double quiskFilt111D4Coefs[196] = { 0.000022860383861326, 0.000070265173417164,
0.000878636067621920, 0.000694724116305488, 0.000487289901409396, 0.000301317969124407, 0.000160689233204503,
0.000070265173417164, 0.000022860383861326} ;
// Sample 53333 Hz, pass 20000, stop 24000, ripple 0.1 dB, atten 100 dB. For SDR-IQ. Stop 0.45000.
// Sample 53333 Hz, pass 20000, stop 24000, ripple 0.1 dB, atten 100 dB. For SDR-IQ. Pass 0.375. Stop 0.45000.
double quiskFilt53D1Coefs[55] = { 0.000318858391362187, 0.001662116369387873, -0.000065237353639775, -0.000854885424783294,
0.001693126335254083, -0.002034310867663472, 0.001354648789538254, 0.000509693525155950, -0.003079059747940221, 0.005242349925004462,
-0.005626507739484119, 0.003235398612833476, 0.001883583294529978, -0.008273775245721798, 0.013282431836060808, -0.013924391581141530,
@@ -426,7 +426,7 @@ double quiskFilt53D1Coefs[55] = { 0.000318858391362187, 0.001662116369387873, -0
-0.003079059747940221, 0.000509693525155950, 0.001354648789538254, -0.002034310867663472, 0.001693126335254083, -0.000854885424783294,
-0.000065237353639775, 0.001662116369387873, 0.000318858391362187 } ;
// Sample 53333 Hz, pass 10000, stop 12000, ripple 0.5 dB, atten 100 dB. For SDR-IQ. Stop 0.22500.
// Sample 53333 Hz, pass 10000, stop 12000, ripple 0.5 dB, atten 100 dB. For SDR-IQ. Pass 0.1875. Stop 0.22500.
double quiskFilt53D2Coefs[93] = { 0.000107966318729964, 0.000421475598772498, 0.000731343669484184, 0.000328611865414609,
-0.001457272471508304, -0.004143803290279123, -0.005736802939367581, -0.004303861070721504, -0.000415676176666607,
0.002561521351129964, 0.001705074918770091, -0.001794491466202784, -0.003398011312785532, -0.000595401366072839,
@@ -603,7 +603,7 @@ double quiskAudio48p6Coefs[71] = { 0.000324677779792651, 0.001038192405843851, 0
0.001132278163758895, 0.002811674671240099, 0.002938055116822665, 0.002082337464742640, 0.001038192405843851,
0.000324677779792651} ;
// Sample 96 kHz, pass 6, stop 40, ripple 0.1 dB, atten 100 dB. Stop 0.41667.
// Sample 96 kHz, pass 6, stop 40, ripple 0.1 dB, atten 100 dB. Pass 0.0625. Stop 0.41667.
double quiskAudio96Coefs[11] = { -0.004756607210954994, -0.022893760473075838, -0.023609616122334920,
0.078161168368381481, 0.277880235815005590, 0.388489826271597950, 0.277880235815005590,
0.078161168368381481, -0.023609616122334920, -0.022893760473075838, -0.004756607210954994} ;
@@ -663,7 +663,7 @@ double quiskAudioFmHpCoefs[309] = { 0.004847574947800705, -0.001211970386842814,
-0.000365311474947399, -0.000434052387522716, -0.000504808183463150, -0.000579117971409598, -0.000658731927598885, -0.000745515693376396,
-0.000841696354699768, -0.000949749961336003, -0.001072277374239886, -0.001211970386842814, 0.004847574947800705} ;
// Sample 24 kHz, pass 3, stop 4, ripple 0.1 dB, atten 100 dB. Stop 0.166667.
// Sample 24 kHz, pass 3, stop 4, ripple 0.1 dB, atten 100 dB. Pass 0.125. Stop 0.166667.
double quiskAudio24p3Coefs[100] = { 0.000040101217607380, 0.000144142945632828, 0.000313272623844654, 0.000472482653992188,
0.000478145332060253, 0.000196098255702949, -0.000370577202968848, -0.000996638165520275, -0.001297816111521122,
-0.000964536466628102, -0.000042557313824444, 0.000962997869327471, 0.001323534221298773, 0.000594056697947640,
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

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

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

+9 -3
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "quisk"
version = "4.2.53"
version = "4.2.55"
requires-python = ">=3.13"
description = 'QUISK is a Software Defined Radio (SDR) transceiver that can control various radio hardware.'
license = "GPL-2.0-or-later"
@@ -25,6 +25,7 @@ classifiers = [
'Programming Language :: C',
'Topic :: Communications :: Ham Radio',
]
dynamic = ["readme"]
[project.gui-scripts]
quisk = "quisk.quisk:main"
@@ -35,7 +36,12 @@ 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 = ["."]
[tool.setuptools.package-data]
"quisk.icons" = ["*"]
[tool.setuptools]
package-dir = {"quisk" = "."}
packages = ["quisk", "quisk.n2adr", "quisk.softrock", "quisk.freedvpkg",
"quisk.hermes", "quisk.hiqsdr", "quisk.afedrinet", "quisk.soapypkg",
"quisk.sdrmicronpkg", "quisk.perseuspkg", "quisk.ac2yd", "quisk.multuspkg"]
+28 -6
View File
@@ -742,6 +742,7 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
self.vfo = "Main"
self.split_mode = 0
self.split_vfo = 'VFO'
self.lock_mode = 0
h = self.Handlers = {}
h[''] = self.ErrProtocol
h['dump_state'] = self.DumpState
@@ -765,6 +766,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
h['set_split_freq'] = self.SetSplitFreq
h['get_split_vfo'] = self.GetSplitVfo
h['set_split_vfo'] = self.SetSplitVfo
h['get_lock_mode'] = self.GetLockMode
h['set_lock_mode'] = self.SetLockMode
self.MakeDumpState()
def MakeDumpState(self):
dump_state = []
@@ -1009,7 +1012,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
self.ErrParam()
else:
freq = int(freq + 0.5)
self.app.ChangeRxTxFrequency(freq, None)
# Like a real radio, keep the mode. Hamlib caches the mode, so the client would not notice a mode change.
self.app.ChangeRxTxFrequency(freq, None, keep_mode=True)
def GetSplitFreq(self): # The Tx Frequency
self.Reply('TX Frequency', self.app.txFreq + self.app.VFO, 0)
def SetSplitFreq(self): # The Tx Frequency
@@ -1022,7 +1026,7 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
else:
freq = int(freq + 0.5)
# This always controls the Tx frequency
self.app.ChangeRxTxFrequency(None, freq)
self.app.ChangeRxTxFrequency(None, freq, keep_mode=True)
def GetSplitVfo(self):
self.Reply('Split', self.split_mode, 'TX VFO', self.split_vfo, 0)
def SetSplitVfo(self):
@@ -1107,6 +1111,22 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
Lab = label
diff = df
self.app.filterButns.SetLabel(Lab, True)
# The code for the lock_mode and keep_mode was supplied by Karl Heinz - K5KHK. Thanks Karl!
def GetLockMode(self):
# Hamlib netrigctl always reads two lines, so an error reply "RPRT -4" makes it wait for its 10 second timeout.
# Reply like rigctld with the lock value and then "RPRT 0".
if self.extended:
self.Reply('Locked', self.lock_mode, 0)
else:
self.Send("%d\nRPRT 0\n" % self.lock_mode)
def SetLockMode(self):
lock = self.GetParamNumber()
try:
self.lock_mode = int(lock)
except:
self.ErrParam()
else:
self.Reply(0)
def GetVfo(self):
self.Reply('VFO', self.vfo, 0)
def SetVfo(self):
@@ -6136,7 +6156,7 @@ class App(wx.App):
self.freqDisplay.Display(self.txFreq + self.VFO)
self.fldigi_new_freq = self.txFreq + self.VFO
return change
def ChangeRxTxFrequency(self, rx_freq=None, tx_freq=None):
def ChangeRxTxFrequency(self, rx_freq=None, tx_freq=None, keep_mode=False):
if not self.split_rxtx and not tx_freq:
tx_freq = rx_freq
if tx_freq:
@@ -6147,7 +6167,7 @@ class App(wx.App):
else: # Change the VFO
vfo = (tx_freq // 5000) * 5000 - 5000
tune = tx_freq - vfo
self.BandFromFreq(tx_freq)
self.BandFromFreq(tx_freq, keep_mode)
self.ChangeHwFrequency(tune, vfo, 'FreqEntry')
if rx_freq and self.split_rxtx: # Frequency must be on-screen
tune = rx_freq - self.VFO
@@ -6337,7 +6357,8 @@ class App(wx.App):
self.pttButton.Enable(False)
else:
self.pttButton.Enable(True)
def BandFromFreq(self, frequency): # Change to a new band based on the frequency
def BandFromFreq(self, frequency, keep_mode=False): # Change to a new band based on the frequency
# If keep_mode is True, do not change to the mode saved for the new band.
if self.screen == self.bandscope_screen:
return
try:
@@ -6359,7 +6380,8 @@ class App(wx.App):
vfo, tune, mode = self.bandState[band]
except KeyError:
vfo, tune, mode = (0, 0, 'LSB')
self.modeButns.SetLabel(mode, True)
if not keep_mode:
self.modeButns.SetLabel(mode, True)
self.ChangeBand(band)
break
def ChangeBand(self, band):
+13
View File
@@ -1508,6 +1508,19 @@ remote_radio_ip = ""
# It is only necessary to enter it once on each computer.
remote_radio_password = ""
## remote_radio_mic_jitter_ms Mic buffer msec for remote radio, integer
# This is set on the remote radio, not the control head. The network can hold the control
# head's microphone packets for 100 msec or more and then deliver them in a burst, which is
# common over Wi-Fi. The transmit audio then has gaps. This buffers the microphone audio
# for the given time before transmitting it, so the bursts are smoothed out, at the cost
# of that much more transmit delay. Zero turns the buffer off, which is best on a wired
# network. If needed, try 150 msec. The maximum is 500. A change takes effect when the
# next control head connects.
remote_radio_mic_jitter_ms = 0
#remote_radio_mic_jitter_ms = 100
#remote_radio_mic_jitter_ms = 150
#remote_radio_mic_jitter_ms = 250
## k4_tcp_ip IP address for K4 TCP, text
# This is the Quisk IP address for the TCP server implementing K4 commands.
k4_tcp_ip = ""
+1 -1
View File
@@ -82,7 +82,7 @@ N1MM+ and software that uses Hamlib.
'quisk.hermes', 'quisk.hiqsdr', 'quisk.afedrinet', 'quisk.soapypkg',
'quisk.sdrmicronpkg', 'quisk.perseuspkg', 'quisk.ac2yd', 'quisk.multuspkg'],
package_dir = {'quisk' : '.'},
package_data = {'' : ['*.txt', '*.html', '*.so', '*.dll']},
package_data = {'' : ['*.txt', '*.html', '*.so', '*.dll'], 'quisk' : ['icons/*']},
entry_points = {'gui_scripts' : ['quisk = quisk.quisk:main', 'quisk_vna = quisk.quisk_vna:main']},
ext_modules = Modules,
provides = ['quisk'],
+57 -15
View File
@@ -80,9 +80,14 @@ struct ClientData { // data for each client
int audio_stream_channels;
int audio_stream_samples;
int audio_stream_bytes_per_sample;
// Rx stream decimation filters
struct quisk_cHB45Filter HalfBand1;
struct quisk_cFilter filtDecim3;
struct quisk_cFilter filtDecim48to24;
// Tx stream interpolation filters
struct quisk_cHB45Filter HalfBandTx1;
struct quisk_cHB45Filter HalfBandTx2;
struct quisk_cFilter filtTxInterp3;
int send_IQ_stream;
int IQ_stream_samplerate;
} ;
@@ -138,6 +143,9 @@ struct _StreamTypes StreamTypes[NUM_STREAM_TYPES] = {
{"lineout ", 0}
} ;
static int interpolate_tx_audio_stream(complex double * cSamples, int nSamples, struct ClientData * ctx);
static int decimate_audio_stream(complex double * cSamples, int nSamples, struct ClientData * ctx);
static void PrintStream(const char * msg, size_t size, int always)
{
struct _Stream * pt = (struct _Stream *)msg;
@@ -203,6 +211,7 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx)
for (i = 0; i < tci_clients_count; i++) {
if (tci_clients_list[i] == client) {
ctx->send_Rx_audio_stream = 1;
// Rx stream filters
memset(&ctx->HalfBand1,
0, sizeof(struct quisk_cHB45Filter));
quisk_filt_cInit(&ctx->filtDecim3,
@@ -361,6 +370,13 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx)
tci_tx_audio_samples = 0;
tci_tx_audio_rate = ctx->audio_stream_samplerate;
pthread_mutex_unlock(&tx_buffer_mutex);
// Tx stream filters
memset(&ctx->HalfBandTx1,
0, sizeof(struct quisk_cHB45Filter));
memset(&ctx->HalfBandTx2,
0, sizeof(struct quisk_cHB45Filter));
quisk_filt_cInit(&ctx->filtTxInterp3,
quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double));
}
}
else if (client == tci_tx_audio_client) {
@@ -426,7 +442,7 @@ static void onopen(ws_cli_conn_t client)
QuiskPrintf("TCI *Connection opened, addr: %s\n", cli);
}
sendframe_txt(client, "protocol:ESDR,1.4;");
sendframe_txt(client, "protocol:ExpertSDR3,2.0;");
//sendframe_txt(client, "vfo_limits:30000,30000000;"); Not known due to transverters
int if_limit = quisk_sound_state.sample_rate / 2;
snprintf(command, TCI_COMMAND_SIZE, "if_limits:%d,%d;", - if_limit, if_limit);
@@ -480,12 +496,14 @@ static void onclose(ws_cli_conn_t client)
static void onmessage(ws_cli_conn_t client, const unsigned char *msg, uint64_t size, int type)
{
char echo_copy[TCI_RX_BUF_SIZE];
complex double sample;
int n;
static complex double * cCopy = NULL;
static int cCopy_size = 0;
struct ClientData * ctx = ws_get_connection_context(client);
if (type == WS_FR_OP_TXT) {
if (size > TCI_RX_BUF_SIZE / 2)
return; // prevent overflow
struct ClientData * ctx = ws_get_connection_context(client);
if (ctx->msg_length + size >= TCI_RX_BUF_SIZE - 2)
ctx->msg_length = 0; // prevent overflow
memcpy(ctx->msg_buf + ctx->msg_length, msg, size); // append msg to buffer
@@ -519,7 +537,6 @@ static void onmessage(ws_cli_conn_t client, const unsigned char *msg, uint64_t s
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);
@@ -544,15 +561,19 @@ static void onmessage(ws_cli_conn_t client, const unsigned char *msg, uint64_t s
}
if (pt->type == TX_AUDIO_STREAM) {
n = pt->length * 7 + 100; // Make room for 6X interpolation
if (cCopy_size < n) {
if (cCopy)
free(cCopy);
cCopy_size = n;
cCopy = (complex double *)malloc(cCopy_size * sizeof(complex double));
}
if (pt->length > 0 && client == tci_tx_audio_client) {
pthread_mutex_lock(&tx_buffer_mutex);
if (verbose >= 2)
buf_start = CircularBuffer(0, &sample, 0, 0);
n = 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;
@@ -566,18 +587,16 @@ static void onmessage(ws_cli_conn_t client, const unsigned char *msg, uint64_t s
else {
im = re;
}
sample = (re + I * im) * (CLIP32 / 2);
CircularBuffer(0, &sample, 0, 1);
cCopy[n++] = (re + I * im) * (CLIP32 / 2);
}
if (verbose && count != pt->length)
QuiskPrintf("TCI *TX count %ld %d %d\n", size, count, pt->length);
n = interpolate_tx_audio_stream(cCopy, n, ctx);
pthread_mutex_lock(&tx_buffer_mutex);
CircularBuffer(0, cCopy, 0, n);
pthread_mutex_unlock(&tx_buffer_mutex);
break;
}
if (verbose >= 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)
@@ -634,6 +653,29 @@ static int decimate_audio_stream(complex double * cSamples, int nSamples, struct
return nSamples;
}
static int interpolate_tx_audio_stream(complex double * cSamples, int nSamples, struct ClientData * ctx)
{
switch(tci_tx_audio_rate) { // Ending audio rate is 48000
case 48000:
break;
case 24000:
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &ctx->HalfBandTx1);
break;
case 12000:
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &ctx->HalfBandTx1);
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &ctx->HalfBandTx2);
break;
case 8000:
nSamples = quisk_cInterpolate(cSamples, nSamples, &ctx->filtTxInterp3, 3);
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &ctx->HalfBandTx1);
break;
default:
nSamples = 0;
break;
}
return nSamples;
}
void tci_send_audio(complex double * cSamples, int nSamples) // called from the sound thread
{
int i, n;