diff --git a/CHANGELOG.txt b/CHANGELOG.txt index aaa1471..75e33ab 100755 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,15 @@ +Quisk Version 4.1.53 March 2020 +================================ +I changed the Afedri radio module to be compatible with Python3. The graph Zoom feature now is available +on the Bandscope screen. Vladimyr Burdeiny provided a patch to make dx cluster work with Python 3. + +I extended the range of the Waterfall color controls Ys and Yz to accommodate hardware with a higher +noise floor such as PlutoSDR. You will have to readjust your waterfall colors. + +I added a hardware setting for the LNA gain during transmit for the HL2. I made some changes to the CW +and PTT hardware interface for the HL2. These work great with the HL2, but test the new code if you have +diferent Hermes hardware such as Red Pitaya. + Quisk Version 4.1.52 December 2019 =================================== I added an On/Off button to Quisk. When Quisk starts the button is "On". When you turn it to "Off", @@ -7,8 +19,8 @@ and to re-start your hardware after a power down. Martin Schaller found and fixed a bug in the Alsa sound system that produced sporadic crashes. Thanks Martin! Max, G7UOZ, found and fixed a bug in the Python3 SoftRock hardware_net.py code. Thanks Max! -This Quisk includes a more recent FreeDV library in both 32-bit and 64-bit versions. The SoapySDR interface -still requires 64-bit Quisk. +This Quisk includes a more recent FreeDV library in both 32-bit and 64-bit versions. The Windows +SoapySDR interface still requires 64-bit Quisk. The Config/Status screen now shows the dB level of each open sound device. The maximum level is zero dB. This is meant as an aid to configuring the sound devices. diff --git a/PKG-INFO b/PKG-INFO index 580d2ad..9163563 100755 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: quisk -Version: 4.1.52 +Version: 4.1.53 Summary: QUISK is a Software Defined Radio (SDR) transceiver that can control various radio hardware. Home-page: http://james.ahlstrom.name/quisk/ Author: James C. Ahlstrom diff --git a/__init__.py b/__init__.py index 53ccd50..d47ec66 100755 --- a/__init__.py +++ b/__init__.py @@ -1 +1 @@ -#Quisk version 4.1.52 +#Quisk version 4.1.53 diff --git a/afedrinet/af_comp.bat.makeit b/afedrinet/af_comp.bat.makeit new file mode 100755 index 0000000..5ab7034 --- /dev/null +++ b/afedrinet/af_comp.bat.makeit @@ -0,0 +1 @@ +gcc -o afedrinet_io.pyd --shared afedrinet_io.c ../is_key_down.c ../import_quisk_api.c -O3 -I"../" -I"C:\Programs\Python27\include" -L"C:\Programs\Python27\libs" -lws2_32 -lpython27 \ No newline at end of file diff --git a/afedrinet/afedri.py b/afedrinet/afedri.py index 7b96af2..ede3027 100755 --- a/afedrinet/afedri.py +++ b/afedrinet/afedri.py @@ -8,10 +8,15 @@ from __future__ import division # AFEDRI Class module # Title: afedri.py # Author: k3it + # Adopted to work with Quisk # by 4Z5LV # Last Changes: Sat Feb 02 2013 # Version: 2.2 + +# Adapted to work with Python3 +# by N2ADR +# Last Changes: January 2020 ################################################## from socket import * @@ -56,10 +61,10 @@ class afedri(object): if not self.s: return 1 __next_freq = target_freq __next_freq = struct.pack(">3) @@ -92,7 +97,7 @@ class afedri(object): __gain = (indx << 3) + 1 # special afedri calculation for the gain byte #__gain = ((__gain+10)/3 << 3) + 1 - __set_gain_cmd = "\x06\x00\x38\x00\x00" + struct.pack("B",__gain) + __set_gain_cmd = b"\x06\x00\x38\x00\x00" + struct.pack("B",__gain) self.s.send(__set_gain_cmd) __data = self.s.recv(6) __rf_gain = -10 + 3 * (struct.unpack("B",__data[5:6])[0]>>3) @@ -103,7 +108,7 @@ class afedri(object): NOT IMPLEMENTED IN AFEDRI?. DON'T USE """ if not self.s: return 1 - __get_gain_cmd = "\x05\x20\x38\x00\x00" + __get_gain_cmd = b"\x05\x20\x38\x00\x00" self.s.send(__get_gain_cmd) __data = self.s.recv(6) __rf_gain = -10 + 3 * (struct.unpack("B",__data[5:])[0]>>3) @@ -111,8 +116,8 @@ class afedri(object): def get_fe_clock(self): if not self.s: return 1 - __get_lword_cmd = "\x09\xE0\x02\x55\x00\x00\x00\x00\x00" - __get_hword_cmd = "\x09\xE0\x02\x55\x01\x00\x00\x00\x00" + __get_lword_cmd = b"\x09\xE0\x02\x55\x00\x00\x00\x00\x00" + __get_hword_cmd = b"\x09\xE0\x02\x55\x01\x00\x00\x00\x00" self.s.send(__get_lword_cmd) __data_l = self.s.recv(9) self.s.send(__get_hword_cmd) @@ -123,7 +128,7 @@ class afedri(object): def start_capture(self): #start 16-bit contiguous capture, complex numbers if not self.s: return 1 - __start_cmd="\x08\x00\x18\x00\x80\x02\x00\x00" + __start_cmd=b"\x08\x00\x18\x00\x80\x02\x00\x00" self.s.send(__start_cmd) __data = self.s.recv(8) return __data @@ -131,14 +136,15 @@ class afedri(object): def get_sdr_name(self): #Request SDR's Name string command = array.array('B',[0x4, 0x20,1,0]) if not self.s: return 1 - __start_cmd="\x04\x20\x01\x00" + __start_cmd=b"\x04\x20\x01\x00" self.s.send(__start_cmd) __data = self.s.recv(16) + __data = __data.decode('utf-8') return __data def stop_capture(self): if not self.s: return 1 - __stop_cmd="\x08\x00\x18\x00\x00\x01\x00\x00" + __stop_cmd=b"\x08\x00\x18\x00\x00\x01\x00\x00" self.s.send(__stop_cmd) __data = self.s.recv(8) return __data @@ -150,8 +156,8 @@ class afedri(object): __DISCOVER_SERVER_PORT=48321 # PC client Tx port, SDR Server Rx Port __DISCOVER_CLIENT_PORT=48322 # PC client Rx port, SDR Server Tx Port - __data="\x38\x00\x5a\xa5" # magic discovery packet - __data=__data.ljust(56,"\x00") # pad with zeroes + __data=b"\x38\x00\x5a\xa5" # magic discovery packet + __data=__data.ljust(56,b"\x00") # pad with zeroes self.s = socket(AF_INET, SOCK_DGRAM) self.s.bind(('', 0)) @@ -168,7 +174,9 @@ class afedri(object): try: __msg=self.sin.recv(256,0) __devname=__msg[5:20] + __devname=__devname.decode('utf-8') __sn=__msg[21:36] + __sn=__sn.decode('utf-8') __ip=inet_ntoa(__msg[40:36:-1]) __port=struct.unpack(" sudo apt-get install libpulse-dev
+sudo apt-get install python-dev +
sudo apt-get install libpython2.7-dev
+sudo apt-get install python3-dev +
sudo apt-get install libpython3-dev
sudo apt-get install python-usb @@ -838,14 +842,6 @@ just changing the sound card names.
-
-For more information try these articles: -
-
http://linuxplanet.com/linuxplanet/tutorials/6465/1/ -
-http://linuxplanet.com/linuxplanet/tutorials/6466/1/ -
-

Windows Names

To see what sound cards you have, use the Control Panel item Sound diff --git a/dxcluster.py b/dxcluster.py index 5a1acbe..eff52d7 100755 --- a/dxcluster.py +++ b/dxcluster.py @@ -110,17 +110,17 @@ class DxCluster(threading.Thread): for i in range(10): try: self.tn.open(conf.dxClHost, conf.dxClPort, 10) - self.tn.read_until('login:', 10) - self.tn.write(str(conf.user_call_sign) + "\n") # user_call_sign may be Unicode + self.tn.read_until(b"login:", 10) + self.tn.write(conf.user_call_sign.encode('utf-8', errors='ignore') + b"\n") # user_call_sign may be Unicode break except: time.sleep(0.5) if conf.dxClPassword: - self.tn.read_until("Password: ") - self.tn.write(str(conf.dxClPassword) + "\n") + self.tn.read_until(b"Password: ") + self.tn.write(conf.dxClPassword.encode('utf-8', errors='ignore') + b"\n") def telnetRead(self): - message = self.tn.read_until('\n', 60).decode(encoding='utf-8', errors='replace') + message = self.tn.read_until(b'\n', 60).decode(encoding='utf-8', errors='replace') if self.doQuit.isSet() == False: dxEntry = DxEntry(); if dxEntry.parseMessage(message): diff --git a/hermes/quisk_hardware.py b/hermes/quisk_hardware.py index 1b4c214..74f2576 100755 --- a/hermes/quisk_hardware.py +++ b/hermes/quisk_hardware.py @@ -71,6 +71,7 @@ class Hardware(BaseHardware): self.SetControlByte(0x10, 1, (value >> 2) & 0xFF) # cw_hang_time self.SetLowPwrEnable(conf.hermes_lowpwr_tr_enable) self.EnablePowerAmp(conf.hermes_power_amp) + self.ChangeTxLNA(conf.hermes_TxLNA_dB) self.MakePowerCalibration() def pre_open(self): # This socket is used for the Metis Discover protocol @@ -395,8 +396,10 @@ class Hardware(BaseHardware): btn = event.GetEventObject() if btn.GetValue(): QS.set_PTT(1) + QS.set_key_down(1) else: QS.set_PTT(0) + QS.set_key_down(0) def OnSpot(self, level): # level is -1 for Spot button Off; else the Spot level 0 to 1000. pass @@ -437,9 +440,9 @@ class Hardware(BaseHardware): QS.pc_to_hermes(self.pc2hermes) if DEBUG: print ("Change AGC to", value) ## Simpler LNA setting for HL2 identifying as version >=40, see HL2 wiki for details - def ChangeLNA(self, value): + def ChangeLNA(self, value): # LNA for Rx # value is -12 to +48 - if self.hermes_code_version < 40: + if self.hermes_code_version < 40: # Is this correct ?? if value < 20: self.pc2hermes[2] |= 0x08 # C0 index == 0, C3[3]: LNA +32 dB disable == 1 value = 19 - value @@ -451,6 +454,12 @@ class Hardware(BaseHardware): self.pc2hermes[4 * 10 + 3] = value # C0 index == 0x1010, C4[4:0] LNA 0-32 dB gain QS.pc_to_hermes(self.pc2hermes) if DEBUG: print ("Change LNA to", value) + def ChangeTxLNA(self, value): # LNA for Tx + # value is -12 to +48 + value = ((value+12) & 0x3f) | 0x40 | 0x80 + self.SetControlByte(0x0e, 3, value) # C0 index == 0x0e, C3 + QS.pc_to_hermes(self.pc2hermes) + if DEBUG: print ("Change Tx LNA to", value) def SetTxLevel(self): try: tx_level = self.conf.tx_level[self.band] diff --git a/quisk.c b/quisk.c index abfb9b9..385a58f 100755 --- a/quisk.c +++ b/quisk.c @@ -3347,17 +3347,15 @@ static int read_rx_udp10(complex double * samp) // Read samples from UDP using t unsigned int seq; unsigned int hlwp = 0; static unsigned int seq0; - static int key_state; static int tx_records; static int max_multirx_count=0; - int i, j, nSamples, xr, xi, index, start, want_samples, dindex, state, num_records; + int i, j, nSamples, xr, xi, index, start, want_samples, dindex, num_records; complex double c; struct timeval tm_wait; fd_set fds; if ( ! quisk_hermes_is_ready(rx_udp_socket)) { seq0 = 0; - key_state = 0; tx_records = 0; quisk_rx_udp_started = 0; multirx_fft_next_index = 0; @@ -3501,23 +3499,8 @@ static int read_rx_udp10(complex double * samp) // Read samples from UDP using t //code_version = quisk_hermes_to_pc[3]; if ((quisk_hermes_to_pc[0] & 0x01) != 0) // C1 quisk_sound_state.overrange++; - if (quisk_hermes_code_version >= 62) { - hardware_ptt = buf[start] & 0x01; // C0 bit zero is PTT - hardware_cwkey = (buf[start] & 0x02) >> 1; // C0 bit one is CW key state - } - else { - hardware_cwkey = buf[start] & 0x01; // C0 bit zero is CW key state - } - if (rxMode == CWL || rxMode == CWU) { - state = hardware_cwkey | is_PTT_down; - } - else { - state = is_PTT_down; - } - if (state != key_state) { - key_state = state; - quisk_set_key_down(state); - } + hardware_ptt = buf[start] & 0x01; // C0 bit zero is PTT + hardware_cwkey = (buf[start] & 0x04) >> 2; // C0 bit two is CW key state } else if(dindex == 1) { // temperature and forward power hermes_temperature += quisk_hermes_to_pc[4] << 8 | quisk_hermes_to_pc[5]; @@ -4548,15 +4531,19 @@ static PyObject * get_multirx_graph(PyObject * self, PyObject * args) // Called return retrn; } -static PyObject * get_bandscope(void) // Called by the GUI thread +static PyObject * get_bandscope(PyObject * self, PyObject * args) // Called by the GUI thread { - int i, j, j1, j2, L; + int i, j, j1, j2, L, clock; + double zoom, deltaf, rate, f1; static int fft_count = 0; static double the_max = 0; static double time0=0; // time of last graph double d1, d2, sample, frac, scale; PyObject * tuple2; + if (!PyArg_ParseTuple (args, "idd", &clock, &zoom, &deltaf)) + return NULL; + if (bandscopeState == 99 && bandscopePlan) { // bandscope samples are ready for (i = 0; i < bandscope_size; i++) { d1 = fabs(bandscopeSamples[i]); @@ -4577,9 +4564,12 @@ static PyObject * get_bandscope(void) // Called by the GUI thread tuple2 = PyTuple_New(graph_width); frac = (double)L / graph_width; scale = 1.0 / frac / fft_count / bandscope_size; + rate = clock / 2.0; for (i = 0; i < graph_width; i++) { // for each pixel - d1 = i * frac; - d2 = (i + 1) * frac; + f1 = deltaf + rate / 2.0 * (1.0 - zoom); // frequency at left of graph + // freq = f1 + pixel / graph_width + zoom * rate = rate * fft_index / L + d1 = L / rate * (f1 + (double)i / graph_width * zoom * rate); + d2 = L / rate * (f1 + (double)(i + 1) / graph_width * zoom * rate); j1 = floor(d1); j2 = floor(d2); if (j1 == j2) { @@ -4627,9 +4617,6 @@ static PyObject * get_graph(PyObject * self, PyObject * args) // Called by the G if (!PyArg_ParseTuple (args, "idd", &k, &zoom, &deltaf)) return NULL; - if (k == 2) { - return get_bandscope(); - } if (k != use_fft) { // change in data return type; re-initialize use_fft = k; count_fft = 0; @@ -5183,6 +5170,7 @@ static PyMethodDef QuiskMethods[] = { {"is_key_down", is_key_down, METH_VARARGS, "Check whether the key is down; return 0 or 1."}, {"get_state", get_state, METH_VARARGS, "Return a count of read and write errors."}, {"get_graph", get_graph, METH_VARARGS, "Return a tuple of graph data."}, + {"get_bandscope", get_bandscope, METH_VARARGS, "Return a tuple of bandscope data."}, {"set_multirx_mode", set_multirx_mode, METH_VARARGS, "Select demodulation mode for sub-receivers."}, {"set_multirx_freq", set_multirx_freq, METH_VARARGS, "Select how to play audio from sub-receivers."}, {"set_multirx_play_method", set_multirx_play_method, METH_VARARGS, "Select how to play audio from sub-receivers."}, diff --git a/quisk.egg-info/PKG-INFO b/quisk.egg-info/PKG-INFO index 580d2ad..9163563 100755 --- a/quisk.egg-info/PKG-INFO +++ b/quisk.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: quisk -Version: 4.1.52 +Version: 4.1.53 Summary: QUISK is a Software Defined Radio (SDR) transceiver that can control various radio hardware. Home-page: http://james.ahlstrom.name/quisk/ Author: James C. Ahlstrom diff --git a/quisk.egg-info/SOURCES.txt b/quisk.egg-info/SOURCES.txt index d858ce4..1b9bbef 100755 --- a/quisk.egg-info/SOURCES.txt +++ b/quisk.egg-info/SOURCES.txt @@ -41,6 +41,7 @@ quisk.py quisk_conf_defaults.py quisk_conf_kx3.py quisk_conf_model.py +quisk_conf_openradio.py quisk_conf_peaberry.py quisk_conf_sdr8600.py quisk_conf_sdriq.py @@ -50,10 +51,12 @@ quisk_hardware_hamlib.py quisk_hardware_model.py quisk_hardware_sdr8600.py quisk_hardware_sdriq.py +quisk_hardware_sdrmicron.py quisk_utils.py quisk_vna.py quisk_widgets.py setup.py +softrock_tune_vfo.py sound.c sound_alsa.c sound_directx.c @@ -86,6 +89,7 @@ winsound.txt ./quisk_conf_defaults.py ./quisk_conf_kx3.py ./quisk_conf_model.py +./quisk_conf_openradio.py ./quisk_conf_peaberry.py ./quisk_conf_sdr8600.py ./quisk_conf_sdriq.py @@ -95,9 +99,11 @@ winsound.txt ./quisk_hardware_model.py ./quisk_hardware_sdr8600.py ./quisk_hardware_sdriq.py +./quisk_hardware_sdrmicron.py ./quisk_utils.py ./quisk_vna.py ./quisk_widgets.py +./softrock_tune_vfo.py ./windows.txt ./winsound.txt ./afedrinet/SOURCE.txt @@ -162,12 +168,11 @@ winsound.txt ./softrock/widgets_tx.py afedrinet/SOURCE.txt afedrinet/__init__.py -afedrinet/af_comp.bat +afedrinet/af_comp.bat.makeit afedrinet/afe_library afedrinet/afe_library.mac afedrinet/afedri.py afedrinet/afedrinet_io.c -afedrinet/afedrinet_io.pyd afedrinet/quisk_conf.py afedrinet/quisk_conf_linux.py afedrinet/quisk_conf_mac.py diff --git a/quisk.py b/quisk.py index 6606517..48146e9 100755 --- a/quisk.py +++ b/quisk.py @@ -857,6 +857,7 @@ class ConfigScreen(wx.Panel): def __init__(self, parent, width, fft_size): self.y_scale = 0 self.y_zero = 0 + self.zoom_control = 0 self.finish_pages = True self.width = width wx.Panel.__init__(self, parent) @@ -920,6 +921,7 @@ class ConfigStatus(wx.ScrolledWindow): self.latencyPlay = -1 self.y_scale = 0 self.y_zero = 0 + self.zoom_control = 0 self.rate_min = -1 self.rate_max = -1 self.chan_min = -1 @@ -1779,6 +1781,7 @@ class GraphScreen(wx.Window): else: self.y_scale = conf.graph_y_scale self.y_zero = conf.graph_y_zero + self.zoom_control = 0 self.y_ticks = [] self.VFO = 0 self.filter_mode = 'AM' @@ -1879,9 +1882,10 @@ class GraphScreen(wx.Window): def ChangeYzero(self, y_zero): self.y_zero = y_zero self.doResize = True - def ChangeZoom(self, zoom, deltaf): + def ChangeZoom(self, zoom, deltaf, zoom_control): self.zoom = zoom self.zoom_deltaf = deltaf + self.zoom_control = zoom_control self.doResize = True def MakeYScale(self): chary = self.chary @@ -2491,14 +2495,19 @@ class WaterfallDisplay(wx.Window): #T('graph start') row = bytearray(0) # Make a new row of pixels for a one-line image gain = self.rf_gain + # y_scale and y_zero range from zero to 160. + # y_zero controls the center position of the colors. Set to a bit over the noise level. + # y_scale controls how much the colors change when the sample deviates from y_zero. for x in data: # x is -130 to 0, or so (dB) - l = int((x - gain + y_zero // 3 + 100) * y_scale / 10) + yz = 40.0 + y_zero * 0.69 # -yz is the color center in dB + l = int((x - gain + yz) * (y_scale + 10) * 0.10 + 128) l = max(l, 0) l = min(l, 255) row.append(self.red[l]) row.append(self.green[l]) row.append(self.blue[l]) row.append(255) + #print ('OnGraphData yz %.0f, slope %.3f, l %4d' % (yz, (y_scale + 10) * 0.10, l)) #T('graph string') if wxVersion in ('2', '3'): bmp = wx.BitmapFromBufferRGBA(len(row) // 4, 1, row) @@ -2523,15 +2532,17 @@ class WaterfallDisplay(wx.Window): dc.DrawLine(tune_tx, self.margin, tune_tx, self.height) self.tune_tx = tune_tx self.tune_rx = tune_rx - def ChangeZoom(self, zoom, deltaf): + def ChangeZoom(self, zoom, deltaf, zoom_control): self.zoom = zoom self.zoom_deltaf = deltaf + self.zoom_control = zoom_control class WaterfallScreen(wx.SplitterWindow): """Create a splitter window with a graph screen and a waterfall screen""" def __init__(self, frame, width, data_width, graph_width): self.y_scale = conf.waterfall_y_scale self.y_zero = conf.waterfall_y_zero + self.zoom_control = 0 wx.SplitterWindow.__init__(self, frame) self.SetSizeHints(width, -1, width) self.SetSashGravity(0.50) @@ -2576,6 +2587,11 @@ class WaterfallScreen(wx.SplitterWindow): self.pane2.OnGraphData(data) def ChangeRfGain(self, gain): # Set the correction for RF gain self.pane2.display.rf_gain = gain + def ChangeZoom(self, zoom, deltaf, zoom_control): + self.zoom_control = zoom_control + self.pane1.ChangeZoom(zoom, deltaf, zoom_control) + self.pane2.ChangeZoom(zoom, deltaf, zoom_control) + self.pane2.display.ChangeZoom(zoom, deltaf, zoom_control) class WaterfallPane(GraphScreen): """Create a waterfall screen with an X axis and a waterfall display.""" @@ -2583,6 +2599,7 @@ class WaterfallPane(GraphScreen): GraphScreen.__init__(self, frame, data_width, graph_width) self.y_scale = conf.waterfall_y_scale self.y_zero = conf.waterfall_y_zero + self.zoom_control = 0 self.oldVFO = self.VFO self.filter_mode = 'AM' self.filter_bandwidth = 0 @@ -3004,6 +3021,7 @@ class ScopeScreen(wx.Window): self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID) self.y_scale = conf.scope_y_scale self.y_zero = conf.scope_y_zero + self.zoom_control = 0 self.yscale = 1 self.running = 1 self.doResize = False @@ -3136,8 +3154,11 @@ class ScopeScreen(wx.Window): class BandscopeScreen(WaterfallScreen): def __init__(self, frame, width, data_width, graph_width, clock): + self.zoom = 1.0 + self.zoom_deltaf = 0 + self.zoom_control = 0 WaterfallScreen.__init__(self, frame, width, data_width, graph_width) - self.pane1.sample_rate = self.pane2.sample_rate = int(clock) // 2 + self.sample_rate = self.pane1.sample_rate = self.pane2.sample_rate = int(clock) // 2 self.VFO = clock // 4 self.SetVFO(self.VFO) def SetTxFreq(self, tx_freq, rx_freq): @@ -3146,6 +3167,26 @@ class BandscopeScreen(WaterfallScreen): def SetFrequency(self, freq): # freq is 7000000, not the offset from VFO freq = freq - self.VFO WaterfallScreen.SetTxFreq(self, freq, freq) + def ChangeZoom(self, zoom_control): # zoom_control is the slider value 0 to 1000 + self.zoom_control = zoom_control + if zoom_control < 50: + zoom = 1.0 + zoom_deltaf = 0 + else: + zoom = 1.0 - zoom_control / 1000.0 * 0.95 + freq = application.rxFreq + application.VFO + srate = int(self.sample_rate * zoom) # reduced (zoomed) sample rate + if freq - srate // 2 < 0: + zoom_deltaf = srate // 2 - self.VFO + elif freq + srate // 2 > self.sample_rate: + zoom_deltaf = self.VFO - srate // 2 + else: + zoom_deltaf = freq - self.VFO + self.zoom = zoom + self.zoom_deltaf = zoom_deltaf + self.pane1.ChangeZoom(zoom, zoom_deltaf, zoom_control) + self.pane2.ChangeZoom(zoom, zoom_deltaf, zoom_control) + self.pane2.display.ChangeZoom(zoom, zoom_deltaf, zoom_control) class FilterScreen(GraphScreen): """Create a graph of the receive filter response.""" @@ -3153,6 +3194,7 @@ class FilterScreen(GraphScreen): GraphScreen.__init__(self, parent, data_width, graph_width) self.y_scale = conf.filter_y_scale self.y_zero = conf.filter_y_zero + self.zoom_control = 0 self.VFO = 0 self.txFreq = 0 self.data = [] @@ -3199,6 +3241,7 @@ class AudioFFTScreen(GraphScreen): GraphScreen.__init__(self, parent, data_width, graph_width) self.y_scale = conf.filter_y_scale self.y_zero = conf.filter_y_zero + self.zoom_control = 0 self.VFO = 0 self.txFreq = 0 self.sample_rate = sample_rate @@ -3216,6 +3259,7 @@ class HelpScreen(wx.html.HtmlWindow): wx.html.HtmlWindow.__init__(self, parent, -1, size=(width, height)) self.y_scale = 0 self.y_zero = 0 + self.zoom_control = 0 if "gtk2" in wx.PlatformInfo: self.SetStandardFonts() self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22]) @@ -4907,6 +4951,7 @@ The new code supports multiple corrections per band.""") self.vertBox.Layout() # This destroys the initialized sash position! self.sliderYs.SetValue(self.screen.y_scale) self.sliderYz.SetValue(self.screen.y_zero) + self.sliderZo.SetValue(self.screen.zoom_control) if name == 'WFall': self.screen.SetSashPosition(sash) def OnBtnFileRecord(self, event): @@ -4929,14 +4974,21 @@ The new code supports multiple corrections per band.""") elif self.multi_rx_screen.rx_zero == self.graph: self.graphScaleZ[self.lastBand] = (self.graph.y_scale, self.graph.y_zero) def OnChangeZoom(self, event): - x = self.sliderZo.GetValue() - if x < 50: + zoom_control = self.sliderZo.GetValue() + if self.screen == self.bandscope_screen: + self.bandscope_screen.ChangeZoom(zoom_control) + self.bandscope_screen.SetTxFreq(self.txFreq, self.rxFreq) + return + # The display runs from f1 to f2. The original sample rate is "rate". + # The new effective sample rate is rate * zoom. + # f1 = deltaf + rate * (1 - zoom) / 2 + if zoom_control < 50: self.zoom = 1.0 # change back to not-zoomed mode self.zoom_deltaf = 0 self.zooming = False else: a = 1000.0 * self.sample_rate / (self.sample_rate - 2500.0) - self.zoom = 1.0 - x / a + self.zoom = 1.0 - zoom_control / a if not self.zooming: # set deltaf when zoom mode starts center = self.multi_rx_screen.graph.filter_center freq = self.rxFreq + center @@ -4944,10 +4996,8 @@ The new code supports multiple corrections per band.""") self.zooming = True zoom = self.zoom deltaf = self.zoom_deltaf - self.graph.ChangeZoom(zoom, deltaf) - self.waterfall.pane1.ChangeZoom(zoom, deltaf) - self.waterfall.pane2.ChangeZoom(zoom, deltaf) - self.waterfall.pane2.display.ChangeZoom(zoom, deltaf) + self.graph.ChangeZoom(zoom, deltaf, zoom_control) + self.waterfall.ChangeZoom(zoom, deltaf, zoom_control) self.screen.SetTxFreq(self.txFreq, self.rxFreq) self.station_screen.Refresh() def OnLevelVOX(self, event): @@ -5851,7 +5901,7 @@ The new code supports multiple corrections per band.""") self.SetPTT(False) self.timer = time.time() if self.bandscope_clock: # Hermes UDP protocol - data = QS.get_graph(2, 1.0, 0) + data = QS.get_bandscope(self.bandscope_clock, self.bandscope_screen.zoom, float(self.bandscope_screen.zoom_deltaf)) if data and self.screen == self.bandscope_screen: self.screen.OnGraphData(data) if self.screen == self.scope: diff --git a/quisk_conf_defaults.py b/quisk_conf_defaults.py index a3c6437..3298d3e 100755 --- a/quisk_conf_defaults.py +++ b/quisk_conf_defaults.py @@ -429,6 +429,11 @@ Hware_Hl2_EepromMAC = '0xA1 0x6B' Hware_Hl2_EepromMACUse = 'Ignore' #Hware_Hl2_EepromMACUse = 'Set address' +## hermes_TxLNA_dB LNA during Tx dB, integer +# During transmit the low noise Rx amplifier gain changes to this value (in dB) if the hardware supports it. +# Changes are immediate (no need to restart). +hermes_TxLNA_dB = 21 + # These are known power meter calibration tables. This table is not present in the JSON settings file. power_meter_std_calibrations = {} power_meter_std_calibrations['HL2FilterE3'] = [[ 0, 0.0 ], [ 25.865384615384617, 0.0025502539351328003 ], [ 101.02453987730061, 0.012752044999999998 ], diff --git a/quisk_conf_openradio.py b/quisk_conf_openradio.py new file mode 100755 index 0000000..18f6f63 --- /dev/null +++ b/quisk_conf_openradio.py @@ -0,0 +1,122 @@ +# OpenRadio v1.1 Quisk Configuration File +# +# IMPORTANT: To be able to control the OpenRadio board from within Quisk, +# you will need to compile and upload the 'openradio_quisk' firmware, which +# is available from: https://github.com/darksidelemm/open_radio_miniconf_2015 +# +# You will also need to install the pyserial package for python. +# + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + + +# SOUND CARD SETTINGS +# +# Uncomment these if you wish to use PortAudio directly +#name_of_sound_capt = "portaudio:(hw:2,0)" +#name_of_sound_play = "portaudio:(hw:1,0)" + +# Uncomment these lines if you wish to use Pulseaudio +name_of_sound_capt = "pulse" +name_of_sound_play = "pulse" + +# SERIAL PORT SETTINGS +# Set this as appropriate for your OS. +openradio_serial_port = "/dev/ttyUSB0" +openradio_serial_rate = 57600 + + +# OpenRadio Frequency limits. +# These are just within the limits set in the openradio_quisk firmware. +openradio_lower = 100001 +openradio_upper = 29999999 + +# OpenRadio Hardware Control Class +# +import serial,time +from quisk_hardware_model import Hardware as BaseHardware + +class Hardware(BaseHardware): + def open(self): + # Called once to open the Hardware + # Open the serial port. + self.or_serial = serial.Serial(openradio_serial_port,openradio_serial_rate,timeout=3) + print("Opened Serial Port.") + # Wait for the Arduino Nano to restart and boot. + time.sleep(2) + # Poll for version. Should probably confirm the response on this. + version = str(self.get_parameter("VER")) + print(version) + # Return an informative message for the config screen + t = version + ". Capture from sound card %s." % self.conf.name_of_sound_capt + return t + + def close(self): + # Called once to close the Hardware + self.or_serial.close() + + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + # Called whenever quisk requests a frequency change. + # This sends the FREQ command to set the centre frequency of the OpenRadio, + # and will also move the 'tune' frequency (the section within the RX passband + # which is to be demodulated) if it falls outside the passband (+/- sample_rate/2). + print("Setting VFO to %d." % vfo) + if(vfoopenradio_upper): + vfo = openradio_upper + print("Outside range! Setting to %d" % openradio_upper) + + success = self.set_parameter("FREQ",str(vfo)) + + # If the tune frequency is outside the RX bandwidth, set it to somewhere within that bandwidth. + if(tune>(vfo + sample_rate/2) or tune<(vfo - sample_rate/2)): + tune = vfo + 10000 + print("Bringing tune frequency back into the RX bandwidth.") + + if success: + print("Frequency change succeeded!") + else: + print("Frequency change failed.") + + return tune, vfo + +# +# Serial comms functions, to communicate with the OpenRadio board +# + + def get_parameter(self,string): + self.or_serial.write(string+"\n") + return self.get_argument() + + def set_parameter(self,string,arg): + self.or_serial.write(string+","+arg+"\n") + if self.get_argument() == arg: + return True + else: + return False + + def get_argument(self): + data1 = self.or_serial.readline() + # Do a couple of quick checks to see if there is useful data here + if len(data1) == 0: + return -1 + + # Maybe we didn't catch an OK line? + if data1.startswith('OK'): + data1 = self.or_serial.readline() + + # Check to see if we have a comma in the string. If not, there is no argument. + if data1.find(',') == -1: + return -1 + + data1 = data1.split(',')[1].rstrip('\r\n') + + # Check for the OK string + data2 = self.or_serial.readline() + if data2.startswith('OK'): + return data1 diff --git a/quisk_hardware_sdrmicron.py b/quisk_hardware_sdrmicron.py new file mode 100755 index 0000000..b9c31ac --- /dev/null +++ b/quisk_hardware_sdrmicron.py @@ -0,0 +1,259 @@ +# -*- coding: cp1251 -*- +# +# It provides support for the SDR Micron + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import sys, wx, traceback + +import ftd2xx as d2xx +import time + +from quisk_hardware_model import Hardware as BaseHardware + +DEBUG = 0 + +# https://github.com/Dfinitski/SDR-Micron +# +# RX control, to device, 32 bytes total +# Preamble + 'RX0' + enable + rate + 4 bytes frequency + attenuation + 14 binary zeroes +# +# where: +# Preamble is 7*0x55, 0xd5 +# bytes: +# enable - binary 0 or 1, for enable receiver +# rate: +# binary +# 0 for 48 kHz +# 1 for 96 kHz +# 2 for 192 kHz +# 3 for 240 kHz +# 4 for 384 kHz +# 5 for 480 kHz +# 6 for 640 kHz +# 7 for 768 kHz +# 8 for 960 kHz +# 9 for 1536 kHz +# 10 for 1920 kHz +# +# frequency - 32 bits of tuning frequency, MSB is first +# attenuation - binary 0, 10, 20, 30 for needed attenuation +# +# RX data, to PC, 508 bytes total +# Preamble + 'RX0' + FW1 + FW2 + CLIP + 2 zeroes + 492 bytes IQ data +# +# Where: +# FW1 and FW2 - char digits firmware version number +# CLIP - ADC overflow indicator, 0 or 1 binary +# IQ data for 0 - 7 rate: +# 82 IQ pairs formatted as "I2 I1 I0 Q2 Q1 Q0.... ", MSB is first, 24 bits per sample +# IQ data for 8 - 10 rate: +# 123 IQ pairs formatted as "I1 I0 Q1 Q0..... ", MSB is first, 16 bits per sample +# +# Band Scope control, to device, 32 bytes total +# Preamble + 'BS0' + enable + period + 19 binary zeroes +# +# Where period is the full frame period in ms, from 50 to 255ms, 100ms is recommended +# for 10Hz refresh rate window. +# +# Band Scope data, to PC, 16384 16bit samples, 32768 bytes by 492 in each packet +# Preamble + 'BS0' + FW1 + FW2 + CLIP + PN + 1 zero + 492 bytes BS data +# +# Where PN is packet number 0, 1, 2, ..., 66 +# BS data in format "BS1, BS0, BS1, BS0, ...", MSB is first +# +# 66 packets with PN = 0 - 65 contain 492 bytes each, and 67-th packet with PN = 66 contains +# the remaining 296 bytes of data and junk data to full 492 bytes size +# + +class Hardware(BaseHardware): + sample_rates = [48, 96, 192, 240, 384, 480, 640 ,768, 960, 1536, 1920] + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.device = None + self.usb = None + self.rf_gain_labels = ('RF +10', 'RF 0', 'RF -10', 'RF -20') + self.index = 1 + self.enable = 0 + self.rate = 0 + self.att = 10 + self.freq = 7220000 + self.old_freq = 0 + self.sdrmicron_clock = 76800000 + self.sdrmicron_decim = 1600 + self.bscope_data = bytearray(0) + self.fw_ver = None + self.frame_msg = '' + + if conf.fft_size_multiplier == 0: + conf.fft_size_multiplier = 3 # Set size needed by VarDecim + + 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 + bs_bytes = 2 + bs_endian = 1 + self.InitBscope(bs_bytes, bs_endian, self.sdrmicron_clock, 16384) # Initialize bandscope + + def open(self): # This method must return a string showing whether the open succeeded or failed. + enum = d2xx.createDeviceInfoList() # quantity of FTDI devices + if(enum==0): + return 'Device was not found' + for i in range(enum): # Searching and openinq needed device + a = d2xx.getDeviceInfoDetail(i) + if(a['description']==b'SDR-Micron'): + try: self.usb = d2xx.openEx(a['serial']) + except: + return 'Device was not found' + Mode = 64 # Configure FT2232H into 0x40 Sync FIFO Mode + self.usb.setBitMode(255, 0) # reset + time.sleep(0.1) + self.usb.setBitMode(255, Mode) #configure FT2232H into Sync FIFO mode + self.usb.setTimeouts(100, 100) # read, write + self.usb.setLatencyTimer(2) + self.usb.setUSBParameters(32, 32) # in_tx_size, out_tx_size + time.sleep(1.5) # waiting for initialisation device + data = self.usb.read(self.usb.getQueueStatus()) # clean the usb data buffer + self.device = 'Opened' + self.frame_msg = a['description'].decode('utf-8') + ' S/N - ' + a['serial'].decode('utf-8') + return self.frame_msg + return 'Device was not found' + + def close(self): + if(self.usb): + if(self.device=='Opened'): + enable = 0 + self.device = None + self.rx_control_upd() + time.sleep(0.5) + self.usb.setBitMode(255, 0) # reset + self.usb.close() + + def OnButtonRfGain(self, event): + btn = event.GetEventObject() + n = btn.index + self.att = n * 10 + self.rx_control_upd() + + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + if vfo: + self.freq = (vfo - self.transverter_offset) + if(self.freq!=self.old_freq): + self.old_freq = self.freq + self.rx_control_upd() + return tune, vfo + + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + BaseHardware.ChangeBand(self, band) + btn = self.application.BtnRfGain + if btn: + if band in ('160', '80', '60', '40'): + btn.SetLabel('RF -10', True) + elif band in ('20',): + btn.SetLabel('RF 0', True) + else: + btn.SetLabel('RF +10', True) + + 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 + rate = self.sample_rates[self.index] * 1000 + self.rate = self.index + if(rate>=960000): + rx_bytes = 2 + rx_endian = 1 + self.InitSamples(rx_bytes, rx_endian) + else: + rx_bytes = 3 + rx_endian = 1 + self.InitSamples(rx_bytes, rx_endian) + self.rx_control_upd() + return rate + + def VarDecimRange(self): # Return the lowest and highest sample rate. + return (48000, 1920000) + + def StartSamples(self): # called by the sound thread + self.enable = 1 + self.rx_control_upd() + self.bscope_control_upd() + + def StopSamples(self): # called by the sound thread + self.enable = 0 + self.rx_control_upd() + self.bscope_control_upd() + + def rx_control_upd(self): + if(self.device=='Opened'): + work = self.freq + freq4 = work & 0xFF + work = work >> 8 + freq3 = work & 0xFF + work = work >> 8 + freq2 = work & 0xFF + work = work >> 8 + freq1 = work & 0xFF + if sys.version_info.major <= 2: + MESSAGE = 7*chr(0x55) + chr(0xd5) + 'RX0' + chr(self.enable) + chr(self.rate) + MESSAGE += chr(freq1) + chr(freq2) + chr(freq3) + chr(freq4) + chr(self.att) + 14*chr(0) + else: + MESSAGE = b"\x55\x55\x55\x55\x55\x55\x55\xd5RX0" + MESSAGE += bytes((self.enable, self.rate, freq1, freq2, freq3, freq4, self.att)) + MESSAGE += bytes(14) + try: self.usb.write(MESSAGE) + except: print('Error while rx_control_upd') + + def bscope_control_upd(self): + if self.device == 'Opened': + if sys.version_info.major <= 2: + MESSAGE = 7*chr(0x55) + chr(0xd5) + 'BS0' + chr(self.enable) + chr(100) + 19 * chr(0) + else: + MESSAGE = b"\x55\x55\x55\x55\x55\x55\x55\xd5BS0" + MESSAGE += bytes((self.enable, 100)) + MESSAGE += bytes(19) + try: self.usb.write(MESSAGE) + except: None + + def GetRxSamples(self): # Read all data from the SDR Micron and process it. + if self.device == None: + return + while (self.usb.getQueueStatus() >= 508): + data = self.usb.read(508) + data = bytearray(data) + if data[8:11] == bytearray(b'RX0'): # Rx I/Q data + if data[13]: + self.GotClip() + if self.fw_ver is None: + self.fw_ver = chr(data[11]) + '.' + chr(data[12]) + self.frame_msg += ' F/W version - ' + self.fw_ver + self.application.main_frame.SetConfigText(self.frame_msg) + self.AddRxSamples(data[16:]) + elif data[8:11] == bytearray(b'BS0'): # bandscope data + packet_number = data[14] + if packet_number == 0: # start of a block of data + self.bscope_data = data[16:] # 492 bytes + elif packet_number < 66: + self.bscope_data += data[16:] # 492 bytes + else: # end of a block of data, 296 bytes + self.bscope_data += data[16:312] + self.AddBscopeSamples(self.bscope_data) + + diff --git a/setup.py b/setup.py index 1df95e5..e5cfc8c 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ import struct # You must define the version here. A title string including # the version will be written to __init__.py and read by quisk.py. -Version = '4.1.52' +Version = '4.1.53' fp = open("__init__.py", "w") # write title string fp.write("#Quisk version %s\n" % Version) diff --git a/soapypkg/build/import_quisk_api.o b/soapypkg/build/import_quisk_api.o index d2ca2b1..54621fe 100755 Binary files a/soapypkg/build/import_quisk_api.o and b/soapypkg/build/import_quisk_api.o differ diff --git a/soapypkg/build/temp.linux-x86_64-2.7/soapy.o b/soapypkg/build/temp.linux-x86_64-2.7/soapy.o index 5c1cead..68657d4 100755 Binary files a/soapypkg/build/temp.linux-x86_64-2.7/soapy.o and b/soapypkg/build/temp.linux-x86_64-2.7/soapy.o differ diff --git a/softrock_tune_vfo.py b/softrock_tune_vfo.py new file mode 100755 index 0000000..ca78d93 --- /dev/null +++ b/softrock_tune_vfo.py @@ -0,0 +1,19 @@ +# This is a replacement hardware file for the Softrock and similar radios. + +# Normally Quisk will change the VFO (center frequency) by large amounts, and perform +# fine tuning within the returned bandwidth. This hardware file does all tuning with the VFO. +# This creates a constant offset between the VFO and the tuning frequency. Specify this file +# as your hardware file on the Config/radio/Hardware screen. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import softrock +from softrock.hardware_usb import Hardware as BaseHardware + +class Hardware(BaseHardware): + def ChangeFrequency(self, tx_freq, vfo_freq, source='', band='', event=None): + vfo_freq = tx_freq - 10000 + tx, vfo = BaseHardware.ChangeFrequency(self, tx_freq, vfo_freq, source, band, event) + return tx, vfo