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 76a600f..9a11e0e 100755 Binary files a/soapypkg/build/temp.linux-x86_64-3.6/soapy.o and b/soapypkg/build/temp.linux-x86_64-3.6/soapy.o differ