diff --git a/_quisk.pyd b/_quisk.pyd index 3d38eaa..5bcae9b 100644 Binary files a/_quisk.pyd and b/_quisk.pyd differ diff --git a/microntrxpkg/quisk_hardware.py b/microntrxpkg/quisk_hardware.py index 73302a2..10fbd79 100644 --- a/microntrxpkg/quisk_hardware.py +++ b/microntrxpkg/quisk_hardware.py @@ -1,24 +1,38 @@ # This provides support for the SDR Micron TRX by David Fainitski, N7DDC +# On Windows this uses Python module ftd2xx. Install with pip. +# On Linux it uses ftdi1. Install python3-ftdi1 with your package manager. + import sys, wx, traceback, time from quisk_hardware_model import Hardware as BaseHardware -DEBUG = 0 # print debug messages during operation +DEBUG = 1 # print debug messages during operation WRITE_LOG = 1 # write the data received from the MicronTRX during self.log_time to the file self.log_name +#WRITE_LOG = 2 # receive data from the file self.log_name instead of from USB # https://github.com/Dfinitski/SDR-Micron -# Control from PC to Micron TRX, 64 bytes, MSB first (big-endian) +# Short Control Packet type "SCP" from PC to Micron TRX, 16 bytes, MSB first (big endian) +# +# (0 - 7) Preamble, seven 0x55 and 0xD5 +# (8, 9, 10) "SCP" +#(11) RX0 enable, 0 - 1 +#(12) RX1 enable, 0 - 1 +#(13) BS0 enable, 0 - 1 +#(14) TX0 enable, 0 - 1 +#(15) CDC enable, 0 - 1 + +# Universal control packet type "UCP" from PC to TRX or from TRX to PC, 64 bytes, MSB first (big-endian) # # (0 - 7) Preamble, seven 0x55 and 0xD5 # (8, 9, 10) "UCP" -# (11) SN - Sequence Number 0 - 255 -# (12) RX0 enable, mode, 0 - off, 1 - 48kHz, 2 - 96kHz, 3 - 192kHz -# (13) RX1 enable, mode, 0 - off, 1 - 48kHz, 2 - 96kHz, 3- 192kHz -# (14) BS0 enable -# (15) BS1 enable -# (16) TX0 enable -# (17) CDC enable +# (11) read only FW prefix MSB +# (12) read only FW prefix +# (13) read only FW prefix LSB +# (14) read only char '-' +# (15) Not used +# (16) Not used +# (17) Not used # (18, 19, 20, 21) - 4 bytes RX0 frequency # (22, 23, 24, 25) - 4 bytes RX1 frequency # (26, 27, 28, 29) - 4 bytes BS0 frequency @@ -31,38 +45,34 @@ WRITE_LOG = 1 # write the data received from the MicronTRX during self.log_time # 3 - 384 kHz, # 4 - 768 kHz, # 5 - 1536 kHz -# 6 - 1920 kHz # (36) LNA enable # (37) TX level 0 - 255 -# (38) Not used +# (38) RX BPF bypass 0 - 1 # (39) BS0 mode # 0 - 4096 buffered samples # 1 - 8192 buffered samples # 2 - 16384 buffered samples -# (40) BS1 mode -# 0 - 4096 buffered samples -# 1 - 8192 buffered samples -# 2 - 16384 buffered samples -# (41) BS0 period 50 - 255 ms (100ms is recommended) -# (42) BS1 period 50 - 255 ms (100ms is recommended) +# (40) Not used +# (41) Not used +# (42) read-only PTT state from device 0-1 # (43) Mic Boost, 0 - 1 -# (44) Not used -# (45) PTT from PC -# (46) CW mode 0 - 1 -# (47) CWX from PC - key manipulation +# (44) TX LPF Bypass 0 - 1 +# (45) PTT to device 1 - 0 +# (46) CW mode on 0 - 1 +# (47) CW key manipulation from PC to device 1 - 0 # (48) Read only PWR, Output power in Watts -# (49) Read only SWR x.x, from 1.0 to 9.9 as a natural number without decimal dot. +# (49) Read only SWR x.x, from 1.0 to 9.9 as a natural number without decimal point # (50) Key Speed 10 - 63 ppm (20 by default) # (51) Paddle swap 0, 1 # (52) Key mode, 0 = straight, 01 = Mode A, 10 = Mode B # (53) Key weight, 33 to 66, (50 by default) -# (54) Key spacing, 0, 1 +# (54) Key auto spacing, 0, 1 # (55) ST Level, 0 - 255 sight tone level -# (56) ST Freq MSB- 4 MSB bits -# (57) ST Freq LSB - 8 LSB bits sight tone frequency in Hertz +# (56) ST Freq in tens Hertz, 40 - 100 (400 - 1000Hz), 70 by default +# (57) Not used # (58) Delay ON, 8 - 255 ms (25 by default) - time for TX carrier delay is needed for transmitter preparing to TX # (59) Hang out time in tens ms 0 - 255 (0 to 2550 msec) (250 ms by default) needed to keep transmitter ready after PTT -# (60) Not used +# (60) Read only CW key manipulation from device to PC # (61) Read only FW2 - firmware version MSB # (62) Read only FW1 # (63) Read only FW0 - firmware version LSB @@ -76,14 +86,16 @@ WRITE_LOG = 1 # write the data received from the MicronTRX during self.log_time class Hardware(BaseHardware): sample_rates = [48] #[48, 96, 192, 384, 768, 1536] - BS1_samples = {0:4096, 1:8192, 2:16384} - BS1_bytes = {0:4096 * 2, 1:8192 * 2, 2:16384 * 2} # Two bytes per sample + BS0_samples = {0:4096, 1:8192, 2:16384} + BS0_bytes = {0:4096 * 2, 1:8192 * 2, 2:16384 * 2} # Two bytes per sample def __init__(self, app, conf): BaseHardware.__init__(self, app, conf) - self.usb = None + self.usb_windows = None + self.usb_linux = None self.prefix = b'\x55\x55\x55\x55\x55\x55\x55\xD5' self.read_data = bytearray() - self.rf_gain_labels = ('RF 0', 'RF -10', 'RF -20', 'RF -31') + self.tx_samples = b'' + self.rf_gain_labels = ('RF 0', 'RF -10', 'RF -20', 'RF -30') self.index = 1 self.old_vfo = 0 self.sdrmicron_clock = 76800000 @@ -91,37 +103,41 @@ class Hardware(BaseHardware): self.bscope_data = bytearray() self.have_version = False self.frame_msg = '' - self.remaining_BS1 = -1 - self.log_name = "microntrx_log.dat" + self.remaining_BS0 = -1 + self.log_name = "microntrxpkg/microntrx_log.dat" self.log_time = 5.0 # seconds of log data self.log_time0 = None # starting time.time() to start log file self.log_fp = None self.pc_control = bytearray(64) self.pc_control[0:8] = self.prefix self.pc_control[8:11] = b'UCP' - self.pc_control[34] = 10 # Attenuator, 0 - 31 (dB value) - self.pc_control[35] = 0 # BS0 rate: 0:48ksps, 1:96, 2:192, 3:384, 4:768, 5:1536 - self.pc_control[36] = 0 # LNA enable - self.pc_control[39] = 0 # Buffered BS0 samples, 0:4096, 1:8192, 2:16384 - self.pc_control[40] = 0 # Buffered BS1 samples, 0:4096, 1:8192, 2:16384 - self.pc_control[41] = 100 # BS0 period msec - self.pc_control[42] = 100 # BS1 period msec - self.pc_control[58] = 25 # time for TX carrier delay needed for transmitter preparing to TX, msec - self.pc_control[59] = 25 # tens of msec needed to keep transmitter ready + self.got_defaults = 0 + self.pc_enables = bytearray(16) + self.pc_enables[0:8] = self.prefix + self.pc_enables[8:11] = b'SCP' + self.fw_version = bytearray if conf.fft_size_multiplier == 0: conf.fft_size_multiplier = 3 # Set size needed by VarDecim def open(self): # This method must return a string showing whether the open succeeded or failed. + if WRITE_LOG == 1: + self.log_fp = open(self.log_name, "wb") + elif WRITE_LOG == 2: + self.log_fp = open(self.log_name, "rb") rx_bytes = 3 # rx_bytes is the number of bytes in each I or Q sample: 1, 2, 3, or 4 rx_endian = 1 # rx_endian is the order of bytes in the sample array: 0 == little endian; 1 == big endian self.InitSamples(rx_bytes, rx_endian) # Initialize: read samples from this hardware file and send them to Quisk + self.InitTxSamples(2, 1, 48000) # Initialize Tx samples to send to the hardware bs_bytes = 2 bs_endian = 1 - self.InitBscope(bs_bytes, bs_endian, self.sdrmicron_clock, self.BS1_samples[self.pc_control[40]]) # Initialize bandscope - if sys.platform == 'win32': + #self.InitBscope(bs_bytes, bs_endian, self.sdrmicron_clock, self.BS1_samples[self.pc_control[40]]) # Initialize bandscope + if WRITE_LOG == 2: + return "Read data from %s" % self.log_name + elif sys.platform == 'win32': return self.open_win() - return "This code is Windows only" + else: + return self.open_linux() def open_win(self): try: @@ -137,48 +153,84 @@ class Hardware(BaseHardware): print ("%d: description %s, serial %s" % (i, a['description'], a['serial'])) if a['description']==b'SDR-Micron': try: - self.usb = d2xx.openEx(a['serial']) + usb = d2xx.openEx(a['serial']) except: return 'Device SDR-Micron could not be opened' - self.usb.setBitMode(255, 0) # reset + usb.setBitMode(255, 0) # reset time.sleep(0.1) - self.usb.setBitMode(255, 64) # 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 + usb.setBitMode(255, 64) # Configure FT2232H into Sync FIFO mode + usb.setTimeouts(100, 100) # read, write + usb.setLatencyTimer(2) + usb.setUSBParameters(32, 32) # in_tx_size, out_tx_size time.sleep(1.5) # waiting for device initialisation - self.usb.read(self.usb.getQueueStatus()) # clean the usb data buffer + usb.read(usb.getQueueStatus()) # clean the usb data buffer self.frame_msg = a['description'].decode('utf-8') + ' S/N - ' + a['serial'].decode('utf-8') + self.usb_windows = usb return self.frame_msg return 'Device SDR-Micron was not found' def open_linux(self): # Not finished try: - import ftdi1 as d2xx + import ftdi1 except: return 'The ftdi1 module is missing' - self.usb = d2xx.new() + usb = ftdi1.new() # Open Channel A (interface 1 in libftdi) - d2xx.set_interface(self.usb, d2xx.INTERFACE_A) - rc = d2xx.usb_open(self.usb, 0x0403, 0x6014) # 0x6014 is the standard FT232H PID + ftdi1.set_interface(usb, ftdi1.INTERFACE_A) + rc = ftdi1.usb_open(usb, 0x0403, 0x6014) # 0x6014 is the standard FT232H PID if rc == 0: - d2xx.usb_reset(self.usb) - d2xx.set_latency_timer(self.usb, 2) + ftdi1.usb_reset(usb) + ftdi1.set_usb_read_timout(usb, 1) + ftdi1.set_usb_write_timout(usb, 5) + ftdi1.set_latency_timer(usb, 2) # Enable Sync FIFO mode # 0xFF = pin mask, 0x40 = BITMODE_SYNCFF - d2xx.set_bitmode(self.usb, 0xFF, d2xx.BITMODE_SYNCFF) + ftdi1.set_bitmode(usb, 0xFF, ftdi1.BITMODE_SYNCFF) if DEBUG: print("Sync FIFO active via ftdi1.") - # To read at maximum speed, use d2xx.read_data() - d2xx.usb_close(self.usb) - d2xx.free(self.usb) + self.usb_linux = usb + return "Device SDR-Micron opened" + return 'Device SDR-Micron was not found' + + def usb_read(self): + if WRITE_LOG == 2: + data = self.log_fp.read(120) + return data + elif self.usb_windows: + count = self.usb_windows.getQueueStatus() # Read all available bytes + if count > 0: + return self.usb_windows.read(count) # return Python bytes + elif self.usb_linux: + count, data = ftdi1.read_data(self.usb_linux, 1024) + if count > 0: + return data # data is Python bytes + elif count < 0 and DEBUG: + print("Error %d in usb_read", count) + return None + + def usb_write(self, data): + data = bytes(data) # Convert bytearray to bytes + if self.usb_windows: + try: + self.usb_windows.write(data) + except: + if DEBUG: + print('Error while usb_write') + elif self.usb_linux: + count = ftdi1.write_data(self.usb_linux, data) + if count != len(data) and DEBUG: + print("Failure in usb_write_data %d %d", count, len(data)) def close(self): - if self.usb: - self.usb.setBitMode(255, 0) # reset + if self.usb_windows: + self.usb_windows.setBitMode(255, 0) # reset time.sleep(0.5) - self.usb.close() - self.usb = None + self.usb_windows.close() + self.usb_windows = None + elif self.usb_linux: + ftdi1.usb_close(self.usb_linux) + ftdi1.free(self.usb_linux) + self.usb_linux = None def OnButtonRfGain(self, event): btn = event.GetEventObject() @@ -190,7 +242,7 @@ class Hardware(BaseHardware): elif n == 2: self.pc_control[34] = 20 elif n == 3: - self.pc_control[34] = 31 + self.pc_control[34] = 30 if DEBUG: print ('pc_control[34]', self.pc_control[34]) self.rx_control_upd() @@ -209,12 +261,15 @@ class Hardware(BaseHardware): vfo = int(vfo - self.transverter_offset) if vfo != self.old_vfo: self.old_vfo = vfo - self.pc_control[26] = (vfo >> 24) & 0xFF # MSB - self.pc_control[27] = (vfo >> 16) & 0xFF - self.pc_control[28] = (vfo >> 8) & 0xFF - self.pc_control[29] = (vfo ) & 0xFF #LSB + self.pc_control[18] = (vfo >> 24) & 0xFF # MSB + self.pc_control[19] = (vfo >> 16) & 0xFF + self.pc_control[20] = (vfo >> 8) & 0xFF + self.pc_control[21] = (vfo ) & 0xFF #LSB self.rx_control_upd() return tx_freq, vfo + + def OnSpot(self, level): + pass def VarDecimGetChoices(self): # Return a list/tuple of strings for the decimation control. return list(map(str, self.sample_rates)) # convert integer to string @@ -246,37 +301,40 @@ class Hardware(BaseHardware): return (48000, 48000) def StartSamples(self): # called by the sound thread - self.pc_control[12] = 1 # RX0 enable - self.pc_control[15] = 1 # BS1 enable - self.rx_control_upd() - if WRITE_LOG: - if self.log_fp is None: - self.log_fp = open(self.log_name, "wb") + self.pc_enables[11] = 1 # RX0 enable + #self.pc_enables[13] = 1 # BS0 enable + self.usb_write(self.pc_enables) + if DEBUG: + print ("Start samples") def StopSamples(self): # called by the sound thread - self.pc_control[12] = 0 - self.pc_control[15] = 0 - self.rx_control_upd() + self.pc_enables[11] = 0 + self.pc_enables[13] = 0 + self.usb_write(self.pc_enables) + if DEBUG: + print ("Stop Samples") def rx_control_upd(self): - if self.usb: - try: - self.usb.write(self.pc_control) - except: - print('Error while rx_control_upd') - if self.pc_control[11] < 255: # sequence number - self.pc_control[11] += 1 - else: - self.pc_control[11] = 0 + if self.got_defaults: + self.usb_write(self.pc_control) def GetRxSamples(self): # Read from the MicronTRX. Called frequently from the sound thread. - if not self.usb: - return - count = self.usb.getQueueStatus() # Read all available bytes - if count > 0: - data = bytearray(self.usb.read(count)) + # Send Tx samples to the hardware + self.tx_samples += self.GetTxSamples() + while len(self.tx_samples) >= 492: + data = self.prefix + b'TX0\x00\x00\x00\x00\x00' + self.tx_samples[0:492] + self.usb_write(data) + #print(data[0:24], len(data)) + #i = data[16] << 8 | data[17] + #print ("%8d" % i) + self.tx_samples = self.tx_samples[492:] + # Read messages from USB + data = self.usb_read() + if data: self.read_data += data - if WRITE_LOG: + if DEBUG > 1: + print("Read %s bytes, buffer length %d" % (len(data), len(self.read_data))) + if WRITE_LOG == 1: if self.log_fp: if self.log_time0 is None: self.log_time0 = time.time() @@ -299,28 +357,28 @@ class Hardware(BaseHardware): if block_type == b'BS0': if len(self.read_data) < 508: return - del self.read_data[0:508] - elif block_type == b'BS1': - if len(self.read_data) < 508: - return - packet_number = self.read_data[12] + packet_number = self.read_data[11] if packet_number == 0: # start of a block of data - self.remaining_BS1 = self.BS1_bytes[self.pc_control[40]] + self.remaining_BS0 = self.BS0_bytes[self.pc_control[39]] self.bscope_data = self.read_data[16:508] # 492 bytes - self.remaining_BS1 -= 492 - elif self.remaining_BS1 >= 492: + self.remaining_BS0 -= 492 + elif self.remaining_BS0 >= 492: self.bscope_data += self.read_data[16:508] # 492 bytes - self.remaining_BS1 -= 492 - elif self.remaining_BS1 <= 0: + self.remaining_BS0 -= 492 + elif self.remaining_BS0 <= 0: pass else: - self.bscope_data += self.read_data[16:self.remaining_BS1 + 16] - self.remaining_BS1 = -1 + self.bscope_data += self.read_data[16:self.remaining_BS0 + 16] + self.remaining_BS0 = -1 # self.AddBscopeSamples(self.bscope_data) + if DEBUG > 1: + print("BS0 buffer length %d" % len(self.bscope_data)) del self.read_data[0:508] elif block_type == b'CDM': if len(self.read_data) < 180: return + if DEBUG > 1: + print("CDM") del self.read_data[0:180] elif block_type == b'RX0': if len(self.read_data) < 508: @@ -328,24 +386,29 @@ class Hardware(BaseHardware): if self.read_data[12]: self.GotClip() self.AddRxSamples(self.read_data[16:508]) + if DEBUG > 1: + print("RX0") del self.read_data[0:508] elif block_type == b'RX1': if len(self.read_data) < 508: return + if DEBUG > 1: + print("RX1") del self.read_data[0:508] - elif block_type == b'SBL': - if len(self.read_data) < 32: - return - del self.read_data[0:32] elif block_type == b'UCP': if len(self.read_data) < 64: return if self.have_version is False: self.have_version = True - self.frame_msg += ' F/W version - %d.%d' % (self.read_data[61], self.read_data[63]) + self.fw_version = self.read_data[11:15] + self.read_data[61:64] + self.frame_msg += ' F/W version - ' + self.fw_version.decode('utf-8') self.application.main_frame.SetConfigText(self.frame_msg) + self.pc_control[11:64] = self.read_data[11:64] # save UCP content with defaults + self.got_defaults = 1; + if DEBUG > 1: + print("UCP") del self.read_data[0:64] else: - del self.read_data[0:8] + del self.read_data[0:11] if DEBUG: print("Unknown USB block type") diff --git a/microphone.c b/microphone.c index dc9056f..18443e6 100644 --- a/microphone.c +++ b/microphone.c @@ -73,7 +73,7 @@ static double modulation_index = 1.6; // For FM transmit, the modulation index static int is_vox = 0; // Is the VOX level exceeded? static int vox_level = CLIP16; // VOX trigger level as a number 0 to CLIP16 static int timeVOX = 2000; // VOX hang time in milliseconds -static int tx_sample_rate = 48000; // Used for SoapySDR +static int tx_sample_rate = 48000; // Used for SoapySDR and Tx samples from Python static int reverse_tx_sideband; static int PsEnable = 0; static int PsCal = 0; @@ -1305,7 +1305,7 @@ int quisk_process_microphone(int mic_sample_rate, complex double * cSamples, int ; // hermes handles this itself else if (quisk_play_state == SOFTWARE_CWKEY) serial_key_samples(cSamples, count); - if(quisk_pt_sample_write) { // Used for SoapySDR + if(quisk_pt_sample_write && key_down) { // Used for SoapySDR and samples from Python // Interpolate the mic samples to the Tx sample rate //printf("Tx sample rate %i\n", tx_sample_rate); switch (tx_sample_rate) { diff --git a/quisk.c b/quisk.c index 7e657ad..40d89db 100644 --- a/quisk.c +++ b/quisk.c @@ -115,6 +115,8 @@ int quiskTxHoldState; // hold Tx until the repeater frequency shift is complet int quisk_is_vna; // zero for normal program, one for the VNA program static int py_sample_rx_bytes=2; // number of bytes in each I or Q sample: 1, 2, 3, or 4 static int py_sample_rx_endian; // order of sample array: 0 == little endian; 1 == big endian +static int py_sample_tx_bytes; // number of bytes in each I or Q sample: 2, 3, or 4 +static int py_sample_tx_endian; // order of sample array: 0 == little endian; 1 == big endian static int py_bscope_bytes; static int py_bscope_endian; static int quisk_auto_notch; // auto notch control @@ -159,6 +161,8 @@ static int softrock_correct_active; // 0 No correction adjustment; 1 Manual adju static complex double PySampleBuf[SAMP_BUFFER_SIZE]; // buffer for samples returned from Python static int PySampleCount; // count of samples in buffer +static uint8_t PySampleTxBytes[SAMP_BUFFER_SIZE * 8]; // buffer for Tx sample bytes to send to the hardware +static int PySampleTxCount; static bool quisk_tx_inhibit; static int multirx_data_width; // width of graph data to return @@ -3018,6 +3022,17 @@ static PyObject * add_bscope_samples(PyObject * self, PyObject * args) return Py_None; } +static PyObject * get_tx_samples(PyObject * self, PyObject * args) +{ + PyObject * by; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + by = PyBytes_FromStringAndSize((const char *)PySampleTxBytes, PySampleTxCount); + PySampleTxCount = 0; + return by; +} + static void py_sample_start(void) { } @@ -3040,6 +3055,83 @@ static int py_sample_read(complex double * cSamples) return n; } +static int py_sample_tx_save(complex double * cSamples, int nSamples) +{ // These cSamples are 16-bit integers from the microphone + int i; + int16_t rr, ii; + uint8_t rrH, rrL, iiH, iiL; + + if (PySampleTxCount + nSamples * 8 >= SAMP_BUFFER_SIZE * 8) { + QuiskPrintf ("py_sample_tx_save: buffer is too full\n"); + return 0; + } + for (i = 0; i < nSamples; i++) { + rr = (int16_t)creal(cSamples[i]); + ii = (int16_t)cimag(cSamples[i]); + rrH = 0xFF & (rr >> 8); + rrL = 0xFF & rr; + iiH = 0xFF & (ii >> 8); + iiL = 0xFF & ii; + if (py_sample_tx_endian == 0) { // byte order of samples is little-endian + switch(py_sample_tx_bytes) { + case 2: + PySampleTxBytes[PySampleTxCount++] = rrL; + PySampleTxBytes[PySampleTxCount++] = rrH; + PySampleTxBytes[PySampleTxCount++] = iiL; + PySampleTxBytes[PySampleTxCount++] = iiH; + break; + case 3: + PySampleTxBytes[PySampleTxCount++] = 0; + PySampleTxBytes[PySampleTxCount++] = rrL; + PySampleTxBytes[PySampleTxCount++] = rrH; + PySampleTxBytes[PySampleTxCount++] = 0; + PySampleTxBytes[PySampleTxCount++] = iiL; + PySampleTxBytes[PySampleTxCount++] = iiH; + break; + case 4: + PySampleTxBytes[PySampleTxCount++] = 0; + PySampleTxBytes[PySampleTxCount++] = 0; + PySampleTxBytes[PySampleTxCount++] = rrL; + PySampleTxBytes[PySampleTxCount++] = rrH; + PySampleTxBytes[PySampleTxCount++] = 0; + PySampleTxBytes[PySampleTxCount++] = 0; + PySampleTxBytes[PySampleTxCount++] = iiL; + PySampleTxBytes[PySampleTxCount++] = iiH; + break; + } + } + else { // byte order of samples is big-endian + switch(py_sample_tx_bytes) { + case 2: + PySampleTxBytes[PySampleTxCount++] = rrH; + PySampleTxBytes[PySampleTxCount++] = rrL; + PySampleTxBytes[PySampleTxCount++] = iiH; + PySampleTxBytes[PySampleTxCount++] = iiL; + break; + case 3: + PySampleTxBytes[PySampleTxCount++] = rrH; + PySampleTxBytes[PySampleTxCount++] = rrL; + PySampleTxBytes[PySampleTxCount++] = 0; + PySampleTxBytes[PySampleTxCount++] = iiH; + PySampleTxBytes[PySampleTxCount++] = iiL; + PySampleTxBytes[PySampleTxCount++] = 0; + break; + case 4: + PySampleTxBytes[PySampleTxCount++] = rrH; + PySampleTxBytes[PySampleTxCount++] = rrL; + PySampleTxBytes[PySampleTxCount++] = 0; + PySampleTxBytes[PySampleTxCount++] = 0; + PySampleTxBytes[PySampleTxCount++] = iiH; + PySampleTxBytes[PySampleTxCount++] = iiL; + PySampleTxBytes[PySampleTxCount++] = 0; + PySampleTxBytes[PySampleTxCount++] = 0; + break; + } + } + } + return 0; +} + static PyObject * get_params(PyObject * self, PyObject * args) { const char * name; @@ -3093,18 +3185,22 @@ static PyObject * set_params(PyObject * self, PyObject * args, PyObject * keywds { /* Call with keyword arguments ONLY; change local parameters */ static char * kwlist[] = {"quisk_is_vna", "rx_bytes", "rx_endian", "read_error", "clip", "bscope_bytes", "bscope_endian", "bscope_size", "bandscopeScale", "hermes_pause", + "tx_bytes", "tx_endian", NULL} ; int i, nbytes, read_error, clip, bscope_size, hermes_pause; nbytes = read_error = clip = bscope_size = hermes_pause = -1; - if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiiiiiidii", kwlist, + if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiiiiiidiii", kwlist, &quisk_is_vna, &nbytes, &py_sample_rx_endian, &read_error, &clip, - &py_bscope_bytes, &py_bscope_endian, &bscope_size, &bandscopeScale, &hermes_pause)) + &py_bscope_bytes, &py_bscope_endian, &bscope_size, &bandscopeScale, &hermes_pause, + &py_sample_tx_bytes, &py_sample_tx_endian)) return NULL; if (nbytes != -1) { py_sample_rx_bytes = nbytes; - quisk_sample_source4(py_sample_start, py_sample_stop, py_sample_read, NULL); + quisk_sample_source(py_sample_start, py_sample_stop, py_sample_read); } + if (py_sample_tx_bytes) + quisk_pt_sample_write = py_sample_tx_save; if (read_error != -1) quisk_sound_state.read_error++; if (clip != -1) @@ -6090,6 +6186,7 @@ static PyMethodDef QuiskMethods[] = { {"get_filter_rate", get_filter_rate, METH_VARARGS, "Return the sample rate used for the filters."}, {"get_tx_filter", quisk_get_tx_filter, METH_VARARGS, "Return the frequency response of the transmit filter."}, {"get_audio_graph", get_audio_graph, METH_VARARGS, "Return a tuple of the audio graph data."}, + {"get_tx_samples", get_tx_samples, METH_VARARGS, "Return Tx samples to send to the hardware."}, {"softrock_corrections", softrock_corrections, METH_VARARGS, "Control and return SoftRock amplitude and phase corrections."}, {"measure_frequency", measure_frequency, METH_VARARGS, "Set the method, return the measured frequency."}, {"measure_audio", measure_audio, METH_VARARGS, "Set the method, return the measured audio voltage."}, diff --git a/quisk.py b/quisk.py index 96b6352..9c251e5 100644 --- a/quisk.py +++ b/quisk.py @@ -65,13 +65,15 @@ else: Q3StringTypes = (str, unicode) class StdOutput(wx.Frame): - def __init__(self, app): + def __init__(self, app): # Called from the GUI thread self.app = app self.ctrl = None self.old_stdout = sys.stdout self.old_stderr = sys.stderr self.old_excepthook = sys.excepthook self.path = os.path.join(app.QuiskFilesDir, "quisk_logfile.txt") + self.gui_thread = threading.get_native_id() + self.text_buffer = '' try: size = os.path.getsize(self.path) except: @@ -116,6 +118,9 @@ class StdOutput(wx.Frame): self.fp.write(text) self.fp.write("\n") def write(self, text): + if threading.get_native_id() != self.gui_thread: + self.text_buffer += text # When called by non-GUI thread, save text + return if self.fp: self.fp.write(text) if self.old_stdout: @@ -124,6 +129,10 @@ class StdOutput(wx.Frame): self.ctrl.write(text) def flush(self): pass + def PrintBuffer(self): # Print any saved text from non-GUI thread + if self.text_buffer: + print(self.text_buffer) + self.text_buffer = '' def ExceptHook(self, typ, value, traceb): self.Show(True) self.Raise() @@ -1502,9 +1511,7 @@ class SoundThread(threading.Thread): #print ("Quisk thread %12.3f" % ((tm - self.time0) * 1E3)) #self.time0 = tm if self.samples_from_python: - samples = Hardware.GetRxSamples() - if samples: - QS.set_params(rx_samples=samples) + Hardware.GetRxSamples() if self.poll_cw: Hardware.PollCwKey() if conf.midi_cwkey_device: @@ -4694,6 +4701,7 @@ class App(wx.App): msg = QS.GetQuiskPrintf() if msg: print(msg, end='') + self.std_out_err.PrintBuffer() QS.close_key() if QS.tci_get_params("tci_clients_count") > 0: QS.tci_set_params(close=1) @@ -4727,6 +4735,7 @@ class App(wx.App): msg = QS.GetQuiskPrintf() if msg: print(msg, end='') + self.std_out_err.PrintBuffer() def OnBtnOnOff(self, event): if event.GetEventObject().GetValue(): # Start samples try: @@ -6894,6 +6903,7 @@ class App(wx.App): msg = QS.GetQuiskPrintf() if msg: print(msg, end='') + self.std_out_err.PrintBuffer() if self.screen == self.config_screen: self.screen.OnGraphData() # Send message to draw new data if self.add_version and Hardware.GetFirmwareVersion() is not None: diff --git a/quisk_hardware_model.py b/quisk_hardware_model.py index ed964f7..98dc0e7 100644 --- a/quisk_hardware_model.py +++ b/quisk_hardware_model.py @@ -118,17 +118,24 @@ class Hardware: # length is the number of samples in each block of ADC samples, and equals the FFT size. QS.set_params(bscope_bytes=int_size, bscope_endian=endian, bscope_size=length) self.application.bandscope_clock = clock + def InitTxSamples(self, int_size, endian, rate): # Tx sample initialization; you must call this from your hardware __init__(). + # int_size is the number of bytes in each I or Q sample: 2, 3, or 4 + # endian is the order of bytes in the sample: 0 == little endian; 1 == big endian + # This can be called again to change the format. For example, a different number of bytes for different sample rates. + QS.set_tx_audio(tx_sample_rate=rate) # Available rates are 48, 50, 96, 100, 192 ksps + QS.set_params(tx_bytes=int_size, tx_endian=endian) #def PollCwKey(self): # Optional. Called frequently by the sound thread to check the CW key status. # pass # Do not define if not needed. #def PollGuiControl(self): # Optional. Called frequently by the GUI thread to change GUI settings (PTT etc.) # pass # Do not define if not needed. - def StartSamples(self): # Quisk calls this from the sound thread to start sending samples. + def StartSamples(self): # Quisk calls this from the sound thread to start samples. # If you return a string, it replaces the string returned from hardware open() pass - def StopSamples(self): # Quisk calls this from the sound thread to stop sending samples. + def StopSamples(self): # Quisk calls this from the sound thread to stop samples. pass def GetRxSamples(self): # Quisk calls this frequently from the sound thread. Poll your hardware for samples. # Return any available samples by calling AddRxSamples() and perhaps AddBscopeSamples() from within this method. + # Call GetTxSamples() from within this method to return any Tx samples that must be sent. pass def AddRxSamples(self, samples): # Call this from within GetRxSamples() to record the Rx samples. # "samples" is int_size of integer I data followed by int_size of integer Q data, repeated. @@ -142,6 +149,8 @@ class Hardware: # For Python 2, "samples" must be a byte array or bytes or a string. # The number of bytes in "samples" must equal the block length times the bytes per sample. QS.add_bscope_samples(samples) + def GetTxSamples(self): # Call this from within GetRxSamples() to return the Tx samples to send. + return QS.get_tx_samples() # Return a Python bytes object def GotClip(self): # Call this to indicate that samples were received with the clip (overrange) indicator true. QS.set_params(clip=1) def GotReadError(self, print_msg, msg): # Call this to indicate an error in receiving the Rx samples.