From 65732f3d1a36012d53d9498628da043d3fa721f7 Mon Sep 17 00:00:00 2001 From: Rob French Date: Wed, 29 Apr 2020 22:09:06 -0500 Subject: [PATCH] Updated local repository to 4.1.54 --- CHANGELOG.txt | 11 +- MANIFEST.in | 2 + PKG-INFO | 2 +- __init__.py | 2 +- configure.py | 34 +- docs.html | 55 ++ makefile | 8 + perseuspkg/README.txt | 10 + perseuspkg/__init__.py | 1 + perseuspkg/makefile | 7 + perseuspkg/perseus.c | 498 +++++++++++++++++++ perseuspkg/quisk_hardware.py | 187 +++++++ perseuspkg/quisk_widgets.py | 37 ++ perseuspkg/setup.py | 48 ++ quisk.egg-info/PKG-INFO | 2 +- quisk.egg-info/SOURCES.txt | 16 +- sdrmicronpkg/quisk_hardware.py | 266 ++++++++++ setup.py | 4 +- soapypkg/build/temp.linux-x86_64-3.6/soapy.o | Bin 89688 -> 89736 bytes 19 files changed, 1175 insertions(+), 15 deletions(-) create mode 100755 perseuspkg/README.txt create mode 100755 perseuspkg/__init__.py create mode 100755 perseuspkg/makefile create mode 100755 perseuspkg/perseus.c create mode 100755 perseuspkg/quisk_hardware.py create mode 100755 perseuspkg/quisk_widgets.py create mode 100755 perseuspkg/setup.py create mode 100755 sdrmicronpkg/quisk_hardware.py diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 75e33ab..f6e6192 100755 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Quisk Version 4.1.54 March 2020 +================================ +There are now two new radios available in Quisk. David Fainitski contributed code for the Sdr Micron, +and Andrea Montefusco IW0HDV contributed code for the Perseus SDR. The radios should appear on the +list of supported radios in Config/Radios screen. Check the Config/radio/Hardware screen to see if you +need any more options. Please test these radios to make sure everything is working. + +I added a general way to add new radios to the configuration screens. See docs.html under Custom Hardware. + 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 @@ -8,7 +17,7 @@ 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. +different Hermes hardware such as Red Pitaya. Quisk Version 4.1.52 December 2019 =================================== diff --git a/MANIFEST.in b/MANIFEST.in index dcd2e88..2084fb0 100755 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,4 +7,6 @@ graft n2adr graft sdriqpkg graft softrock graft soapypkg +graft sdrmicronpkg +graft perseuspkg global-exclude *.pyc diff --git a/PKG-INFO b/PKG-INFO index 9163563..909e803 100755 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: quisk -Version: 4.1.53 +Version: 4.1.54 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 d47ec66..9af07ff 100755 --- a/__init__.py +++ b/__init__.py @@ -1 +1 @@ -#Quisk version 4.1.53 +#Quisk version 4.1.54 diff --git a/configure.py b/configure.py index 4714992..3e37718 100755 --- a/configure.py +++ b/configure.py @@ -157,8 +157,16 @@ class Configuration: try: if fmt4 == 'text': # Note: JSON returns Unicode strings !!! setattr(conf, k, v) - elif fmt4 in ('dict', 'list'): - setattr(conf, k, v) + elif fmt4 == 'dict': + if isinstance(v, dict): + setattr(conf, k, v) + else: + raise ValueError() + elif fmt4 == 'list': + if isinstance(v, list): + setattr(conf, k, v) + else: + raise ValueError() elif fmt4 == 'inte': setattr(conf, k, int(v, base=0)) elif fmt4 == 'numb': @@ -448,13 +456,27 @@ class Configuration: # Then some help text starting with "# " # Then a list of possible value#explain with the default first # Then a blank line to end. - self.format4name = {} self.format4name['hardware_file_type'] = 'text' + self._ParserConf('quisk_conf_defaults.py') + # Read any user-defined radio types + for dirname in os.listdir('.'): + if not os.path.isdir(dirname) or dirname[-3:] != 'pkg': + continue + if dirname in ('freedvpkg', 'sdriqpkg', 'soapypkg'): + continue + filename = os.path.join(dirname, 'quisk_hardware.py') + if not os.path.isfile(filename): + continue + try: + self._ParserConf(filename) + except: + traceback.print_exc() + def _ParserConf(self, filename): re_AeqB = re.compile("^#?(\w+)\s*=\s*([^#]+)#*(.*)") # item values "a = b" section = None data_name = None - fp = open("quisk_conf_defaults.py", "r") + fp = open(filename, "r") for line in fp: line = line.strip() if not line: @@ -486,7 +508,7 @@ class Configuration: value_list = [] if data_name in self.format4name: if self.format4name[data_name] != fmt: - print ("Inconsistent format for", data_name, self.format4name[data_name], fmt) + print (filename, ": Inconsistent format for", data_name, self.format4name[data_name], fmt) else: self.format4name[data_name] = fmt section_data.append([data_name, dspl, fmt, '', value_list]) @@ -495,7 +517,7 @@ class Configuration: mo = re_AeqB.match(line) if mo: if data_name != mo.group(1): - print ("Parse error for", data_name) + print (filename, ": Parse error for", data_name) continue value = mo.group(2).strip() expln = mo.group(3).strip() diff --git a/docs.html b/docs.html index 3c1092d..4365627 100755 --- a/docs.html +++ b/docs.html @@ -50,6 +50,7 @@ div.contents {
  • Configuration
  • Sound Cards
  • SDR-IQ
  • +
  • Perseus
  • Timing
  • USB Control
  • Custom Hardware
  • @@ -80,6 +81,7 @@ complete transceiver. Quisk works with this hardware:
  • SoftRock connected to the sound card
  • Many other SDR's connected to the sound card
  • SDR-IQ connected by USB
  • +
  • Perseus connected by USB
  • N2ADR hardware connected by Ethernet and IP
  • HiQSDR hardware connected by Ethernet and IP
  • The Hermes-Lite project at hermeslite.com
  • @@ -874,6 +876,35 @@ In earlier versions of Windows, port names are COM1, COM2 etc. and use the "USB Windows should find this driver by itself.


    +

    Perseus as Input

    +

    +Quisk can use an Perseus HF receiver from Microtelecom instead of a sound card as input. +Set up a radio of type Perseus. The Perseus uses a native USB interface to connect to Quisk. +The Quisk perseuspkg extension relies on libperseus-sdr + open source library to manage Perseus hardware and receive the I/Q samples stream. +

    +
    +

    +Follow the instruction into GitHub repository to compile and install the library. +On Suse distribution the library is available as binary package. +Next compile the perseuspkg using the command: +

    +
    +
    +make perseus3
    +
    +
    +

    +The several sample rates can be selected opening Config panel: in +the Config tab there is the Samples rates dropdown. +

    +The input analog filter can be switched in using the button Wideband.
    +The input attenuator is operate via the button RF, that allows to select +the four attenuator steps.
    +The ADC commands for dithering and preamplifier are found on +left bottom corner as ADC Dither and ADC Preamp.
    +

    +

    Timing


    There are several configuration parameters devoted to tuning; read the @@ -988,6 +1019,7 @@ can put code there to poll a serial port or to perform other housekeeping functions (try to be efficient). The two remaining functions deserve more documentation.
    +

    ChangeFrequency(self, tune, vfo, source='', band='', event=None)

    Quisk calls the ChangeFrequency() function when the user changes the Tx frequency with a mouse click on the graph or waterfall, with the entry @@ -1117,6 +1149,29 @@ return the new frequencies from ReturnFrequency() or else Quisk will be unaware of the change.
    +
    +

    Adding Custom Hardware to the Config/Radios Screen

    +Once you write your own hardware file for your custom hardware, you still need to add it to the +list of available radio types, and add its configuration options to its Config/radios/Hardware screen. +First create a subdirectory of the Quisk directory with a name ending in "pkg"; for example, +"myradiopkg". Then put your hardware file in this subdirectory with the name "quisk_hardware.py". +You will need at least one configuration option to specify the hardware file name. +Add this code (all comments) near the top of quisk_hardware.py: +
    +
    +# Define the name of the hardware and the items on the hardware screen (see quisk_conf_defaults.py):
    +################ Receivers MyRadio, The special radio that I own
    +## hardware_file_name		Hardware file path, rfile
    +# This is the file that contains the control logic for each radio.
    +#hardware_file_name = 'myradiopkg/quisk_hardware.py'
    +
    +
    +Of course, change the names of the radio and subdirectory as appropriate. Your radio of general type "MyRadio" +will now appear in the list of radios on the Config/Radios screen, and its configuration items will appear +on the Config/radio/Hardware screen. You can add additional items. See quisk_conf_defaults.py and the *pkg +subdirectories for examples. + +

    Extension Packages

    Quisk comes with two extension packages. The freedvpkg package diff --git a/makefile b/makefile index 078bf1f..87903ac 100755 --- a/makefile +++ b/makefile @@ -7,11 +7,13 @@ quisk2: python2 setup.py build_ext --force --inplace @echo @echo 'Use "make soapy2" to make the Python2 soapy module' + @echo 'Use "make perseus2" to make the Python2 perseus package' quisk3: python3 setup.py build_ext --force --inplace @echo @echo 'Use "make soapy3" to make the Python3 soapy module' + @echo 'Use "make perseus3" to make the Python3 perseus package' soapy2: (cd soapypkg; make soapy2) @@ -19,5 +21,11 @@ soapy2: soapy3: (cd soapypkg; make soapy3) +perseus2: + (cd perseuspkg; make perseus2) + +perseus3: + (cd perseuspkg; make perseus3) + macports: env ARCHFLAGS="-arch x86_64" python setup.py build_ext --force --inplace -D USE_MACPORTS diff --git a/perseuspkg/README.txt b/perseuspkg/README.txt new file mode 100755 index 0000000..b6fe841 --- /dev/null +++ b/perseuspkg/README.txt @@ -0,0 +1,10 @@ +Microtelecom Perseus HF receiver + +Prerequisite are: + +libusb-1.0-0-dev (found in all major Linux distro) + +libperseus-sdr as found in https://github.com/Microtelecom/libperseus-sdr/archive/master.zip + + + diff --git a/perseuspkg/__init__.py b/perseuspkg/__init__.py new file mode 100755 index 0000000..792d600 --- /dev/null +++ b/perseuspkg/__init__.py @@ -0,0 +1 @@ +# diff --git a/perseuspkg/makefile b/perseuspkg/makefile new file mode 100755 index 0000000..08b7f11 --- /dev/null +++ b/perseuspkg/makefile @@ -0,0 +1,7 @@ + +perseus2: + python2 setup.py build_ext --force --inplace + +perseus3: + python3 setup.py build_ext --force --inplace + diff --git a/perseuspkg/perseus.c b/perseuspkg/perseus.c new file mode 100755 index 0000000..2bfb534 --- /dev/null +++ b/perseuspkg/perseus.c @@ -0,0 +1,498 @@ +/* + * + * Microtelecom perseus HF receiver + * + * access module: exposes Python functions needed in quisk_hardware.py + * to control hardware + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define IMPORT_QUISK_API +#include "quisk.h" +#include "filter.h" + +// This module was written by Andrea Montefusco IW0HDV. + +typedef union { + struct { + int32_t i; + int32_t q; + } __attribute__((__packed__)) iq; + struct { + uint8_t i1; + uint8_t i2; + uint8_t i3; + uint8_t i4; + uint8_t q1; + uint8_t q2; + uint8_t q3; + uint8_t q4; + } __attribute__((__packed__)) ; +} iq_sample; + + +// buffer size for libperseus-sdr +const static int nb = 6; +const static int bs = 1024; + + +// This module uses the Python interface to import symbols from the parent _quisk +// extension module. It must be linked with import_quisk_api.c. See the documentation +// at the start of import_quisk_api.c. + +#define DEBUG 1 + +static int num_perseus = 0; +static perseus_descr *descr = 0; +static int sr = 48000; +static float freq = 7050000.0; +static int adc_dither = 0; +static int adc_preamp = 0; + +static void quisk_stop_samples(void); + +static const char *fname = "/tmp/quiskperseus"; +static int rfd = 0; +static int wfd = 0; +static int running = 0; +static int wb_filter = 0; + +// Called in a loop to read samples; called from the sound thread. +static int quisk_read_samples(complex double * cSamples) +{ + //fprintf (stderr, "r"); fflush(stderr); + + int n = read(rfd, cSamples, sizeof(complex double)*SAMP_BUFFER_SIZE); + //fprintf(stderr, "%d ", n); + if (n >= 0) + return n/sizeof(complex double); // return number of samples + else + return 0; +} + +// Called in a loop to write samples; called from the sound thread. +static int quisk_write_samples(complex double * cSamples, int nSamples) +{ + return 0; +} + + +// +// callback that writes in the output pipe IQ values as +// complex floating point +// +static int user_data_callback_c_f(void *buf, int buf_size, void *extra) +{ + // The buffer received contains 24-bit IQ samples (6 bytes per sample) + // Here we save the received IQ samples as 32 bit + // (msb aligned) integer IQ samples. + + uint8_t *samplebuf = (uint8_t*)buf; + int nSamples = buf_size/6; + int k; + iq_sample s; + + // the 24 bit data is scaled to a 32bit value (so that the machine's + // natural signed arithmetic will work) + for (k=0; k < nSamples; k++) { + s.i1 = s.q1 = 0; + s.i2 = *samplebuf++; + s.i3 = *samplebuf++; + s.i4 = *samplebuf++; + s.q2 = *samplebuf++; + s.q3 = *samplebuf++; + s.q4 = *samplebuf++; + + // move I/Q to complex number + complex double x = (double)(s.iq.i)*10 + (double)(s.iq.q)*10 * _Complex_I; + if (wfd > 0) { + int n = write(wfd, &x, sizeof(complex double)); + if (n<0 && ! -EAGAIN ) + fprintf(stderr, "perseus c: Can't write output file: %s, descriptor: %d\n", strerror(errno), wfd); + } + } + return 0; +} + + + +// Start sample capture; called from the sound thread. +static void quisk_start_samples(void) +{ + fprintf (stderr, "perseus c: quisk_start_samples\n"); fflush(stderr); + + int rc = mkfifo(fname, 0666); + + if ((rc == -1) && (errno != EEXIST)) { + perror("perseus c: Error creating the named pipe"); + } + + rfd = open(fname, O_RDONLY|O_NONBLOCK); + if (rfd < 0) fprintf(stderr, "perseus c: Can't open read FIFO (%s)\n", strerror(errno)); + else fprintf(stderr, "perseus c: read FIFO (%d)\n", rfd); + + wfd = open(fname, O_WRONLY|O_NONBLOCK); + if (wfd < 0) fprintf(stderr, "perseus c: Can't open write FIFO (%s)\n", strerror(errno)); + else fprintf(stderr, "perseus c: write FIFO (%d)\n", wfd); + + if (perseus_set_sampling_rate(descr, sr) < 0) { // specify the sampling rate value in Samples/second + fprintf(stderr, "perseus c: fpga configuration error: %s\n", perseus_errorstr()); + } else { + fprintf(stderr, "perseus c: sampling rate set to: %d\n", sr); + + // Re-enable preselection filters (WB_MODE Off) + perseus_set_ddc_center_freq(descr, freq, wb_filter); + // start sampling ops + if (perseus_start_async_input(descr, nb*bs, user_data_callback_c_f, 0)<0) { + fprintf(stderr, "perseus c: start async input error: %s\n", perseus_errorstr()); + } else + fprintf(stderr, "perseus c: start async input\n"); + + running = 1; + } +} + +// Stop sample capture; called from the sound thread. +static void quisk_stop_samples(void) +{ + fprintf (stderr, "perseus c: quisk_stop_samples\n"); fflush(stderr); + + // We stop the acquisition... + fprintf(stderr, "perseus c: stopping async data acquisition...\n"); + perseus_stop_async_input(descr); + running = 0; + // clearing FIFO... + close(rfd); + close(wfd); + unlink(fname); +} + + +// Called to close the sample source; called from the GUI thread. +static PyObject * close_device(PyObject * self, PyObject * args) +{ + fprintf (stderr, "perseus c: close_device\n"); + int sample_device; // for now one only Perseus can be managed + + if (!PyArg_ParseTuple (args, "i", &sample_device)) + return NULL; + + if (descr) { + // We stop the acquisition... + if (running) { + perseus_stop_async_input(descr); + running = 0; + } + perseus_close(descr); + descr = 0; + } + Py_INCREF (Py_None); + return Py_None; +} + +// Called to open the Perseus SDR device; called from the GUI thread. +static PyObject * open_device(PyObject * self, PyObject * args) +{ + char buf128[128] = "Capture Microtelecom Perseus HF receiver"; + eeprom_prodid prodid; + + fprintf (stderr, "perseus c: open device (%d)\n", num_perseus); fflush(stderr); + + + // Check how many Perseus receivers are connected to the system + if (num_perseus == 0) num_perseus = perseus_init(); + fprintf(stderr, "perseus c: %d Perseus receivers found\n",num_perseus); + + if (num_perseus == 0) { + sprintf(buf128, "No Perseus receivers detected\n"); + perseus_exit(); + goto main_cleanup; + } + + // Open the first one... + if ((descr = perseus_open(0)) == NULL) { + sprintf(buf128, "error: %s\n", perseus_errorstr()); + fprintf(stderr, "perseus c: open error: %s\n", perseus_errorstr()); + goto main_cleanup; + } + + // Download the standard firmware to the unit + fprintf(stderr, "perseus c: Downloading firmware...\n"); + if (perseus_firmware_download(descr,NULL)<0) { + sprintf(buf128, "perseus c: firmware download error: %s", perseus_errorstr()); + goto main_cleanup; + } + // Dump some information about the receiver (S/N and HW rev) + if (perseus_is_preserie(descr, 0) == PERSEUS_SNNOTAVAILABLE) + fprintf(stderr, "perseus c: The device is a preserie unit"); + else + if (perseus_get_product_id(descr,&prodid)<0) + fprintf(stderr, "perseus c: get product id error: %s", perseus_errorstr()); + else + fprintf(stderr, "perseus c: Receiver S/N: %05d-%02hX%02hX-%02hX%02hX-%02hX%02hX - HW Release:%hd.%hd\n", + (uint16_t) prodid.sn, + (uint16_t) prodid.signature[5], + (uint16_t) prodid.signature[4], + (uint16_t) prodid.signature[3], + (uint16_t) prodid.signature[2], + (uint16_t) prodid.signature[1], + (uint16_t) prodid.signature[0], + (uint16_t) prodid.hwrel, + (uint16_t) prodid.hwver); + + // Printing all sampling rates available ..... + { + int buf[BUFSIZ]; + + if (perseus_get_sampling_rates (descr, buf, sizeof(buf)/sizeof(buf[0])) < 0) { + fprintf(stderr, "perseus c: get sampling rates error: %s\n", perseus_errorstr()); + goto main_cleanup; + } else { + int i = 0; + while (buf[i]) { + fprintf(stderr, "perseus c: #%d: sample rate: %d\n", i, buf[i]); + i++; + } + } + } + + // Configure the receiver for 2 MS/s operations + fprintf(stderr, "perseus c: Configuring FPGA...\n"); + if (perseus_set_sampling_rate(descr, sr) < 0) { // specify the sampling rate value in Samples/second + //if (perseus_set_sampling_rate_n(descr, 0)<0) // specify the sampling rate value as ordinal in the vector + fprintf(stderr, "perseus c: fpga configuration error: %s\n", perseus_errorstr()); + goto main_cleanup; + } + + // ADC settings + perseus_set_adc (descr, adc_dither, adc_preamp); + + // Disable preselection filters (WB_MODE On) + //perseus_set_ddc_center_freq(descr, freq, 0); + //sleep(1); + // Re-enable preselection filters (WB_MODE Off) + perseus_set_ddc_center_freq(descr, freq, wb_filter); + + quisk_sample_source4(&quisk_start_samples, &quisk_stop_samples, &quisk_read_samples, &quisk_write_samples); + + fprintf (stderr, "perseus c: quisk sample source callbacks established\n"); fflush(stderr); + goto exit_success; + + + + main_cleanup: + return PyString_FromString("ERROR"); + + exit_success: + + return PyString_FromString(buf128); + + +} + +static PyObject * set_frequency(PyObject * self, PyObject * args) // Called from GUI thread +{ + float param; + + if (!PyArg_ParseTuple (args, "f", ¶m)) + return NULL; + if (DEBUG) + fprintf (stderr, "perseus c: set DDC frequency%lf\n", param); + freq= param; + if (descr) perseus_set_ddc_center_freq(descr, freq, wb_filter == 0); + + Py_INCREF (Py_None); + return Py_None; +} + + +static PyObject * set_input_filter(PyObject * self, PyObject * args) // Called from GUI thread +{ + int param; + + if (!PyArg_ParseTuple (args, "i", ¶m)) + return NULL; + if (DEBUG) + fprintf (stderr, "perseus c: set input filter%d\n", param); + wb_filter = param; + if (descr) perseus_set_ddc_center_freq(descr, freq, wb_filter == 0); + + Py_INCREF (Py_None); + return Py_None; +} + + +static PyObject * set_sampling_rate(PyObject * self, PyObject * args) // Called from GUI thread +{ + int param; + + if (!PyArg_ParseTuple (args, "i", ¶m)) + return NULL; + + fprintf (stderr, "perseus c: Set sampling rate %d\n", param); + if (param < 48000) sr = param * 1000; + else sr = param; + + if (descr) { + if (running) { + fprintf(stderr, "perseus c: stop async input\n"); + perseus_stop_async_input(descr); + } + + // specify the sampling rate value in Samples/secon + if (perseus_set_sampling_rate(descr, sr) < 0) { + fprintf(stderr, "perseus c: fpga configuration error: %s\n", perseus_errorstr()); + } + + if (running) { + if (perseus_start_async_input(descr, nb*bs, user_data_callback_c_f, 0)<0) { + fprintf(stderr, "perseus c: start async input error: %s\n", perseus_errorstr()); + } else + fprintf(stderr, "perseus c: start async input @%d\n", sr); + } + } else { + fprintf(stderr, "perseus c: tryng to start async input with no device open\n"); + } + Py_INCREF (Py_None); + return Py_None; +} + + +static PyObject * set_attenuator(PyObject * self, PyObject * args) // Called from GUI thread +{ int param; + + if (!PyArg_ParseTuple (args, "i", ¶m)) + return NULL; + if (DEBUG) + fprintf (stderr, "perseus c: Set attenuator %d\n", param); + + if (descr) { + // specify the sampling rate value in Samples/secon + if (perseus_set_attenuator_n(descr, (int)(param / -10)) < 0) { + fprintf(stderr, "perseus c: fpga configuration error: %s\n", perseus_errorstr()); + } + } + Py_INCREF (Py_None); + return Py_None; +} + + +// Enable ADC Dither, Disable ADC Preamp +// perseus_set_adc(descr, true, false); + +static PyObject * set_adc_dither (PyObject * self, PyObject * args) // Called from GUI thread +{ int dither_; + + if (!PyArg_ParseTuple (args, "i", &dither_)) + return NULL; + if (DEBUG) + fprintf (stderr, "perseus c: Set ADC: dither %d\n", dither_); + + adc_dither = dither_; + if (descr) { + // specify the ADC dithering + if (perseus_set_adc(descr, adc_dither == 1, adc_preamp == 1) < 0) { + fprintf(stderr, "perseus c: ADC configuration error: %s\n", perseus_errorstr()); + } + } + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_adc_preamp (PyObject * self, PyObject * args) // Called from GUI thread +{ int preamp_; + + if (!PyArg_ParseTuple (args, "i", &preamp_)) + return NULL; + if (DEBUG) + fprintf (stderr, "perseus c: Set ADC: preamp: %d\n", preamp_); + + adc_preamp = preamp_; + if (descr) { + // specify the sampling rate value in Samples/secon + if (perseus_set_adc(descr, adc_dither == 1, adc_preamp == 1) < 0) { + fprintf(stderr, "perseus c: ADC configuration error: %s\n", perseus_errorstr()); + } + } + Py_INCREF (Py_None); + return Py_None; +} + + +static PyObject * deinit(PyObject * self, PyObject * args) // Called from dctor +{ + perseus_exit(); + + Py_INCREF (Py_None); + return Py_None; +} + + +// Functions callable from Python are listed here: +static PyMethodDef QuiskMethods[] = { + {"open_device", open_device, METH_VARARGS, "Open the hardware."}, + {"close_device", close_device, METH_VARARGS, "Close the hardware"}, + {"set_frequency", set_frequency, METH_VARARGS, "set frequency"}, + {"set_input_filter", set_input_filter, METH_VARARGS, "set input filter"}, + {"set_sampling_rate", set_sampling_rate, METH_VARARGS, "set sampling rate"}, + {"set_attenuator", set_attenuator, METH_VARARGS, "set attenuator"}, + {"set_adc_dither", set_adc_dither, METH_VARARGS, "set ADC dither"}, + {"set_adc_preamp", set_adc_preamp, METH_VARARGS, "set ADC preamplifier"}, + {"deinit", deinit, METH_VARARGS, "deinit"}, +// {"get_device_list", get_device_list, METH_VARARGS, "Return a list of Perseus SDR devices"}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +#if PY_MAJOR_VERSION < 3 +// Python 2.7: +// Initialization, and registration of public symbol "initperseus": +PyMODINIT_FUNC initperseus (void) +{ + if (Py_InitModule ("perseus", QuiskMethods) == NULL) { + fprintf(stderr, "perseus c: Py_InitModule failed!\n"); + return; + } + // Import pointers to functions and variables from module _quisk + if (import_quisk_api()) { + fprintf(stderr, "perseus c: Failure to import pointers from _quisk\n"); + return; //Error + } +} + +// Python 3: +#else +static struct PyModuleDef perseusmodule = { + PyModuleDef_HEAD_INIT, + "perseus", + NULL, + -1, + QuiskMethods +} ; + +PyMODINIT_FUNC PyInit_perseus(void) +{ + PyObject * m; + + m = PyModule_Create(&perseusmodule); + if (m == NULL) + return NULL; + + // Import pointers to functions and variables from module _quisk + if (import_quisk_api()) { + fprintf(stderr, "perseus c: Failure to import pointers from _quisk\n"); + return m; //Error + } + return m; +} +#endif + diff --git a/perseuspkg/quisk_hardware.py b/perseuspkg/quisk_hardware.py new file mode 100755 index 0000000..aaea0b8 --- /dev/null +++ b/perseuspkg/quisk_hardware.py @@ -0,0 +1,187 @@ +# This is the hardware file to support radios accessed by the PerseusSDR interface. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import socket, traceback, time, math +import _quisk as QS +try: + from perseuspkg import perseus +except: + #traceback.print_exc() + perseus = None + print ("Error: Perseus package not found.\n") + +from quisk_hardware_model import Hardware as BaseHardware + +DEBUG = 1 + +# Define the name of the hardware and the items on the hardware screen (see quisk_conf_defaults.py): +################ Receivers PerseusSDR, The PerseusSDR interface to multiple hardware SDRs. +## hardware_file_name Hardware file path, rfile +# This is the file that contains the control logic for each radio. +#hardware_file_name = 'perseuspkg/quisk_hardware.py' + +## widgets_file_name Widget file path, rfile +# This optional file adds additional controls for the radio. +#widgets_file_name = 'perseuspkg/quisk_widgets.py' + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + + self.rf_gain_labels = ('RF +0', 'RF -10', 'RF -20', 'RF -30') + self.antenna_labels = ('Wide Band', 'Band Filter') + + self.vardecim_index = 0 + self.fVFO = 0.0 # Careful, this is a float + print ("__init__: %s" % conf) + self.rates = [ 48000, \ + 95000, \ + 96000, \ + 125000, \ + 192000, \ + 250000, \ + 500000, \ + 1000000, \ + 1600000, \ + 2000000 \ + ] + self.current_rate = 192000 + self.att = 0; + self.wb = 0 + + def __del__(self): + # try to clear hardware + if perseus: + perseus.close() + perseus.deinit() + + def get_hw (self): + return perseus + + def pre_open(self): + print ("pre_open") + pass + + def set_parameter(self, *args): + pass + + def open(self): # Called once to open the Hardware + + if not perseus: + return "Perseus module not available" + + txt = perseus.open_device("perseus",2,3) + print ("perseus hardware: open") + + return txt + + def close(self): # Called once to close the Hardware + print ("perseus hardware: close") + if perseus: + perseus.close_device(1) + + def ChangeGain(self, rxtx): # rxtx is '_rx' or '_tx' + if not perseus: + return + print ("perseus hardware: ChangeGain", rxtx) + pass + + def OnButtonRfGain(self, event): + #btn = event.GetEventObject() + n = event.GetEventObject().index + self.att = n * -10 + print ("perseus hardware: OnButtonRfGain: %d new attenuation: %d" % (n, self.att)) + perseus.set_attenuator (self.att) + + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + fVFO = float(vfo) + if self.fVFO != fVFO: + self.fVFO = fVFO + perseus.set_frequency(fVFO) + return tune, vfo + + + def ReturnFrequency(self): + # Return the current tuning and VFO frequency. If neither have changed, + # you can return (None, None). This is called at about 10 Hz by the main. + # return (tune, vfo) # return changed frequencies + return None, None # frequencies have not changed + + def ReturnVfoFloat(self): + # Return the accurate VFO frequency as a floating point number. + # You can return None to indicate that the integer VFO frequency is valid. + return self.fVFO + +# def OnBtnFDX(self, fdx): # fdx is 0 or 1 +# pass +# +# def OnButtonPTT(self, event): +# pass +# +# def OnSpot(self, level): +# # level is -1 for Spot button Off; else the Spot level 0 to 1000. +# pass +# +# def ChangeMode(self, mode): # Change the tx/rx mode +# # mode is a string: "USB", "AM", etc. +# pass +# +# def ChangeBand(self, band): +# pass +# +# def HeartBeat(self): # Called at about 10 Hz by the main +# pass + + def ImmediateChange(self, name, value): + print ("perseus hardware: ImmediateChange: perseus: name: %s value: %s" % (name, value)) + if name == 'perseus_setSampleRate_rx': + value = int(value) + self.application.OnBtnDecimation(rate=value) + perseus.set_sampling_rate(value) + self.curren_dec = value + + + def VarDecimGetChoices(self): # Not used to set sample rate + print ("perseus hardware: VarDecimGetChoices") + return list(map(str, self.rates)) # convert integer to string + + def VarDecimGetLabel(self): # Return a text label for the decimation control. + return 'Sample rates: ' + + def VarDecimGetIndex(self): # Return the index 0, 1, ... of the current decimation. + for i in range(len(self.rates)): + if self.rates[i] == self.current_rate: + return i + return 0 + + def VarDecimSet(self, index=None): # Called when the control is operated; if index==None, called on startup. + print ("perseus hardware: VarDecimSet: index: %s" % (index)) + if index == None: + print ("perseus hardware: VarDecimSet: current sampling rate: %d" % self.current_rate) + new_rate = self.current_rate + else: + new_rate = self.rates[index] + + print ("perseus hardware: VarDecimSet: New sampling rate: %d" % new_rate) + perseus.set_sampling_rate(int(new_rate)) + + return int(new_rate) + + def VarDecimRange(self): # Return the lowest and highest sample rate. + print ("perseus hardware: VarDecimRange: %s" % self.rates) + return (self.rates[0], self.rates[-1]) + + def OnButtonAntenna(self, event): + btn = event.GetEventObject() + self.wb_filter = n = btn.index + print ("OnButtonAntenna: %d" % n) + perseus.set_input_filter (self.wb_filter) + +# def StartSamples(self): # called by the sound thread +# print("perseus hardware: StartSamples") + +# def StopSamples(self): # called by the sound thread +# print("perseus hardware: StopSamples") diff --git a/perseuspkg/quisk_widgets.py b/perseuspkg/quisk_widgets.py new file mode 100755 index 0000000..e1b4721 --- /dev/null +++ b/perseuspkg/quisk_widgets.py @@ -0,0 +1,37 @@ +# Please do not change this widgets module for Quisk. Instead copy +# it to your own quisk_widgets.py and make changes there. +# +# This module is used to add extra widgets to the QUISK screen. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import math, wx + +class BottomWidgets: # Add extra widgets to the bottom of the screen + def __init__(self, app, hardware, conf, frame, gbs, vertBox): + self.config = conf + self.hardware = hardware + self.application = app + self.start_row = app.widget_row # The first available row + self.start_col = app.button_start_col # The start of the button columns + self.Widgets_0x06(app, hardware, conf, frame, gbs, vertBox) + + def Widgets_0x06(self, app, hardware, conf, frame, gbs, vertBox): + self.num_rows_added = 1 + start_row = self.start_row + b1 = app.QuiskCheckbutton(frame, self.OnADC_dither, 'ADC Dither') + gbs.Add(b1, (start_row, self.start_col), (1, 2), flag=wx.EXPAND) + b2 = app.QuiskCheckbutton(frame, self.OnADC_preamp, 'ADC Preamp') + gbs.Add(b2, (start_row, self.start_col + 2), (1, 2), flag=wx.EXPAND) + + def OnADC_dither(self, event): + btn = event.GetEventObject() + value = btn.GetValue() + self.hardware.get_hw().set_adc_dither (value) + + def OnADC_preamp(self, event): + btn = event.GetEventObject() + value = btn.GetValue() + self.hardware.get_hw().set_adc_preamp (value) diff --git a/perseuspkg/setup.py b/perseuspkg/setup.py new file mode 100755 index 0000000..ef48fc7 --- /dev/null +++ b/perseuspkg/setup.py @@ -0,0 +1,48 @@ +from distutils.core import setup, Extension +import sys + +module2 = Extension ('perseus', + libraries = ['m', 'perseus-sdr'], + sources = ['../import_quisk_api.c', 'perseus.c'], + include_dirs = ['.', '..'], + ) + +modulew2 = Extension ('perseus', + sources = ['../import_quisk_api.c', 'perseus.c'], + include_dirs = ['.', '..'], + libraries = ['WS2_32', 'perseus-sdr'], + ) + +if sys.platform == "win32": + Modules = [modulew2] +else: + Modules = [module2] + +setup (name = 'perseus', + version = '0.1', + description = 'perseus is an extension to Quisk to support Microtelecom Perseus SDR hardware', + long_description = """Microtelecom Perseus SDR HF receiver. +""", + author = 'Andrea Montefusco IW0HDV', + author_email = 'andrew@montefusco.com', + url = 'http://www.montefusco.com', + download_url = 'http://james.ahlstrom.name/quisk/', + packages = ['quisk.perseuspkg'], + package_dir = {'perseus' : '.'}, + ext_modules = Modules, + classifiers = [ + 'Development Status :: 6 - Mature', + 'Environment :: X11 Applications', + 'Environment :: Win32 (MS Windows)', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Natural Language :: English', + 'Operating System :: POSIX :: Linux', + 'Operating System :: Microsoft :: Windows', + 'Programming Language :: Python', + 'Programming Language :: C', + 'Topic :: Communications :: Ham Radio', + ], +) + + diff --git a/quisk.egg-info/PKG-INFO b/quisk.egg-info/PKG-INFO index 9163563..909e803 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.53 +Version: 4.1.54 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 1b9bbef..c23424b 100755 --- a/quisk.egg-info/SOURCES.txt +++ b/quisk.egg-info/SOURCES.txt @@ -7,7 +7,6 @@ WinQuisk.pyw WinQuiskVna.pyw __init__.py __main__.py -_quisk.pyd configure.py defaults.html docs.html @@ -51,7 +50,6 @@ 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 @@ -99,7 +97,6 @@ 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 @@ -151,6 +148,11 @@ winsound.txt ./n2adr/uhf_conf.py ./n2adr/uhf_hardware.py ./n2adr/uhf_widgets.py +./perseuspkg/README.txt +./perseuspkg/__init__.py +./perseuspkg/quisk_hardware.py +./perseuspkg/quisk_widgets.py +./perseuspkg/setup.py ./sdriqpkg/README.txt ./sdriqpkg/__init__.py ./sdriqpkg/quisk_hardware.py @@ -217,6 +219,13 @@ n2adr/uhf_conf.py n2adr/uhf_hardware.old n2adr/uhf_hardware.py n2adr/uhf_widgets.py +perseuspkg/README.txt +perseuspkg/__init__.py +perseuspkg/makefile +perseuspkg/perseus.c +perseuspkg/quisk_hardware.py +perseuspkg/quisk_widgets.py +perseuspkg/setup.py quisk.egg-info/PKG-INFO quisk.egg-info/SOURCES.txt quisk.egg-info/dependency_links.txt @@ -228,6 +237,7 @@ sdriqpkg/quisk_hardware.py sdriqpkg/sdriq.c sdriqpkg/sdriq.h sdriqpkg/sdriq.pyd +sdrmicronpkg/quisk_hardware.py soapypkg/__init__.py soapypkg/makefile soapypkg/quisk_hardware.py diff --git a/sdrmicronpkg/quisk_hardware.py b/sdrmicronpkg/quisk_hardware.py new file mode 100755 index 0000000..7c48db7 --- /dev/null +++ b/sdrmicronpkg/quisk_hardware.py @@ -0,0 +1,266 @@ +# -*- 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 +# + +# Define the name of the hardware and the items on the hardware screen (see quisk_conf_defaults.py): +################ Receivers SdrMicron, The SDR Micron 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 = 'sdrmicronpkg/quisk_hardware.py' + + +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 e5cfc8c..0b8883a 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.53' +Version = '4.1.54' fp = open("__init__.py", "w") # write title string fp.write("#Quisk version %s\n" % Version) @@ -155,7 +155,7 @@ N1MM+ and software that uses Hamlib. author_email = 'jahlstr@gmail.com', url = 'http://james.ahlstrom.name/quisk/', packages = ['quisk', 'quisk.sdriqpkg', 'quisk.n2adr', 'quisk.softrock', 'quisk.freedvpkg', - 'quisk.hermes', 'quisk.hiqsdr', 'quisk.afedrinet', 'quisk.soapypkg'], + 'quisk.hermes', 'quisk.hiqsdr', 'quisk.afedrinet', 'quisk.soapypkg', 'quisk.perseuspkg'], package_dir = {'quisk' : '.'}, package_data = {'' : ['*.txt', '*.html', '*.so', '*.dll']}, entry_points = {'gui_scripts' : ['quisk = quisk.quisk:main', 'quisk_vna = quisk.quisk_vna:main']}, diff --git a/soapypkg/build/temp.linux-x86_64-3.6/soapy.o b/soapypkg/build/temp.linux-x86_64-3.6/soapy.o index 76a600f48e6e789ae25aed06849ef45565abde8c..9a11e0eaaa4e9ecaa6f8cd9158a6615c527516de 100755 GIT binary patch delta 19757 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 C{AmpU delta 19413 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