charleston-1.0/0000755000175000017500000000000011430152203012225 5ustar tfoxtfoxcharleston-1.0/chas_rx1.c0000644000175000017500000010217411424616262014123 0ustar tfoxtfox#include // needed for SDR-IQ support #include // NOT needed for SDR-IQ support #include // NOT needed for SDR-IQ support #include // NOT NEEDED for SDR-IQ support #include // needed for SDR-IQ support #include // needed for SDR-IQ support #include // NOT NEEDED for SDR-IQ support #include // NOT NEEDED for SDR-IQ support #include // NOT NEEDED for SDR-IQ support #include // NOT NEEDED for SDR-IQ support #include // NOT NEEDED for SDR-IQ support #include // needed for SDR-IQ support #include // TF added for Charleston Rx1 support #include "quisk.h" #include "chas_rx1.h" #define DEBUG 0 #define NAME_SIZE 16 //int quisk_use_chas_rx1 = 0; // WB4JFI: Added for Charleston Rx Ver1 static char sdr_name[NAME_SIZE]; // control item 1 (only used in this file 7 times) static char sdr_serial[NAME_SIZE]; // item 2 (only used in this file five tmes) /////////////////////////////////////////////////////////////////////////////////////////// //////////////// THIS IS NEW CODE FOR CHARLESTON SDR /////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////// static int chas_rx1_freq;// = 7220000; // set Chas Rx initially to this freq static int chas_rx1_sample_freq = NEXSDR_ADC_RATE; // Charleston Rx RF sample frequency static int chas_rx1_gain = 0; // set Chas Rx PGA gain to this value static int chas_rx1_rf_path = NEXSDR_RFPATH_LPFLNA; // set RF PATH TO LNA and LPF static int chas_rx1_lna_range = NEXSDR_LNAGAIN_HIGH; // set LNA range to LOW static int chas_rx1_lna_slope = NEXSDR_LNASLOPE_POS; // set LNA slope to positive static int chas_rx1_lna_gain = 2047; // set LNA gain initially to mid-range static int chas_rx1_decim = 400; // set decimation initially to 400 static int cur_freq, cur_gain, cur_path; // current value of frequency and gain static int cur_lnarange, cur_lnaslope; // current values storage static int cur_lnagain; // current values storage static int cur_decimation; static int sdr_close_read = 0; // time to close read thread flag static int chas_rx1_pwd = 0; // Chas power-down storage, 0=oper, 1=pwr-dn static int next_block_out; static int next_buffer; static long overrun; static int running; static int buffer_full[NEXSDR_NUM_BUFFERS]; short buffer[NEXSDR_NUM_BUFFERS*NEXSDR_BLOCK_SIZE*2]; float adc_adj; /////////////////////////////////////////////////////////////////////////////////////////// ////////////////// Charleston Internal Functions Only /////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////// //////////////////////////// LOW-LEVEL COMMUNICATIONS /////////////////////////// //////////////////////// put_fpga_reg() /////////////////////////////////// int put_fpga_reg(unsigned char addr, unsigned char v) { char buffer[64]; sem_wait(&iflock); memset(buffer,0x00,16); int rv; buffer[0] = 0x01; buffer[1] = addr; buffer[2] = v; rv = usb_bulk_write(dev, 0x01, buffer, 64, 100); // usb_bulk_write if (rv < 0) { fprintf(stderr,"Register write request failed (Send). [%d]\n",rv); sem_post(&iflock); return 0; } rv = usb_bulk_read(dev, 0x01, buffer, 64, 100); // usb_bulk_read if (rv <= 0) { fprintf(stderr,"Register write request failed (Receive). [%d]\n",rv); sem_post(&iflock); return 0; } sem_post(&iflock); return 1; } ////////////////////////// get_fpga_reg() /////////////////////////////////// int get_fpga_reg(unsigned char addr, unsigned char *v) { char buffer[64]; sem_wait(&iflock); memset(buffer,0x00,16); int rv; buffer[0] = 0x02; buffer[1] = addr; rv = usb_bulk_write(dev, 0x01, buffer, 64, 100); // usb_bulk_write if (rv < 0) { fprintf(stderr,"get_fpga_reg read request failed (Send). [%d]\n",rv); sem_post(&iflock); return 0; } rv = usb_bulk_read(dev, 0x01, buffer, 64, 100); // usb_bulk_read if (rv <= 0) { fprintf(stderr,"get_fpga_reg read request failed (Receive). [%d]\n",rv); sem_post(&iflock); return 0; } *v = buffer[1]; sem_post(&iflock); return 1; } ////////////////////////// get_fpga_stream() /////////////////////////////////////// ////////////////////////// returns 0 if fail, nread if success /////////////////////// ////////////////////////// nread has number of bytes read /////////////////////// int get_fpga_stream(char addr, char *data, int nbytes) { char buffer[64]; sem_wait(&iflock); memset(buffer,0x00,16); int rv; buffer[0] = 0x06; buffer[1] = addr; buffer[4] = (nbytes >> 8) & 0xFF; buffer[5] = nbytes & 0xFF; rv = usb_bulk_write(dev, 0x01, buffer, 64, 100); // usb_bulk_write if (rv < 0) { fprintf(stderr,"get_fpga_stream: Block read request failed (Send). [%d]\n",rv); sem_post(&iflock); return 0; } rv = usb_bulk_read(dev, 0x01, buffer, 64, 100); // usb_bulk_read if (rv <= 0) { fprintf(stderr,"get_fpga_stream: Block read request failed (Receive). [%d]\n",rv); sem_post(&iflock); return 0; } int nread = rv = usb_bulk_read(dev, 0x06, data, nbytes & 0xFFFF, 100); // usb_bulk_read if (rv < 0) { fprintf(stderr,"get_fpga_stream: Block read request failed (Send). [%d]\n",rv); sem_post(&iflock); return 0; } if (nread != nbytes) fprintf(stderr,"get_fpga_stream: Incomplete block read. [%d vs %d]\n",nread,nbytes); rv = usb_bulk_read(dev, 0x01, buffer, 64, 100); // usb_bulk_read if (rv <= 0) { fprintf(stderr,"get_fpga_stream: Block read request failed (Receive). [%d]\n",rv); sem_post(&iflock); return 0; } sem_post(&iflock); return nread; } //////////////////////////// send_spi_command() /////////////////////////////////// //////////////////////////// used in get and set 8201 functions /////////////////// static int send_spi_command(unsigned short cmd, unsigned short dat, unsigned short *resp) { unsigned char datlo; unsigned char dathi; if (!put_fpga_reg(REGADR_SPI_CMD_LO, (unsigned char) (cmd & 0xFF))) return 0; if (!put_fpga_reg(REGADR_SPI_CMD_HI, (unsigned char) ((cmd >> 8) & 0xFF))) return 0; if (!put_fpga_reg(REGADR_SPI_DAT_LO, (unsigned char) (dat & 0xFF))) return 0; if (!put_fpga_reg(REGADR_SPI_DAT_HI, (unsigned char) ((dat >> 8) & 0xFF))) return 0; if (!put_fpga_reg(REGADR_SPI_SEND, 0)) return 0; if (!get_fpga_reg(REGADR_SPI_DAT_LO, &datlo)) return 0; if (!get_fpga_reg(REGADR_SPI_DAT_HI, &dathi)) return 0; *resp = (((unsigned short) dathi) << 8) | datlo; return 1; } /////////////////////////// set_8201_register() /////////////////////////////////////// static int set_8201_register(int r, unsigned short v) { unsigned short resp; unsigned short cmd = (0x80 | (0x1F & r)) << 8; int rv = send_spi_command(cmd,v,&resp); if (rv != 1) { fprintf(stdout,"set_8201_register call failed.\n"); } return rv; } //////////////////////////// get_8201_register() /////////////////////////////////// //static int get_8201_register(int r, unsigned short *v) { // unsigned short cmd = (0x40 | (0x1F & r)) << 8; // int rv = send_spi_command(cmd,0x0000,v); // printf("%d\t%04X\n",r,*v); // if (rv != 1) { // fprintf(stdout,"get_8201_register call failed.\n"); // } // return rv; //} //////////////////////////// set_8201_memory() /////////////////////////////////// static int set_8201_memory(int mem, int maddr, unsigned short v) { int cmd = ((0xA0 | (0x3 & mem)) << 8) | (maddr & 0xFF); unsigned short resp; int rv = send_spi_command(cmd,v,&resp); return rv; } /////////////////////////// get_8201_memory() /////////////////////////////////// //static int get_8201_memory(int mem, int maddr, unsigned short *v) { // int cmd = ((0x60 | (0x3 & mem)) << 8) | (maddr & 0xFF); // int rv = send_spi_command(cmd,0x0000,v); // return rv; //} /////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////// MID-LEVEL FUNCTIONS /////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////// /////////////////////// reset_8201() resets the 8201 ////////////////////////// int reset_8201(void) { // Toggle the AFEDRI8201 reset flag (active low) unsigned char v; if (!get_fpga_reg(REGADR_CTRL,&v)) return 0; v = (v & ~CTRL_RESET) | (0 ? CTRL_RESET : 0 ); if (!put_fpga_reg(REGADR_CTRL,v)) return 0; v = (v & ~CTRL_RESET) | (1 ? CTRL_RESET : 0 ); if (!put_fpga_reg(REGADR_CTRL,v)) return 0; return 1; } ///////////////////// sync_8201() resyncs the 8201 //////////////////////////// int sync_8201(void) { // Toggle the AFEDRI8201 sync flag (active high) unsigned char v; if (!get_fpga_reg(REGADR_CTRL,&v)) return 0; v = (v & ~CTRL_SYNC) | (1 ? CTRL_SYNC : 0 ); if (!put_fpga_reg(REGADR_CTRL,v)) return 0; v = (v & ~CTRL_SYNC) | (0 ? CTRL_SYNC : 0 ); if (!put_fpga_reg(REGADR_CTRL,v)) return 0; return 1; } ///////////////////// reset_fifo_overrun() /////////////////////////////////////// int reset_fifo_overrun(void) { // Reset FPGA FIFO overflow flags unsigned char v; if (!get_fpga_reg(REGADR_CTRL,&v)) return 0; v = (v & ~CTRL_OVF_RST) | (1 ? CTRL_OVF_RST : 0 ); if (!put_fpga_reg(REGADR_CTRL,v)) return 0; v = (v & ~CTRL_OVF_RST) | (0 ? CTRL_OVF_RST : 0 ); if (!put_fpga_reg(REGADR_CTRL,v)) return 0; return 1; } ///////////////////// reset_fifo0() /////////////////////////////////////////// int reset_fifo0(void) { // Reset FPGA FIFO 0 (clear the FIFO) unsigned char v; if (!get_fpga_reg(REGADR_CTRL,&v)) return 0; v = (v & ~CTRL_FIFO0_RST) | (1 ? CTRL_FIFO0_RST : 0 ); if (!put_fpga_reg(REGADR_CTRL,v)) return 0; v = (v & ~CTRL_FIFO0_RST) | (0 ? CTRL_FIFO0_RST : 0 ); if (!put_fpga_reg(REGADR_CTRL,v)) return 0; return 1; } ////////////////////// reset_fifo1() /////////////////////////////////// ////////////////////// dont think this is necessary /////////////////////////// ////////////////////// LEAVE IN FOR NOW, part of init /////////////////////////// int reset_fifo1(void) { // Reset FPGA FIFO 1 (clear the FIFO) unsigned char v; if (!get_fpga_reg(REGADR_CTRL,&v)) return 0; v = (v & ~CTRL_FIFO1_RST) | (1 ? CTRL_FIFO1_RST : 0 ); if (!put_fpga_reg(REGADR_CTRL,v)) return 0; v = (v & ~CTRL_FIFO1_RST) | (0 ? CTRL_FIFO1_RST : 0 ); if (!put_fpga_reg(REGADR_CTRL,v)) return 0; return 1; } ////////////////////////// set_lna_range /////////////////////////////////// ////////////////////////// CALLED IN NexysSDR::start() /////////////////////////// static void set_lna_range(void) { unsigned char v; if (!get_fpga_reg(REGADR_CTRL,&v)) return; v = (v & ~CTRL_HILO) | (chas_rx1_lna_range ? CTRL_HILO : 0 ); put_fpga_reg(REGADR_CTRL,v); cur_lnarange = chas_rx1_lna_range; } /////////////////////////// set_lna_slope() /////////////////////////////////// /////////////////////////// CALLED IN NexysSDR::start() /////////////////////////// static void set_lna_slope(void) { unsigned char v; if (!get_fpga_reg(REGADR_CTRL,&v)) return; v = (v & ~CTRL_MODE) | (chas_rx1_lna_slope ? CTRL_MODE : 0 ); put_fpga_reg(REGADR_CTRL,v); cur_lnaslope = chas_rx1_lna_slope; } /////////////// set_lna_gain() (from NexysSDR.cc) /////////////////////////// ///////////////////// NOTE: need to redo this to remove min & max calls /////////// static void set_lna_gain(void) { if(chas_rx1_lna_gain > 4095) chas_rx1_lna_gain = 4095; set_8201_register(11,chas_rx1_lna_gain); cur_lnagain = chas_rx1_lna_gain; } //////////////// set_rf_path() (from NexysSDR.cc) /////////////////////////// static void set_rf_path(void) { unsigned char v; if (!get_fpga_reg(REGADR_CTRL,&v)) return; v = (v & ~CTRL_RFSW) | (chas_rx1_rf_path ? CTRL_RFSW : 0 ); put_fpga_reg(REGADR_CTRL,v); cur_path = chas_rx1_rf_path; } //////////////////////////// set_decimation_scale() /////////////////////////////////// //////////////////////////// CALLED IN set_decimation_rate() /////////////////////// int set_decimation_scale(int d) { int scale = 0; int shift = 0; int s, t; unsigned short v; double gain = 0, g; for (t=0;t<64;t++) { // go through the shift values (t) for (s=0;s<64;s++) { // go through the scale values (s) g = pow(d,5)*(((double) s)/32.0/(pow(2,t))); // calculate a gain value #if DEBUG printf("\ncalc Decim: %d, s: %d, t: %d, g: %f, GAIN: %f, abs(g*1000) %d, abs(gain*1000) %d\n", d, s, t, g, gain, abs((g-1.0)*1000), abs((gain-1.0)*1000)); #endif if ((g <= 1.0) && (abs((g-1.0)*1000) < abs((gain-1.0)*1000))) { // if we have a winner... gain = g; // set gain to interim result shift = t; // set shift to interim result scale = s; // set scale to interim result #if DEBUG printf("\nInterim Decim: %d, Scale: %d, Shift %d, Gain: %f\n", d, scale, shift, gain); #endif } } // try some more values of s (scale) } // try some more values of t (shift) #if DEBUG printf("\nFINAL Decim: %d, Scale: %d, Shift %d, Gain: %f\n", d, scale, shift, gain); #endif v = (scale << 6) | shift; if (!set_8201_register(6, v)) return 0; return 1; } //////////////////////////// set_decimation_rate() //////////////////////////////// //////////////////////////// sets BOTH decimation rate and scale //////////////////// //////////////////////////// valid decimation rates are: //////////////////// //////////////////////////// 1600,800,640,400,320,300,200,160 //////////////////// int set_decimation_rate(int d) { if (d%4 != 0) { fprintf(stderr,"Decimation must be divisible by 4.\n"); return 0; } if (!set_8201_register(5, abs(d)/4)) return 0; if (!set_decimation_scale(abs(d)/4)) return 0; cur_decimation = d; return 1; } ////////////////////////// set_freq_chas_rx1() /////////////////////////////////// static void set_freq_chas_rx1(void) { unsigned long fx; unsigned short fx0, fx1; fx = (long)((double)chas_rx1_freq/chas_rx1_sample_freq*pow(2.0, 32.0)); // calc FTW fx0 = (unsigned short) (fx & 0xFFFF); // setup bits 15-0 fx1 = (unsigned short) ((fx >> 16) & 0xFFFF); // setup bits 31-16 set_8201_register(1, fx0); // send the low bits set_8201_register(2, fx1); // and the high bits cur_freq = chas_rx1_freq; // update current freq storage } ////////////////////////// set_gain_chas_rx1 /////////////////////////////////// // gain button index 0 = PGA gain 1.00, 0 dB, 8201 code = 000 // gain button index 1 = PGA gain 1.14, 1 dB, 8201 code = 100 // gain button index 2 = PGA gain 1.33, 2.5 dB, 8201 code = 010 // gain button index 3 = PGA gain 1.60, 4 dB, 8201 code = 110 // gain button index 4 = PGA gain 2.00, 6 dB, 8201 code = 001 // gain button index 5 = PGA gain 2.67, 8.5 dB, 8201 code = 101 // gain button index 6 = PGA gain 4.00, 12dB, 8201 code = 011 static void set_gain_chas_rx1(void) { int code = 0; // preset code variable to 0 switch (chas_rx1_gain) { case 0: // button index 0, gain is 0 code = 0x00; break; // 8201 code is 000 (binary) case 1: // button index 1, gain is 1dB code = 0x10; break; // 8201 code is 100 (shifted two bits) case 2: // button index 2, gain is 2.5dB code = 0x08; break; // 8201 code is 010 case 3: // button index 3, gain is 4dB code = 0x18; break; // 8201 code is 110 case 4: // button index 4, gain is 6dB code = 0x04; break; // 8201 code is 001 case 5: // button index 5, gain is 8.5dB code = 0x14; break; // 8201 code is 101 case 6: // button index 6, gain is 12dB code = 0x0C; break; // 8201 code is 011 default: // shold not get here, but... code = 0x00; break; // set gain to 0 anyway } code = (code & 0x001C) | chas_rx1_pwd; // add power-down flag to gain "code" set_8201_register(12,code); // send it off to the board cur_gain = chas_rx1_gain; // update current gain storage } //////////////////////// NOTE: WAS A SEPARATE READ THREAD, /////////// //////////////////////// fiforead() CALLS get_fpga_stream() /////////// //////////////////////// USED IN: NexysSDR::start() to create read thread /////////// void fiforead(void) { short v[NEXSDR_BLOCK_SIZE*2]; int i; int fifobytes; //#if DEBUG // printf("Separate fiforead thread started\n"); // printf("fiforead() running: %d\n", running); //#endif if(running) { fifobytes = get_fpga_stream(REGADR_FIFO0,( char *) v, NEXSDR_BLOCK_SIZE*sizeof(short)*2); // read block from FGPA // printf("Fiforead: Get_fpga = %d, next_buffer = %d\n", fifobytes, next_buffer); if(fifobytes) { if (buffer_full[next_buffer] == 0) // If space is available, store data { for (i=0;inext) { for (udev = bus->devices; udev; udev = udev->next) { if ((udev->descriptor.idVendor == DIGILENT_VENDOR_ID) && (udev->descriptor.idProduct == DIGILENT_PRODUCT_ID)) { digidev = udev; #if DEBUG printf("Found Digilent Board\n"); #endif } } } dev = NULL; if (digidev != 0) { dev = usb_open(digidev); // FINALLY, open the USB device if (dev != NULL) { #if DEBUG printf("USB Open Success\n"); #endif usleep(1000); sem_wait(&iflock); memset(buffer,0x00,16); rv = usb_control_msg(dev, 0xC0, 0xE4, 0x00, 0x00, buffer, 13, 100); if (rv < 0) { fprintf(stderr,"Vendor request failed (First Request). [%d]\n",rv); sem_post(&iflock); rq1 = 1; } #if DEBUG else fprintf(stderr,"Vendor request SUCCESS (First Request). [%d]\n",rv); #endif rv = usb_control_msg(dev, 0xC0, 0xE6, 0x00, 0x00, buffer, 4, 100); if (rv < 0) { fprintf(stderr,"Vendor request failed (Second Request). [%d]\n",rv); sem_post(&iflock); rq2 = 1; } #if DEBUG else fprintf(stderr,"Vendor request SUCCESS (Second Request). [%d]\n",rv); #endif sem_post(&iflock); if(rq1 || rq2) return 0; else return 1; } else { fprintf(stderr,"Vendor Open failed.\n"); return 0; } } #if DEBUG fprintf(stderr,"Digilent Open failed.\n"); #endif return 0; } //////////////////////// init_chas_rx1() /////////////////////////////////////////// //////////////////////// actually, initializes hardware /////////////////////////// //////////////////////// returns 0 if error, 1 if OK /////////////////////////// int init_chas_rx1(void) { int i, j; #if DEBUG printf("Init Chas Rx1: starting USB\n"); #endif int rv = open_USB(); // Open connection to the board if (!rv) { printf("\nInit Chas Rx1: Chas Init failed!"); return 0; } usleep(1000); #if DEBUG printf("Init Chas Rx1: Initializing Charleston Receiver\n"); #endif // Set control lines sync_8201(); sync_8201(); sync_8201(); set_rf_path(); // not in 8201, go ahead and set up RF path HW set_lna_slope(); // not in 8201, go ahead and set up LNA slope HW set_lna_range(); // not in 8201, go ahead and set up LNA range HW reset_8201(); // Issue reset to AFEDRI8201 // Init the AFEDRI8201 set_8201_register(0, 0x0003); // MCLK DIV=0, Data Out MODE=1 set_8201_register(1, 0x6666); // NCO FREQUENCY bits 15-0 set_8201_register(2, 0x0266); // NCO FREQUENCY bits 31-16 set_8201_register(3, 0x0000); // NCO PHASE bits 15-0 set_8201_register(4, 0x0000); // NCO PHASE bits 31-16 // set_8201_register(5, 0x0028); // CIC DECIMATION_RATE=160/4=40 for 76.8 clock, 480k // set_8201_register(5, 0x0032); // CIC DECIMATION_RATE=200/4=50 for 76.8 clock, 384k // set_8201_register(5, 0x004B); // CIC DECIMATION_RATE=300/4=75 for 76.8 clock, 256k // set_8201_register(5, 0x0050); // CIC DECIMATION_RATE=320/4=80 for 76.8 clock, 240k set_8201_register(5, 0x0064); // CIC DECIMATION_RATE=400/4=100 for 76.8 clock, 192k // set_8201_register(5, 0x00A0); // CIC DECIMATION_RATE=640/4=160 for 76.8 clock, 120k // set_8201_register(5, 0x00C8); // CIC DECIMATION_RATE=800/4=200 for 76.8 clock, 96k set_8201_register(6, 0x0819); // SCALE=21, SHIFT=26 set_8201_register(7, 0x0080); // BASE_ADDR=0, NCOEFF=32, FIR1 MODE=0 set_8201_register(8, 0x00FC); // BASE_ADDR=0, NCOEFF=63, FIR2A MODE=0 set_8201_register(9, 0x00FC); // BASE_ADDR=0, NCOEFF=63, FIR2B MODE=0 set_8201_register(10,0x0000); // Set FIR1 and FIR2 into non-interleaved mode set_8201_register(11,0x0000); // AUXILIARY DAC=0x000 set_8201_register(12,0x000C); // PGA GAIN=12dB, PWD=0 set_8201_register(0, 0x0003); // MCLK DIV=0, Data Out MODE=1 set_decimation_rate(400); // Set FIR filter coefficient memory for (j=0;j<32;j++) set_8201_memory(0,j,fir1[j]); for (j=0;j<63;j++) set_8201_memory(1,j,fir2[j]); for (j=0;j<63;j++) set_8201_memory(2,j,fir2[j]); // Clear the FIFOs reset_fifo0(); reset_fifo1(); reset_fifo_overrun(); set_lna_gain(); chas_rx1_gain = 6; set_gain_chas_rx1(); // Clear buffer full flags for (i=0;i self.ptime: # time to print results self.time0 = 0 # end data recording, wait for reset self.time_print = tm if self.heading: self.heading = 0 print "count, msg, avg, max (msec)" print "%4d" % count, for msg in self.names: # keep names in order count, average, highest = self.timers[msg] if not count: continue average /= count print " %s %7.3f %7.3f" % (msg, average * 1e3, highest * 1e3), self.timers[msg] = (0, 0.0, 0.0) print else: # reset the time to zero self.time0 = tm # Start timer if not self.time_print: self.time_print = tm ## T = Timer() # Make a timer instance class SoundThread(threading.Thread): """Create a second (non-GUI) thread to read, process and play sound.""" def __init__(self): self.do_init = 1 threading.Thread.__init__(self) self.doQuit = threading.Event() self.doQuit.clear() def run(self): """Read, process, play sound; then notify the GUI thread to check for FFT data.""" if self.do_init: # Open sound using this thread self.do_init = 0 QS.start_sound() wx.CallAfter(application.PostStartup) while not self.doQuit.isSet(): QS.read_sound() wx.CallAfter(application.OnReadSound) QS.close_sound() def stop(self): """Set a flag to indicate that the sound thread should end.""" self.doQuit.set() class FrequencyDisplay(wx.lib.stattext.GenStaticText): """Create a frequency display widget.""" def __init__(self, frame, gbs, width, height): wx.lib.stattext.GenStaticText.__init__(self, frame, -1, '3', style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE|wx.RAISED_BORDER) border = 4 for points in range(30, 6, -1): font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(font) w, h = self.GetTextExtent('333 444 555 Hz') if w < width and h < height - border * 2: break self.SetSizeHints(w, h, w * 5, h) self.height = h self.points = points border = self.border = (height - self.height) / 2 self.height_and_border = h + border * 2 self.SetBackgroundColour(conf.color_freq) gbs.Add(self, (0, 0), (1, 3), flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=border) def Clip(self, clip): """Change color to indicate clipping.""" if clip: self.SetBackgroundColour('deep pink') else: self.SetBackgroundColour(conf.color_freq) def Display(self, freq): """Set the frequency to be displayed.""" freq = int(freq) if freq >= 0: t = str(freq) minus = '' else: t = str(-freq) minus = '- ' l = len(t) if l > 9: txt = "%s%s %s %s %s" % (minus, t[0:-9], t[-9:-6], t[-6:-3], t[-3:]) elif l > 6: txt = "%s%s %s %s" % (minus, t[0:-6], t[-6:-3], t[-3:]) elif l > 3: txt = "%s%s %s" % (minus, t[0:-3], t[-3:]) else: txt = minus + t self.SetLabel('%s Hz' % txt) class SliderBoxV(wx.BoxSizer): """A vertical box containing a slider and a text heading""" # Note: A vertical wx slider has the max value at the bottom. This is # reversed for this control. def __init__(self, parent, text, init, themax, handler, display=False): wx.BoxSizer.__init__(self, wx.VERTICAL) self.slider = wx.Slider(parent, -1, init, 0, themax, style=wx.SL_VERTICAL) self.slider.Bind(wx.EVT_SCROLL, handler) sw, sh = self.slider.GetSize() self.text = text self.themax = themax if display: # Display the slider value when it is thumb'd self.text_ctrl = wx.StaticText(parent, -1, str(themax), style=wx.ALIGN_CENTER) w1, h1 = self.text_ctrl.GetSize() # Measure size with max number self.text_ctrl.SetLabel(text) w2, h2 = self.text_ctrl.GetSize() # Measure size with text self.width = max(w1, w2, sw) self.text_ctrl.SetSizeHints(self.width, -1, self.width) self.slider.Bind(wx.EVT_SCROLL_THUMBTRACK, self.Change) self.slider.Bind(wx.EVT_SCROLL_THUMBRELEASE, self.ChangeDone) else: self.text_ctrl = wx.StaticText(parent, -1, text) w2, h2 = self.text_ctrl.GetSize() # Measure size with text self.width = max(w2, sw) self.Add(self.text_ctrl, 0, wx.ALIGN_CENTER) self.Add(self.slider, 1, wx.ALIGN_CENTER) def Change(self, event): event.Skip() self.text_ctrl.SetLabel(str(self.themax - self.slider.GetValue())) def ChangeDone(self, event): event.Skip() self.text_ctrl.SetLabel(self.text) def GetValue(self): return self.themax - self.slider.GetValue() def SetValue(self, value): # Set slider visual position; does not call handler self.slider.SetValue(self.themax - value) # Start of our button classes. They are compatible with wxPython GenButton # buttons. Use the usual methods for access: # GetLabel(self), SetLabel(self, label): Get and set the label # Enable(self, flag), Disable(self), IsEnabled(self): Enable / Disable # GetValue(self), SetValue(self, value): Get / Set check button state True / False # SetIndex(self, index): For cycle buttons, set the label from its index class QuiskButtons: """Base class for special buttons.""" button_bezel = 3 # size of button bezel in pixels def InitButtons(self, text): self.SetBezelWidth(self.button_bezel) self.SetBackgroundColour(conf.color_btn) self.SetUseFocusIndicator(False) self.font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) if text: w, h = self.GetTextExtent(text) else: w, h = self.GetTextExtent("OK") self.Disable() # create a size for null text, but Disable() w += self.button_bezel * 2 + self.GetCharWidth() h = h * 12 / 10 h += self.button_bezel * 2 self.SetSizeHints(w, h, w * 6, h, 1, 1) def OnKeyDown(self, event): pass def OnKeyUp(self, event): pass class QuiskPushbutton(QuiskButtons, wx.lib.buttons.GenButton): """A plain push button widget.""" def __init__(self, parent, command, text, use_right=False): wx.lib.buttons.GenButton.__init__(self, parent, -1, text) self.command = command self.Bind(wx.EVT_BUTTON, self.OnButton) self.InitButtons(text) self.direction = 1 if use_right: self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) def OnButton(self, event): if self.command: self.command(event) def OnRightDown(self, event): self.direction = -1 self.OnLeftDown(event) def OnRightUp(self, event): self.OnLeftUp(event) self.direction = 1 class QuiskRepeatbutton(QuiskButtons, wx.lib.buttons.GenButton): """A push button that repeats when held down.""" def __init__(self, parent, command, text, up_command=None, use_right=False): wx.lib.buttons.GenButton.__init__(self, parent, -1, text) self.command = command self.up_command = up_command self.timer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.OnTimer) self.Bind(wx.EVT_BUTTON, self.OnButton) self.InitButtons(text) self.repeat_state = 0 # repeater button inactive self.direction = 1 if use_right: self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) def SendCommand(self, command): if command: event = wx.PyEvent() event.SetEventObject(self) command(event) def OnLeftDown(self, event): if self.IsEnabled(): self.shift = event.ShiftDown() self.control = event.ControlDown() self.SendCommand(self.command) self.repeat_state = 1 # first button push self.timer.Start(milliseconds=300, oneShot=True) wx.lib.buttons.GenButton.OnLeftDown(self, event) def OnLeftUp(self, event): if self.IsEnabled(): self.SendCommand(self.up_command) self.repeat_state = 0 self.timer.Stop() wx.lib.buttons.GenButton.OnLeftUp(self, event) def OnRightDown(self, event): if self.IsEnabled(): self.shift = event.ShiftDown() self.control = event.ControlDown() self.direction = -1 self.OnLeftDown(event) def OnRightUp(self, event): if self.IsEnabled(): self.OnLeftUp(event) self.direction = 1 def OnTimer(self, event): if self.repeat_state == 1: # after first push, turn on repeats self.timer.Start(milliseconds=150, oneShot=False) self.repeat_state = 2 if self.repeat_state: # send commands until button is released self.SendCommand(self.command) def OnButton(self, event): pass # button command not used class QuiskCheckbutton(QuiskButtons, wx.lib.buttons.GenToggleButton): """A button that pops up and down, and changes color with each push.""" # Check button; get the checked state with self.GetValue() def __init__(self, parent, command, text, color=None): wx.lib.buttons.GenToggleButton.__init__(self, parent, -1, text) self.InitButtons(text) self.Bind(wx.EVT_BUTTON, self.OnButton) self.button_down = 0 # used for radio buttons self.command = command if color is None: self.color = conf.color_check_btn else: self.color = color def SetValue(self, value, do_cmd=False): wx.lib.buttons.GenToggleButton.SetValue(self, value) self.button_down = value if value: self.SetBackgroundColour(self.color) else: self.SetBackgroundColour(conf.color_btn) if do_cmd and self.command: event = wx.PyEvent() event.SetEventObject(self) self.command(event) def OnButton(self, event): if self.GetValue(): self.SetBackgroundColour(self.color) else: self.SetBackgroundColour(conf.color_btn) if self.command: self.command(event) class QuiskCycleCheckbutton(QuiskCheckbutton): """A button that cycles through its labels with each push. The button is up for labels[0], down for all other labels. Change to the next label for each push. If you call SetLabel(), the label must be in the list. The self.index is the index of the current label. """ def __init__(self, parent, command, labels, color=None, is_radio=False): self.labels = list(labels) # Be careful if you change this list self.index = 0 # index of selected label 0, 1, ... self.direction = 0 # 1 for up, -1 for down, 0 for no change to index self.is_radio = is_radio # Is this a radio cycle button? if color is None: color = conf.color_cycle_btn QuiskCheckbutton.__init__(self, parent, command, labels[0], color) self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) def SetLabel(self, label, do_cmd=False): self.index = self.labels.index(label) QuiskCheckbutton.SetLabel(self, label) QuiskCheckbutton.SetValue(self, self.index) if do_cmd and self.command: event = wx.PyEvent() event.SetEventObject(self) self.command(event) def SetIndex(self, index, do_cmd=False): self.index = index QuiskCheckbutton.SetLabel(self, self.labels[index]) QuiskCheckbutton.SetValue(self, index) if do_cmd and self.command: event = wx.PyEvent() event.SetEventObject(self) self.command(event) def OnButton(self, event): if not self.is_radio or self.button_down: self.direction = 1 self.index += 1 if self.index >= len(self.labels): self.index = 0 self.SetIndex(self.index) else: self.direction = 0 if self.command: self.command(event) def OnRightDown(self, event): # Move left in the list of labels if not self.is_radio or self.GetValue(): self.index -= 1 if self.index < 0: self.index = len(self.labels) - 1 self.SetIndex(self.index) self.direction = -1 if self.command: self.command(event) class RadioButtonGroup: """This class encapsulates a group of radio buttons. This class is not a button! The "labels" is a list of labels for the toggle buttons. An item of labels can be a list/tuple, and the corresponding button will be a cycle button. """ def __init__(self, parent, command, labels, default): self.command = command self.buttons = [] self.button = None for text in labels: if type(text) in (ListType, TupleType): b = QuiskCycleCheckbutton(parent, self.OnButton, text, is_radio=True) for t in text: if t == default and self.button is None: b.SetLabel(t) self.button = b else: b = QuiskCheckbutton(parent, self.OnButton, text) if text == default and self.button is None: b.SetValue(True) self.button = b self.buttons.append(b) def SetLabel(self, label, do_cmd=False): self.button = None for b in self.buttons: if self.button is not None: b.SetValue(False) elif isinstance(b, QuiskCycleCheckbutton): try: index = b.labels.index(label) except ValueError: b.SetValue(False) continue else: b.SetIndex(index) self.button = b b.SetValue(True) elif b.GetLabel() == label: b.SetValue(True) self.button = b else: b.SetValue(False) if do_cmd and self.command and self.button: event = wx.PyEvent() event.SetEventObject(self.button) self.command(event) def GetButtons(self): return self.buttons def OnButton(self, event): win = event.GetEventObject() for b in self.buttons: if b is win: self.button = b b.SetValue(True) else: b.SetValue(False) if self.command: self.command(event) def GetLabel(self): if not self.button: return None return self.button.GetLabel() def GetSelectedButton(self): # return the selected button return self.button class ConfigScreen(wx.ScrolledWindow): """Display the configuration and status screen.""" def __init__(self, parent, width, fft_size): wx.ScrolledWindow.__init__(self, parent, pos = (0, 0), size = (width, 100), style = wx.VSCROLL | wx.NO_BORDER) self.SetBackgroundColour(conf.color_graph) self.Bind(wx.EVT_PAINT, self.OnPaint) self.width = width self.setscroll = True self.fft_size = fft_size self.interupts = 0 self.read_error = -1 self.write_error = -1 self.underrun_error = -1 self.fft_error = -1 self.latencyCapt = -1 self.latencyPlay = -1 self.y_scale = 0 self.y_zero = 0 self.rate_min = -1 self.rate_max = -1 self.chan_min = -1 self.chan_max = -1 self.mic_max_display = 0 self.w_phase = None self.err_msg = "No response" self.msg1 = "" self.tabstops = [1] self.tabstops.append(self.tabstops[-1] + 18) self.tabstops.append(self.tabstops[-1] + 2) self.tabstops.append(self.tabstops[-1] + 20) self.tabstops.append(self.tabstops[-1] + 2) self.tabstops.append(self.tabstops[-1] + 17) self.tabstops.append(self.tabstops[-1] + 2) self.tabstops.append(self.tabstops[-1] + 17) self.tabstops.append(self.tabstops[-1] + 2) points = 24 while points > 4: self.font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) charx = self.charx = self.GetCharWidth() chary = self.chary = self.GetCharHeight() if self.tabstops[-1] * charx < width: break points -= 2 for i in range(len(self.tabstops)): self.tabstops[i] *= charx self.dy = chary # line spacing def OnPaint(self, event): dc = wx.PaintDC(self) dc.SetFont(self.font) dc.SetTextForeground('Black') x0 = self.tabstops[0] x, y = self.GetViewStart() self.y = -y + self.dy # blank line at top self.row = 1 p = conf.name_of_sound_play if p: p = "Output to " + p else: p = "Output to (None)" self.MakeRow(dc, 'Interrupts', self.interupts, p, None, None, None, 'Play rate', conf.playback_rate) self.MakeRow(dc, 'Minimum rate', self.rate_min, 'Maximum rate', self.rate_max, 'Min channels', self.chan_min, 'Max channels', self.chan_max) self.MakeRow(dc, 'Capture errors', self.read_error, 'Playback errors', self.write_error, 'Underrun errors', self.underrun_error, 'FFT errors', self.fft_error) self.MakeRow(dc, 'Capture latency', self.latencyCapt, 'Playback latency', self.latencyPlay, 'Total latency', self.latencyCapt + self.latencyPlay, 'FFT points', self.fft_size) self.y += self.dy * 5 / 10 # extra half line if self.err_msg: # Error message dc.SetTextForeground('Red') dc.DrawText(self.err_msg, x0, self.y) # fill='#F00') dc.SetTextForeground('Black') self.y += self.dy if self.msg1: dc.DrawText(self.msg1, x0, self.y) self.y += self.dy t = "Capture rate %d %s" % (application.sample_rate, application.config_text) if application.sound_error: dc.SetTextForeground('Red') dc.DrawText(t, x0, self.y) dc.SetTextForeground('Black') else: dc.DrawText(t, x0, self.y) self.y += self.dy if conf.config_file_exists: t = "Using configuration file %s" % conf.config_file_path else: t = "Configuration file %s was not found" % conf.config_file_path dc.DrawText(t, x0, self.y) self.y += self.dy name = conf.microphone_name if name: if self.mic_max_display > -0.1: t = "Microphone %s maximum level %3.0f db CLIP" % (name, self.mic_max_display) dc.SetTextForeground('Red') dc.DrawText(t, x0, self.y) dc.SetTextForeground('Black') else: t = "Microphone %s maximum level %3.0f db" % (name, self.mic_max_display) dc.DrawText(t, x0, self.y) else: t = "The microphone is not used (null name)." dc.DrawText(t, x0, self.y) self.height = self.y + 2 * self.dy if self.setscroll: self.setscroll = False sp = self.chary # Make controls # Button for phase adjust dialog t = wx.StaticText(self, -1, "Sound card phase", pos=(x0, self.height)) x, y = t.GetSizeTuple() self.phase = wx.Button(self, -1, "Adjust...") self.Bind(wx.EVT_BUTTON, self.OnBtnPhase, self.phase) x1, y1 = self.phase.GetSizeTuple() yoff = (y1 - y) / 2 self.phase.SetPosition((x0 + x + sp, self.height - yoff)) # Choice (combo) box for decimation lst = Hardware.VarDecimGetChoices() if lst: txt = Hardware.VarDecimGetLabel() x2 = self.width / 2 t = wx.StaticText(self, -1, txt, pos=(x2, self.height)) x, y = t.GetSizeTuple() c = wx.Choice(self, -1, pos=(x2 + x + sp, self.height - yoff), choices=lst) self.Bind(wx.EVT_CHOICE, application.OnBtnDecimation, c) index = Hardware.VarDecimGetIndex() c.SetSelection(index) self.height += y1 # The height is now known; set scroll size self.SetScrollbars(1, 1, self.width, self.height + 2 * self.dy) def MakeRow(self, dc, *args): y = self.y for col in range(len(args)): x = self.tabstops[col] t = args[col] if t is not None: t = str(t) if col % 2 == 1: w, h = dc.GetTextExtent(t) x -= w dc.DrawText(t, x, y) self.row += 1 self.y += self.dy def OnGraphData(self, data=None): (self.rate_min, self.rate_max, sample_rate, self.chan_min, self.chan_max, self.msg1, self.unused, self.err_msg, self.read_error, self.write_error, self.underrun_error, self.latencyCapt, self.latencyPlay, self.interupts, self.fft_error, self.mic_max_display, self.data_poll_usec ) = QS.get_state() self.mic_max_display = 20.0 * math.log10((self.mic_max_display + 1) / 32767.0) self.Refresh() def ChangeYscale(self, y_scale): pass def ChangeYzero(self, y_zero): pass def OnIdle(self, event): pass def SetTxFreq(self, freq): pass def OnBtnPhase(self, event): application.screenBtnGroup.SetLabel('Graph', do_cmd=True) if self.w_phase: self.w_phase.Raise() else: self.w_phase = QAdjustPhase(self, self.width) def OnPhaseClose(self, event): self.w_phase.Destroy() self.w_phase = None class GraphDisplay(wx.Window): """Display the FFT graph within the graph screen.""" def __init__(self, parent, x, y, graph_width, height, chary): wx.Window.__init__(self, parent, pos = (x, y), size = (graph_width, height), style = wx.NO_BORDER) self.parent = parent self.chary = chary self.graph_width = graph_width self.line = [(0, 0), (1,1)] # initial fake graph data self.SetBackgroundColour(conf.color_graph) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown) self.Bind(wx.EVT_RIGHT_DOWN, parent.OnRightDown) self.Bind(wx.EVT_LEFT_UP, parent.OnLeftUp) self.Bind(wx.EVT_MOTION, parent.OnMotion) self.Bind(wx.EVT_MOUSEWHEEL, parent.OnWheel) self.tune_x = graph_width / 2 self.scale = 20 # pixels per 10 dB self.height = 10 self.y_min = 1000 self.y_max = 0 self.max_height = application.screen_height self.tuningPen = wx.Pen('Red', 1) self.fltrPen = wx.Pen(conf.color_bandwidth, 1) # WB4JFI ADD filter shadow self.fltrBrush = wx.Brush(conf.color_bandwidth, wx.SOLID) # WB4JFI ADD filter shadow self.fltr_disp_start = -100 # WB4JFI ADD filter shadow self.fltr_disp_size = 100 # WB4JFI ADD filter shadow self.fltr_disp_fill = -99 # WB4JFI ADD filter shadow self.fltr_disp_show = 0 # WB4JFI ADD filter shadow self.backgroundPen = wx.Pen(self.GetBackgroundColour(), 1) self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID) def OnPaint(self, event): #print 'GraphDisplay', self.GetUpdateRegion().GetBox() dc = wx.PaintDC(self) x = self.tune_x # setx x to tune freq horizontal position self.UpdateFilterDisplay() # WB4JFI ADD filter shadow if self.fltr_disp_show == 3: # WB4JFI ADD filter shadow dc.SetPen(self.fltrPen) # WB4JFI ADD filter shadow dc.SetBrush(self.fltrBrush) # WB4JFI ADD filter shadow dc.DrawRectangle(x + self.fltr_disp_start, 0, # WB4JFI ADD filter shadow self.fltr_disp_size, self.max_height) # WB4JFI ADD filter shadow dc.FloodFill(x + self.fltr_disp_fill, 1, conf.color_bandwidth, 1) # WB4JFI ADD filter shadow dc.SetPen(wx.BLACK_PEN) dc.DrawLines(self.line) # x = self.tune_x # WB4JFI moved up for filter shadow dc.SetPen(self.tuningPen) dc.SetPen(wx.BLACK_PEN) dc.DrawLine(x, 0, x, self.max_height) if not self.parent.in_splitter: dc.SetPen(self.horizPen) chary = self.chary y = self.zeroDB for i in range(0, -99999, -10): if y >= chary / 2: dc.DrawLine(0, y, self.graph_width, y) # y line y = y + self.scale if y > self.height: break def SetHeight(self, height): self.height = height self.SetSize((self.graph_width, height)) def OnGraphData(self, data): line = [] x = 0 y_min = 1000 y_max = 0 for y in data: # y is in dB, -130 to 0 y = self.zeroDB - int(y * self.scale / 10.0 + 0.5) if y > y_max: y_max = y if y < y_min: y_min = y line.append((x, y)) x = x + 1 ymax = max(y_max, self.y_max) ymin = min(y_min, self.y_min) rect = wx.Rect(0, ymin, 1000, ymax - ymin) self.y_min = y_min self.y_max = y_max self.line = line self.Refresh() #rect=rect) def UpdateFilterDisplay(self): # WB4JFI ADD - Update filter display if application.fltr_display: # WB4JFI, check if OK to display filter self.fltr_disp_show = self.fltr_disp_show | 1 # set bit 0, show OK from OUTSIDE else: # otherwise, OUTSIDE don't display self.fltr_disp_show = self.fltr_disp_show & 2 # by clearing bit 0 if application.screen == application.graph or application.screen == application.waterfall: self.fltr_disp_show = self.fltr_disp_show | 2 # bit 1 shows proper screen to display else: self.fltr_disp_show = self.fltr_disp_show & 1 # clear bit 1 to indicate not proper display self.filter_pixels = (application.filterbw / (application.sample_rate /application.data_width) + 0.5) if self.filter_pixels < 2: # if calculated less than 2 pixels self.filter_pixels = 2 # make it at least 2 pixels wide if application.mode == 'LSB' or application.mode == 'CWL': # if filter on lower sideband... self.fltr_disp_start = -(self.filter_pixels - 1) # set start to be at lower side self.fltr_disp_size = self.filter_pixels # and set rectangle size self.fltr_disp_fill = self.fltr_disp_start # start filling at rectangle start if application.mode == 'USB' or application.mode == 'CWU': # if filter on upper sideband... self.fltr_disp_start = 0 # start is at tuning freq self.fltr_disp_size = self.filter_pixels # set rectangle size self.fltr_disp_fill = 1 # start filling at beginning of tune if application.mode == 'AM' or application.mode == 'FM': # if filter is centered at tune freq self.fltr_disp_start = -((self.filter_pixels - 1) /2) # set start at 1/2 bandidth self.fltr_disp_size = self.filter_pixels # set size to cover both sidebands self.fltr_disp_fill = self.filter_pixels # start filling at lower edge # # WB4JFI - end of filter add def SetTuningLine(self, x): dc = wx.ClientDC(self) dc.SetPen(self.backgroundPen) dc.DrawLine(self.tune_x, 0, self.tune_x, self.max_height) dc.SetPen(self.tuningPen) dc.DrawLine(x, 0, x, self.max_height) self.tune_x = x class GraphScreen(wx.Window): """Display the graph screen X and Y axis, and create a graph display.""" def __init__(self, parent, data_width, graph_width, in_splitter=0): wx.Window.__init__(self, parent, pos = (0, 0)) self.in_splitter = in_splitter # Are we in the top of a splitter window? if in_splitter: self.y_scale = conf.waterfall_graph_y_scale self.y_zero = conf.waterfall_graph_y_zero else: self.y_scale = conf.graph_y_scale self.y_zero = conf.graph_y_zero self.VFO = 0 self.WheelMod = 50 # Round frequency when using mouse wheel self.txFreq = 0 self.sample_rate = application.sample_rate self.data_width = data_width self.graph_width = graph_width self.doResize = False self.pen_tick = wx.Pen("Black", 1, wx.SOLID) self.font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) w = self.GetCharWidth() * 14 / 10 h = self.GetCharHeight() self.charx = w self.chary = h self.tick = max(2, h * 3 / 10) self.originX = w * 5 self.offsetY = h + self.tick self.width = self.originX + self.graph_width + self.tick + self.charx * 2 self.height = application.screen_height * 3 / 10 self.x0 = self.originX + self.graph_width / 2 # center of graph self.tuningX = self.x0 self.originY = 10 self.zeroDB = 10 # y location of zero dB; may be above the top of the graph self.scale = 10 self.SetSize((self.width, self.height)) self.SetSizeHints(self.width, 1, self.width) self.SetBackgroundColour(conf.color_graph) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_MOTION, self.OnMotion) self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) self.MakeDisplay() def MakeDisplay(self): self.display = GraphDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) self.display.zeroDB = self.zeroDB def OnPaint(self, event): dc = wx.PaintDC(self) if not self.in_splitter: dc.SetFont(self.font) self.MakeYTicks(dc) self.MakeXTicks(dc) def OnIdle(self, event): if self.doResize: self.ResizeGraph() def OnSize(self, event): self.doResize = True event.Skip() def ResizeGraph(self): """Change the height of the graph. Changing the width interactively is not allowed because the FFT size is fixed. Call after changing the zero or scale to recalculate the X and Y axis marks. """ w, h = self.GetClientSize() if self.in_splitter: # Splitter window has no X axis scale self.height = h self.originY = h else: self.height = h - self.chary # Leave space for X scale self.originY = self.height - self.offsetY self.MakeYScale() self.display.SetHeight(self.originY) self.display.scale = self.scale self.doResize = False self.Refresh() def ChangeYscale(self, y_scale): self.y_scale = y_scale self.doResize = True def ChangeYzero(self, y_zero): self.y_zero = y_zero self.doResize = True def MakeYScale(self): chary = self.chary scale = (self.originY - chary) * 10 / (self.y_scale + 20) # Number of pixels per 10 dB scale = max(1, scale) q = (self.originY - chary ) / scale / 2 zeroDB = chary + q * scale - self.y_zero * scale / 10 if zeroDB > chary: zeroDB = chary self.scale = scale self.zeroDB = zeroDB self.display.zeroDB = self.zeroDB QS.record_graph(self.originX, self.zeroDB, self.scale) def MakeYTicks(self, dc): chary = self.chary x1 = self.originX - self.tick * 3 # left of tick mark x2 = self.originX - 1 # x location of y axis x3 = self.originX + self.graph_width # end of graph data dc.SetPen(self.pen_tick) dc.DrawLine(x2, 0, x2, self.originY + 1) # y axis y = self.zeroDB for i in range(0, -99999, -10): if y >= chary / 2: dc.SetPen(self.pen_tick) dc.DrawLine(x1, y, x2, y) # y tick t = `i` w, h = dc.GetTextExtent(t) dc.DrawText(`i`, x1 - w, y - h / 2) # y text y = y + self.scale if y > self.originY: break def MakeXTicks(self, dc): originY = self.originY x3 = self.originX + self.graph_width # end of fft data charx , z = dc.GetTextExtent('-30000XX') tick0 = self.tick tick1 = tick0 * 2 tick2 = tick0 * 3 # Draw the X axis dc.SetPen(self.pen_tick) dc.DrawLine(self.originX, originY, x3, originY) # Draw the band plan colors below the X axis x = self.originX f = float(x - self.x0) * self.sample_rate / self.data_width c = None y = originY + 1 for freq, color in conf.BandPlan: freq -= self.VFO if f < freq: xend = int(self.x0 + float(freq) * self.data_width / self.sample_rate + 0.5) if c is not None: dc.SetPen(wx.TRANSPARENT_PEN) dc.SetBrush(wx.Brush(c)) dc.DrawRectangle(x, y, min(x3, xend) - x, tick0) # x axis if xend >= x3: break x = xend f = freq c = color stick = 1000 # small tick in Hertz mtick = 5000 # medium tick ltick = 10000 # large tick # check the width of the frequency label versus frequency span df = charx * self.sample_rate / self.data_width if df < 5000: tfreq = 5000 # tick frequency for labels elif df < 10000: tfreq = 10000 elif df < 20000: tfreq = 20000 elif df < 50000: tfreq = 50000 stick = 5000 mtick = 10000 ltick = 50000 else: tfreq = 100000 stick = 5000 mtick = 10000 ltick = 50000 # Draw the X axis ticks and frequency in kHz dc.SetPen(self.pen_tick) freq1 = self.VFO - self.sample_rate / 2 freq1 = (freq1 / stick) * stick freq2 = freq1 + self.sample_rate + stick + 1 y_end = 0 for f in range (freq1, freq2, stick): x = self.x0 + int(float(f - self.VFO) / self.sample_rate * self.data_width) if self.originX <= x <= x3: if f % ltick is 0: # large tick dc.DrawLine(x, originY, x, originY + tick2) elif f % mtick is 0: # medium tick dc.DrawLine(x, originY, x, originY + tick1) else: # small tick dc.DrawLine(x, originY, x, originY + tick0) if f % tfreq is 0: # place frequency label t = str(f/1000) w, h = dc.GetTextExtent(t) dc.DrawText(t, x - w / 2, originY + tick2) y_end = originY + tick2 + h if y_end: # mark the center of the display dc.DrawLine(self.x0, y_end, self.x0, application.screen_height) def OnGraphData(self, data): i1 = (self.data_width - self.graph_width) / 2 i2 = i1 + self.graph_width self.display.OnGraphData(data[i1:i2]) def SetVFO(self, vfo): self.VFO = vfo self.doResize = True def SetTxFreq(self, freq): self.txFreq = freq x = self.x0 + int(float(freq) / self.sample_rate * self.data_width) self.display.SetTuningLine(x - self.originX) self.tuningX = x def GetMousePosition(self, event): """For mouse clicks in our display, translate to our screen coordinates.""" mouse_x, mouse_y = event.GetPositionTuple() win = event.GetEventObject() if win is not self: x, y = win.GetPositionTuple() mouse_x += x mouse_y += y return mouse_x, mouse_y def OnRightDown(self, event): mouse_x, mouse_y = self.GetMousePosition(event) freq = float(mouse_x - self.x0) * self.sample_rate / self.data_width freq = int(freq) if self.VFO > 0: vfo = self.VFO + freq vfo = (vfo + 5000) / 10000 * 10000 # round to even number tune = freq + self.VFO - vfo self.ChangeHwFrequency(tune, vfo, 'MouseBtn3', event) def OnLeftDown(self, event): mouse_x, mouse_y = self.GetMousePosition(event) self.mouse_x = mouse_x if mouse_y > self.originY: # click below X axis self.mouse_origin = self.tuningX else: # click above X axis self.mouse_origin = mouse_x freq = float(mouse_x - self.x0) * self.sample_rate / self.data_width freq = int(freq) self.ChangeHwFrequency(freq, self.VFO, 'MouseBtn1', event) self.CaptureMouse() def OnLeftUp(self, event): if self.HasCapture(): self.ReleaseMouse() def OnMotion(self, event): if event.Dragging() and event.LeftIsDown(): mouse_x, mouse_y = self.GetMousePosition(event) if conf.mouse_tune_method: # Mouse motion changes the VFO frequency x = (mouse_x - self.mouse_x) # Thanks to VK6JBL self.mouse_x = mouse_x freq = x * self.sample_rate / self.data_width freq = int(freq) self.ChangeHwFrequency(self.txFreq, self.VFO - freq, 'MouseMotion', event) else: # Mouse motion changes the tuning frequency # Frequency changes more rapidly for higher mouse Y position speed = max(10, self.originY - mouse_y) / float(self.originY) x = (mouse_x - self.mouse_x) self.mouse_x = mouse_x freq = speed * x * self.sample_rate / self.data_width freq = int(freq) self.ChangeHwFrequency(self.txFreq + freq, self.VFO, 'MouseMotion', event) def OnWheel(self, event): wm = self.WheelMod # Round frequency when using mouse wheel tune = self.txFreq + wm * event.GetWheelRotation() / event.GetWheelDelta() if tune >= 0: tune = tune / wm * wm else: # tune can be negative when the VFO is zero tune = - (- tune / wm * wm) self.ChangeHwFrequency(tune, self.VFO, 'MouseWheel', event) def ChangeHwFrequency(self, tune, vfo, source, event): application.ChangeHwFrequency(tune, vfo, source, event=event) class WaterfallDisplay(wx.Window): """Create a waterfall display within the waterfall screen.""" def __init__(self, parent, x, y, graph_width, height, margin): wx.Window.__init__(self, parent, pos = (x, y), size = (graph_width, height), style = wx.NO_BORDER) self.parent = parent self.graph_width = graph_width self.margin = margin self.height = 10 self.sample_rate = application.sample_rate self.SetBackgroundColour('Black') self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown) self.Bind(wx.EVT_RIGHT_DOWN, parent.OnRightDown) self.Bind(wx.EVT_LEFT_UP, parent.OnLeftUp) self.Bind(wx.EVT_MOTION, parent.OnMotion) self.Bind(wx.EVT_MOUSEWHEEL, parent.OnWheel) self.tune_x = graph_width / 2 self.tuningPen = wx.Pen('White', 3) self.marginPen = wx.Pen(conf.color_graph, 1) # Size of top faster scroll region is (top_key + 2) * (top_key - 1) / 2 self.top_key = 8 self.top_size = (self.top_key + 2) * (self.top_key - 1) / 2 # Make the palette pal2 = conf.waterfallPalette red = [] green = [] blue = [] n = 0 for i in range(256): if i > pal2[n+1][0]: n = n + 1 red.append((i - pal2[n][0]) * (long)(pal2[n+1][1] - pal2[n][1]) / (long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][1]) green.append((i - pal2[n][0]) * (long)(pal2[n+1][2] - pal2[n][2]) / (long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][2]) blue.append((i - pal2[n][0]) * (long)(pal2[n+1][3] - pal2[n][3]) / (long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][3]) self.red = red self.green = green self.blue = blue bmp = wx.EmptyBitmap(0, 0) bmp.x_origin = 0 self.bitmaps = [bmp] * application.screen_height def OnPaint(self, event): dc = wx.PaintDC(self) y = 0 dc.SetPen(self.marginPen) x_origin = int(float(self.VFO) / self.sample_rate * self.data_width + 0.5) for i in range(0, self.margin): dc.DrawLine(0, y, self.graph_width, y) y += 1 index = 0 if conf.waterfall_scroll_mode: # Draw the first few lines multiple times for i in range(self.top_key, 1, -1): b = self.bitmaps[index] x = b.x_origin - x_origin for j in range(0, i): dc.DrawBitmap(b, x, y) y += 1 index += 1 while y < self.height: b = self.bitmaps[index] x = b.x_origin - x_origin dc.DrawBitmap(b, x, y) y += 1 index += 1 dc.SetPen(self.tuningPen) dc.SetLogicalFunction(wx.XOR) dc.DrawLine(self.tune_x, 0, self.tune_x, self.height) def SetHeight(self, height): self.height = height self.SetSize((self.graph_width, height)) def OnGraphData(self, data, y_zero, y_scale): #T('graph start') row = '' # Make a new row of pixels for a one-line image for x in data: # x is -130 to 0, or so (dB) l = int((x + y_zero / 3 + 100) * y_scale / 10) l = max(l, 0) l = min(l, 255) row = row + "%c%c%c" % (chr(self.red[l]), chr(self.green[l]), chr(self.blue[l])) #T('graph string') bmp = wx.BitmapFromBuffer(len(row) / 3, 1, row) bmp.x_origin = int(float(self.VFO) / self.sample_rate * self.data_width + 0.5) self.bitmaps.insert(0, bmp) del self.bitmaps[-1] self.ScrollWindow(0, 1, None) self.Refresh(False, (0, 0, self.graph_width, self.top_size + self.margin)) #T('graph end') def SetTuningLine(self, x): dc = wx.ClientDC(self) dc.SetPen(self.tuningPen) dc.SetLogicalFunction(wx.XOR) dc.DrawLine(self.tune_x, 0, self.tune_x, self.height) dc.DrawLine(x, 0, x, self.height) self.tune_x = x class WaterfallScreen(wx.SplitterWindow): """Create a splitter window with a graph screen and a waterfall screen""" def __init__(self, frame, width, data_width, graph_width): self.y_scale = conf.waterfall_y_scale self.y_zero = conf.waterfall_y_zero wx.SplitterWindow.__init__(self, frame) self.SetSizeHints(width, -1, width) self.SetMinimumPaneSize(1) self.SetSize((width, conf.waterfall_graph_size + 100)) # be able to set sash size self.pane1 = GraphScreen(self, data_width, graph_width, 1) self.pane2 = WaterfallPane(self, data_width, graph_width) self.SplitHorizontally(self.pane1, self.pane2, conf.waterfall_graph_size) def OnIdle(self, event): self.pane1.OnIdle(event) self.pane2.OnIdle(event) def SetTxFreq(self, freq): self.pane1.SetTxFreq(freq) self.pane2.SetTxFreq(freq) def SetVFO(self, vfo): self.pane1.SetVFO(vfo) self.pane2.SetVFO(vfo) def ChangeYscale(self, y_scale): # Test if the shift key is down if wx.GetKeyState(wx.WXK_SHIFT): # Set graph screen self.pane1.ChangeYscale(y_scale) else: # Set waterfall screen self.y_scale = y_scale self.pane2.ChangeYscale(y_scale) def ChangeYzero(self, y_zero): # Test if the shift key is down if wx.GetKeyState(wx.WXK_SHIFT): # Set graph screen self.pane1.ChangeYzero(y_zero) else: # Set waterfall screen self.y_zero = y_zero self.pane2.ChangeYzero(y_zero) def OnGraphData(self, data): self.pane1.OnGraphData(data) self.pane2.OnGraphData(data) class WaterfallPane(GraphScreen): """Create a waterfall screen with an X axis and a waterfall display.""" def __init__(self, frame, data_width, graph_width): GraphScreen.__init__(self, frame, data_width, graph_width) self.y_scale = conf.waterfall_y_scale self.y_zero = conf.waterfall_y_zero self.oldVFO = self.VFO def MakeDisplay(self): self.display = WaterfallDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) self.display.VFO = self.VFO self.display.data_width = self.data_width def SetVFO(self, vfo): GraphScreen.SetVFO(self, vfo) self.display.VFO = vfo if self.oldVFO != vfo: self.oldVFO = vfo self.Refresh() def MakeYTicks(self, dc): pass def ChangeYscale(self, y_scale): self.y_scale = y_scale def ChangeYzero(self, y_zero): self.y_zero = y_zero def OnGraphData(self, data): i1 = (self.data_width - self.graph_width) / 2 i2 = i1 + self.graph_width self.display.OnGraphData(data[i1:i2], self.y_zero, self.y_scale) class ScopeScreen(wx.Window): """Create an oscilloscope screen (mostly used for debug).""" def __init__(self, parent, width, data_width, graph_width): wx.Window.__init__(self, parent, pos = (0, 0), size=(width, -1), style = wx.NO_BORDER) self.SetBackgroundColour(conf.color_graph) self.font = wx.Font(16, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_PAINT, self.OnPaint) self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID) self.y_scale = conf.scope_y_scale self.y_zero = conf.scope_y_zero self.running = 1 self.doResize = False self.width = width self.height = 100 self.originY = self.height / 2 self.data_width = data_width self.graph_width = graph_width w = self.charx = self.GetCharWidth() h = self.chary = self.GetCharHeight() tick = max(2, h * 3 / 10) self.originX = w * 3 self.width = self.originX + self.graph_width + tick + self.charx * 2 self.line = [(0,0), (1,1)] # initial fake graph data self.fpout = None #open("jim96.txt", "w") def OnIdle(self, event): if self.doResize: self.ResizeGraph() def OnSize(self, event): self.doResize = True event.Skip() def ResizeGraph(self, event=None): # Change the height of the graph. Changing the width interactively is not allowed. w, h = self.GetClientSize() self.height = h self.originY = h / 2 self.doResize = False self.Refresh() def OnPaint(self, event): dc = wx.PaintDC(self) dc.SetFont(self.font) self.MakeYTicks(dc) self.MakeXTicks(dc) self.MakeText(dc) dc.SetPen(wx.BLACK_PEN) dc.DrawLines(self.line) def MakeYTicks(self, dc): chary = self.chary originX = self.originX x3 = self.x3 = originX + self.graph_width # end of graph data dc.SetPen(wx.BLACK_PEN) dc.DrawLine(originX, 0, originX, self.originY * 3) # y axis # Find the size of the Y scale markings themax = 2.5e9 * 10.0 ** - ((160 - self.y_scale) / 50.0) # value at top of screen themax = int(themax) l = [] for j in (5, 6, 7, 8): for i in (1, 2, 5): l.append(i * 10 ** j) for yvalue in l: n = themax / yvalue + 1 # Number of lines ypixels = self.height / n if n < 20: break dc.SetPen(self.horizPen) for i in range(1, 1000): y = self.originY - ypixels * i if y < chary: break # Above axis dc.DrawLine(originX, y, x3, y) # y line # Below axis y = self.originY + ypixels * i dc.DrawLine(originX, y, x3, y) # y line self.yscale = float(ypixels) / yvalue self.yvalue = yvalue def MakeXTicks(self, dc): originY = self.originY x3 = self.x3 # Draw the X axis dc.SetPen(wx.BLACK_PEN) dc.DrawLine(self.originX, originY, x3, originY) # Find the size of the X scale markings in microseconds for i in (20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000): xscale = i # X scale in microseconds if application.sample_rate * xscale * 0.000001 > self.width / 30: break # Draw the X lines dc.SetPen(self.horizPen) for i in range(1, 999): x = int(self.originX + application.sample_rate * xscale * 0.000001 * i + 0.5) if x > x3: break dc.DrawLine(x, 0, x, self.height) # x line self.xscale = xscale def MakeText(self, dc): if self.running: t = " RUN" else: t = " STOP" if self.xscale >= 1000: t = "%s X: %d millisec/div" % (t, self.xscale) else: t = "%s X: %d microsec/div" % (t, self.xscale) yt = `self.yvalue` t = "%s Y: %sE%d/div" % (t, yt[0], len(yt) - 1) dc.DrawText(t, self.originX, self.height - self.chary) def OnGraphData(self, data): if not self.running: if self.fpout: for cpx in data: re = int(cpx.real) im = int(cpx.imag) ab = int(abs(cpx)) self.fpout.write("%12d %12d %12d\n" % (re, im, ab)) return # Preserve data on screen line = [] x = self.originX ymax = self.height for cpx in data: # cpx is complex raw samples +/- 0 to 2**31-1 y = cpx.real #y = abs(cpx) y = self.originY - int(y * self.yscale + 0.5) if y > ymax: y = ymax elif y < 0: y = 0 line.append((x, y)) x = x + 1 self.line = line self.Refresh() def ChangeYscale(self, y_scale): self.y_scale = y_scale self.doResize = True def ChangeYzero(self, y_zero): self.y_zero = y_zero def SetTxFreq(self, freq): pass class FilterScreen(GraphScreen): """Create a graph of the receive filter response.""" def __init__(self, parent, data_width, graph_width): GraphScreen.__init__(self, parent, data_width, graph_width) self.y_scale = conf.filter_y_scale self.y_zero = conf.filter_y_zero self.VFO = 0 self.txFreq = 0 self.data = [] self.sample_rate = QS.get_filter_rate() def NewFilter(self): self.data = QS.get_filter() def OnGraphData(self, data): GraphScreen.OnGraphData(self, self.data) def ChangeHwFrequency(self, tune, vfo, source, event): self.SetTxFreq(tune) application.freqDisplay.Display(tune) class HelpScreen(wx.html.HtmlWindow): """Create the screen for the Help button.""" def __init__(self, parent, width, height): wx.html.HtmlWindow.__init__(self, parent, -1, size=(width, height)) self.y_scale = 0 self.y_zero = 0 if "gtk2" in wx.PlatformInfo: self.SetStandardFonts() self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22]) # read in text from file help.html in the directory of this module self.LoadFile('help.html') def OnGraphData(self, data): pass def ChangeYscale(self, y_scale): pass def ChangeYzero(self, y_zero): pass def OnIdle(self, event): pass def SetTxFreq(self, freq): pass class QMainFrame(wx.Frame): """Create the main top-level window.""" def __init__(self, width, height): fp = open('__init__.py') # Read in the title title = fp.readline().strip()[1:] fp.close() wx.Frame.__init__(self, None, -1, title, wx.DefaultPosition, (width, height), wx.DEFAULT_FRAME_STYLE, 'MainFrame') self.SetBackgroundColour(conf.color_bg) self.Bind(wx.EVT_CLOSE, self.OnBtnClose) def OnBtnClose(self, event): application.OnBtnClose(event) self.Destroy() class QAdjustPhase(wx.Frame): """Create a window with amplitude and phase adjustment controls""" f_ampl = "Amplitude adjustment %.6f" f_phase = "Phase adjustment %.6f" def __init__(self, parent, width): wx.Frame.__init__(self, application.main_frame, -1, "Adjust Sound Card Amplitude and Phase", pos=(50, 100)) self.Bind(wx.EVT_CLOSE, parent.OnPhaseClose) self.ampl, self.phase = application.GetAmplPhase() self.MakeControls(width) self.Show() def MakeControls(self, width): # Make controls for phase/amplitude adjustment chary = self.GetCharHeight() y = chary * 3 / 10 self.t_ampl = wx.StaticText(self, -1, self.f_ampl % self.ampl, pos=(0, y)) y += self.t_ampl.GetSizeTuple()[1] scale = width * 4 / 10 self.scale = float(scale) self.ampl1 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.ampl1.GetSizeTuple()[1] self.ampl2 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.ampl2.GetSizeTuple()[1] self.t_phase = wx.StaticText(self, -1, self.f_phase % self.phase, pos=(0, y)) y += self.t_phase.GetSizeTuple()[1] self.phase1 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.phase1.GetSizeTuple()[1] self.phase2 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.phase2.GetSizeTuple()[1] self.SetSizeHints(width, y, width, y) # no change in size self.ampl1.Bind(wx.EVT_SCROLL, self.OnAmpl1) self.ampl2.Bind(wx.EVT_SCROLL, self.OnAmpl1) self.phase1.Bind(wx.EVT_SCROLL, self.OnAmpl1) self.phase2.Bind(wx.EVT_SCROLL, self.OnAmpl1) def OnAmpl1(self, event): s2 = self.scale * 10.0 # maximum 0.10 change s1 = s2 * 20.0 # smaller maximum change ampl = self.ampl + self.ampl1.GetValue() / s1 + self.ampl2.GetValue() / s2 self.t_ampl.SetLabel(self.f_ampl % ampl) phase = self.phase + self.phase1.GetValue() / s1 + self.phase2.GetValue() / s2 self.t_phase.SetLabel(self.f_phase % phase) application.SetAmplPhase(ampl, phase) class Spacer(wx.Window): """Create a bar between the graph screen and the controls""" def __init__(self, parent): wx.Window.__init__(self, parent, pos = (0, 0), size=(-1, 6), style = wx.NO_BORDER) self.Bind(wx.EVT_PAINT, self.OnPaint) r, g, b = parent.GetBackgroundColour().Get() dark = (r * 7 / 10, g * 7 / 10, b * 7 / 10) light = (r + (255 - r) * 5 / 10, g + (255 - g) * 5 / 10, b + (255 - b) * 5 / 10) self.dark_pen = wx.Pen(dark, 1, wx.SOLID) self.light_pen = wx.Pen(light, 1, wx.SOLID) self.width = application.screen_width def OnPaint(self, event): dc = wx.PaintDC(self) w = self.width dc.SetPen(self.dark_pen) dc.DrawLine(0, 0, w, 0) dc.DrawLine(0, 1, w, 1) dc.DrawLine(0, 2, w, 2) dc.SetPen(self.light_pen) dc.DrawLine(0, 3, w, 3) dc.DrawLine(0, 4, w, 4) dc.DrawLine(0, 5, w, 5) class App(wx.App): """Class representing the application.""" freq60 = (5330500, 5346500, 5366500, 5371500, 5403500) StateNames = [ # Names of state attributes to save and restore 'bandState', 'bandAmplPhase', 'lastBand', 'VFO', 'txFreq', 'mode', 'vardecim_set', ] def __init__(self): global application application = self self.init_path = None if sys.stdout.isatty(): wx.App.__init__(self, redirect=False) else: wx.App.__init__(self, redirect=True) def QuiskPushbutton(self, *args, **kw): # Make our buttons available to widget files return QuiskPushbutton(*args, **kw) def QuiskRepeatbutton(self, *args, **kw): return QuiskRepeatbutton(*args, **kw) def QuiskCheckbutton(self, *args, **kw): return QuiskCheckbutton(*args, **kw) def QuiskCycleCheckbutton(self, *args, **kw): return QuiskCycleCheckbutton(*args, **kw) def RadioButtonGroup(self, *args, **kw): return RadioButtonGroup(*args, **kw) def OnInit(self): """Perform most initialization of the app here (called by wxPython on startup).""" wx.lib.colourdb.updateColourDB() # Add additional color names global conf # conf is the module for all configuration data import quisk_conf_defaults as conf cpath = argv_options.config_file_path # Get config file path if not cpath: cpath = os.path.expanduser('~/.quisk_conf.py') # Default path setattr(conf, 'config_file_path', cpath) if os.path.isfile(cpath): # See if the user has a config file setattr(conf, 'config_file_exists', True) d = {} d.update(conf.__dict__) # make items from conf available execfile(cpath, d) # execute the user's config file for k, v in d.items(): # add user's config items to conf if k[0] != '_': # omit items starting with '_' setattr(conf, k, v) else: setattr(conf, 'config_file_exists', False) if conf.invertSpectrum: QS.invert_spectrum(1) self.bandState = {} self.bandState.update(conf.bandState) self.bandAmplPhase = conf.bandAmplPhase # Open hardware file global Hardware if hasattr(conf, "Hardware"): # Hardware defined in config file Hardware = conf.Hardware(self, conf) else: Hardware = conf.quisk_hardware.Hardware(self, conf) # Initialization - may be over-written by persistent state self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow self.smeter_db_count = 0 # average the S-meter self.smeter_db_sum = 0 self.smeter_db = 0 self.smeter_sunits = -87.0 self.timer = time.time() # A seconds clock self.heart_time0 = self.timer # timer to call HeartBeat at intervals self.smeter_db_time0 = self.timer self.smeter_sunits_time0 = self.timer self.band_up_down = 0 # Are band Up/Down buttons in use? self.lastBand = 'Audio' self.VFO = 0 self.ritFreq = 0 self.txFreq = 0 self.screen = None self.audio_volume = 0.0 # Set output volume, 0.0 to 1.0 self.sidetone_volume = 0.0 # Set sidetone volume, 0.0 to 1.0 self.sound_error = 0 self.sound_thread = None self.mode = conf.default_mode self.bottom_widgets = None self.color_list = None self.color_index = 0 self.filterbw = 0 # WB4JFI Added for filter display self.fltr_display = conf.filter_display # WB4JFI added filter display 1=OK, 0=do not display self.vardecim_set = None dc = wx.ScreenDC() # get the screen size (self.screen_width, self.screen_height) = dc.GetSizeTuple() del dc self.Bind(wx.EVT_IDLE, self.OnIdle) self.Bind(wx.EVT_QUERY_END_SESSION, self.OnEndSession) # Restore persistent program state if conf.persistent_state: self.init_path = os.path.join(os.path.dirname(cpath), '.quisk_init.pkl') try: fp = open(self.init_path, "rb") d = pickle.load(fp) fp.close() for k, v in d.items(): if k in self.StateNames: if k == 'bandState': self.bandState.update(v) else: setattr(self, k, v) except: pass #traceback.print_exc() for k, (vfo, tune, mode) in self.bandState.items(): # Historical: fix bad frequencies try: f1, f2 = conf.BandEdge[k] if not f1 <= vfo + tune <= f2: self.bandState[k] = conf.bandState[k] except KeyError: pass if Hardware.VarDecimGetChoices(): # Hardware can change the decimation. self.sample_rate = Hardware.VarDecimSet() # Get the sample rate. self.vardecim_set = self.sample_rate else: # Use the sample rate from the config file. self.sample_rate = conf.sample_rate if not hasattr(conf, 'playback_rate'): if conf.use_sdriq or conf.use_rx_udp: conf.playback_rate = 48000 else: conf.playback_rate = conf.sample_rate # Find the data width from a list of prefered sizes; it is the width of returned graph data. # The graph_width is the width of data_width that is displayed. width = self.screen_width * conf.graph_width percent = conf.display_fraction # display central fraction of total width percent = int(percent * 100.0 + 0.4) width = width * 100 / percent for x in fftPreferedSizes: if x > width: self.data_width = x break else: self.data_width = fftPreferedSizes[-1] self.graph_width = self.data_width * percent / 100 if self.graph_width % 2 == 1: # Both data_width and graph_width are even numbers self.graph_width += 1 # The FFT size times the average_count controls the graph refresh rate factor = float(self.sample_rate) / conf.graph_refresh / self.data_width ifactor = int(factor + 0.5) if conf.fft_size_multiplier >= ifactor: # Use large FFT and average count 1 fft_mult = ifactor average_count = 1 elif conf.fft_size_multiplier > 0: # Specified fft_size_multiplier fft_mult = conf.fft_size_multiplier average_count = int(factor / fft_mult + 0.5) if average_count < 1: average_count = 1 else: # Calculate the split between fft size and average if self.sample_rate <= 240000: maxfft = 8000 # Maximum fft size else: maxfft = 15000 fft1 = maxfft / self.data_width if fft1 >= ifactor: fft_mult = ifactor average_count = 1 else: av1 = int(factor / fft1 + 0.5) if av1 < 1: av1 = 1 err1 = factor / (fft1 * av1) av2 = av1 + 1 fft2 = int(factor / av2 + 0.5) err2 = factor / (fft2 * av2) if 0.9 < err1 < 1.1 or abs(1.0 - err1) <= abs(1.0 - err2): fft_mult = fft1 average_count = av1 else: fft_mult = fft2 average_count = av2 self.fft_size = self.data_width * fft_mult # Record the basic application parameters QS.record_app(self, conf, self.data_width, self.fft_size, average_count, self.sample_rate) #print 'FFT size %d, FFT mult %d, average_count %d' % ( # self.fft_size, self.fft_size / self.data_width, average_count) #print 'Refresh %.2f Hz' % (float(self.sample_rate) / self.fft_size / average_count) QS.record_graph(0, 0, 1.0) self.width = self.screen_width * 8 / 10 self.height = self.screen_height * 5 / 10 self.main_frame = frame = QMainFrame(self.width, self.height) self.SetTopWindow(frame) # Make all the screens and hide all but one self.graph = GraphScreen(frame, self.data_width, self.graph_width) self.screen = self.graph width = self.graph.width button_width = width # try to estimate the final button width self.config_screen = ConfigScreen(frame, width, self.fft_size) self.config_screen.Hide() self.waterfall = WaterfallScreen(frame, width, self.data_width, self.graph_width) self.waterfall.Hide() self.scope = ScopeScreen(frame, width, self.data_width, self.graph_width) self.scope.Hide() self.filter_screen = FilterScreen(frame, self.data_width, self.graph_width) self.filter_screen.Hide() self.help_screen = HelpScreen(frame, width, self.screen_height / 10) self.help_screen.Hide() frame.SetSizeHints(width, 100) # Make a vertical box to hold all the screens and the bottom box vertBox = self.vertBox = wx.BoxSizer(wx.VERTICAL) frame.SetSizer(vertBox) # Add the screens vertBox.Add(self.config_screen, 1) vertBox.Add(self.graph, 1) vertBox.Add(self.waterfall, 1) vertBox.Add(self.scope, 1) vertBox.Add(self.filter_screen, 1) vertBox.Add(self.help_screen, 1) # Add the spacer vertBox.Add(Spacer(frame), 0, wx.EXPAND) # Add the bottom box hBoxA = wx.BoxSizer(wx.HORIZONTAL) vertBox.Add(hBoxA, 0, wx.EXPAND) # End of vertical box. Add items to the horizontal box. # Add two sliders on the left margin = 3 self.sliderVol = SliderBoxV(frame, 'Vol', 300, 1000, self.ChangeVolume) button_width -= self.sliderVol.width + margin * 2 self.ChangeVolume() # set initial volume level hBoxA.Add(self.sliderVol, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) if Hardware.use_sidetone: self.sliderSto = SliderBoxV(frame, 'STo', 300, 1000, self.ChangeSidetone) button_width -= self.sliderSto.width + margin * 2 self.ChangeSidetone() hBoxA.Add(self.sliderSto, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) # Add the sizer for the middle gap = 2 gbs = wx.GridBagSizer(gap, gap) self.gbs = gbs button_width -= gap * 15 hBoxA.Add(gbs, 1, wx.EXPAND, 0) gbs.SetEmptyCellSize((5, 5)) button_width -= 5 for i in range(0, 6) + range(7, 13): gbs.AddGrowableCol(i) # Add two sliders on the right self.sliderYs = SliderBoxV(frame, 'Ys', 0, 160, self.ChangeYscale, True) button_width -= self.sliderYs.width + margin * 2 hBoxA.Add(self.sliderYs, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) self.sliderYz = SliderBoxV(frame, 'Yz', 0, 160, self.ChangeYzero, True) button_width -= self.sliderYz.width + margin * 2 hBoxA.Add(self.sliderYz, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) button_height = self.MakeButtons(frame, gbs) button_width /= 12 # This is our estimate of the final button size self.MakeTopRow(frame, gbs, button_width, button_height) if conf.quisk_widgets: self.bottom_widgets = conf.quisk_widgets.BottomWidgets(self, Hardware, conf, frame, gbs, vertBox) if QS.open_key(conf.key_method): print 'open_key failed for name "%s"' % conf.key_method if hasattr(conf, 'mixer_settings'): for dev, numid, value in conf.mixer_settings: err_msg = QS.mixer_set(dev, numid, value) if err_msg: print "Mixer", err_msg # Create transmit audio filters if conf.microphone_name: filtI, filtQ = self.MakeFilterCoef(conf.mic_sample_rate, 540, 2500, 1550) QS.set_tx_filters(filtI, filtQ, ()) # Open the hardware. This must be called before open_sound(). self.config_text = Hardware.open() if not self.config_text: self.config_text = "Missing config_text" QS.capt_channels (conf.channel_i, conf.channel_q) QS.play_channels (conf.channel_i, conf.channel_q) QS.micplay_channels (conf.mic_play_chan_I, conf.mic_play_chan_Q) # Note: Subsequent calls to set channels must not name a higher channel number. # Normally, these calls are only used to reverse the channels. QS.open_sound(conf.name_of_sound_capt, conf.name_of_sound_play, self.sample_rate, conf.data_poll_usec, conf.latency_millisecs, conf.microphone_name, conf.tx_ip, conf.tx_audio_port, conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q, conf.mic_out_volume, conf.name_of_mic_play, conf.mic_playback_rate) tune, vfo = Hardware.ReturnFrequency() # Request initial frequency to set band if tune is not None: for band, (f1, f2) in conf.BandEdge.items(): if f1 <= tune <= f2: # Change to the correct band based on frequency self.lastBand = band break self.bandBtnGroup.SetLabel(self.lastBand, do_cmd=True) self.ChangeHwFrequency(None, None) # Request initial VFO and tuning # Note: The filter rate is not valid until after the call to open_sound(). # Create FM audio filter frate = QS.get_filter_rate() # filter rate filtI, filtQ = self.MakeFmFilterCoef(frate, 600, 340, 2800) QS.set_fm_filters(filtI) # Record filter rate for the filter screen self.filter_screen.sample_rate = frate #if info[8]: # error message # self.sound_error = 1 # self.config_screen.err_msg = info[8] # print info[8] if self.sound_error: self.screenBtnGroup.SetLabel('Config', do_cmd=True) frame.Show() else: self.screenBtnGroup.SetLabel(conf.default_screen, do_cmd=True) frame.Show() self.Yield() self.sound_thread = SoundThread() self.sound_thread.start() return True def OnIdle(self, event): if self.screen: self.screen.OnIdle(event) def OnEndSession(self, event): event.Skip() self.OnBtnClose() def OnBtnClose(self, event): if self.sound_thread: self.sound_thread.stop() for i in range(0, 20): if threading.activeCount() == 1: break time.sleep(0.1) def OnExit(self): QS.close_rx_udp() Hardware.close() if self.init_path: # save current program state d = {} for n in self.StateNames: d[n] = getattr(self, n) try: fp = open(self.init_path, "wb") pickle.dump(d, fp) fp.close() except: pass #traceback.print_exc() def MakeTopRow(self, frame, gbs, button_width, button_height): # Down button b_down = QuiskRepeatbutton(frame, self.OnBtnDownBand, "Down", self.OnBtnUpDnBandDone, use_right=True) gbs.Add(b_down, (0, 4), flag=wx.ALIGN_CENTER) # RIT button self.ritButton = QuiskCheckbutton(frame, self.OnBtnRit, "RIT") gbs.Add(self.ritButton, (0, 7), flag=wx.ALIGN_CENTER) # Up button b_up = QuiskRepeatbutton(frame, self.OnBtnUpBand, "Up", self.OnBtnUpDnBandDone, use_right=True) gbs.Add(b_up, (0, 5), flag=wx.ALIGN_CENTER) bw, bh = b_down.GetMinSize() # make top row buttons the same size bw = (bw + button_width) / 2 bh = max(bh, button_height) b_down.SetSizeHints(bw, bh, bw * 5, bh) b_up.SetSizeHints(bw, bh, bw * 5, bh) self.ritButton.SetSizeHints(bw, bh, bw * 5, bh) # RIT slider self.ritScale = wx.Slider(frame, -1, self.ritFreq, -2000, 2000, size=(-1, -1), style=wx.SL_LABELS) self.ritScale.Bind(wx.EVT_SCROLL, self.OnRitScale) gbs.Add(self.ritScale, (0, 8), (1, 3), flag=wx.EXPAND) sw, sh = self.ritScale.GetSize() # Frequency display h = max(bh, sh) # larger of button and slider height self.freqDisplay = FrequencyDisplay(frame, gbs, button_width * 3, h) self.freqDisplay.Display(self.txFreq + self.VFO) # Frequency entry e = wx.TextCtrl(frame, -1, '', style=wx.TE_PROCESS_ENTER|wx.RAISED_BORDER) w, h = e.GetTextExtent('33333333') h = self.freqDisplay.height e.SetSizeHints(w, h, w * 2, h) e.SetBackgroundColour(conf.color_entry) gbs.Add(e, (0, 3), flag= wx.EXPAND | wx.TOP | wx.BOTTOM, border=self.freqDisplay.border) frame.Bind(wx.EVT_TEXT_ENTER, self.FreqEntry, source=e) # S-meter self.smeter = t = wx.lib.stattext.GenStaticText(frame, -1, '', style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE|wx.RAISED_BORDER) for points in range(20, 6, -1): font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) t.SetFont(font) w, h = t.GetTextExtent("ZZS 9 -100.00 dBZZ") if w < button_width * 2: break h = h * 12 / 10 t.SetSizeHints(w, h, -1, h) border = (self.freqDisplay.height_and_border - h) / 2 t.SetBackgroundColour(conf.color_freq) gbs.Add(t, (0, 11), (1, 2), flag=wx.ALIGN_CENTER|wx.EXPAND| wx.TOP | wx.BOTTOM, border=border) def MakeButtons(self, frame, gbs): all_buttons = [] # There are six columns, a small gap column, and then six more columns flag = wx.EXPAND ### Left bank of buttons self.bandBtnGroup = RadioButtonGroup(frame, self.OnBtnBand, conf.bandLabels, None) btns = self.bandBtnGroup.buttons all_buttons += btns i = 0 n1 = len(conf.bandLabels) / 2 n2 = len(conf.bandLabels) - n1 for col in range(0, n1): gbs.Add(btns[i], (1, col), flag=flag) i += 1 for col in range(0, n2): gbs.Add(btns[i], (2, col), flag=flag) i += 1 # Mute, AGC buttons = [] b = QuiskCheckbutton(frame, self.OnBtnMute, text='Mute') buttons.append(b) b = QuiskCycleCheckbutton(frame, self.OnBtnAGC, ('AGC', 'AGC 1', 'AGC 2')) buttons.append(b) b.SetLabel('AGC 1', True) b = QuiskCheckbutton(frame, self.OnBtnNB, text='') buttons.append(b) try: labels = Hardware.rf_gain_labels except: labels = () if labels: self.BtnRfGain = QuiskCycleCheckbutton(frame, Hardware.OnButtonRfGain, labels) buttons.append(self.BtnRfGain) else: b = QuiskCheckbutton(frame, None, text='') buttons.append(b) self.BtnRfGain = None #b = QuiskRepeatbutton(frame, self.OnBtnColor, '', use_right=True) if conf.add_fdx_button: b = QuiskCheckbutton(frame, self.OnBtnFDX, 'FDX', color=conf.color_test) else: b = QuiskCheckbutton(frame, None, text='') buttons.append(b) b = QuiskCheckbutton(frame, self.OnBtnTest1, 'Test 1', color=conf.color_test) buttons.append(b) all_buttons += buttons for col in range(0, 6): gbs.Add(buttons[col], (3, col), flag=flag) ### Right bank of buttons labels = [('CWL', 'CWU'), ('LSB', 'USB'), 'AM', 'FM', conf.add_extern_demod, ''] if conf.add_imd_button: labels[-1] = ('IMD', 'IMD -3dB', 'IMD -6dB') self.modeButns = RadioButtonGroup(frame, self.OnBtnMode, labels, None) btns = self.modeButns.GetButtons() all_buttons += btns btns[-1].color = conf.color_test for col in range(0, 6): gbs.Add(btns[col], (1, col + 7), flag=flag) labels = ('0',) * 6 self.filterButns = RadioButtonGroup(frame, self.OnBtnFilter, labels, None) btns = self.filterButns.GetButtons() all_buttons += btns for col in range(0, 6): gbs.Add(btns[col], (2, col + 7), flag=flag) labels = ('Graph', 'WFall', ('Scope', 'Scope'), 'Config', 'RX Filter', 'Help') self.screenBtnGroup = RadioButtonGroup(frame, self.OnBtnScreen, labels, conf.default_screen) btns = self.screenBtnGroup.GetButtons() all_buttons += btns for col in range(0, 6): gbs.Add(btns[col], (3, col + 7), flag=flag) bw = bh = 0 for b in all_buttons: # find the largest button size w, h = b.GetMinSize() bw = max(bw, w) bh = max(bh, h) for b in all_buttons: # set all buttons to the same size b.SetSizeHints(bw, bh, bw * 5, bh) return bh # return the button height def NewSmeter(self): #avg_seconds = 5.0 # seconds for S-meter average avg_seconds = 1.0 self.smeter_db_count += 1 # count for average x = QS.get_smeter() self.smeter_db_sum += x # sum for average if self.timer - self.smeter_db_time0 > avg_seconds: # average time reached self.smeter_db = self.smeter_db_sum / self.smeter_db_count self.smeter_db_count = self.smeter_db_sum = 0 self.smeter_db_time0 = self.timer if self.smeter_sunits < x: # S-meter moves to peak value self.smeter_sunits = x else: # S-meter decays at this time constant self.smeter_sunits -= (self.smeter_sunits - x) * (self.timer - self.smeter_sunits_time0) self.smeter_sunits_time0 = self.timer s = self.smeter_sunits / 6.0 # change to S units; 6db per S unit s += Hardware.correct_smeter # S-meter correction for the gain, band, etc. if s >= 9.5: s = (s - 9.0) * 6 t = "S9 + %.0f %.2f dB" % (s, self.smeter_db) else: t = "S %.0f %.2f dB" % (s, self.smeter_db) self.smeter.SetLabel(t) def MakeFilterButtons(self, *args): # Change the filter selections depending on the mode: CW, SSB, etc. i = 0 for b in self.filterButns.GetButtons(): b.SetLabel(str(args[i])) b.Refresh() i += 1 def MakeFilterCoef(self, rate, N, bw, center): """Make an I/Q filter with rectangular passband.""" K = bw * N / rate filtI = [] filtQ = [] pi = math.pi sin = math.sin cos = math.cos tune = 2. * pi * center / rate for k in range(-N/2, N/2 + 1): # Make a lowpass filter if k == 0: z = float(K) / N else: z = 1.0 / N * sin(pi * k * K / N) / sin(pi * k / N) # Apply a windowing function if 1: # Blackman window w = 0.42 + 0.5 * cos(2. * pi * k / N) + 0.08 * cos(4. * pi * k / N) elif 0: # Hamming w = 0.54 + 0.46 * cos(2. * pi * k / N) elif 0: # Hanning w = 0.5 + 0.5 * cos(2. * pi * k / N) else: w = 1 z *= w # Make a bandpass filter by tuning the low pass filter to new center frequency. # Make two quadrature filters. if tune: z *= 2.0 * cmath.exp(-1j * tune * k) filtI.append(z.real) filtQ.append(z.imag) else: filtI.append(z) filtQ.append(z) return filtI, filtQ def MakeFmFilterCoef(self, rate, N, f1, f2): """Make an audio filter with FM de-emphasis; remove CTCSS tones.""" bw = f2 - f1 center = (f1 + f2) / 2 N2 = N / 2 # Half the number of points K2 = bw * N / rate / 2 # Half the bandwidth in points filtI = [] filtQ = [] passb = [0] * (N + 1) # desired passband response idft = [0] * (N + 1) # inverse DFT of desired passband pi = math.pi sin = math.sin cos = math.cos tune = 2. * pi * center / rate # indexing can be from - N2 thru + N2 inclusive; total points is 2 * N2 + 1 # indexing can be from 0 thru 2 * N2 inclusive; total points is 2 * N2 + 1 for j in range(-K2, K2 + 1): # Filter shape is -6 bB per octave jj = j + N2 freq = center - bw / 2.0 * float(j) / K2 passb[jj] = float(center) / freq * 0.3 for k in range(-N2 + 1, N2 + 1): # Take inverse DFT of passband response kk = k + N2 x = 0 + 0J for m in range(-N2, N2 + 1): mm = m + N2 if passb[mm]: x += passb[mm] * cmath.exp(1J * 2.0 * pi * m * k / N) x /= N idft[kk] = x idft[0] = idft[-1] # this value is missing for k in range(-N2, N2 + 1): kk = k + N2 z = idft[kk] # Apply a windowing function if 1: # Blackman window w = 0.42 + 0.5 * cos(2. * pi * k / N) + 0.08 * cos(4. * pi * k / N) elif 0: # Hamming w = 0.54 + 0.46 * cos(2. * pi * k / N) elif 0: # Hanning w = 0.5 + 0.5 * cos(2. * pi * k / N) else: w = 1 z *= w # Make a bandpass filter by tuning the low pass filter to new center frequency. # Make two quadrature filters. if tune: z *= 2.0 * cmath.exp(-1j * tune * k) filtI.append(z.real) filtQ.append(z.imag) else: filtI.append(z.real) filtQ.append(z.real) return filtI, filtQ def OnBtnFilter(self, event, bw=None): if event is None: # called by application self.filterButns.SetLabel(str(bw)) else: # called by button btn = event.GetEventObject() bw = int(btn.GetLabel()) self.filterbw = int(self.filterButns.GetLabel()) # WB4JFI ADD - udate filter bandwidth application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp mode = self.mode if mode in ("CWL", "CWU"): N = 1000 center = max(conf.cwTone, bw/2) elif mode in ('LSB', 'USB'): N = 540 center = 300 + bw / 2 else: # AM and FM N = 140 center = 0 frate = QS.get_filter_rate() filtI, filtQ = self.MakeFilterCoef(frate, N, bw, center) QS.set_filters(filtI, filtQ, bw) if self.screen is self.filter_screen: self.screen.NewFilter() def OnBtnScreen(self, event, name=None): if event is not None: win = event.GetEventObject() name = win.GetLabel() self.screen.Hide() if name == 'Config': self.screen = self.config_screen elif name == 'Graph': self.screen = self.graph self.screen.SetTxFreq(self.txFreq) self.freqDisplay.Display(self.VFO + self.txFreq) application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp elif name == 'WFall': self.screen = self.waterfall self.screen.SetTxFreq(self.txFreq) self.freqDisplay.Display(self.VFO + self.txFreq) sash = self.screen.GetSashPosition() application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp elif name == 'Scope': if win.direction: # Another push on the same button self.scope.running = 1 - self.scope.running # Toggle run state else: # Initial push of button self.scope.running = 1 self.screen = self.scope elif name == 'RX Filter': self.screen = self.filter_screen self.screen.SetTxFreq(self.screen.txFreq) self.freqDisplay.Display(self.screen.txFreq) self.screen.NewFilter() elif name == 'Help': self.screen = self.help_screen self.screen.Show() self.vertBox.Layout() # This destroys the initialized sash position! self.sliderYs.SetValue(self.screen.y_scale) self.sliderYz.SetValue(self.screen.y_zero) if name == 'WFall': self.screen.SetSashPosition(sash) def ChangeYscale(self, event): self.screen.ChangeYscale(self.sliderYs.GetValue()) def ChangeYzero(self, event): self.screen.ChangeYzero(self.sliderYz.GetValue()) def OnBtnMute(self, event): btn = event.GetEventObject() if btn.GetValue(): QS.set_volume(0) else: QS.set_volume(self.audio_volume) def OnBtnDecimation(self, event): i = event.GetSelection() rate = Hardware.VarDecimSet(i) self.vardecim_set = rate if rate != self.sample_rate: self.sample_rate = rate self.graph.sample_rate = rate self.waterfall.pane1.sample_rate = rate self.waterfall.pane2.sample_rate = rate self.waterfall.pane2.display.sample_rate = rate average_count = float(rate) / conf.graph_refresh / self.fft_size average_count = int(average_count + 0.5) average_count = max (1, average_count) QS.change_rate(rate, average_count) tune = self.txFreq vfo = self.VFO self.txFreq = self.VFO = -1 # demand change self.ChangeHwFrequency(tune, vfo, 'NewDecim') def ChangeVolume(self, event=None): # Caution: event can be None value = self.sliderVol.GetValue() # Simulate log taper pot x = (10.0 ** (float(value) * 0.003000434077) - 1) / 1000.0 self.audio_volume = x # audio_volume is 0 to 1.000 QS.set_volume(x) def ChangeSidetone(self, event=None): # Caution: event can be None value = self.sliderSto.GetValue() # Simulate log taper pot x = (10.0 ** (float(value) * 0.003) - 1) / 1000.0 self.sidetone_volume = x QS.set_sidetone(x, self.ritFreq, conf.keyupDelay) def OnRitScale(self, event=None): # Called when the RIT slider is moved # Caution: event can be None if self.ritButton.GetValue(): value = self.ritScale.GetValue() value = int(value) self.ritFreq = value QS.set_tune(self.txFreq + self.ritFreq, self.txFreq) QS.set_sidetone(self.sidetone_volume, self.ritFreq, conf.keyupDelay) def OnBtnRit(self, event=None): # Called when the RIT check button is pressed # Caution: event can be None if self.ritButton.GetValue(): self.ritFreq = self.ritScale.GetValue() else: self.ritFreq = 0 QS.set_tune(self.txFreq + self.ritFreq, self.txFreq) QS.set_sidetone(self.sidetone_volume, self.ritFreq, conf.keyupDelay) def SetRit(self, freq): if freq: self.ritButton.SetValue(1) else: self.ritButton.SetValue(0) self.ritScale.SetValue(freq) self.OnBtnRit() def OnBtnFDX(self, event): btn = event.GetEventObject() if btn.GetValue(): QS.set_fdx(1) else: QS.set_fdx(0) def OnBtnTest1(self, event): btn = event.GetEventObject() if btn.GetValue(): QS.add_tone(10000) else: QS.add_tone(0) def OnBtnTest2(self, event): return def OnBtnColor(self, event): if not self.color_list: clist = wx.lib.colourdb.getColourInfoList() self.color_list = [(0, clist[0][0])] self.color_index = 0 for i in range(1, len(clist)): if self.color_list[-1][1].replace(' ', '') != clist[i][0].replace(' ', ''): #if 'BLUE' in clist[i][0]: self.color_list.append((i, clist[i][0])) else: btn = event.GetEventObject() if btn.shift: del self.color_list[self.color_index] else: self.color_index += btn.direction if self.color_index >= len(self.color_list): self.color_index = 0 elif self.color_index < 0: self.color_index = len(self.color_list) -1 color = self.color_list[self.color_index][1] print self.color_index, color self.main_frame.SetBackgroundColour(color) self.main_frame.Refresh() self.screen.Refresh() def OnBtnAGC(self, event): btn = event.GetEventObject() # Set AGC: agcInUse, agcAttack, agcRelease if btn.index == 1: QS.set_agc(1, 1.0, 0.01) elif btn.index == 2: QS.set_agc(2, 1.0, 0.1) else: QS.set_agc(0, 0, 0) def OnBtnNB(self, event): pass def FreqEntry(self, event): freq = event.GetString() if not freq: return try: if '.' in freq: freq = int(float(freq) * 1E6 + 0.1) else: freq = int(freq) except ValueError: win = event.GetEventObject() win.Clear() win.AppendText("Error") else: for band, (f1, f2) in conf.BandEdge.items(): if f1 <= freq <= f2: # Change to the correct band based on frequency self.bandBtnGroup.SetLabel(band, do_cmd=True) break tune = freq % 10000 vfo = freq - tune self.ChangeHwFrequency(tune, vfo, 'FreqEntry') def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None): """Change the VFO and tuning frequencies, and notify the hardware. tune: the new tuning frequency in +- sample_rate/2; vfo: the new vfo frequency in Hertz; this is the RF frequency at zero Hz audio source: a string indicating the source or widget requesting the change; band: if source is "BtnBand", the band requested; event: for a widget, the event (used to access control/shift key state). Try to update the hardware by calling Hardware.ChangeFrequency(). The hardware will reply with the updated frequencies which may be different from those requested; use and display the returned tune and vfo. If tune or vfo is None, query the hardware for the current frequency. """ if tune is None or vfo is None: tune, vfo = Hardware.ReturnFrequency() if tune is None or vfo is None: # hardware did not change the frequency return else: tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event) tune -= vfo change = 0 if tune != self.txFreq: change = 1 self.txFreq = tune self.screen.SetTxFreq(self.txFreq) QS.set_tune(tune + self.ritFreq, tune) if vfo != self.VFO: change = 1 self.VFO = vfo self.graph.SetVFO(vfo) self.waterfall.SetVFO(vfo) if change: self.freqDisplay.Display(self.txFreq + self.VFO) def DisplayVFO(self, vfo, tune=None): """Change the frequencies internally and display the screen, but do not update the hardware. vfo: the new vfo frequency in Hertz; tune: the new tuning frequency in +- sample_rate/2. """ self.VFO = vfo self.graph.SetVFO(vfo) self.waterfall.SetVFO(vfo) if tune is not None: self.txFreq = tune self.screen.SetTxFreq(tune) self.freqDisplay.Display(self.txFreq + self.VFO) def OnBtnMode(self, event, mode=None): if event is None: # called by application self.modeButns.SetLabel(mode) else: # called by button mode = self.modeButns.GetLabel() Hardware.ChangeMode(mode) self.mode = mode if mode in ('CWL', 'CWU'): if mode == 'CWL': QS.set_rx_mode(0) self.SetRit(conf.cwTone) else: # CWU QS.set_rx_mode(1) self.SetRit(-conf.cwTone) self.MakeFilterButtons(200, 300, 400, 500, 1000, 3000) self.OnBtnFilter(None, 1000) elif mode in ('LSB', 'USB'): if mode == 'LSB': QS.set_rx_mode(2) # LSB else: QS.set_rx_mode(3) # USB self.SetRit(0) self.MakeFilterButtons(1800, 2000, 2200, 2500, 2800, 3300) self.OnBtnFilter(None, 2800) elif mode == 'AM': QS.set_rx_mode(4) self.SetRit(0) self.MakeFilterButtons(4000, 5000, 6000, 7000, 8000, 9000) self.OnBtnFilter(None, 6000) elif mode == 'FM': QS.set_rx_mode(5) self.SetRit(0) self.MakeFilterButtons(10000, 12000, 15000, 25000, 35000, 45000) self.OnBtnFilter(None, 12000) elif mode[0:3] == 'IMD': QS.set_rx_mode(10 + self.modeButns.GetSelectedButton().index) # 10, 11, 12 self.SetRit(0) self.MakeFilterButtons(1800, 2000, 2200, 2500, 2800, 3300) self.OnBtnFilter(None, 2800) elif mode == conf.add_extern_demod: # External demodulation QS.set_rx_mode(6) self.SetRit(0) self.MakeFilterButtons(10000, 12000, 15000, 25000, 35000, 45000) self.OnBtnFilter(None, 12000) application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp def OnBtnBand(self, event): band = self.lastBand # former band in use try: f1, f2 = conf.BandEdge[band] if f1 <= self.VFO + self.txFreq <= f2: self.bandState[band] = (self.VFO, self.txFreq, self.mode) except KeyError: pass btn = event.GetEventObject() band = btn.GetLabel() # new band self.lastBand = band try: vfo, tune, mode = self.bandState[band] except KeyError: vfo, tune, mode = (0, 0, 'LSB') if band == '60': freq = vfo + tune if btn.direction: vfo = self.VFO if 5100000 < vfo < 5600000: if btn.direction > 0: # Move up for f in self.freq60: if f > vfo + self.txFreq: freq = f break else: freq = self.freq60[0] else: # move down l = list(self.freq60) l.reverse() for f in l: if f < vfo + self.txFreq: freq = f break else: freq = self.freq60[-1] half = self.sample_rate / 2 * self.graph_width / self.data_width while freq - vfo <= -half + 1000: vfo -= 10000 while freq - vfo >= +half - 5000: vfo += 10000 tune = freq - vfo elif band == 'Time': vfo, tune, mode = conf.bandTime[btn.index] self.OnBtnMode(None, mode) ampl, phase = self.GetAmplPhase() QS.set_ampl_phase(ampl, phase) self.txFreq = self.VFO = -1 # demand change self.ChangeHwFrequency(tune, vfo, 'BtnBand', band=band) Hardware.ChangeBand(band) def OnBtnUpDnBandDelta(self, event): btn = event.GetEventObject() if btn.direction > 0: # left button was used, move a bit d = max(10000, int(self.sample_rate / 9)) else: # right button was used, move to edge d = max(10000, int(self.sample_rate * 49 / 100)) d = (d / 10000) * 10000 return d def OnBtnDownBand(self, event): self.band_up_down = 1 d = self.OnBtnUpDnBandDelta(event) self.DisplayVFO(self.VFO - d, self.txFreq + d) def OnBtnUpBand(self, event): self.band_up_down = 1 d = self.OnBtnUpDnBandDelta(event) self.DisplayVFO(self.VFO + d, self.txFreq - d) def OnBtnUpDnBandDone(self, event): self.band_up_down = 0 tune = self.txFreq vfo = self.VFO self.txFreq = self.VFO = 0 # Force an update self.ChangeHwFrequency(tune, vfo, 'BtnUpDown') def GetAmplPhase(self): if self.bandAmplPhase.has_key("panadapter"): return self.bandAmplPhase["panadapter"] try: return self.bandAmplPhase[self.lastBand] except KeyError: return (0.0, 0.0) def SetAmplPhase(self, ampl, phase): if self.bandAmplPhase.has_key("panadapter"): self.bandAmplPhase["panadapter"] = (ampl, phase) else: self.bandAmplPhase[self.lastBand] = (ampl, phase) QS.set_ampl_phase(ampl, phase) def PostStartup(self): # called once after sound attempts to start self.config_screen.OnGraphData(None) # update config in case sound is not running def OnReadSound(self): # called at frequent intervals self.timer = time.time() if self.screen == self.scope: data = QS.get_graph(0) # get raw data if data: self.scope.OnGraphData(data) # Send message to draw new data return 1 # we got new scope data else: data = QS.get_graph(1) # get FFT data if data: #T('') self.NewSmeter() # update the S-meter if self.screen == self.graph: self.waterfall.OnGraphData(data) # save waterfall data self.graph.OnGraphData(data) # Send message to draw new data else: self.screen.OnGraphData(data) # Send message to draw new data #T('graph data') #application.Yield() #T('Yield') return 1 # We got new graph/scope data if QS.get_overrange(): self.clip_time0 = self.timer self.freqDisplay.Clip(1) if self.clip_time0: if self.timer - self.clip_time0 > 1.0: self.clip_time0 = 0 self.freqDisplay.Clip(0) if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks self.heart_time0 = self.timer Hardware.HeartBeat() if not self.band_up_down: self.ChangeHwFrequency(None, None) # poll for changed frequency def main(): """If quisk is installed as a package, you can run it with quisk.main().""" App() application.MainLoop() if __name__ == '__main__': main() charleston-1.0/filter_display/3.4.8/original_3.4.8_quisk.py0000755000175000017500000025520711426315113022061 0ustar tfoxtfox#! /usr/bin/python # All QUISK software is Copyright (C) 2006-2010 by James C. Ahlstrom. # This free software is licensed for use under the GNU General Public # License (GPL), see http://www.opensource.org. # Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!! """The main program for Quisk, a software defined radio. Usage: python quisk.py [-c | --config config_file_path] This can also be installed as a package and run as quisk.main(). """ # Change to the directory of quisk.py. This is necessary to import Quisk packages # and to load other extension modules that link against _quisk.so. It also helps to # find ./__init__.py and ./help.html. import sys, os os.chdir(os.path.normpath(os.path.dirname(__file__))) import wx, wx.html, wx.lib.buttons, wx.lib.stattext, wx.lib.colourdb import math, cmath, time, traceback import threading, pickle import _quisk as QS from types import * # Command line parsing: be able to specify the config file. from optparse import OptionParser parser = OptionParser() parser.add_option('-c', '--config', dest='config_file_path', help='Specify the configuration file path') argv_options = parser.parse_args()[0] # These FFT sizes have multiple small factors, and are prefered for efficiency: fftPreferedSizes = (416, 448, 480, 512, 576, 640, 672, 704, 768, 800, 832, 864, 896, 960, 1024, 1056, 1120, 1152, 1248, 1280, 1344, 1408, 1440, 1536, 1568, 1600, 1664, 1728, 1760, 1792, 1920, 2016, 2048, 2080, 2112, 2240, 2304, 2400, 2464, 2496, 2560, 2592, 2688, 2816, 2880, 2912) class Timer: """Debug: measure and print times every ptime seconds. Call with msg == '' to start timer, then with a msg to record the time. """ def __init__(self, ptime = 1.0): self.ptime = ptime # frequency to print in seconds self.time0 = 0 # time zero; measure from this time self.time_print = 0 # last time data was printed self.timers = {} # one timer for each msg self.names = [] # ordered list of msg self.heading = 1 # print heading on first use def __call__(self, msg): tm = time.time() if msg: if not self.time0: # Not recording data return if self.timers.has_key(msg): count, average, highest = self.timers[msg] else: self.names.append(msg) count = 0 average = highest = 0.0 count += 1 delta = tm - self.time0 average += delta if highest < delta: highest = delta self.timers[msg] = (count, average, highest) if tm - self.time_print > self.ptime: # time to print results self.time0 = 0 # end data recording, wait for reset self.time_print = tm if self.heading: self.heading = 0 print "count, msg, avg, max (msec)" print "%4d" % count, for msg in self.names: # keep names in order count, average, highest = self.timers[msg] if not count: continue average /= count print " %s %7.3f %7.3f" % (msg, average * 1e3, highest * 1e3), self.timers[msg] = (0, 0.0, 0.0) print else: # reset the time to zero self.time0 = tm # Start timer if not self.time_print: self.time_print = tm ## T = Timer() # Make a timer instance class SoundThread(threading.Thread): """Create a second (non-GUI) thread to read, process and play sound.""" def __init__(self): self.do_init = 1 threading.Thread.__init__(self) self.doQuit = threading.Event() self.doQuit.clear() def run(self): """Read, process, play sound; then notify the GUI thread to check for FFT data.""" if self.do_init: # Open sound using this thread self.do_init = 0 QS.start_sound() wx.CallAfter(application.PostStartup) while not self.doQuit.isSet(): QS.read_sound() wx.CallAfter(application.OnReadSound) QS.close_sound() def stop(self): """Set a flag to indicate that the sound thread should end.""" self.doQuit.set() class FrequencyDisplay(wx.lib.stattext.GenStaticText): """Create a frequency display widget.""" def __init__(self, frame, gbs, width, height): wx.lib.stattext.GenStaticText.__init__(self, frame, -1, '3', style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE|wx.RAISED_BORDER) border = 4 for points in range(30, 6, -1): font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(font) w, h = self.GetTextExtent('333 444 555 Hz') if w < width and h < height - border * 2: break self.SetSizeHints(w, h, w * 5, h) self.height = h self.points = points border = self.border = (height - self.height) / 2 self.height_and_border = h + border * 2 self.SetBackgroundColour(conf.color_freq) gbs.Add(self, (0, 0), (1, 3), flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=border) def Clip(self, clip): """Change color to indicate clipping.""" if clip: self.SetBackgroundColour('deep pink') else: self.SetBackgroundColour(conf.color_freq) def Display(self, freq): """Set the frequency to be displayed.""" freq = int(freq) if freq >= 0: t = str(freq) minus = '' else: t = str(-freq) minus = '- ' l = len(t) if l > 9: txt = "%s%s %s %s %s" % (minus, t[0:-9], t[-9:-6], t[-6:-3], t[-3:]) elif l > 6: txt = "%s%s %s %s" % (minus, t[0:-6], t[-6:-3], t[-3:]) elif l > 3: txt = "%s%s %s" % (minus, t[0:-3], t[-3:]) else: txt = minus + t self.SetLabel('%s Hz' % txt) class SliderBoxV(wx.BoxSizer): """A vertical box containing a slider and a text heading""" # Note: A vertical wx slider has the max value at the bottom. This is # reversed for this control. def __init__(self, parent, text, init, themax, handler, display=False): wx.BoxSizer.__init__(self, wx.VERTICAL) self.slider = wx.Slider(parent, -1, init, 0, themax, style=wx.SL_VERTICAL) self.slider.Bind(wx.EVT_SCROLL, handler) sw, sh = self.slider.GetSize() self.text = text self.themax = themax if display: # Display the slider value when it is thumb'd self.text_ctrl = wx.StaticText(parent, -1, str(themax), style=wx.ALIGN_CENTER) w1, h1 = self.text_ctrl.GetSize() # Measure size with max number self.text_ctrl.SetLabel(text) w2, h2 = self.text_ctrl.GetSize() # Measure size with text self.width = max(w1, w2, sw) self.text_ctrl.SetSizeHints(self.width, -1, self.width) self.slider.Bind(wx.EVT_SCROLL_THUMBTRACK, self.Change) self.slider.Bind(wx.EVT_SCROLL_THUMBRELEASE, self.ChangeDone) else: self.text_ctrl = wx.StaticText(parent, -1, text) w2, h2 = self.text_ctrl.GetSize() # Measure size with text self.width = max(w2, sw) self.Add(self.text_ctrl, 0, wx.ALIGN_CENTER) self.Add(self.slider, 1, wx.ALIGN_CENTER) def Change(self, event): event.Skip() self.text_ctrl.SetLabel(str(self.themax - self.slider.GetValue())) def ChangeDone(self, event): event.Skip() self.text_ctrl.SetLabel(self.text) def GetValue(self): return self.themax - self.slider.GetValue() def SetValue(self, value): # Set slider visual position; does not call handler self.slider.SetValue(self.themax - value) # Start of our button classes. They are compatible with wxPython GenButton # buttons. Use the usual methods for access: # GetLabel(self), SetLabel(self, label): Get and set the label # Enable(self, flag), Disable(self), IsEnabled(self): Enable / Disable # GetValue(self), SetValue(self, value): Get / Set check button state True / False # SetIndex(self, index): For cycle buttons, set the label from its index class QuiskButtons: """Base class for special buttons.""" button_bezel = 3 # size of button bezel in pixels def InitButtons(self, text): self.SetBezelWidth(self.button_bezel) self.SetBackgroundColour(conf.color_btn) self.SetUseFocusIndicator(False) self.font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) if text: w, h = self.GetTextExtent(text) else: w, h = self.GetTextExtent("OK") self.Disable() # create a size for null text, but Disable() w += self.button_bezel * 2 + self.GetCharWidth() h = h * 12 / 10 h += self.button_bezel * 2 self.SetSizeHints(w, h, w * 6, h, 1, 1) def OnKeyDown(self, event): pass def OnKeyUp(self, event): pass class QuiskPushbutton(QuiskButtons, wx.lib.buttons.GenButton): """A plain push button widget.""" def __init__(self, parent, command, text, use_right=False): wx.lib.buttons.GenButton.__init__(self, parent, -1, text) self.command = command self.Bind(wx.EVT_BUTTON, self.OnButton) self.InitButtons(text) self.direction = 1 if use_right: self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) def OnButton(self, event): if self.command: self.command(event) def OnRightDown(self, event): self.direction = -1 self.OnLeftDown(event) def OnRightUp(self, event): self.OnLeftUp(event) self.direction = 1 class QuiskRepeatbutton(QuiskButtons, wx.lib.buttons.GenButton): """A push button that repeats when held down.""" def __init__(self, parent, command, text, up_command=None, use_right=False): wx.lib.buttons.GenButton.__init__(self, parent, -1, text) self.command = command self.up_command = up_command self.timer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.OnTimer) self.Bind(wx.EVT_BUTTON, self.OnButton) self.InitButtons(text) self.repeat_state = 0 # repeater button inactive self.direction = 1 if use_right: self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) def SendCommand(self, command): if command: event = wx.PyEvent() event.SetEventObject(self) command(event) def OnLeftDown(self, event): if self.IsEnabled(): self.shift = event.ShiftDown() self.control = event.ControlDown() self.SendCommand(self.command) self.repeat_state = 1 # first button push self.timer.Start(milliseconds=300, oneShot=True) wx.lib.buttons.GenButton.OnLeftDown(self, event) def OnLeftUp(self, event): if self.IsEnabled(): self.SendCommand(self.up_command) self.repeat_state = 0 self.timer.Stop() wx.lib.buttons.GenButton.OnLeftUp(self, event) def OnRightDown(self, event): if self.IsEnabled(): self.shift = event.ShiftDown() self.control = event.ControlDown() self.direction = -1 self.OnLeftDown(event) def OnRightUp(self, event): if self.IsEnabled(): self.OnLeftUp(event) self.direction = 1 def OnTimer(self, event): if self.repeat_state == 1: # after first push, turn on repeats self.timer.Start(milliseconds=150, oneShot=False) self.repeat_state = 2 if self.repeat_state: # send commands until button is released self.SendCommand(self.command) def OnButton(self, event): pass # button command not used class QuiskCheckbutton(QuiskButtons, wx.lib.buttons.GenToggleButton): """A button that pops up and down, and changes color with each push.""" # Check button; get the checked state with self.GetValue() def __init__(self, parent, command, text, color=None): wx.lib.buttons.GenToggleButton.__init__(self, parent, -1, text) self.InitButtons(text) self.Bind(wx.EVT_BUTTON, self.OnButton) self.button_down = 0 # used for radio buttons self.command = command if color is None: self.color = conf.color_check_btn else: self.color = color def SetValue(self, value, do_cmd=False): wx.lib.buttons.GenToggleButton.SetValue(self, value) self.button_down = value if value: self.SetBackgroundColour(self.color) else: self.SetBackgroundColour(conf.color_btn) if do_cmd and self.command: event = wx.PyEvent() event.SetEventObject(self) self.command(event) def OnButton(self, event): if self.GetValue(): self.SetBackgroundColour(self.color) else: self.SetBackgroundColour(conf.color_btn) if self.command: self.command(event) class QuiskCycleCheckbutton(QuiskCheckbutton): """A button that cycles through its labels with each push. The button is up for labels[0], down for all other labels. Change to the next label for each push. If you call SetLabel(), the label must be in the list. The self.index is the index of the current label. """ def __init__(self, parent, command, labels, color=None, is_radio=False): self.labels = list(labels) # Be careful if you change this list self.index = 0 # index of selected label 0, 1, ... self.direction = 0 # 1 for up, -1 for down, 0 for no change to index self.is_radio = is_radio # Is this a radio cycle button? if color is None: color = conf.color_cycle_btn QuiskCheckbutton.__init__(self, parent, command, labels[0], color) self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) def SetLabel(self, label, do_cmd=False): self.index = self.labels.index(label) QuiskCheckbutton.SetLabel(self, label) QuiskCheckbutton.SetValue(self, self.index) if do_cmd and self.command: event = wx.PyEvent() event.SetEventObject(self) self.command(event) def SetIndex(self, index, do_cmd=False): self.index = index QuiskCheckbutton.SetLabel(self, self.labels[index]) QuiskCheckbutton.SetValue(self, index) if do_cmd and self.command: event = wx.PyEvent() event.SetEventObject(self) self.command(event) def OnButton(self, event): if not self.is_radio or self.button_down: self.direction = 1 self.index += 1 if self.index >= len(self.labels): self.index = 0 self.SetIndex(self.index) else: self.direction = 0 if self.command: self.command(event) def OnRightDown(self, event): # Move left in the list of labels if not self.is_radio or self.GetValue(): self.index -= 1 if self.index < 0: self.index = len(self.labels) - 1 self.SetIndex(self.index) self.direction = -1 if self.command: self.command(event) class RadioButtonGroup: """This class encapsulates a group of radio buttons. This class is not a button! The "labels" is a list of labels for the toggle buttons. An item of labels can be a list/tuple, and the corresponding button will be a cycle button. """ def __init__(self, parent, command, labels, default): self.command = command self.buttons = [] self.button = None for text in labels: if type(text) in (ListType, TupleType): b = QuiskCycleCheckbutton(parent, self.OnButton, text, is_radio=True) for t in text: if t == default and self.button is None: b.SetLabel(t) self.button = b else: b = QuiskCheckbutton(parent, self.OnButton, text) if text == default and self.button is None: b.SetValue(True) self.button = b self.buttons.append(b) def SetLabel(self, label, do_cmd=False): self.button = None for b in self.buttons: if self.button is not None: b.SetValue(False) elif isinstance(b, QuiskCycleCheckbutton): try: index = b.labels.index(label) except ValueError: b.SetValue(False) continue else: b.SetIndex(index) self.button = b b.SetValue(True) elif b.GetLabel() == label: b.SetValue(True) self.button = b else: b.SetValue(False) if do_cmd and self.command and self.button: event = wx.PyEvent() event.SetEventObject(self.button) self.command(event) def GetButtons(self): return self.buttons def OnButton(self, event): win = event.GetEventObject() for b in self.buttons: if b is win: self.button = b b.SetValue(True) else: b.SetValue(False) if self.command: self.command(event) def GetLabel(self): if not self.button: return None return self.button.GetLabel() def GetSelectedButton(self): # return the selected button return self.button class ConfigScreen(wx.ScrolledWindow): """Display the configuration and status screen.""" def __init__(self, parent, width, fft_size): wx.ScrolledWindow.__init__(self, parent, pos = (0, 0), size = (width, 100), style = wx.VSCROLL | wx.NO_BORDER) self.SetBackgroundColour(conf.color_graph) self.Bind(wx.EVT_PAINT, self.OnPaint) self.width = width self.setscroll = True self.fft_size = fft_size self.interupts = 0 self.read_error = -1 self.write_error = -1 self.underrun_error = -1 self.fft_error = -1 self.latencyCapt = -1 self.latencyPlay = -1 self.y_scale = 0 self.y_zero = 0 self.rate_min = -1 self.rate_max = -1 self.chan_min = -1 self.chan_max = -1 self.mic_max_display = 0 self.w_phase = None self.err_msg = "No response" self.msg1 = "" self.tabstops = [1] self.tabstops.append(self.tabstops[-1] + 18) self.tabstops.append(self.tabstops[-1] + 2) self.tabstops.append(self.tabstops[-1] + 20) self.tabstops.append(self.tabstops[-1] + 2) self.tabstops.append(self.tabstops[-1] + 17) self.tabstops.append(self.tabstops[-1] + 2) self.tabstops.append(self.tabstops[-1] + 17) self.tabstops.append(self.tabstops[-1] + 2) points = 24 while points > 4: self.font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) charx = self.charx = self.GetCharWidth() chary = self.chary = self.GetCharHeight() if self.tabstops[-1] * charx < width: break points -= 2 for i in range(len(self.tabstops)): self.tabstops[i] *= charx self.dy = chary # line spacing def OnPaint(self, event): dc = wx.PaintDC(self) dc.SetFont(self.font) dc.SetTextForeground('Black') x0 = self.tabstops[0] x, y = self.GetViewStart() self.y = -y + self.dy # blank line at top self.row = 1 p = conf.name_of_sound_play if p: p = "Output to " + p else: p = "Output to (None)" self.MakeRow(dc, 'Interrupts', self.interupts, p, None, None, None, 'Play rate', conf.playback_rate) self.MakeRow(dc, 'Minimum rate', self.rate_min, 'Maximum rate', self.rate_max, 'Min channels', self.chan_min, 'Max channels', self.chan_max) self.MakeRow(dc, 'Capture errors', self.read_error, 'Playback errors', self.write_error, 'Underrun errors', self.underrun_error, 'FFT errors', self.fft_error) self.MakeRow(dc, 'Capture latency', self.latencyCapt, 'Playback latency', self.latencyPlay, 'Total latency', self.latencyCapt + self.latencyPlay, 'FFT points', self.fft_size) self.y += self.dy * 5 / 10 # extra half line if self.err_msg: # Error message dc.SetTextForeground('Red') dc.DrawText(self.err_msg, x0, self.y) # fill='#F00') dc.SetTextForeground('Black') self.y += self.dy if self.msg1: dc.DrawText(self.msg1, x0, self.y) self.y += self.dy t = "Capture rate %d %s" % (application.sample_rate, application.config_text) if application.sound_error: dc.SetTextForeground('Red') dc.DrawText(t, x0, self.y) dc.SetTextForeground('Black') else: dc.DrawText(t, x0, self.y) self.y += self.dy if conf.config_file_exists: t = "Using configuration file %s" % conf.config_file_path else: t = "Configuration file %s was not found" % conf.config_file_path dc.DrawText(t, x0, self.y) self.y += self.dy name = conf.microphone_name if name: if self.mic_max_display > -0.1: t = "Microphone %s maximum level %3.0f db CLIP" % (name, self.mic_max_display) dc.SetTextForeground('Red') dc.DrawText(t, x0, self.y) dc.SetTextForeground('Black') else: t = "Microphone %s maximum level %3.0f db" % (name, self.mic_max_display) dc.DrawText(t, x0, self.y) else: t = "The microphone is not used (null name)." dc.DrawText(t, x0, self.y) self.height = self.y + 2 * self.dy if self.setscroll: self.setscroll = False sp = self.chary # Make controls # Button for phase adjust dialog t = wx.StaticText(self, -1, "Sound card phase", pos=(x0, self.height)) x, y = t.GetSizeTuple() self.phase = wx.Button(self, -1, "Adjust...") self.Bind(wx.EVT_BUTTON, self.OnBtnPhase, self.phase) x1, y1 = self.phase.GetSizeTuple() yoff = (y1 - y) / 2 self.phase.SetPosition((x0 + x + sp, self.height - yoff)) # Choice (combo) box for decimation lst = Hardware.VarDecimGetChoices() if lst: txt = Hardware.VarDecimGetLabel() x2 = self.width / 2 t = wx.StaticText(self, -1, txt, pos=(x2, self.height)) x, y = t.GetSizeTuple() c = wx.Choice(self, -1, pos=(x2 + x + sp, self.height - yoff), choices=lst) self.Bind(wx.EVT_CHOICE, application.OnBtnDecimation, c) index = Hardware.VarDecimGetIndex() c.SetSelection(index) self.height += y1 # The height is now known; set scroll size self.SetScrollbars(1, 1, self.width, self.height + 2 * self.dy) def MakeRow(self, dc, *args): y = self.y for col in range(len(args)): x = self.tabstops[col] t = args[col] if t is not None: t = str(t) if col % 2 == 1: w, h = dc.GetTextExtent(t) x -= w dc.DrawText(t, x, y) self.row += 1 self.y += self.dy def OnGraphData(self, data=None): (self.rate_min, self.rate_max, sample_rate, self.chan_min, self.chan_max, self.msg1, self.unused, self.err_msg, self.read_error, self.write_error, self.underrun_error, self.latencyCapt, self.latencyPlay, self.interupts, self.fft_error, self.mic_max_display, self.data_poll_usec ) = QS.get_state() self.mic_max_display = 20.0 * math.log10((self.mic_max_display + 1) / 32767.0) self.Refresh() def ChangeYscale(self, y_scale): pass def ChangeYzero(self, y_zero): pass def OnIdle(self, event): pass def SetTxFreq(self, freq): pass def OnBtnPhase(self, event): application.screenBtnGroup.SetLabel('Graph', do_cmd=True) if self.w_phase: self.w_phase.Raise() else: self.w_phase = QAdjustPhase(self, self.width) def OnPhaseClose(self, event): self.w_phase.Destroy() self.w_phase = None class GraphDisplay(wx.Window): """Display the FFT graph within the graph screen.""" def __init__(self, parent, x, y, graph_width, height, chary): wx.Window.__init__(self, parent, pos = (x, y), size = (graph_width, height), style = wx.NO_BORDER) self.parent = parent self.chary = chary self.graph_width = graph_width self.line = [(0, 0), (1,1)] # initial fake graph data self.SetBackgroundColour(conf.color_graph) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown) self.Bind(wx.EVT_RIGHT_DOWN, parent.OnRightDown) self.Bind(wx.EVT_LEFT_UP, parent.OnLeftUp) self.Bind(wx.EVT_MOTION, parent.OnMotion) self.Bind(wx.EVT_MOUSEWHEEL, parent.OnWheel) self.tune_x = graph_width / 2 self.scale = 20 # pixels per 10 dB self.height = 10 self.y_min = 1000 self.y_max = 0 self.max_height = application.screen_height self.tuningPen = wx.Pen('Red', 1) self.backgroundPen = wx.Pen(self.GetBackgroundColour(), 1) self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID) def OnPaint(self, event): #print 'GraphDisplay', self.GetUpdateRegion().GetBox() dc = wx.PaintDC(self) dc.SetPen(wx.BLACK_PEN) dc.DrawLines(self.line) x = self.tune_x dc.SetPen(self.tuningPen) dc.DrawLine(x, 0, x, self.max_height) if not self.parent.in_splitter: dc.SetPen(self.horizPen) chary = self.chary y = self.zeroDB for i in range(0, -99999, -10): if y >= chary / 2: dc.DrawLine(0, y, self.graph_width, y) # y line y = y + self.scale if y > self.height: break def SetHeight(self, height): self.height = height self.SetSize((self.graph_width, height)) def OnGraphData(self, data): line = [] x = 0 y_min = 1000 y_max = 0 for y in data: # y is in dB, -130 to 0 y = self.zeroDB - int(y * self.scale / 10.0 + 0.5) if y > y_max: y_max = y if y < y_min: y_min = y line.append((x, y)) x = x + 1 ymax = max(y_max, self.y_max) ymin = min(y_min, self.y_min) rect = wx.Rect(0, ymin, 1000, ymax - ymin) self.y_min = y_min self.y_max = y_max self.line = line self.Refresh() #rect=rect) def SetTuningLine(self, x): dc = wx.ClientDC(self) dc.SetPen(self.backgroundPen) dc.DrawLine(self.tune_x, 0, self.tune_x, self.max_height) dc.SetPen(self.tuningPen) dc.DrawLine(x, 0, x, self.max_height) self.tune_x = x class GraphScreen(wx.Window): """Display the graph screen X and Y axis, and create a graph display.""" def __init__(self, parent, data_width, graph_width, in_splitter=0): wx.Window.__init__(self, parent, pos = (0, 0)) self.in_splitter = in_splitter # Are we in the top of a splitter window? if in_splitter: self.y_scale = conf.waterfall_graph_y_scale self.y_zero = conf.waterfall_graph_y_zero else: self.y_scale = conf.graph_y_scale self.y_zero = conf.graph_y_zero self.VFO = 0 self.WheelMod = 50 # Round frequency when using mouse wheel self.txFreq = 0 self.sample_rate = application.sample_rate self.data_width = data_width self.graph_width = graph_width self.doResize = False self.pen_tick = wx.Pen("Black", 1, wx.SOLID) self.font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) w = self.GetCharWidth() * 14 / 10 h = self.GetCharHeight() self.charx = w self.chary = h self.tick = max(2, h * 3 / 10) self.originX = w * 5 self.offsetY = h + self.tick self.width = self.originX + self.graph_width + self.tick + self.charx * 2 self.height = application.screen_height * 3 / 10 self.x0 = self.originX + self.graph_width / 2 # center of graph self.tuningX = self.x0 self.originY = 10 self.zeroDB = 10 # y location of zero dB; may be above the top of the graph self.scale = 10 self.SetSize((self.width, self.height)) self.SetSizeHints(self.width, 1, self.width) self.SetBackgroundColour(conf.color_graph) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_MOTION, self.OnMotion) self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) self.MakeDisplay() def MakeDisplay(self): self.display = GraphDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) self.display.zeroDB = self.zeroDB def OnPaint(self, event): dc = wx.PaintDC(self) if not self.in_splitter: dc.SetFont(self.font) self.MakeYTicks(dc) self.MakeXTicks(dc) def OnIdle(self, event): if self.doResize: self.ResizeGraph() def OnSize(self, event): self.doResize = True event.Skip() def ResizeGraph(self): """Change the height of the graph. Changing the width interactively is not allowed because the FFT size is fixed. Call after changing the zero or scale to recalculate the X and Y axis marks. """ w, h = self.GetClientSize() if self.in_splitter: # Splitter window has no X axis scale self.height = h self.originY = h else: self.height = h - self.chary # Leave space for X scale self.originY = self.height - self.offsetY self.MakeYScale() self.display.SetHeight(self.originY) self.display.scale = self.scale self.doResize = False self.Refresh() def ChangeYscale(self, y_scale): self.y_scale = y_scale self.doResize = True def ChangeYzero(self, y_zero): self.y_zero = y_zero self.doResize = True def MakeYScale(self): chary = self.chary scale = (self.originY - chary) * 10 / (self.y_scale + 20) # Number of pixels per 10 dB scale = max(1, scale) q = (self.originY - chary ) / scale / 2 zeroDB = chary + q * scale - self.y_zero * scale / 10 if zeroDB > chary: zeroDB = chary self.scale = scale self.zeroDB = zeroDB self.display.zeroDB = self.zeroDB QS.record_graph(self.originX, self.zeroDB, self.scale) def MakeYTicks(self, dc): chary = self.chary x1 = self.originX - self.tick * 3 # left of tick mark x2 = self.originX - 1 # x location of y axis x3 = self.originX + self.graph_width # end of graph data dc.SetPen(self.pen_tick) dc.DrawLine(x2, 0, x2, self.originY + 1) # y axis y = self.zeroDB for i in range(0, -99999, -10): if y >= chary / 2: dc.SetPen(self.pen_tick) dc.DrawLine(x1, y, x2, y) # y tick t = `i` w, h = dc.GetTextExtent(t) dc.DrawText(`i`, x1 - w, y - h / 2) # y text y = y + self.scale if y > self.originY: break def MakeXTicks(self, dc): originY = self.originY x3 = self.originX + self.graph_width # end of fft data charx , z = dc.GetTextExtent('-30000XX') tick0 = self.tick tick1 = tick0 * 2 tick2 = tick0 * 3 # Draw the X axis dc.SetPen(self.pen_tick) dc.DrawLine(self.originX, originY, x3, originY) # Draw the band plan colors below the X axis x = self.originX f = float(x - self.x0) * self.sample_rate / self.data_width c = None y = originY + 1 for freq, color in conf.BandPlan: freq -= self.VFO if f < freq: xend = int(self.x0 + float(freq) * self.data_width / self.sample_rate + 0.5) if c is not None: dc.SetPen(wx.TRANSPARENT_PEN) dc.SetBrush(wx.Brush(c)) dc.DrawRectangle(x, y, min(x3, xend) - x, tick0) # x axis if xend >= x3: break x = xend f = freq c = color stick = 1000 # small tick in Hertz mtick = 5000 # medium tick ltick = 10000 # large tick # check the width of the frequency label versus frequency span df = charx * self.sample_rate / self.data_width if df < 5000: tfreq = 5000 # tick frequency for labels elif df < 10000: tfreq = 10000 elif df < 20000: tfreq = 20000 elif df < 50000: tfreq = 50000 stick = 5000 mtick = 10000 ltick = 50000 else: tfreq = 100000 stick = 5000 mtick = 10000 ltick = 50000 # Draw the X axis ticks and frequency in kHz dc.SetPen(self.pen_tick) freq1 = self.VFO - self.sample_rate / 2 freq1 = (freq1 / stick) * stick freq2 = freq1 + self.sample_rate + stick + 1 y_end = 0 for f in range (freq1, freq2, stick): x = self.x0 + int(float(f - self.VFO) / self.sample_rate * self.data_width) if self.originX <= x <= x3: if f % ltick is 0: # large tick dc.DrawLine(x, originY, x, originY + tick2) elif f % mtick is 0: # medium tick dc.DrawLine(x, originY, x, originY + tick1) else: # small tick dc.DrawLine(x, originY, x, originY + tick0) if f % tfreq is 0: # place frequency label t = str(f/1000) w, h = dc.GetTextExtent(t) dc.DrawText(t, x - w / 2, originY + tick2) y_end = originY + tick2 + h if y_end: # mark the center of the display dc.DrawLine(self.x0, y_end, self.x0, application.screen_height) def OnGraphData(self, data): i1 = (self.data_width - self.graph_width) / 2 i2 = i1 + self.graph_width self.display.OnGraphData(data[i1:i2]) def SetVFO(self, vfo): self.VFO = vfo self.doResize = True def SetTxFreq(self, freq): self.txFreq = freq x = self.x0 + int(float(freq) / self.sample_rate * self.data_width) self.display.SetTuningLine(x - self.originX) self.tuningX = x def GetMousePosition(self, event): """For mouse clicks in our display, translate to our screen coordinates.""" mouse_x, mouse_y = event.GetPositionTuple() win = event.GetEventObject() if win is not self: x, y = win.GetPositionTuple() mouse_x += x mouse_y += y return mouse_x, mouse_y def OnRightDown(self, event): mouse_x, mouse_y = self.GetMousePosition(event) freq = float(mouse_x - self.x0) * self.sample_rate / self.data_width freq = int(freq) if self.VFO > 0: vfo = self.VFO + freq vfo = (vfo + 5000) / 10000 * 10000 # round to even number tune = freq + self.VFO - vfo self.ChangeHwFrequency(tune, vfo, 'MouseBtn3', event) def OnLeftDown(self, event): mouse_x, mouse_y = self.GetMousePosition(event) self.mouse_x = mouse_x if mouse_y > self.originY: # click below X axis self.mouse_origin = self.tuningX else: # click above X axis self.mouse_origin = mouse_x freq = float(mouse_x - self.x0) * self.sample_rate / self.data_width freq = int(freq) self.ChangeHwFrequency(freq, self.VFO, 'MouseBtn1', event) self.CaptureMouse() def OnLeftUp(self, event): if self.HasCapture(): self.ReleaseMouse() def OnMotion(self, event): if event.Dragging() and event.LeftIsDown(): mouse_x, mouse_y = self.GetMousePosition(event) if conf.mouse_tune_method: # Mouse motion changes the VFO frequency x = (mouse_x - self.mouse_x) # Thanks to VK6JBL self.mouse_x = mouse_x freq = x * self.sample_rate / self.data_width freq = int(freq) self.ChangeHwFrequency(self.txFreq, self.VFO - freq, 'MouseMotion', event) else: # Mouse motion changes the tuning frequency # Frequency changes more rapidly for higher mouse Y position speed = max(10, self.originY - mouse_y) / float(self.originY) x = (mouse_x - self.mouse_x) self.mouse_x = mouse_x freq = speed * x * self.sample_rate / self.data_width freq = int(freq) self.ChangeHwFrequency(self.txFreq + freq, self.VFO, 'MouseMotion', event) def OnWheel(self, event): wm = self.WheelMod # Round frequency when using mouse wheel tune = self.txFreq + wm * event.GetWheelRotation() / event.GetWheelDelta() if tune >= 0: tune = tune / wm * wm else: # tune can be negative when the VFO is zero tune = - (- tune / wm * wm) self.ChangeHwFrequency(tune, self.VFO, 'MouseWheel', event) def ChangeHwFrequency(self, tune, vfo, source, event): application.ChangeHwFrequency(tune, vfo, source, event=event) class WaterfallDisplay(wx.Window): """Create a waterfall display within the waterfall screen.""" def __init__(self, parent, x, y, graph_width, height, margin): wx.Window.__init__(self, parent, pos = (x, y), size = (graph_width, height), style = wx.NO_BORDER) self.parent = parent self.graph_width = graph_width self.margin = margin self.height = 10 self.sample_rate = application.sample_rate self.SetBackgroundColour('Black') self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown) self.Bind(wx.EVT_RIGHT_DOWN, parent.OnRightDown) self.Bind(wx.EVT_LEFT_UP, parent.OnLeftUp) self.Bind(wx.EVT_MOTION, parent.OnMotion) self.Bind(wx.EVT_MOUSEWHEEL, parent.OnWheel) self.tune_x = graph_width / 2 self.tuningPen = wx.Pen('White', 3) self.marginPen = wx.Pen(conf.color_graph, 1) # Size of top faster scroll region is (top_key + 2) * (top_key - 1) / 2 self.top_key = 8 self.top_size = (self.top_key + 2) * (self.top_key - 1) / 2 # Make the palette pal2 = conf.waterfallPalette red = [] green = [] blue = [] n = 0 for i in range(256): if i > pal2[n+1][0]: n = n + 1 red.append((i - pal2[n][0]) * (long)(pal2[n+1][1] - pal2[n][1]) / (long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][1]) green.append((i - pal2[n][0]) * (long)(pal2[n+1][2] - pal2[n][2]) / (long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][2]) blue.append((i - pal2[n][0]) * (long)(pal2[n+1][3] - pal2[n][3]) / (long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][3]) self.red = red self.green = green self.blue = blue bmp = wx.EmptyBitmap(0, 0) bmp.x_origin = 0 self.bitmaps = [bmp] * application.screen_height def OnPaint(self, event): dc = wx.PaintDC(self) y = 0 dc.SetPen(self.marginPen) x_origin = int(float(self.VFO) / self.sample_rate * self.data_width + 0.5) for i in range(0, self.margin): dc.DrawLine(0, y, self.graph_width, y) y += 1 index = 0 if conf.waterfall_scroll_mode: # Draw the first few lines multiple times for i in range(self.top_key, 1, -1): b = self.bitmaps[index] x = b.x_origin - x_origin for j in range(0, i): dc.DrawBitmap(b, x, y) y += 1 index += 1 while y < self.height: b = self.bitmaps[index] x = b.x_origin - x_origin dc.DrawBitmap(b, x, y) y += 1 index += 1 dc.SetPen(self.tuningPen) dc.SetLogicalFunction(wx.XOR) dc.DrawLine(self.tune_x, 0, self.tune_x, self.height) def SetHeight(self, height): self.height = height self.SetSize((self.graph_width, height)) def OnGraphData(self, data, y_zero, y_scale): #T('graph start') row = '' # Make a new row of pixels for a one-line image for x in data: # x is -130 to 0, or so (dB) l = int((x + y_zero / 3 + 100) * y_scale / 10) l = max(l, 0) l = min(l, 255) row = row + "%c%c%c" % (chr(self.red[l]), chr(self.green[l]), chr(self.blue[l])) #T('graph string') bmp = wx.BitmapFromBuffer(len(row) / 3, 1, row) bmp.x_origin = int(float(self.VFO) / self.sample_rate * self.data_width + 0.5) self.bitmaps.insert(0, bmp) del self.bitmaps[-1] self.ScrollWindow(0, 1, None) self.Refresh(False, (0, 0, self.graph_width, self.top_size + self.margin)) #T('graph end') def SetTuningLine(self, x): dc = wx.ClientDC(self) dc.SetPen(self.tuningPen) dc.SetLogicalFunction(wx.XOR) dc.DrawLine(self.tune_x, 0, self.tune_x, self.height) dc.DrawLine(x, 0, x, self.height) self.tune_x = x class WaterfallScreen(wx.SplitterWindow): """Create a splitter window with a graph screen and a waterfall screen""" def __init__(self, frame, width, data_width, graph_width): self.y_scale = conf.waterfall_y_scale self.y_zero = conf.waterfall_y_zero wx.SplitterWindow.__init__(self, frame) self.SetSizeHints(width, -1, width) self.SetMinimumPaneSize(1) self.SetSize((width, conf.waterfall_graph_size + 100)) # be able to set sash size self.pane1 = GraphScreen(self, data_width, graph_width, 1) self.pane2 = WaterfallPane(self, data_width, graph_width) self.SplitHorizontally(self.pane1, self.pane2, conf.waterfall_graph_size) def OnIdle(self, event): self.pane1.OnIdle(event) self.pane2.OnIdle(event) def SetTxFreq(self, freq): self.pane1.SetTxFreq(freq) self.pane2.SetTxFreq(freq) def SetVFO(self, vfo): self.pane1.SetVFO(vfo) self.pane2.SetVFO(vfo) def ChangeYscale(self, y_scale): # Test if the shift key is down if wx.GetKeyState(wx.WXK_SHIFT): # Set graph screen self.pane1.ChangeYscale(y_scale) else: # Set waterfall screen self.y_scale = y_scale self.pane2.ChangeYscale(y_scale) def ChangeYzero(self, y_zero): # Test if the shift key is down if wx.GetKeyState(wx.WXK_SHIFT): # Set graph screen self.pane1.ChangeYzero(y_zero) else: # Set waterfall screen self.y_zero = y_zero self.pane2.ChangeYzero(y_zero) def OnGraphData(self, data): self.pane1.OnGraphData(data) self.pane2.OnGraphData(data) class WaterfallPane(GraphScreen): """Create a waterfall screen with an X axis and a waterfall display.""" def __init__(self, frame, data_width, graph_width): GraphScreen.__init__(self, frame, data_width, graph_width) self.y_scale = conf.waterfall_y_scale self.y_zero = conf.waterfall_y_zero self.oldVFO = self.VFO def MakeDisplay(self): self.display = WaterfallDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) self.display.VFO = self.VFO self.display.data_width = self.data_width def SetVFO(self, vfo): GraphScreen.SetVFO(self, vfo) self.display.VFO = vfo if self.oldVFO != vfo: self.oldVFO = vfo self.Refresh() def MakeYTicks(self, dc): pass def ChangeYscale(self, y_scale): self.y_scale = y_scale def ChangeYzero(self, y_zero): self.y_zero = y_zero def OnGraphData(self, data): i1 = (self.data_width - self.graph_width) / 2 i2 = i1 + self.graph_width self.display.OnGraphData(data[i1:i2], self.y_zero, self.y_scale) class ScopeScreen(wx.Window): """Create an oscilloscope screen (mostly used for debug).""" def __init__(self, parent, width, data_width, graph_width): wx.Window.__init__(self, parent, pos = (0, 0), size=(width, -1), style = wx.NO_BORDER) self.SetBackgroundColour(conf.color_graph) self.font = wx.Font(16, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_PAINT, self.OnPaint) self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID) self.y_scale = conf.scope_y_scale self.y_zero = conf.scope_y_zero self.running = 1 self.doResize = False self.width = width self.height = 100 self.originY = self.height / 2 self.data_width = data_width self.graph_width = graph_width w = self.charx = self.GetCharWidth() h = self.chary = self.GetCharHeight() tick = max(2, h * 3 / 10) self.originX = w * 3 self.width = self.originX + self.graph_width + tick + self.charx * 2 self.line = [(0,0), (1,1)] # initial fake graph data self.fpout = None #open("jim96.txt", "w") def OnIdle(self, event): if self.doResize: self.ResizeGraph() def OnSize(self, event): self.doResize = True event.Skip() def ResizeGraph(self, event=None): # Change the height of the graph. Changing the width interactively is not allowed. w, h = self.GetClientSize() self.height = h self.originY = h / 2 self.doResize = False self.Refresh() def OnPaint(self, event): dc = wx.PaintDC(self) dc.SetFont(self.font) self.MakeYTicks(dc) self.MakeXTicks(dc) self.MakeText(dc) dc.SetPen(wx.BLACK_PEN) dc.DrawLines(self.line) def MakeYTicks(self, dc): chary = self.chary originX = self.originX x3 = self.x3 = originX + self.graph_width # end of graph data dc.SetPen(wx.BLACK_PEN) dc.DrawLine(originX, 0, originX, self.originY * 3) # y axis # Find the size of the Y scale markings themax = 2.5e9 * 10.0 ** - ((160 - self.y_scale) / 50.0) # value at top of screen themax = int(themax) l = [] for j in (5, 6, 7, 8): for i in (1, 2, 5): l.append(i * 10 ** j) for yvalue in l: n = themax / yvalue + 1 # Number of lines ypixels = self.height / n if n < 20: break dc.SetPen(self.horizPen) for i in range(1, 1000): y = self.originY - ypixels * i if y < chary: break # Above axis dc.DrawLine(originX, y, x3, y) # y line # Below axis y = self.originY + ypixels * i dc.DrawLine(originX, y, x3, y) # y line self.yscale = float(ypixels) / yvalue self.yvalue = yvalue def MakeXTicks(self, dc): originY = self.originY x3 = self.x3 # Draw the X axis dc.SetPen(wx.BLACK_PEN) dc.DrawLine(self.originX, originY, x3, originY) # Find the size of the X scale markings in microseconds for i in (20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000): xscale = i # X scale in microseconds if application.sample_rate * xscale * 0.000001 > self.width / 30: break # Draw the X lines dc.SetPen(self.horizPen) for i in range(1, 999): x = int(self.originX + application.sample_rate * xscale * 0.000001 * i + 0.5) if x > x3: break dc.DrawLine(x, 0, x, self.height) # x line self.xscale = xscale def MakeText(self, dc): if self.running: t = " RUN" else: t = " STOP" if self.xscale >= 1000: t = "%s X: %d millisec/div" % (t, self.xscale) else: t = "%s X: %d microsec/div" % (t, self.xscale) yt = `self.yvalue` t = "%s Y: %sE%d/div" % (t, yt[0], len(yt) - 1) dc.DrawText(t, self.originX, self.height - self.chary) def OnGraphData(self, data): if not self.running: if self.fpout: for cpx in data: re = int(cpx.real) im = int(cpx.imag) ab = int(abs(cpx)) self.fpout.write("%12d %12d %12d\n" % (re, im, ab)) return # Preserve data on screen line = [] x = self.originX ymax = self.height for cpx in data: # cpx is complex raw samples +/- 0 to 2**31-1 y = cpx.real #y = abs(cpx) y = self.originY - int(y * self.yscale + 0.5) if y > ymax: y = ymax elif y < 0: y = 0 line.append((x, y)) x = x + 1 self.line = line self.Refresh() def ChangeYscale(self, y_scale): self.y_scale = y_scale self.doResize = True def ChangeYzero(self, y_zero): self.y_zero = y_zero def SetTxFreq(self, freq): pass class FilterScreen(GraphScreen): """Create a graph of the receive filter response.""" def __init__(self, parent, data_width, graph_width): GraphScreen.__init__(self, parent, data_width, graph_width) self.y_scale = conf.filter_y_scale self.y_zero = conf.filter_y_zero self.VFO = 0 self.txFreq = 0 self.data = [] self.sample_rate = QS.get_filter_rate() def NewFilter(self): self.data = QS.get_filter() def OnGraphData(self, data): GraphScreen.OnGraphData(self, self.data) def ChangeHwFrequency(self, tune, vfo, source, event): self.SetTxFreq(tune) application.freqDisplay.Display(tune) class HelpScreen(wx.html.HtmlWindow): """Create the screen for the Help button.""" def __init__(self, parent, width, height): wx.html.HtmlWindow.__init__(self, parent, -1, size=(width, height)) self.y_scale = 0 self.y_zero = 0 if "gtk2" in wx.PlatformInfo: self.SetStandardFonts() self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22]) # read in text from file help.html in the directory of this module self.LoadFile('help.html') def OnGraphData(self, data): pass def ChangeYscale(self, y_scale): pass def ChangeYzero(self, y_zero): pass def OnIdle(self, event): pass def SetTxFreq(self, freq): pass class QMainFrame(wx.Frame): """Create the main top-level window.""" def __init__(self, width, height): fp = open('__init__.py') # Read in the title title = fp.readline().strip()[1:] fp.close() wx.Frame.__init__(self, None, -1, title, wx.DefaultPosition, (width, height), wx.DEFAULT_FRAME_STYLE, 'MainFrame') self.SetBackgroundColour(conf.color_bg) self.Bind(wx.EVT_CLOSE, self.OnBtnClose) def OnBtnClose(self, event): application.OnBtnClose(event) self.Destroy() class QAdjustPhase(wx.Frame): """Create a window with amplitude and phase adjustment controls""" f_ampl = "Amplitude adjustment %.6f" f_phase = "Phase adjustment %.6f" def __init__(self, parent, width): wx.Frame.__init__(self, application.main_frame, -1, "Adjust Sound Card Amplitude and Phase", pos=(50, 100)) self.Bind(wx.EVT_CLOSE, parent.OnPhaseClose) self.ampl, self.phase = application.GetAmplPhase() self.MakeControls(width) self.Show() def MakeControls(self, width): # Make controls for phase/amplitude adjustment chary = self.GetCharHeight() y = chary * 3 / 10 self.t_ampl = wx.StaticText(self, -1, self.f_ampl % self.ampl, pos=(0, y)) y += self.t_ampl.GetSizeTuple()[1] scale = width * 4 / 10 self.scale = float(scale) self.ampl1 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.ampl1.GetSizeTuple()[1] self.ampl2 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.ampl2.GetSizeTuple()[1] self.t_phase = wx.StaticText(self, -1, self.f_phase % self.phase, pos=(0, y)) y += self.t_phase.GetSizeTuple()[1] self.phase1 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.phase1.GetSizeTuple()[1] self.phase2 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.phase2.GetSizeTuple()[1] self.SetSizeHints(width, y, width, y) # no change in size self.ampl1.Bind(wx.EVT_SCROLL, self.OnAmpl1) self.ampl2.Bind(wx.EVT_SCROLL, self.OnAmpl1) self.phase1.Bind(wx.EVT_SCROLL, self.OnAmpl1) self.phase2.Bind(wx.EVT_SCROLL, self.OnAmpl1) def OnAmpl1(self, event): s2 = self.scale * 10.0 # maximum 0.10 change s1 = s2 * 20.0 # smaller maximum change ampl = self.ampl + self.ampl1.GetValue() / s1 + self.ampl2.GetValue() / s2 self.t_ampl.SetLabel(self.f_ampl % ampl) phase = self.phase + self.phase1.GetValue() / s1 + self.phase2.GetValue() / s2 self.t_phase.SetLabel(self.f_phase % phase) application.SetAmplPhase(ampl, phase) class Spacer(wx.Window): """Create a bar between the graph screen and the controls""" def __init__(self, parent): wx.Window.__init__(self, parent, pos = (0, 0), size=(-1, 6), style = wx.NO_BORDER) self.Bind(wx.EVT_PAINT, self.OnPaint) r, g, b = parent.GetBackgroundColour().Get() dark = (r * 7 / 10, g * 7 / 10, b * 7 / 10) light = (r + (255 - r) * 5 / 10, g + (255 - g) * 5 / 10, b + (255 - b) * 5 / 10) self.dark_pen = wx.Pen(dark, 1, wx.SOLID) self.light_pen = wx.Pen(light, 1, wx.SOLID) self.width = application.screen_width def OnPaint(self, event): dc = wx.PaintDC(self) w = self.width dc.SetPen(self.dark_pen) dc.DrawLine(0, 0, w, 0) dc.DrawLine(0, 1, w, 1) dc.DrawLine(0, 2, w, 2) dc.SetPen(self.light_pen) dc.DrawLine(0, 3, w, 3) dc.DrawLine(0, 4, w, 4) dc.DrawLine(0, 5, w, 5) class App(wx.App): """Class representing the application.""" freq60 = (5330500, 5346500, 5366500, 5371500, 5403500) StateNames = [ # Names of state attributes to save and restore 'bandState', 'bandAmplPhase', 'lastBand', 'VFO', 'txFreq', 'mode', 'vardecim_set', ] def __init__(self): global application application = self self.init_path = None if sys.stdout.isatty(): wx.App.__init__(self, redirect=False) else: wx.App.__init__(self, redirect=True) def QuiskPushbutton(self, *args, **kw): # Make our buttons available to widget files return QuiskPushbutton(*args, **kw) def QuiskRepeatbutton(self, *args, **kw): return QuiskRepeatbutton(*args, **kw) def QuiskCheckbutton(self, *args, **kw): return QuiskCheckbutton(*args, **kw) def QuiskCycleCheckbutton(self, *args, **kw): return QuiskCycleCheckbutton(*args, **kw) def RadioButtonGroup(self, *args, **kw): return RadioButtonGroup(*args, **kw) def OnInit(self): """Perform most initialization of the app here (called by wxPython on startup).""" wx.lib.colourdb.updateColourDB() # Add additional color names global conf # conf is the module for all configuration data import quisk_conf_defaults as conf cpath = argv_options.config_file_path # Get config file path if not cpath: cpath = os.path.expanduser('~/.quisk_conf.py') # Default path setattr(conf, 'config_file_path', cpath) if os.path.isfile(cpath): # See if the user has a config file setattr(conf, 'config_file_exists', True) d = {} d.update(conf.__dict__) # make items from conf available execfile(cpath, d) # execute the user's config file for k, v in d.items(): # add user's config items to conf if k[0] != '_': # omit items starting with '_' setattr(conf, k, v) else: setattr(conf, 'config_file_exists', False) if conf.invertSpectrum: QS.invert_spectrum(1) self.bandState = {} self.bandState.update(conf.bandState) self.bandAmplPhase = conf.bandAmplPhase # Open hardware file global Hardware if hasattr(conf, "Hardware"): # Hardware defined in config file Hardware = conf.Hardware(self, conf) else: Hardware = conf.quisk_hardware.Hardware(self, conf) # Initialization - may be over-written by persistent state self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow self.smeter_db_count = 0 # average the S-meter self.smeter_db_sum = 0 self.smeter_db = 0 self.smeter_sunits = -87.0 self.timer = time.time() # A seconds clock self.heart_time0 = self.timer # timer to call HeartBeat at intervals self.smeter_db_time0 = self.timer self.smeter_sunits_time0 = self.timer self.band_up_down = 0 # Are band Up/Down buttons in use? self.lastBand = 'Audio' self.VFO = 0 self.ritFreq = 0 self.txFreq = 0 self.screen = None self.audio_volume = 0.0 # Set output volume, 0.0 to 1.0 self.sidetone_volume = 0.0 # Set sidetone volume, 0.0 to 1.0 self.sound_error = 0 self.sound_thread = None self.mode = conf.default_mode self.bottom_widgets = None self.color_list = None self.color_index = 0 self.vardecim_set = None dc = wx.ScreenDC() # get the screen size (self.screen_width, self.screen_height) = dc.GetSizeTuple() del dc self.Bind(wx.EVT_IDLE, self.OnIdle) self.Bind(wx.EVT_QUERY_END_SESSION, self.OnEndSession) # Restore persistent program state if conf.persistent_state: self.init_path = os.path.join(os.path.dirname(cpath), '.quisk_init.pkl') try: fp = open(self.init_path, "rb") d = pickle.load(fp) fp.close() for k, v in d.items(): if k in self.StateNames: if k == 'bandState': self.bandState.update(v) else: setattr(self, k, v) except: pass #traceback.print_exc() for k, (vfo, tune, mode) in self.bandState.items(): # Historical: fix bad frequencies try: f1, f2 = conf.BandEdge[k] if not f1 <= vfo + tune <= f2: self.bandState[k] = conf.bandState[k] except KeyError: pass if Hardware.VarDecimGetChoices(): # Hardware can change the decimation. self.sample_rate = Hardware.VarDecimSet() # Get the sample rate. self.vardecim_set = self.sample_rate else: # Use the sample rate from the config file. self.sample_rate = conf.sample_rate if not hasattr(conf, 'playback_rate'): if conf.use_sdriq or conf.use_rx_udp: conf.playback_rate = 48000 else: conf.playback_rate = conf.sample_rate # Find the data width from a list of prefered sizes; it is the width of returned graph data. # The graph_width is the width of data_width that is displayed. width = self.screen_width * conf.graph_width percent = conf.display_fraction # display central fraction of total width percent = int(percent * 100.0 + 0.4) width = width * 100 / percent for x in fftPreferedSizes: if x > width: self.data_width = x break else: self.data_width = fftPreferedSizes[-1] self.graph_width = self.data_width * percent / 100 if self.graph_width % 2 == 1: # Both data_width and graph_width are even numbers self.graph_width += 1 # The FFT size times the average_count controls the graph refresh rate factor = float(self.sample_rate) / conf.graph_refresh / self.data_width ifactor = int(factor + 0.5) if conf.fft_size_multiplier >= ifactor: # Use large FFT and average count 1 fft_mult = ifactor average_count = 1 elif conf.fft_size_multiplier > 0: # Specified fft_size_multiplier fft_mult = conf.fft_size_multiplier average_count = int(factor / fft_mult + 0.5) if average_count < 1: average_count = 1 else: # Calculate the split between fft size and average if self.sample_rate <= 240000: maxfft = 8000 # Maximum fft size else: maxfft = 15000 fft1 = maxfft / self.data_width if fft1 >= ifactor: fft_mult = ifactor average_count = 1 else: av1 = int(factor / fft1 + 0.5) if av1 < 1: av1 = 1 err1 = factor / (fft1 * av1) av2 = av1 + 1 fft2 = int(factor / av2 + 0.5) err2 = factor / (fft2 * av2) if 0.9 < err1 < 1.1 or abs(1.0 - err1) <= abs(1.0 - err2): fft_mult = fft1 average_count = av1 else: fft_mult = fft2 average_count = av2 self.fft_size = self.data_width * fft_mult # Record the basic application parameters QS.record_app(self, conf, self.data_width, self.fft_size, average_count, self.sample_rate) #print 'FFT size %d, FFT mult %d, average_count %d' % ( # self.fft_size, self.fft_size / self.data_width, average_count) #print 'Refresh %.2f Hz' % (float(self.sample_rate) / self.fft_size / average_count) QS.record_graph(0, 0, 1.0) self.width = self.screen_width * 8 / 10 self.height = self.screen_height * 5 / 10 self.main_frame = frame = QMainFrame(self.width, self.height) self.SetTopWindow(frame) # Make all the screens and hide all but one self.graph = GraphScreen(frame, self.data_width, self.graph_width) self.screen = self.graph width = self.graph.width button_width = width # try to estimate the final button width self.config_screen = ConfigScreen(frame, width, self.fft_size) self.config_screen.Hide() self.waterfall = WaterfallScreen(frame, width, self.data_width, self.graph_width) self.waterfall.Hide() self.scope = ScopeScreen(frame, width, self.data_width, self.graph_width) self.scope.Hide() self.filter_screen = FilterScreen(frame, self.data_width, self.graph_width) self.filter_screen.Hide() self.help_screen = HelpScreen(frame, width, self.screen_height / 10) self.help_screen.Hide() frame.SetSizeHints(width, 100) # Make a vertical box to hold all the screens and the bottom box vertBox = self.vertBox = wx.BoxSizer(wx.VERTICAL) frame.SetSizer(vertBox) # Add the screens vertBox.Add(self.config_screen, 1) vertBox.Add(self.graph, 1) vertBox.Add(self.waterfall, 1) vertBox.Add(self.scope, 1) vertBox.Add(self.filter_screen, 1) vertBox.Add(self.help_screen, 1) # Add the spacer vertBox.Add(Spacer(frame), 0, wx.EXPAND) # Add the bottom box hBoxA = wx.BoxSizer(wx.HORIZONTAL) vertBox.Add(hBoxA, 0, wx.EXPAND) # End of vertical box. Add items to the horizontal box. # Add two sliders on the left margin = 3 self.sliderVol = SliderBoxV(frame, 'Vol', 300, 1000, self.ChangeVolume) button_width -= self.sliderVol.width + margin * 2 self.ChangeVolume() # set initial volume level hBoxA.Add(self.sliderVol, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) if Hardware.use_sidetone: self.sliderSto = SliderBoxV(frame, 'STo', 300, 1000, self.ChangeSidetone) button_width -= self.sliderSto.width + margin * 2 self.ChangeSidetone() hBoxA.Add(self.sliderSto, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) # Add the sizer for the middle gap = 2 gbs = wx.GridBagSizer(gap, gap) self.gbs = gbs button_width -= gap * 15 hBoxA.Add(gbs, 1, wx.EXPAND, 0) gbs.SetEmptyCellSize((5, 5)) button_width -= 5 for i in range(0, 6) + range(7, 13): gbs.AddGrowableCol(i) # Add two sliders on the right self.sliderYs = SliderBoxV(frame, 'Ys', 0, 160, self.ChangeYscale, True) button_width -= self.sliderYs.width + margin * 2 hBoxA.Add(self.sliderYs, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) self.sliderYz = SliderBoxV(frame, 'Yz', 0, 160, self.ChangeYzero, True) button_width -= self.sliderYz.width + margin * 2 hBoxA.Add(self.sliderYz, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) button_height = self.MakeButtons(frame, gbs) button_width /= 12 # This is our estimate of the final button size self.MakeTopRow(frame, gbs, button_width, button_height) if conf.quisk_widgets: self.bottom_widgets = conf.quisk_widgets.BottomWidgets(self, Hardware, conf, frame, gbs, vertBox) if QS.open_key(conf.key_method): print 'open_key failed for name "%s"' % conf.key_method if hasattr(conf, 'mixer_settings'): for dev, numid, value in conf.mixer_settings: err_msg = QS.mixer_set(dev, numid, value) if err_msg: print "Mixer", err_msg # Create transmit audio filters if conf.microphone_name: filtI, filtQ = self.MakeFilterCoef(conf.mic_sample_rate, 540, 2500, 1550) QS.set_tx_filters(filtI, filtQ, ()) # Open the hardware. This must be called before open_sound(). self.config_text = Hardware.open() if not self.config_text: self.config_text = "Missing config_text" QS.capt_channels (conf.channel_i, conf.channel_q) QS.play_channels (conf.channel_i, conf.channel_q) QS.micplay_channels (conf.mic_play_chan_I, conf.mic_play_chan_Q) # Note: Subsequent calls to set channels must not name a higher channel number. # Normally, these calls are only used to reverse the channels. QS.open_sound(conf.name_of_sound_capt, conf.name_of_sound_play, self.sample_rate, conf.data_poll_usec, conf.latency_millisecs, conf.microphone_name, conf.tx_ip, conf.tx_audio_port, conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q, conf.mic_out_volume, conf.name_of_mic_play, conf.mic_playback_rate) tune, vfo = Hardware.ReturnFrequency() # Request initial frequency to set band if tune is not None: for band, (f1, f2) in conf.BandEdge.items(): if f1 <= tune <= f2: # Change to the correct band based on frequency self.lastBand = band break self.bandBtnGroup.SetLabel(self.lastBand, do_cmd=True) self.ChangeHwFrequency(None, None) # Request initial VFO and tuning # Note: The filter rate is not valid until after the call to open_sound(). # Create FM audio filter frate = QS.get_filter_rate() # filter rate filtI, filtQ = self.MakeFmFilterCoef(frate, 600, 340, 2800) QS.set_fm_filters(filtI) # Record filter rate for the filter screen self.filter_screen.sample_rate = frate #if info[8]: # error message # self.sound_error = 1 # self.config_screen.err_msg = info[8] # print info[8] if self.sound_error: self.screenBtnGroup.SetLabel('Config', do_cmd=True) frame.Show() else: self.screenBtnGroup.SetLabel(conf.default_screen, do_cmd=True) frame.Show() self.Yield() self.sound_thread = SoundThread() self.sound_thread.start() return True def OnIdle(self, event): if self.screen: self.screen.OnIdle(event) def OnEndSession(self, event): event.Skip() self.OnBtnClose() def OnBtnClose(self, event): if self.sound_thread: self.sound_thread.stop() for i in range(0, 20): if threading.activeCount() == 1: break time.sleep(0.1) def OnExit(self): QS.close_rx_udp() Hardware.close() if self.init_path: # save current program state d = {} for n in self.StateNames: d[n] = getattr(self, n) try: fp = open(self.init_path, "wb") pickle.dump(d, fp) fp.close() except: pass #traceback.print_exc() def MakeTopRow(self, frame, gbs, button_width, button_height): # Down button b_down = QuiskRepeatbutton(frame, self.OnBtnDownBand, "Down", self.OnBtnUpDnBandDone, use_right=True) gbs.Add(b_down, (0, 4), flag=wx.ALIGN_CENTER) # RIT button self.ritButton = QuiskCheckbutton(frame, self.OnBtnRit, "RIT") gbs.Add(self.ritButton, (0, 7), flag=wx.ALIGN_CENTER) # Up button b_up = QuiskRepeatbutton(frame, self.OnBtnUpBand, "Up", self.OnBtnUpDnBandDone, use_right=True) gbs.Add(b_up, (0, 5), flag=wx.ALIGN_CENTER) bw, bh = b_down.GetMinSize() # make top row buttons the same size bw = (bw + button_width) / 2 bh = max(bh, button_height) b_down.SetSizeHints(bw, bh, bw * 5, bh) b_up.SetSizeHints(bw, bh, bw * 5, bh) self.ritButton.SetSizeHints(bw, bh, bw * 5, bh) # RIT slider self.ritScale = wx.Slider(frame, -1, self.ritFreq, -2000, 2000, size=(-1, -1), style=wx.SL_LABELS) self.ritScale.Bind(wx.EVT_SCROLL, self.OnRitScale) gbs.Add(self.ritScale, (0, 8), (1, 3), flag=wx.EXPAND) sw, sh = self.ritScale.GetSize() # Frequency display h = max(bh, sh) # larger of button and slider height self.freqDisplay = FrequencyDisplay(frame, gbs, button_width * 3, h) self.freqDisplay.Display(self.txFreq + self.VFO) # Frequency entry e = wx.TextCtrl(frame, -1, '', style=wx.TE_PROCESS_ENTER|wx.RAISED_BORDER) w, h = e.GetTextExtent('33333333') h = self.freqDisplay.height e.SetSizeHints(w, h, w * 2, h) e.SetBackgroundColour(conf.color_entry) gbs.Add(e, (0, 3), flag= wx.EXPAND | wx.TOP | wx.BOTTOM, border=self.freqDisplay.border) frame.Bind(wx.EVT_TEXT_ENTER, self.FreqEntry, source=e) # S-meter self.smeter = t = wx.lib.stattext.GenStaticText(frame, -1, '', style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE|wx.RAISED_BORDER) for points in range(20, 6, -1): font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) t.SetFont(font) w, h = t.GetTextExtent("ZZS 9 -100.00 dBZZ") if w < button_width * 2: break h = h * 12 / 10 t.SetSizeHints(w, h, -1, h) border = (self.freqDisplay.height_and_border - h) / 2 t.SetBackgroundColour(conf.color_freq) gbs.Add(t, (0, 11), (1, 2), flag=wx.ALIGN_CENTER|wx.EXPAND| wx.TOP | wx.BOTTOM, border=border) def MakeButtons(self, frame, gbs): all_buttons = [] # There are six columns, a small gap column, and then six more columns flag = wx.EXPAND ### Left bank of buttons self.bandBtnGroup = RadioButtonGroup(frame, self.OnBtnBand, conf.bandLabels, None) btns = self.bandBtnGroup.buttons all_buttons += btns i = 0 n1 = len(conf.bandLabels) / 2 n2 = len(conf.bandLabels) - n1 for col in range(0, n1): gbs.Add(btns[i], (1, col), flag=flag) i += 1 for col in range(0, n2): gbs.Add(btns[i], (2, col), flag=flag) i += 1 # Mute, AGC buttons = [] b = QuiskCheckbutton(frame, self.OnBtnMute, text='Mute') buttons.append(b) b = QuiskCycleCheckbutton(frame, self.OnBtnAGC, ('AGC', 'AGC 1', 'AGC 2')) buttons.append(b) b.SetLabel('AGC 1', True) b = QuiskCheckbutton(frame, self.OnBtnNB, text='') buttons.append(b) try: labels = Hardware.rf_gain_labels except: labels = () if labels: self.BtnRfGain = QuiskCycleCheckbutton(frame, Hardware.OnButtonRfGain, labels) buttons.append(self.BtnRfGain) else: b = QuiskCheckbutton(frame, None, text='') buttons.append(b) self.BtnRfGain = None #b = QuiskRepeatbutton(frame, self.OnBtnColor, '', use_right=True) if conf.add_fdx_button: b = QuiskCheckbutton(frame, self.OnBtnFDX, 'FDX', color=conf.color_test) else: b = QuiskCheckbutton(frame, None, text='') buttons.append(b) b = QuiskCheckbutton(frame, self.OnBtnTest1, 'Test 1', color=conf.color_test) buttons.append(b) all_buttons += buttons for col in range(0, 6): gbs.Add(buttons[col], (3, col), flag=flag) ### Right bank of buttons labels = [('CWL', 'CWU'), ('LSB', 'USB'), 'AM', 'FM', conf.add_extern_demod, ''] if conf.add_imd_button: labels[-1] = ('IMD', 'IMD -3dB', 'IMD -6dB') self.modeButns = RadioButtonGroup(frame, self.OnBtnMode, labels, None) btns = self.modeButns.GetButtons() all_buttons += btns btns[-1].color = conf.color_test for col in range(0, 6): gbs.Add(btns[col], (1, col + 7), flag=flag) labels = ('0',) * 6 self.filterButns = RadioButtonGroup(frame, self.OnBtnFilter, labels, None) btns = self.filterButns.GetButtons() all_buttons += btns for col in range(0, 6): gbs.Add(btns[col], (2, col + 7), flag=flag) labels = ('Graph', 'WFall', ('Scope', 'Scope'), 'Config', 'RX Filter', 'Help') self.screenBtnGroup = RadioButtonGroup(frame, self.OnBtnScreen, labels, conf.default_screen) btns = self.screenBtnGroup.GetButtons() all_buttons += btns for col in range(0, 6): gbs.Add(btns[col], (3, col + 7), flag=flag) bw = bh = 0 for b in all_buttons: # find the largest button size w, h = b.GetMinSize() bw = max(bw, w) bh = max(bh, h) for b in all_buttons: # set all buttons to the same size b.SetSizeHints(bw, bh, bw * 5, bh) return bh # return the button height def NewSmeter(self): #avg_seconds = 5.0 # seconds for S-meter average avg_seconds = 1.0 self.smeter_db_count += 1 # count for average x = QS.get_smeter() self.smeter_db_sum += x # sum for average if self.timer - self.smeter_db_time0 > avg_seconds: # average time reached self.smeter_db = self.smeter_db_sum / self.smeter_db_count self.smeter_db_count = self.smeter_db_sum = 0 self.smeter_db_time0 = self.timer if self.smeter_sunits < x: # S-meter moves to peak value self.smeter_sunits = x else: # S-meter decays at this time constant self.smeter_sunits -= (self.smeter_sunits - x) * (self.timer - self.smeter_sunits_time0) self.smeter_sunits_time0 = self.timer s = self.smeter_sunits / 6.0 # change to S units; 6db per S unit s += Hardware.correct_smeter # S-meter correction for the gain, band, etc. if s >= 9.5: s = (s - 9.0) * 6 t = "S9 + %.0f %.2f dB" % (s, self.smeter_db) else: t = "S %.0f %.2f dB" % (s, self.smeter_db) self.smeter.SetLabel(t) def MakeFilterButtons(self, *args): # Change the filter selections depending on the mode: CW, SSB, etc. i = 0 for b in self.filterButns.GetButtons(): b.SetLabel(str(args[i])) b.Refresh() i += 1 def MakeFilterCoef(self, rate, N, bw, center): """Make an I/Q filter with rectangular passband.""" K = bw * N / rate filtI = [] filtQ = [] pi = math.pi sin = math.sin cos = math.cos tune = 2. * pi * center / rate for k in range(-N/2, N/2 + 1): # Make a lowpass filter if k == 0: z = float(K) / N else: z = 1.0 / N * sin(pi * k * K / N) / sin(pi * k / N) # Apply a windowing function if 1: # Blackman window w = 0.42 + 0.5 * cos(2. * pi * k / N) + 0.08 * cos(4. * pi * k / N) elif 0: # Hamming w = 0.54 + 0.46 * cos(2. * pi * k / N) elif 0: # Hanning w = 0.5 + 0.5 * cos(2. * pi * k / N) else: w = 1 z *= w # Make a bandpass filter by tuning the low pass filter to new center frequency. # Make two quadrature filters. if tune: z *= 2.0 * cmath.exp(-1j * tune * k) filtI.append(z.real) filtQ.append(z.imag) else: filtI.append(z) filtQ.append(z) return filtI, filtQ def MakeFmFilterCoef(self, rate, N, f1, f2): """Make an audio filter with FM de-emphasis; remove CTCSS tones.""" bw = f2 - f1 center = (f1 + f2) / 2 N2 = N / 2 # Half the number of points K2 = bw * N / rate / 2 # Half the bandwidth in points filtI = [] filtQ = [] passb = [0] * (N + 1) # desired passband response idft = [0] * (N + 1) # inverse DFT of desired passband pi = math.pi sin = math.sin cos = math.cos tune = 2. * pi * center / rate # indexing can be from - N2 thru + N2 inclusive; total points is 2 * N2 + 1 # indexing can be from 0 thru 2 * N2 inclusive; total points is 2 * N2 + 1 for j in range(-K2, K2 + 1): # Filter shape is -6 bB per octave jj = j + N2 freq = center - bw / 2.0 * float(j) / K2 passb[jj] = float(center) / freq * 0.3 for k in range(-N2 + 1, N2 + 1): # Take inverse DFT of passband response kk = k + N2 x = 0 + 0J for m in range(-N2, N2 + 1): mm = m + N2 if passb[mm]: x += passb[mm] * cmath.exp(1J * 2.0 * pi * m * k / N) x /= N idft[kk] = x idft[0] = idft[-1] # this value is missing for k in range(-N2, N2 + 1): kk = k + N2 z = idft[kk] # Apply a windowing function if 1: # Blackman window w = 0.42 + 0.5 * cos(2. * pi * k / N) + 0.08 * cos(4. * pi * k / N) elif 0: # Hamming w = 0.54 + 0.46 * cos(2. * pi * k / N) elif 0: # Hanning w = 0.5 + 0.5 * cos(2. * pi * k / N) else: w = 1 z *= w # Make a bandpass filter by tuning the low pass filter to new center frequency. # Make two quadrature filters. if tune: z *= 2.0 * cmath.exp(-1j * tune * k) filtI.append(z.real) filtQ.append(z.imag) else: filtI.append(z.real) filtQ.append(z.real) return filtI, filtQ def OnBtnFilter(self, event, bw=None): if event is None: # called by application self.filterButns.SetLabel(str(bw)) else: # called by button btn = event.GetEventObject() bw = int(btn.GetLabel()) mode = self.mode if mode in ("CWL", "CWU"): N = 1000 center = max(conf.cwTone, bw/2) elif mode in ('LSB', 'USB'): N = 540 center = 300 + bw / 2 else: # AM and FM N = 140 center = 0 frate = QS.get_filter_rate() filtI, filtQ = self.MakeFilterCoef(frate, N, bw, center) QS.set_filters(filtI, filtQ, bw) if self.screen is self.filter_screen: self.screen.NewFilter() def OnBtnScreen(self, event, name=None): if event is not None: win = event.GetEventObject() name = win.GetLabel() self.screen.Hide() if name == 'Config': self.screen = self.config_screen elif name == 'Graph': self.screen = self.graph self.screen.SetTxFreq(self.txFreq) self.freqDisplay.Display(self.VFO + self.txFreq) elif name == 'WFall': self.screen = self.waterfall self.screen.SetTxFreq(self.txFreq) self.freqDisplay.Display(self.VFO + self.txFreq) sash = self.screen.GetSashPosition() elif name == 'Scope': if win.direction: # Another push on the same button self.scope.running = 1 - self.scope.running # Toggle run state else: # Initial push of button self.scope.running = 1 self.screen = self.scope elif name == 'RX Filter': self.screen = self.filter_screen self.screen.SetTxFreq(self.screen.txFreq) self.freqDisplay.Display(self.screen.txFreq) self.screen.NewFilter() elif name == 'Help': self.screen = self.help_screen self.screen.Show() self.vertBox.Layout() # This destroys the initialized sash position! self.sliderYs.SetValue(self.screen.y_scale) self.sliderYz.SetValue(self.screen.y_zero) if name == 'WFall': self.screen.SetSashPosition(sash) def ChangeYscale(self, event): self.screen.ChangeYscale(self.sliderYs.GetValue()) def ChangeYzero(self, event): self.screen.ChangeYzero(self.sliderYz.GetValue()) def OnBtnMute(self, event): btn = event.GetEventObject() if btn.GetValue(): QS.set_volume(0) else: QS.set_volume(self.audio_volume) def OnBtnDecimation(self, event): i = event.GetSelection() rate = Hardware.VarDecimSet(i) self.vardecim_set = rate if rate != self.sample_rate: self.sample_rate = rate self.graph.sample_rate = rate self.waterfall.pane1.sample_rate = rate self.waterfall.pane2.sample_rate = rate self.waterfall.pane2.display.sample_rate = rate average_count = float(rate) / conf.graph_refresh / self.fft_size average_count = int(average_count + 0.5) average_count = max (1, average_count) QS.change_rate(rate, average_count) tune = self.txFreq vfo = self.VFO self.txFreq = self.VFO = -1 # demand change self.ChangeHwFrequency(tune, vfo, 'NewDecim') def ChangeVolume(self, event=None): # Caution: event can be None value = self.sliderVol.GetValue() # Simulate log taper pot x = (10.0 ** (float(value) * 0.003000434077) - 1) / 1000.0 self.audio_volume = x # audio_volume is 0 to 1.000 QS.set_volume(x) def ChangeSidetone(self, event=None): # Caution: event can be None value = self.sliderSto.GetValue() # Simulate log taper pot x = (10.0 ** (float(value) * 0.003) - 1) / 1000.0 self.sidetone_volume = x QS.set_sidetone(x, self.ritFreq, conf.keyupDelay) def OnRitScale(self, event=None): # Called when the RIT slider is moved # Caution: event can be None if self.ritButton.GetValue(): value = self.ritScale.GetValue() value = int(value) self.ritFreq = value QS.set_tune(self.txFreq + self.ritFreq, self.txFreq) QS.set_sidetone(self.sidetone_volume, self.ritFreq, conf.keyupDelay) def OnBtnRit(self, event=None): # Called when the RIT check button is pressed # Caution: event can be None if self.ritButton.GetValue(): self.ritFreq = self.ritScale.GetValue() else: self.ritFreq = 0 QS.set_tune(self.txFreq + self.ritFreq, self.txFreq) QS.set_sidetone(self.sidetone_volume, self.ritFreq, conf.keyupDelay) def SetRit(self, freq): if freq: self.ritButton.SetValue(1) else: self.ritButton.SetValue(0) self.ritScale.SetValue(freq) self.OnBtnRit() def OnBtnFDX(self, event): btn = event.GetEventObject() if btn.GetValue(): QS.set_fdx(1) else: QS.set_fdx(0) def OnBtnTest1(self, event): btn = event.GetEventObject() if btn.GetValue(): QS.add_tone(10000) else: QS.add_tone(0) def OnBtnTest2(self, event): return def OnBtnColor(self, event): if not self.color_list: clist = wx.lib.colourdb.getColourInfoList() self.color_list = [(0, clist[0][0])] self.color_index = 0 for i in range(1, len(clist)): if self.color_list[-1][1].replace(' ', '') != clist[i][0].replace(' ', ''): #if 'BLUE' in clist[i][0]: self.color_list.append((i, clist[i][0])) else: btn = event.GetEventObject() if btn.shift: del self.color_list[self.color_index] else: self.color_index += btn.direction if self.color_index >= len(self.color_list): self.color_index = 0 elif self.color_index < 0: self.color_index = len(self.color_list) -1 color = self.color_list[self.color_index][1] print self.color_index, color self.main_frame.SetBackgroundColour(color) self.main_frame.Refresh() self.screen.Refresh() def OnBtnAGC(self, event): btn = event.GetEventObject() # Set AGC: agcInUse, agcAttack, agcRelease if btn.index == 1: QS.set_agc(1, 1.0, 0.01) elif btn.index == 2: QS.set_agc(2, 1.0, 0.1) else: QS.set_agc(0, 0, 0) def OnBtnNB(self, event): pass def FreqEntry(self, event): freq = event.GetString() if not freq: return try: if '.' in freq: freq = int(float(freq) * 1E6 + 0.1) else: freq = int(freq) except ValueError: win = event.GetEventObject() win.Clear() win.AppendText("Error") else: for band, (f1, f2) in conf.BandEdge.items(): if f1 <= freq <= f2: # Change to the correct band based on frequency self.bandBtnGroup.SetLabel(band, do_cmd=True) break tune = freq % 10000 vfo = freq - tune self.ChangeHwFrequency(tune, vfo, 'FreqEntry') def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None): """Change the VFO and tuning frequencies, and notify the hardware. tune: the new tuning frequency in +- sample_rate/2; vfo: the new vfo frequency in Hertz; this is the RF frequency at zero Hz audio source: a string indicating the source or widget requesting the change; band: if source is "BtnBand", the band requested; event: for a widget, the event (used to access control/shift key state). Try to update the hardware by calling Hardware.ChangeFrequency(). The hardware will reply with the updated frequencies which may be different from those requested; use and display the returned tune and vfo. If tune or vfo is None, query the hardware for the current frequency. """ if tune is None or vfo is None: tune, vfo = Hardware.ReturnFrequency() if tune is None or vfo is None: # hardware did not change the frequency return else: tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event) tune -= vfo change = 0 if tune != self.txFreq: change = 1 self.txFreq = tune self.screen.SetTxFreq(self.txFreq) QS.set_tune(tune + self.ritFreq, tune) if vfo != self.VFO: change = 1 self.VFO = vfo self.graph.SetVFO(vfo) self.waterfall.SetVFO(vfo) if change: self.freqDisplay.Display(self.txFreq + self.VFO) def DisplayVFO(self, vfo, tune=None): """Change the frequencies internally and display the screen, but do not update the hardware. vfo: the new vfo frequency in Hertz; tune: the new tuning frequency in +- sample_rate/2. """ self.VFO = vfo self.graph.SetVFO(vfo) self.waterfall.SetVFO(vfo) if tune is not None: self.txFreq = tune self.screen.SetTxFreq(tune) self.freqDisplay.Display(self.txFreq + self.VFO) def OnBtnMode(self, event, mode=None): if event is None: # called by application self.modeButns.SetLabel(mode) else: # called by button mode = self.modeButns.GetLabel() Hardware.ChangeMode(mode) self.mode = mode if mode in ('CWL', 'CWU'): if mode == 'CWL': QS.set_rx_mode(0) self.SetRit(conf.cwTone) else: # CWU QS.set_rx_mode(1) self.SetRit(-conf.cwTone) self.MakeFilterButtons(200, 300, 400, 500, 1000, 3000) self.OnBtnFilter(None, 1000) elif mode in ('LSB', 'USB'): if mode == 'LSB': QS.set_rx_mode(2) # LSB else: QS.set_rx_mode(3) # USB self.SetRit(0) self.MakeFilterButtons(1800, 2000, 2200, 2500, 2800, 3300) self.OnBtnFilter(None, 2800) elif mode == 'AM': QS.set_rx_mode(4) self.SetRit(0) self.MakeFilterButtons(4000, 5000, 6000, 7000, 8000, 9000) self.OnBtnFilter(None, 6000) elif mode == 'FM': QS.set_rx_mode(5) self.SetRit(0) self.MakeFilterButtons(10000, 12000, 15000, 25000, 35000, 45000) self.OnBtnFilter(None, 12000) elif mode[0:3] == 'IMD': QS.set_rx_mode(10 + self.modeButns.GetSelectedButton().index) # 10, 11, 12 self.SetRit(0) self.MakeFilterButtons(1800, 2000, 2200, 2500, 2800, 3300) self.OnBtnFilter(None, 2800) elif mode == conf.add_extern_demod: # External demodulation QS.set_rx_mode(6) self.SetRit(0) self.MakeFilterButtons(10000, 12000, 15000, 25000, 35000, 45000) self.OnBtnFilter(None, 12000) def OnBtnBand(self, event): band = self.lastBand # former band in use try: f1, f2 = conf.BandEdge[band] if f1 <= self.VFO + self.txFreq <= f2: self.bandState[band] = (self.VFO, self.txFreq, self.mode) except KeyError: pass btn = event.GetEventObject() band = btn.GetLabel() # new band self.lastBand = band try: vfo, tune, mode = self.bandState[band] except KeyError: vfo, tune, mode = (0, 0, 'LSB') if band == '60': freq = vfo + tune if btn.direction: vfo = self.VFO if 5100000 < vfo < 5600000: if btn.direction > 0: # Move up for f in self.freq60: if f > vfo + self.txFreq: freq = f break else: freq = self.freq60[0] else: # move down l = list(self.freq60) l.reverse() for f in l: if f < vfo + self.txFreq: freq = f break else: freq = self.freq60[-1] half = self.sample_rate / 2 * self.graph_width / self.data_width while freq - vfo <= -half + 1000: vfo -= 10000 while freq - vfo >= +half - 5000: vfo += 10000 tune = freq - vfo elif band == 'Time': vfo, tune, mode = conf.bandTime[btn.index] self.OnBtnMode(None, mode) ampl, phase = self.GetAmplPhase() QS.set_ampl_phase(ampl, phase) self.txFreq = self.VFO = -1 # demand change self.ChangeHwFrequency(tune, vfo, 'BtnBand', band=band) Hardware.ChangeBand(band) def OnBtnUpDnBandDelta(self, event): btn = event.GetEventObject() if btn.direction > 0: # left button was used, move a bit d = max(10000, int(self.sample_rate / 9)) else: # right button was used, move to edge d = max(10000, int(self.sample_rate * 49 / 100)) d = (d / 10000) * 10000 return d def OnBtnDownBand(self, event): self.band_up_down = 1 d = self.OnBtnUpDnBandDelta(event) self.DisplayVFO(self.VFO - d, self.txFreq + d) def OnBtnUpBand(self, event): self.band_up_down = 1 d = self.OnBtnUpDnBandDelta(event) self.DisplayVFO(self.VFO + d, self.txFreq - d) def OnBtnUpDnBandDone(self, event): self.band_up_down = 0 tune = self.txFreq vfo = self.VFO self.txFreq = self.VFO = 0 # Force an update self.ChangeHwFrequency(tune, vfo, 'BtnUpDown') def GetAmplPhase(self): if self.bandAmplPhase.has_key("panadapter"): return self.bandAmplPhase["panadapter"] try: return self.bandAmplPhase[self.lastBand] except KeyError: return (0.0, 0.0) def SetAmplPhase(self, ampl, phase): if self.bandAmplPhase.has_key("panadapter"): self.bandAmplPhase["panadapter"] = (ampl, phase) else: self.bandAmplPhase[self.lastBand] = (ampl, phase) QS.set_ampl_phase(ampl, phase) def PostStartup(self): # called once after sound attempts to start self.config_screen.OnGraphData(None) # update config in case sound is not running def OnReadSound(self): # called at frequent intervals self.timer = time.time() if self.screen == self.scope: data = QS.get_graph(0) # get raw data if data: self.scope.OnGraphData(data) # Send message to draw new data return 1 # we got new scope data else: data = QS.get_graph(1) # get FFT data if data: #T('') self.NewSmeter() # update the S-meter if self.screen == self.graph: self.waterfall.OnGraphData(data) # save waterfall data self.graph.OnGraphData(data) # Send message to draw new data else: self.screen.OnGraphData(data) # Send message to draw new data #T('graph data') #application.Yield() #T('Yield') return 1 # We got new graph/scope data if QS.get_overrange(): self.clip_time0 = self.timer self.freqDisplay.Clip(1) if self.clip_time0: if self.timer - self.clip_time0 > 1.0: self.clip_time0 = 0 self.freqDisplay.Clip(0) if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks self.heart_time0 = self.timer Hardware.HeartBeat() if not self.band_up_down: self.ChangeHwFrequency(None, None) # poll for changed frequency def main(): """If quisk is installed as a package, you can run it with quisk.main().""" App() application.MainLoop() if __name__ == '__main__': main() charleston-1.0/filter_display/3.4.8/filter_3.4.8_quisk_conf_defaults.py0000644000175000017500000004720111426710215024426 0ustar tfoxtfox# Please do not change the configuration file quisk_conf_defaults.py. # Instead copy one of the other quisk_conf_*.py files to your own # .quisk_conf.py and make changes there. For a normal sound card # configuration, copy quisk_conf_model.py to your .quisk_conf.py. # # Quisk imports quisk_conf_defaults to set its configuration. # If you have a configuration file, it then overwrites the defaults # with your parameters. Your configuration file must be named # ~/.quisk_conf.py, where "~" means your home directory. Or # you may specify a different name with the -c or --config command # line option. Try --help. Check the config screen to make sure that # the correct configuration file is in use. # # The Quisk receiver can use a high quality sound card for capture and playback, # or it can use the SDR-IQ by RfSpace for capture and a lower quality # sound card for playback. Quisk can also be used as a panadapter. # Quisk can control some rigs. See quisk_hardware_*.py. If you have a rig # to control, copy one of the quisk_hardware_*.py files to your own file named # quisk_hardware.py, and edit that file. If there is no quisk_hardware.py, then # quisk_hardware_model.py is used instead. # Import the default Hardware module. You can import a different module in # your .quisk_conf.py. import quisk_hardware_model as quisk_hardware # Module for additional widgets (advanced usage). quisk_widgets = None # Select the default screen when Quisk starts: default_screen = 'Graph' #default_screen = 'WFall' #default_screen = 'Config' # The width of the graph data as a fraction of the screen size. This # will be adjusted by Quisk to accommodate preferred FFT sizes. It can # not be changed once Quisk starts. It can not be made too small because # of the space needed for all the buttons. graph_width = 0.8 # Select the default mode when Quisk starts (overruled by persistent_state): # default_mode = 'FM' default_mode = 'USB' # Select the way the waterfall screen scrolls: # waterfall_scroll_mode = 0 # scroll at a constant rate. waterfall_scroll_mode = 1 # scroll faster at the top so that a new signal appears sooner. # Select the initial size in pixels (minimum 1) of the graph at the top of the waterfall. waterfall_graph_size = 80 # These are the initial values for the Y-scale and Y-zero sliders for each screen. # The sliders go from zero to 160. graph_y_scale = 100 graph_y_zero = 0 waterfall_y_scale = 80 waterfall_y_zero = 40 waterfall_graph_y_scale = 100 waterfall_graph_y_zero = 60 scope_y_scale = 80 scope_y_zero = 0 # Currently doesn't do anything filter_y_scale = 90 filter_y_zero = 0 filter_display = 0 # Quisk can save its current state in a file on exit, and restore it when you restart. # State includes band, frequency and mode, but not every item of state (not screen). # The file is .quisk_init.pkl in the same directory as your config file. If this file # becomes corrupted, just delete it and it will be reconstructed. #persistent_state = False persistent_state = True # This converts from dB to S-units for the S-meter (it is in S-units). correct_smeter = 15.5 # This is the fraction of spectrum to display from zero to one. It is needed if # the passband edges are not valid. Use 0.85 for the SDR-IQ. display_fraction = 1.00 # Define colors used by all widgets in wxPython colour format: color_bg = 'light steel blue' # Lower screen background color_graph = 'lemonchiffon1' # Graph background color_gl = 'grey' # Lines on the graph color_btn = 'steelblue2' # button color color_check_btn = 'yellow2' # color of a check button when it is checked color_cycle_btn = 'goldenrod3' # color of a cycle button when it is checked color_test = 'hot pink' # color of a button used for test (turn off for tx) color_freq = 'lightcyan1' # background color of frequency and s-meter color_entry = color_freq # frequency entry box color_bandwidth = 'lemonchiffon2' # audio bandwidth on spectrum display # WB4JFI ADD filter shadow # These are the palettes for the waterfall. The one used is named waterfallPallette, # so to use a different one, overwrite this name in your .quisk_conf.py. waterfallPalette = ( ( 0, 0, 0, 0), ( 36, 85, 0, 255), ( 73, 153, 0, 255), (109, 255, 0, 128), (146, 255, 119, 0), (182, 85, 255, 100), (219, 255, 255, 0), (255, 255, 255, 255) ) digipanWaterfallPalette = ( ( 0, 0, 0, 0), ( 32, 0, 0, 62), ( 64, 0, 0, 126), ( 96, 145, 142, 96), (128, 181, 184, 48), (160, 223, 226, 105), (192, 254, 254, 4), (255, 255, 58, 0) ) # Quisk can access your sound card through PortAudio or through ALSA drivers. # In PortAudio, soundcards have an index number 0, 1, 2, ... and a name. # The name can be something like "HDA NVidia: AD198x Analog (hw:0,0)" or # "surround41". In Quisk, all PortAudio device names start with "portaudio". # A device name like "portaudio#6" directly specifies the index. A name like # "portaudio:text" means to search for "text" in all available devices. And # there is a default device "portaudiodefault". So these portaudio names are useful: #name_of_sound_capt = "portaudio:(hw:0,0)" # First sound card #name_of_sound_capt = "portaudio:(hw:1,0)" # Second sound card, etc. #name_of_sound_capt = "portaudio#1" # Directly specified index #name_of_sound_capt = "portaudiodefault" # May give poor performance on capture # In ALSA, soundcards have these names. The "hw" devices are the raw # hardware devices, and should be used for soundcard capture. #name_of_sound_capt = "hw:0" # First sound card #name_of_sound_capt = "hw:1" # Second sound card, etc. #name_of_sound_capt = "plughw" #name_of_sound_capt = "plughw:1" #name_of_sound_capt = "default" # Normally you would capture and play on the same soundcard to avoid problems with the # two clocks running at slightly different rates. But you can define name_of_sound_play # to play back on a different device. Define this as the empty string "" to turn off # play (for a panadapter). # # For the SDR-IQ the soundcard is not used for capture; it only plays back audio. # Playback is always 48 kHz stereo. # Configuration for soundcard capture and playback use_sdriq = 0 # Get ADC samples from SDR-IQ is not used use_rx_udp = 0 # Get ADC samples from UDP is not used sample_rate = 48000 # ADC hardware sample rate in Hertz name_of_sound_capt = "hw:0" # Name of soundcard capture hardware device. name_of_sound_play = name_of_sound_capt # Use the same device for play back #name_of_sound_play = "" # Panadapter: Do not play channel_i = 0 # Soundcard index of in-phase channel: 0, 1, 2, ... channel_q = 1 # Soundcard index of quadrature channel: 0, 1, 2, ... # If you use a soundcard with Ethernet control of the VFO, set these parameters: rx_ip = "" # Receiver IP address for VFO control # If you use an SDR-IQ for capture, set these parameters: # import quisk_hardware_sdriq as quisk_hardware # Use different hardware file # use_sdriq = 1 # Capture device is the SDR-IQ # sdriq_name = "/dev/ft2450" # Name of the SDR-IQ device to open # sdriq_clock = 66666667.0 # actual sample rate (66666667 nominal) # sdriq_decimation = 500 # Must be 360, 500, 600, or 1250 # sample_rate = int(float(sdriq_clock) / sdriq_decimation + 0.5) # Don't change this # name_of_sound_capt = "" # We do not capture from the soundcard # name_of_sound_play = "hw:0" # Play back on this soundcard # playback_rate = 48000 # Radio sound play rate, default 48000 # channel_i = 0 # Soundcard index of left channel # channel_q = 1 # Soundcard index of right channel # display_fraction = 0.85 # The edges of the full bandwidth are not valid # If you receive ADC samples from a UDP port, set these parameters: # import quisk_hardware_n2adr as quisk_hardware # Use different hardware file # use_rx_udp = 1 # Get ADC samples from UDP # rx_udp_ip = "192.168.1.91" # Sample source IP address # rx_udp_port = 0xBC77 # Sample source UDP port # rx_udp_clock = 122880000 # ADC sample rate in Hertz # rx_udp_decimation = 8 * 8 * 8 # Decimation from clock to UDP sample rate # The allowable decimations are 8 times 8 times (2 or 4 or 8) times (1 or 5). # So you could enter 8 * 8 * one of (2, 4, 5, 8, 10, 20, 40). # These decimations result in a sample rate of 48 to 960 kHz. # sample_rate = int(float(rx_udp_clock) /rx_udp_decimation + 0.5) # Don't change this # name_of_sound_capt = "" # We do not capture from the soundcard # name_of_sound_play = "hw:0" # Play back on this soundcard # playback_rate = 48000 # Radio sound play rate, default 48000 # This is the received radio sound playback rate. The default will # be 48 kHz for the SDR-IQ and UDP port samples, and sample_rate for sound # card capture. Set it yourself for other rates or hardware. # playback_rate = 48000 # If you use quisk_hardware_fixed.py, this is the fixed VFO frequency in Hertz fixed_vfo_freq = 7056000 # This determines what happens when you tune by dragging the mouse. The correct # choice depends on how your hardware performs tuning. You may want to use a # custom hardware file with a custom ChangeFrequency() method too. mouse_tune_method = 0 # The Quisk tune frequency changes and the VFO frequency is unchanged. #mouse_tune_method = 1 # The Quisk tune frequency is unchanged and the VFO changes. # This is the CW tone frequency in Hertz cwTone = 600 # If you use the microphone feature, the mic_channel_I and Q are the two capture # microphone channels. Quisk uses a monophonic mic, so audio is taken from the I # channel, and the Q channel is (currently) ignored. It is OK to set the same # channel number for both, and this is necessary for a USB mono mic. If you # change the sample rate, you will need to change the C code to use different filters. # Mic samples can be sent to an Ethernet device (use tx_ip and name_of_mic_play = "") # or to a sound card (use name_of_mic_play="hw:1" or other device). # If mic samples are sent to a sound card for Tx, the samples are tuned to the audio # transmit frequency, and are set to zero unless the key is down. # If there is no mic (microphone_name = ""), it is still possible to transmit CW, # and you should set mic_playback_rate to the I/Q receive capture rate. # Microphone capture: microphone_name = "" # Name of microphone capture device (or "hw:1") mic_sample_rate = 48000 # Microphone capture sample rate in Hertz, must be 48000 mic_channel_I = 0 # Soundcard index of mic capture audio channel mic_channel_Q = 0 # Soundcard index of ignored capture channel # Microphone samples sent to soundcard: name_of_mic_play = "" # Name of play device if mic I/Q is sent to a sound card mic_playback_rate = 48000 # Playback rate must be a multiple 1, 2, ... of mic_sample_rate mic_play_chan_I = 0 # Soundcard index of mic I play channel mic_play_chan_Q = 1 # Soundcard index of mic Q play channel mic_out_volume = 1.0 # Microphone output volume (after all processing) as a fraction 0.0 to 1.0 # Microphone samples sent to UDP: tx_ip = "" # Transmit IP address for mic sent to UDP (or "192.168.2.195") tx_audio_port = 0 # UDP port for mic samples (or 0x553B) # If your mixing scheme inverts the RF spectrum, set this option to un-invert it invertSpectrum = 0 # Use "amixer -c 1 contents" to get a list of mixer controls and their numid's for # card 1 (or "-c 0" for card 0). Then make a list of (device_name, numid, value) # for each control you need to set. The sample settings are for my USB microphone. #mixer_settings = [ # ("hw:1", 2, 0.80), # numid of microphone volume control, volume 0.0 to 1.0; # ("hw:1", 1, 1.0) # numid of capture on/off control, turn on with 1.0; # ] # If you want Quisk to add a button to generate a 2-tone IMD test signal, # set this to 1. This feature requires the microphone to work. add_imd_button = 0 # If you want Quisk to add a full duplex button (transmit and receive at the # same time), set this to 1. add_fdx_button = 0 # If you want to write your own I/Q filter and demodulation module, set # this to the name of the button to add, and change extdemod.c. # add_extern_demod = "WFM" add_extern_demod = "" # This is the data used to draw colored lines on the frequency X axis to # indicate CW and Phone sub-bands. You can make it anything you want. # These are the colors used for sub-bands: CW = '#FF4444' # General class CW eCW = '#FF8888' # Extra class CW Phone = '#4444FF' # General class phone ePhone = '#8888FF' # Extra class phone # ARRL band plan special frequencies Data = '#FF9900' DxData = '#CC6600' RTTY = '#FF9900' SSTV = '#FFFF00' AM = '#00FF00' Packet = '#00FFFF' Beacons = '#66FF66' Satellite = '#22AA88' Repeater = '#AA00FF' Simplex = '#00FF44' Other = '#888888' # Colors start at the indicated frequency and continue until the # next frequency. The special color "None" turns off color. BandPlan = [ # 160 meters [ 1800000, Data], [ 1809000, Other], [ 1811000, CW], [ 1843000, Phone], [ 1908000, Other], [ 1912000, Phone], [ 1995000, Other], [ 2000000, None], # 80 meters [ 3500000, eCW], [ 3525000, CW], [ 3570000, Data], [ 3589000, DxData], [ 3591000, Data], [ 3600000, ePhone], [ 3790000, Other], [ 3800000, Phone], [ 3844000, SSTV], [ 3846000, Phone], [ 3880000, AM], [ 3890000, Phone], [ 4000000, None], # 60 meters [ 5330600, Phone], [ 5333400, None], [ 5346600, Phone], [ 5349400, None], [ 5366600, Phone], [ 5369400, None], [ 5371600, Phone], [ 5374400, None], [ 5403600, Phone], [ 5406400, None], # 40 meters [ 7000000, eCW], [ 7025000, CW], [ 7039000, DxData], [ 7041000, CW], [ 7080000, Data], [ 7125000, ePhone], [ 7170000, SSTV], [ 7172000, ePhone], [ 7175000, Phone], [ 7285000, AM], [ 7295000, Phone], [ 7300000, None], # 30 meters [10100000, CW], [10130000, RTTY], [10140000, Packet], [10150000, None], # 20 meters [14000000, eCW], [14025000, CW], [14070000, RTTY], [14095000, Packet], [14099500, Other], [14100500, Packet], [14112000, CW], [14150000, ePhone], [14225000, Phone], [14229000, SSTV], [14231000, Phone], [14281000, AM], [14291000, Phone], [14350000, None], # 17 meters [18068000, CW], [18100000, RTTY], [18105000, Packet], [18110000, Phone], [18168000, None], # 15 meters [21000000, eCW], [21025000, CW], [21070000, RTTY], [21110000, CW], [21200000, ePhone], [21275000, Phone], [21339000, SSTV], [21341000, Phone], [21450000, None], # 12 meters [24890000, CW], [24920000, RTTY], [24925000, Packet], [24930000, Phone], [24990000, None], # 10 meters [28000000, CW], [28070000, RTTY], [28150000, CW], [28200000, Beacons], [28300000, Phone], [28679000, SSTV], [28681000, Phone], [29000000, AM], [29200000, Phone], [29300000, Satellite], [29520000, Repeater], [29590000, Simplex], [29610000, Repeater], [29700000, None], # 6 meters [50000000, Beacons], [50100000, Phone], [54000000, None], ] # For each band, this dictionary gives the lower and upper band edges. Frequencies # outside these limits will not be remembered as the last frequency in the band. BandEdge = { '160':( 1800000, 2000000), '80' :( 3500000, 4000000), '60' :( 5300000, 5430000), '40' :( 7000000, 7300000), '30' :(10100000, 10150000), '20' :(14000000, 14350000), '17' :(18068000, 18168000), '15' :(21000000, 21450000), '12' :(24890000, 24990000), '10' :(28000000, 29700000), '6' :(50000000, 54000000), } # For each band, this dictionary gives the initial center frequency, tuning # frequency as an offset from the center frequency, and the mode. This is # no longer too useful because the persistent_state feature saves and then # overwrites these values anyway. bandState = {'Audio':(0, 0, 'LSB'), '160':( 1890000, -10000, 'LSB'), '80' :( 3660000, -10000, 'LSB'), '60' :( 5370000, 1500, 'USB'), '40' :( 7180000, -5000, 'LSB'), '30':(10120000, -10000, 'CWL'), '20' :(14200000, -10000, 'USB'), '17' :(18120000, 10000, 'USB'), '15':(21250000, -10000, 'USB'), '12' :(24940000, 10000, 'USB'), '10' :(28400000, -10000, 'USB'), 'Time':( 5000000, 0, 'AM'), '6' :(50040000, 10000, 'USB'), } # For the Time band, this is the center frequency, tuning frequency and mode: bandTime = [ ( 2500000-10000, 10000, 'AM'), ( 3330000-10000, 10000, 'AM'), ( 5000000-10000, 10000, 'AM'), ( 7335000-10000, 10000, 'AM'), (10000000-10000, 10000, 'AM'), (14670000-10000, 10000, 'AM'), (15000000-10000, 10000, 'AM'), (20000000-10000, 10000, 'AM'), ] # This is the list of band buttons that Quisk displays, and it should have # a length of 12. Empty buttons can have a null string "" label. # Note that the 60 meter band and the Time band have buttons that support # multiple presses. bandLabels = ['Audio', '160', '80', ('60',) * 5, '40', '30', '20', '17', '15', '12', '10', ('Time',) * len(bandTime)] # For each band, this dictionary gives the amplitude and phase corrections for # sound card data. The corrections are small floating point numbers between # about +/- 0.200000. A correction of (0.0, 0.0) means no correction. # Use the button on the configuration screen to add data. The corrections # are saved by the persistent_state feature, so you should not enter them # in your config file. # If you use Quisk as a panadapter, the corrections will not depend on the band. # In that case create a band "panadapter" in your config file, and all corrections # will be read/written to that band. bandAmplPhase = {'40':(0.0, 0.0)} #bandAmplPhase = {'panadapter':(0.0, 0.0)} # The program polls the soundcard or SDR-IQ for data every data_poll_usec microseconds. # A lower time reduces latency; a higher time is less taxing on the hardware. data_poll_usec = 5000 # poll time in microseconds # The fft_size is the width of the data on the screen (about 800 to # 1200 pixels) times the fft_size_multiplier. Multiple FFTs are averaged # together to achieve your graph refresh rate. If fft_size_multiplier is # too small you will get many fft errors. You can specify fft_size_multiplier, # or enter a large number (use 9999) to maximize it, or enter zero to let # quisk calculate it for you. Look for fft_size_multiplier in quisk.py. # If your hardware can change the decimation, there are further compilcations. # The FFT size is fixed, and only the average count can change to adjust the # refresh rate. fft_size_multiplier = 0 # The graph_refresh is the frequency at which the graph is updated, # and should be about 5 to 10 Hertz. Higher rates require more processor power. graph_refresh = 7 # update the graph at this rate in Hertz # latency_millisecs determines how many samples are in the soundcard play buffer. # A larger number makes it less likely that you will run out of samples to play, # but increases latency. It is OK to suffer a certain number of play buffer # underruns in order to get lower latency. latency_millisecs = 150 # latency time in milliseconds # Select the method to test the state of the key; see is_key_down.c key_method = "" # No keying, or internal method # key_method = "/dev/parport0" # Use the named parallel port # key_method = "/dev/ttyS0" # Use the named serial port # key_method = "192.168.1.44" # Use UDP from this address # If you are using keying, key-down throws away the current capture buffer # and starts a sidetone with a rise time of 5 milliseconds. For # key-up, the sidetone is ended with a fall time of 5 milliseconds, then # a silent period starts, then normal audio starts with a rise time of # 5 milliseconds. The length of the silent period is given by keyupDelay, # but will be at least the time necessary to collect enough samples to # refill the filters. A larger keyupDelay may be needed to accomodate # antenna switching or other requirement of your hardware. keyupDelay = 23 # extra milliseconds silence on key up charleston-1.0/filter_display/3.4.8/original_3.4.8_quisk_conf_defaults.py0000644000175000017500000004701211424052112024736 0ustar tfoxtfox# Please do not change the configuration file quisk_conf_defaults.py. # Instead copy one of the other quisk_conf_*.py files to your own # .quisk_conf.py and make changes there. For a normal sound card # configuration, copy quisk_conf_model.py to your .quisk_conf.py. # # Quisk imports quisk_conf_defaults to set its configuration. # If you have a configuration file, it then overwrites the defaults # with your parameters. Your configuration file must be named # ~/.quisk_conf.py, where "~" means your home directory. Or # you may specify a different name with the -c or --config command # line option. Try --help. Check the config screen to make sure that # the correct configuration file is in use. # # The Quisk receiver can use a high quality sound card for capture and playback, # or it can use the SDR-IQ by RfSpace for capture and a lower quality # sound card for playback. Quisk can also be used as a panadapter. # Quisk can control some rigs. See quisk_hardware_*.py. If you have a rig # to control, copy one of the quisk_hardware_*.py files to your own file named # quisk_hardware.py, and edit that file. If there is no quisk_hardware.py, then # quisk_hardware_model.py is used instead. # Import the default Hardware module. You can import a different module in # your .quisk_conf.py. import quisk_hardware_model as quisk_hardware # Module for additional widgets (advanced usage). quisk_widgets = None # Select the default screen when Quisk starts: default_screen = 'Graph' #default_screen = 'WFall' #default_screen = 'Config' # The width of the graph data as a fraction of the screen size. This # will be adjusted by Quisk to accommodate preferred FFT sizes. It can # not be changed once Quisk starts. It can not be made too small because # of the space needed for all the buttons. graph_width = 0.8 # Select the default mode when Quisk starts (overruled by persistent_state): # default_mode = 'FM' default_mode = 'USB' # Select the way the waterfall screen scrolls: # waterfall_scroll_mode = 0 # scroll at a constant rate. waterfall_scroll_mode = 1 # scroll faster at the top so that a new signal appears sooner. # Select the initial size in pixels (minimum 1) of the graph at the top of the waterfall. waterfall_graph_size = 80 # These are the initial values for the Y-scale and Y-zero sliders for each screen. # The sliders go from zero to 160. graph_y_scale = 100 graph_y_zero = 0 waterfall_y_scale = 80 waterfall_y_zero = 40 waterfall_graph_y_scale = 100 waterfall_graph_y_zero = 60 scope_y_scale = 80 scope_y_zero = 0 # Currently doesn't do anything filter_y_scale = 90 filter_y_zero = 0 # Quisk can save its current state in a file on exit, and restore it when you restart. # State includes band, frequency and mode, but not every item of state (not screen). # The file is .quisk_init.pkl in the same directory as your config file. If this file # becomes corrupted, just delete it and it will be reconstructed. #persistent_state = False persistent_state = True # This converts from dB to S-units for the S-meter (it is in S-units). correct_smeter = 15.5 # This is the fraction of spectrum to display from zero to one. It is needed if # the passband edges are not valid. Use 0.85 for the SDR-IQ. display_fraction = 1.00 # Define colors used by all widgets in wxPython colour format: color_bg = 'light steel blue' # Lower screen background color_graph = 'lemonchiffon1' # Graph background color_gl = 'grey' # Lines on the graph color_btn = 'steelblue2' # button color color_check_btn = 'yellow2' # color of a check button when it is checked color_cycle_btn = 'goldenrod3' # color of a cycle button when it is checked color_test = 'hot pink' # color of a button used for test (turn off for tx) color_freq = 'lightcyan1' # background color of frequency and s-meter color_entry = color_freq # frequency entry box # These are the palettes for the waterfall. The one used is named waterfallPallette, # so to use a different one, overwrite this name in your .quisk_conf.py. waterfallPalette = ( ( 0, 0, 0, 0), ( 36, 85, 0, 255), ( 73, 153, 0, 255), (109, 255, 0, 128), (146, 255, 119, 0), (182, 85, 255, 100), (219, 255, 255, 0), (255, 255, 255, 255) ) digipanWaterfallPalette = ( ( 0, 0, 0, 0), ( 32, 0, 0, 62), ( 64, 0, 0, 126), ( 96, 145, 142, 96), (128, 181, 184, 48), (160, 223, 226, 105), (192, 254, 254, 4), (255, 255, 58, 0) ) # Quisk can access your sound card through PortAudio or through ALSA drivers. # In PortAudio, soundcards have an index number 0, 1, 2, ... and a name. # The name can be something like "HDA NVidia: AD198x Analog (hw:0,0)" or # "surround41". In Quisk, all PortAudio device names start with "portaudio". # A device name like "portaudio#6" directly specifies the index. A name like # "portaudio:text" means to search for "text" in all available devices. And # there is a default device "portaudiodefault". So these portaudio names are useful: #name_of_sound_capt = "portaudio:(hw:0,0)" # First sound card #name_of_sound_capt = "portaudio:(hw:1,0)" # Second sound card, etc. #name_of_sound_capt = "portaudio#1" # Directly specified index #name_of_sound_capt = "portaudiodefault" # May give poor performance on capture # In ALSA, soundcards have these names. The "hw" devices are the raw # hardware devices, and should be used for soundcard capture. #name_of_sound_capt = "hw:0" # First sound card #name_of_sound_capt = "hw:1" # Second sound card, etc. #name_of_sound_capt = "plughw" #name_of_sound_capt = "plughw:1" #name_of_sound_capt = "default" # Normally you would capture and play on the same soundcard to avoid problems with the # two clocks running at slightly different rates. But you can define name_of_sound_play # to play back on a different device. Define this as the empty string "" to turn off # play (for a panadapter). # # For the SDR-IQ the soundcard is not used for capture; it only plays back audio. # Playback is always 48 kHz stereo. # Configuration for soundcard capture and playback use_sdriq = 0 # Get ADC samples from SDR-IQ is not used use_rx_udp = 0 # Get ADC samples from UDP is not used sample_rate = 48000 # ADC hardware sample rate in Hertz name_of_sound_capt = "hw:0" # Name of soundcard capture hardware device. name_of_sound_play = name_of_sound_capt # Use the same device for play back #name_of_sound_play = "" # Panadapter: Do not play channel_i = 0 # Soundcard index of in-phase channel: 0, 1, 2, ... channel_q = 1 # Soundcard index of quadrature channel: 0, 1, 2, ... # If you use a soundcard with Ethernet control of the VFO, set these parameters: rx_ip = "" # Receiver IP address for VFO control # If you use an SDR-IQ for capture, set these parameters: # import quisk_hardware_sdriq as quisk_hardware # Use different hardware file # use_sdriq = 1 # Capture device is the SDR-IQ # sdriq_name = "/dev/ft2450" # Name of the SDR-IQ device to open # sdriq_clock = 66666667.0 # actual sample rate (66666667 nominal) # sdriq_decimation = 500 # Must be 360, 500, 600, or 1250 # sample_rate = int(float(sdriq_clock) / sdriq_decimation + 0.5) # Don't change this # name_of_sound_capt = "" # We do not capture from the soundcard # name_of_sound_play = "hw:0" # Play back on this soundcard # playback_rate = 48000 # Radio sound play rate, default 48000 # channel_i = 0 # Soundcard index of left channel # channel_q = 1 # Soundcard index of right channel # display_fraction = 0.85 # The edges of the full bandwidth are not valid # If you receive ADC samples from a UDP port, set these parameters: # import quisk_hardware_n2adr as quisk_hardware # Use different hardware file # use_rx_udp = 1 # Get ADC samples from UDP # rx_udp_ip = "192.168.1.91" # Sample source IP address # rx_udp_port = 0xBC77 # Sample source UDP port # rx_udp_clock = 122880000 # ADC sample rate in Hertz # rx_udp_decimation = 8 * 8 * 8 # Decimation from clock to UDP sample rate # The allowable decimations are 8 times 8 times (2 or 4 or 8) times (1 or 5). # So you could enter 8 * 8 * one of (2, 4, 5, 8, 10, 20, 40). # These decimations result in a sample rate of 48 to 960 kHz. # sample_rate = int(float(rx_udp_clock) /rx_udp_decimation + 0.5) # Don't change this # name_of_sound_capt = "" # We do not capture from the soundcard # name_of_sound_play = "hw:0" # Play back on this soundcard # playback_rate = 48000 # Radio sound play rate, default 48000 # This is the received radio sound playback rate. The default will # be 48 kHz for the SDR-IQ and UDP port samples, and sample_rate for sound # card capture. Set it yourself for other rates or hardware. # playback_rate = 48000 # If you use quisk_hardware_fixed.py, this is the fixed VFO frequency in Hertz fixed_vfo_freq = 7056000 # This determines what happens when you tune by dragging the mouse. The correct # choice depends on how your hardware performs tuning. You may want to use a # custom hardware file with a custom ChangeFrequency() method too. mouse_tune_method = 0 # The Quisk tune frequency changes and the VFO frequency is unchanged. #mouse_tune_method = 1 # The Quisk tune frequency is unchanged and the VFO changes. # This is the CW tone frequency in Hertz cwTone = 600 # If you use the microphone feature, the mic_channel_I and Q are the two capture # microphone channels. Quisk uses a monophonic mic, so audio is taken from the I # channel, and the Q channel is (currently) ignored. It is OK to set the same # channel number for both, and this is necessary for a USB mono mic. If you # change the sample rate, you will need to change the C code to use different filters. # Mic samples can be sent to an Ethernet device (use tx_ip and name_of_mic_play = "") # or to a sound card (use name_of_mic_play="hw:1" or other device). # If mic samples are sent to a sound card for Tx, the samples are tuned to the audio # transmit frequency, and are set to zero unless the key is down. # If there is no mic (microphone_name = ""), it is still possible to transmit CW, # and you should set mic_playback_rate to the I/Q receive capture rate. # Microphone capture: microphone_name = "" # Name of microphone capture device (or "hw:1") mic_sample_rate = 48000 # Microphone capture sample rate in Hertz, must be 48000 mic_channel_I = 0 # Soundcard index of mic capture audio channel mic_channel_Q = 0 # Soundcard index of ignored capture channel # Microphone samples sent to soundcard: name_of_mic_play = "" # Name of play device if mic I/Q is sent to a sound card mic_playback_rate = 48000 # Playback rate must be a multiple 1, 2, ... of mic_sample_rate mic_play_chan_I = 0 # Soundcard index of mic I play channel mic_play_chan_Q = 1 # Soundcard index of mic Q play channel mic_out_volume = 1.0 # Microphone output volume (after all processing) as a fraction 0.0 to 1.0 # Microphone samples sent to UDP: tx_ip = "" # Transmit IP address for mic sent to UDP (or "192.168.2.195") tx_audio_port = 0 # UDP port for mic samples (or 0x553B) # If your mixing scheme inverts the RF spectrum, set this option to un-invert it invertSpectrum = 0 # Use "amixer -c 1 contents" to get a list of mixer controls and their numid's for # card 1 (or "-c 0" for card 0). Then make a list of (device_name, numid, value) # for each control you need to set. The sample settings are for my USB microphone. #mixer_settings = [ # ("hw:1", 2, 0.80), # numid of microphone volume control, volume 0.0 to 1.0; # ("hw:1", 1, 1.0) # numid of capture on/off control, turn on with 1.0; # ] # If you want Quisk to add a button to generate a 2-tone IMD test signal, # set this to 1. This feature requires the microphone to work. add_imd_button = 0 # If you want Quisk to add a full duplex button (transmit and receive at the # same time), set this to 1. add_fdx_button = 0 # If you want to write your own I/Q filter and demodulation module, set # this to the name of the button to add, and change extdemod.c. # add_extern_demod = "WFM" add_extern_demod = "" # This is the data used to draw colored lines on the frequency X axis to # indicate CW and Phone sub-bands. You can make it anything you want. # These are the colors used for sub-bands: CW = '#FF4444' # General class CW eCW = '#FF8888' # Extra class CW Phone = '#4444FF' # General class phone ePhone = '#8888FF' # Extra class phone # ARRL band plan special frequencies Data = '#FF9900' DxData = '#CC6600' RTTY = '#FF9900' SSTV = '#FFFF00' AM = '#00FF00' Packet = '#00FFFF' Beacons = '#66FF66' Satellite = '#22AA88' Repeater = '#AA00FF' Simplex = '#00FF44' Other = '#888888' # Colors start at the indicated frequency and continue until the # next frequency. The special color "None" turns off color. BandPlan = [ # 160 meters [ 1800000, Data], [ 1809000, Other], [ 1811000, CW], [ 1843000, Phone], [ 1908000, Other], [ 1912000, Phone], [ 1995000, Other], [ 2000000, None], # 80 meters [ 3500000, eCW], [ 3525000, CW], [ 3570000, Data], [ 3589000, DxData], [ 3591000, Data], [ 3600000, ePhone], [ 3790000, Other], [ 3800000, Phone], [ 3844000, SSTV], [ 3846000, Phone], [ 3880000, AM], [ 3890000, Phone], [ 4000000, None], # 60 meters [ 5330600, Phone], [ 5333400, None], [ 5346600, Phone], [ 5349400, None], [ 5366600, Phone], [ 5369400, None], [ 5371600, Phone], [ 5374400, None], [ 5403600, Phone], [ 5406400, None], # 40 meters [ 7000000, eCW], [ 7025000, CW], [ 7039000, DxData], [ 7041000, CW], [ 7080000, Data], [ 7125000, ePhone], [ 7170000, SSTV], [ 7172000, ePhone], [ 7175000, Phone], [ 7285000, AM], [ 7295000, Phone], [ 7300000, None], # 30 meters [10100000, CW], [10130000, RTTY], [10140000, Packet], [10150000, None], # 20 meters [14000000, eCW], [14025000, CW], [14070000, RTTY], [14095000, Packet], [14099500, Other], [14100500, Packet], [14112000, CW], [14150000, ePhone], [14225000, Phone], [14229000, SSTV], [14231000, Phone], [14281000, AM], [14291000, Phone], [14350000, None], # 17 meters [18068000, CW], [18100000, RTTY], [18105000, Packet], [18110000, Phone], [18168000, None], # 15 meters [21000000, eCW], [21025000, CW], [21070000, RTTY], [21110000, CW], [21200000, ePhone], [21275000, Phone], [21339000, SSTV], [21341000, Phone], [21450000, None], # 12 meters [24890000, CW], [24920000, RTTY], [24925000, Packet], [24930000, Phone], [24990000, None], # 10 meters [28000000, CW], [28070000, RTTY], [28150000, CW], [28200000, Beacons], [28300000, Phone], [28679000, SSTV], [28681000, Phone], [29000000, AM], [29200000, Phone], [29300000, Satellite], [29520000, Repeater], [29590000, Simplex], [29610000, Repeater], [29700000, None], # 6 meters [50000000, Beacons], [50100000, Phone], [54000000, None], ] # For each band, this dictionary gives the lower and upper band edges. Frequencies # outside these limits will not be remembered as the last frequency in the band. BandEdge = { '160':( 1800000, 2000000), '80' :( 3500000, 4000000), '60' :( 5300000, 5430000), '40' :( 7000000, 7300000), '30' :(10100000, 10150000), '20' :(14000000, 14350000), '17' :(18068000, 18168000), '15' :(21000000, 21450000), '12' :(24890000, 24990000), '10' :(28000000, 29700000), '6' :(50000000, 54000000), } # For each band, this dictionary gives the initial center frequency, tuning # frequency as an offset from the center frequency, and the mode. This is # no longer too useful because the persistent_state feature saves and then # overwrites these values anyway. bandState = {'Audio':(0, 0, 'LSB'), '160':( 1890000, -10000, 'LSB'), '80' :( 3660000, -10000, 'LSB'), '60' :( 5370000, 1500, 'USB'), '40' :( 7180000, -5000, 'LSB'), '30':(10120000, -10000, 'CWL'), '20' :(14200000, -10000, 'USB'), '17' :(18120000, 10000, 'USB'), '15':(21250000, -10000, 'USB'), '12' :(24940000, 10000, 'USB'), '10' :(28400000, -10000, 'USB'), 'Time':( 5000000, 0, 'AM'), '6' :(50040000, 10000, 'USB'), } # For the Time band, this is the center frequency, tuning frequency and mode: bandTime = [ ( 2500000-10000, 10000, 'AM'), ( 3330000-10000, 10000, 'AM'), ( 5000000-10000, 10000, 'AM'), ( 7335000-10000, 10000, 'AM'), (10000000-10000, 10000, 'AM'), (14670000-10000, 10000, 'AM'), (15000000-10000, 10000, 'AM'), (20000000-10000, 10000, 'AM'), ] # This is the list of band buttons that Quisk displays, and it should have # a length of 12. Empty buttons can have a null string "" label. # Note that the 60 meter band and the Time band have buttons that support # multiple presses. bandLabels = ['Audio', '160', '80', ('60',) * 5, '40', '30', '20', '17', '15', '12', '10', ('Time',) * len(bandTime)] # For each band, this dictionary gives the amplitude and phase corrections for # sound card data. The corrections are small floating point numbers between # about +/- 0.200000. A correction of (0.0, 0.0) means no correction. # Use the button on the configuration screen to add data. The corrections # are saved by the persistent_state feature, so you should not enter them # in your config file. # If you use Quisk as a panadapter, the corrections will not depend on the band. # In that case create a band "panadapter" in your config file, and all corrections # will be read/written to that band. bandAmplPhase = {'40':(0.0, 0.0)} #bandAmplPhase = {'panadapter':(0.0, 0.0)} # The program polls the soundcard or SDR-IQ for data every data_poll_usec microseconds. # A lower time reduces latency; a higher time is less taxing on the hardware. data_poll_usec = 5000 # poll time in microseconds # The fft_size is the width of the data on the screen (about 800 to # 1200 pixels) times the fft_size_multiplier. Multiple FFTs are averaged # together to achieve your graph refresh rate. If fft_size_multiplier is # too small you will get many fft errors. You can specify fft_size_multiplier, # or enter a large number (use 9999) to maximize it, or enter zero to let # quisk calculate it for you. Look for fft_size_multiplier in quisk.py. # If your hardware can change the decimation, there are further compilcations. # The FFT size is fixed, and only the average count can change to adjust the # refresh rate. fft_size_multiplier = 0 # The graph_refresh is the frequency at which the graph is updated, # and should be about 5 to 10 Hertz. Higher rates require more processor power. graph_refresh = 7 # update the graph at this rate in Hertz # latency_millisecs determines how many samples are in the soundcard play buffer. # A larger number makes it less likely that you will run out of samples to play, # but increases latency. It is OK to suffer a certain number of play buffer # underruns in order to get lower latency. latency_millisecs = 150 # latency time in milliseconds # Select the method to test the state of the key; see is_key_down.c key_method = "" # No keying, or internal method # key_method = "/dev/parport0" # Use the named parallel port # key_method = "/dev/ttyS0" # Use the named serial port # key_method = "192.168.1.44" # Use UDP from this address # If you are using keying, key-down throws away the current capture buffer # and starts a sidetone with a rise time of 5 milliseconds. For # key-up, the sidetone is ended with a fall time of 5 milliseconds, then # a silent period starts, then normal audio starts with a rise time of # 5 milliseconds. The length of the silent period is given by keyupDelay, # but will be at least the time necessary to collect enough samples to # refill the filters. A larger keyupDelay may be needed to accomodate # antenna switching or other requirement of your hardware. keyupDelay = 23 # extra milliseconds silence on key up charleston-1.0/filter_display/3.4.8/filter_mods_3.4.8.txt0000644000175000017500000001501411426710773021535 0ustar tfoxtfox #################### version 3.4.8 FILTER SHADOW ADDITIONS (Aug.5, 2010) ############### FILE QUISK.PY CHANGES FOR FILTER SHADOW: 1. Added the following lines at beginning of file. # NOTE: THIS FILE HAD BEEN MODIFIED BY WB4JFI TO ADD A FILTER SHADOW # ON THE DISPLAY TO INDICATE FILTER BANDWIDTH. LINES WITH # "WB4JFI" IN THE COMMENTS HAVE BEEN ADDED TO James # Ahlstrom's ORIGINAL CODE. DO NOT bug HIM ABOUT IT!! 2. IN: class GraphDisplay(wx.Window): def __init__(self, parent, x, y, graph_width, height, chary): ADDED FOLLOWING LINES NEAR 657 #line 657 reads: self.tuningPen = wx.Pen('Red', 1) self.fltrPen = wx.Pen(conf.color_bandwidth, 1) # WB4JFI ADD filter shadow self.fltrBrush = wx.Brush(conf.color_bandwidth, wx.SOLID) # WB4JFI ADD filter shadow self.fltr_disp_start = -100 # WB4JFI ADD filter shadow self.fltr_disp_size = 100 # WB4JFI ADD filter shadow self.fltr_disp_fill = -99 # WB4JFI ADD filter shadow self.fltr_disp_show = 0 # WB4JFI ADD filter shadow #old line 658 reads: self.backgroundPen = wx.Pen(self.GetBackgroundColour(), 1) 3. IN: class GraphDisplay(wx.Window): def OnPaint(self, event): ADDED AND CHANGED LINES TO line 669 area: # line 669 reads: dc = wx.PaintDC(self) x = self.tune_x # setx x to tune freq horizontal position self.UpdateFilterDisplay() # WB4JFI ADD filter shadow if self.fltr_disp_show == 3: # WB4JFI ADD filter shadow dc.SetPen(self.fltrPen) # WB4JFI ADD filter shadow dc.SetBrush(self.fltrBrush) # WB4JFI ADD filter shadow dc.DrawRectangle(x + self.fltr_disp_start, 0, # WB4JFI ADD filter shadow self.fltr_disp_size, self.max_height) # WB4JFI ADD filter shadow dc.FloodFill(x + self.fltr_disp_fill, 1, conf.color_bandwidth, 1) # WB4JFI ADD filter shadow dc.SetPen(wx.BLACK_PEN) dc.DrawLines(self.line) # x = self.tune_x # WB4JFI moved up for filter shadow dc.SetPen(self.tuningPen) #old line 670 (now 682): dc.DrawLine(x, 0, x, self.max_height) #NOTE NOTE NOTE: These next three lines are now duplicated. # Remove the SECOND COPIES so the "old line 670" shown above is now line 682: dc.DrawLines(self.line) # REMOVE ME AS DUPLICATE x = self.tune_x # REMOVE ME AS DUPLICATE dc.SetPen(self.tuningPen) # REMOVE ME AS DUPLICATE 4. IN: class GraphDisplay(wx.Window): add the following def section after line 716: # line 716 reads: self.Refresh() #rect=rect) def UpdateFilterDisplay(self): # WB4JFI ADD - Update filter display if application.fltr_display: # WB4JFI, check if OK to display filter self.fltr_disp_show = self.fltr_disp_show | 1 # set bit 0, show OK from OUTSIDE else: # otherwise, OUTSIDE don't display self.fltr_disp_show = self.fltr_disp_show & 2 # by clearing bit 0 if application.screen == application.graph or application.screen == application.waterfall: self.fltr_disp_show = self.fltr_disp_show | 2 # bit 1 shows proper screen to display else: self.fltr_disp_show = self.fltr_disp_show & 1 # clear bit 1 to indicate not proper display self.filter_pixels = (application.filterbw / (application.sample_rate /application.data_width) + 0.5) if self.filter_pixels < 2: # if calculated less than 2 pixels self.filter_pixels = 2 # make it at least 2 pixels wide if application.mode == 'LSB' or application.mode == 'CWL': # if filter on lower sideband... self.fltr_disp_start = -(self.filter_pixels - 1) # set start to be at lower side self.fltr_disp_size = self.filter_pixels # and set rectangle size self.fltr_disp_fill = self.fltr_disp_start # start filling at rectangle start if application.mode == 'USB' or application.mode == 'CWU': # if filter on upper sideband... self.fltr_disp_start = 0 # start is at tuning freq self.fltr_disp_size = self.filter_pixels # set rectangle size self.fltr_disp_fill = 1 # start filling at beginning of tune if application.mode == 'AM' or application.mode == 'FM': # if filter is centered at tune freq self.fltr_disp_start = -((self.filter_pixels - 1) /2) # set start at 1/2 bandidth self.fltr_disp_size = self.filter_pixels # set size to cover both sidebands self.fltr_disp_fill = self.filter_pixels # start filling at lower edge # # WB4JFI - end of filter add # previous line 717: def SetTuningLine(self, x): 5. IN: class App(wx.App): def OnInit(self): Add the following lines after line 1517 # line 1517 reads: self.color_index = 0 self.filterbw = 0 # WB4JFI Added for filter display self.fltr_display = conf.filter_display # WB4JFI added filter display 1=OK, 0=do not display # old line 1518: dc = wx.ScreenDC() # get the screen size 6. IN: class App(wx.App): def OnBtnFilter(self, event, bw=None): Add the following lines after line 2016: # line 2016 reads: bw = int(btn.GetLabel()) self.filterbw = int(self.filterButns.GetLabel()) # WB4JFI ADD - udate filter bandwidth application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp # old line 2017: mode = self.mode 7. IN: def OnBtnScreen(self, event, name=None): Add the following line after line 2044: # line 2044 reads: self.freqDisplay.Display(self.VFO + self.txFreq) application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp # old line 2045: elif name == 'WFall': 8. IN: def OnBtnScreen(self, event, name=None): Add the following line after line 2050: # line 2050 reads: sash = self.screen.GetSashPosition() application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp old line 2051: elif name == 'Scope': 9. IN: def OnBtnMode(self, event, mode=None): Add the following line after line 2297: # Line 2297 reads: self.OnBtnFilter(None, 12000) application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp # old line 2298: def OnBtnBand(self, event): 10. IN FILE quisk_conf_defaults.py, Add the following line after 62: # Line 62 reads: filter_y_zero = 0 filter_display = 0 # old line 63 is a blank line 11 IN FILE quisk_conf_defaults.py, Add the following line after line 88: # line 88 reads: color_entry = color_freq # frequency entry box color_bandwidth = 'lemonchiffon2' # audio bandwidth on spectrum display # WB4JFI ADD filter shadow # old line 89 is blank line charleston-1.0/filter_display/3.4.6/0000755000175000017500000000000011430152203015707 5ustar tfoxtfoxcharleston-1.0/filter_display/3.4.6/filter_mods_3.4.6.txt0000644000175000017500000001425311425737470021537 0ustar tfoxtfox ############################ version 3.4.6 FILTER SHADOW ADDITIONS ################### FILE QUISK.PY CHANGES FOR FILTER SHADOW: 1. Added the following lines at beginning of file. # NOTE: THIS FILE HAD BEEN MODIFIED BY WB4JFI TO ADD A FILTER SHADOW # ON THE DISPLAY TO INDICATE FILTER BANDWIDTH. LINES WITH # "WB4JFI" IN THE COMMENTS HAVE BEEN ADDED TO James # Ahlstrom's ORIGINAL CODE. DO NOT bug HIM ABOUT IT!! 2. IN: class GraphDisplay(wx.Window): def __init__(self, parent, x, y, graph_width, height, chary): ADDED FOLLOWING LINES NEAR 640 #line 640 reads: self.tuningPen = wx.Pen('Red', 1) self.fltrPen = wx.Pen(conf.color_bandwidth, 1) # WB4JFI ADD filter shadow self.fltrBrush = wx.Brush(conf.color_bandwidth, wx.SOLID) # WB4JFI ADD filter shadow self.fltr_disp_start = -100 # WB4JFI ADD filter shadow self.fltr_disp_size = 100 # WB4JFI ADD filter shadow self.fltr_disp_fill = -99 # WB4JFI ADD filter shadow self.fltr_disp_show = 0 # WB4JFI ADD filter shadow #old line 641 reads: self.backgroundPen = wx.Pen(self.GetBackgroundColour(), 1) 3. IN: class GraphDisplay(wx.Window): def OnPaint(self, event): ADDED AND CHANGED LINES TO: # line 651 reads: dc = wx.PaintDC(self) x = self.tune_x # setx x to tune freq horizontal position self.UpdateFilterDisplay() # WB4JFI ADD filter shadow if self.fltr_disp_show == 3: # WB4JFI ADD filter shadow dc.SetPen(self.fltrPen) # WB4JFI ADD filter shadow dc.SetBrush(self.fltrBrush) # WB4JFI ADD filter shadow dc.DrawRectangle(x + self.fltr_disp_start, 0, # WB4JFI ADD filter shadow self.fltr_disp_size, self.max_height) # WB4JFI ADD filter shadow dc.FloodFill(x + self.fltr_disp_fill, 1, conf.color_bandwidth, 1) # WB4JFI ADD filter shadow dc.SetPen(wx.BLACK_PEN) dc.DrawLines(self.line) # x = self.tune_x # WB4JFI moved up for filter shadow dc.SetPen(self.tuningPen) #old line 656: dc.DrawLine(x, 0, x, self.max_height) 4. IN: class GraphDisplay(wx.Window): add the following def section after line 697: # line 697 reads: self.Refresh() #rect=rect) def UpdateFilterDisplay(self): # WB4JFI ADD - Update filter display if application.fltr_display: # WB4JFI, check if OK to display filter self.fltr_disp_show = self.fltr_disp_show | 1 # set bit 0, show OK from OUTSIDE else: # otherwise, OUTSIDE don't display self.fltr_disp_show = self.fltr_disp_show & 2 # by clearing bit 0 if application.screen == application.graph or application.screen == application.waterfall: self.fltr_disp_show = self.fltr_disp_show | 2 # bit 1 shows proper screen to display else: self.fltr_disp_show = self.fltr_disp_show & 1 # clear bit 1 to indicate not proper display self.filter_pixels = (application.filterbw / (conf.sample_rate /application.data_width) + 0.5) if self.filter_pixels < 2: # if calculated less than 2 pixels self.filter_pixels = 2 # make it at least 2 pixels wide if application.mode == 'LSB' or application.mode == 'CWL': # if filter on lower sideband... self.fltr_disp_start = -(self.filter_pixels - 1) # set start to be at lower side self.fltr_disp_size = self.filter_pixels # and set rectangle size self.fltr_disp_fill = self.fltr_disp_start # start filling at rectangle start if application.mode == 'USB' or application.mode == 'CWU': # if filter on upper sideband... self.fltr_disp_start = 0 # start is at tuning freq self.fltr_disp_size = self.filter_pixels # set rectangle size self.fltr_disp_fill = 1 # start filling at beginning of tune if application.mode == 'AM' or application.mode == 'FM': # if filter is centered at tune freq self.fltr_disp_start = -((self.filter_pixels - 1) /2) # set start at 1/2 bandidth self.fltr_disp_size = self.filter_pixels # set size to cover both sidebands self.fltr_disp_fill = self.filter_pixels # start filling at lower edge # # WB4JFI - end of filter add # previous line 698: def SetTuningLine(self, x): 5. IN: class App(wx.App): def OnInit(self): Add the following lines after line 1489 # line 1489 reads: self.color_index = 0 self.filterbw = 0 # WB4JFI Added for filter display self.fltr_display = conf.filter_display # WB4JFI added filter display 1=OK, 0=do not display # old line 1490: dc = wx.ScreenDC() # get the screen size 6. IN: class App(wx.App): def OnBtnFilter(self, event, bw=None): Add the following lines after line 1977: # line 1977 reads: bw = int(btn.GetLabel()) self.filterbw = int(self.filterButns.GetLabel()) # WB4JFI ADD - udate filter bandwidth application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp # old line 1978: mode = self.mode 7. IN: def OnBtnScreen(self, event, name=None): Add the following line after line 2005: # line 2005 reads: self.freqDisplay.Display(self.VFO + self.txFreq) application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp # old line 2006: elif name == 'WFall': 8. IN: def OnBtnScreen(self, event, name=None): Add the following line after line 2011: # line 2011 reads: sash = self.screen.GetSashPosition() application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp old line 2012: elif name == 'Scope': 9. IN: def OnBtnMode(self, event, mode=None): Add the following line after line 2240: # Line 2240 reads: self.OnBtnFilter(None, 12000) application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp # old line 2241: def OnBtnBand(self, event): 10. IN FILE quisk_conf_defaults.py, Add the following line after 62: # Line 62 reads: filter_y_zero = 0 filter_display = 0 # old line 63 is a blank line 11. IN FILE quisk_conf_defaults.py, Add the following line after line 87: # line 87 reads: color_entry = color_freq # frequency entry box color_bandwidth = 'lemonchiffon2' # audio bandwidth on spectrum display # WB4JFI ADD filter shadow # old line 88 is blank line charleston-1.0/filter_display/3.4.7/0000755000175000017500000000000011430152203015710 5ustar tfoxtfoxcharleston-1.0/filter_display/3.4.7/filter_display_quisk.py0000755000175000017500000026327511425736411022547 0ustar tfoxtfox#! /usr/bin/python # All QUISK software is Copyright (C) 2006-2010 by James C. Ahlstrom. # This free software is licensed for use under the GNU General Public # License (GPL), see http://www.opensource.org. # Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!! # NOTE: THIS FILE HAD BEEN MODIFIED BY WB4JFI TO ADD A FILTER SHADOW # ON THE DISPLAY TO INDICATE FILTER BANDWIDTH. LINES WITH # "WB4JFI" IN THE COMMENTS HAVE BEEN ADDED TO James # Ahlstrom's ORIGINAL CODE. DO NOT bug HIM ABOUT IT!! """The main program for Quisk, a software defined radio. Usage: python quisk.py [-c | --config config_file_path] This can also be installed as a package and run as quisk.main(). """ # Change to the directory of quisk.py. This is necessary to import Quisk packages # and to load other extension modules that link against _quisk.so. It also helps to # find ./__init__.py and ./help.html. import sys, os os.chdir(os.path.normpath(os.path.dirname(__file__))) import wx, wx.html, wx.lib.buttons, wx.lib.stattext, wx.lib.colourdb import math, cmath, time, traceback import threading, pickle import _quisk as QS from types import * # Command line parsing: be able to specify the config file. from optparse import OptionParser parser = OptionParser() parser.add_option('-c', '--config', dest='config_file_path', help='Specify the configuration file path') argv_options = parser.parse_args()[0] # These FFT sizes have multiple small factors, and are prefered for efficiency: fftPreferedSizes = (416, 448, 480, 512, 576, 640, 672, 704, 768, 800, 832, 864, 896, 960, 1024, 1056, 1120, 1152, 1248, 1280, 1344, 1408, 1440, 1536, 1568, 1600, 1664, 1728, 1760, 1792, 1920, 2016, 2048, 2080, 2112, 2240, 2304, 2400, 2464, 2496, 2560, 2592, 2688, 2816, 2880, 2912) class Timer: """Debug: measure and print times every ptime seconds. Call with msg == '' to start timer, then with a msg to record the time. """ def __init__(self, ptime = 1.0): self.ptime = ptime # frequency to print in seconds self.time0 = 0 # time zero; measure from this time self.time_print = 0 # last time data was printed self.timers = {} # one timer for each msg self.names = [] # ordered list of msg self.heading = 1 # print heading on first use def __call__(self, msg): tm = time.time() if msg: if not self.time0: # Not recording data return if self.timers.has_key(msg): count, average, highest = self.timers[msg] else: self.names.append(msg) count = 0 average = highest = 0.0 count += 1 delta = tm - self.time0 average += delta if highest < delta: highest = delta self.timers[msg] = (count, average, highest) if tm - self.time_print > self.ptime: # time to print results self.time0 = 0 # end data recording, wait for reset self.time_print = tm if self.heading: self.heading = 0 print "count, msg, avg, max (msec)" print "%4d" % count, for msg in self.names: # keep names in order count, average, highest = self.timers[msg] if not count: continue average /= count print " %s %7.3f %7.3f" % (msg, average * 1e3, highest * 1e3), self.timers[msg] = (0, 0.0, 0.0) print else: # reset the time to zero self.time0 = tm # Start timer if not self.time_print: self.time_print = tm ## T = Timer() # Make a timer instance class SoundThread(threading.Thread): """Create a second (non-GUI) thread to read, process and play sound.""" def __init__(self): self.do_init = 1 threading.Thread.__init__(self) self.doQuit = threading.Event() self.doQuit.clear() def run(self): """Read, process, play sound; then notify the GUI thread to check for FFT data.""" if self.do_init: # Open sound using this thread self.do_init = 0 QS.start_sound() wx.CallAfter(application.PostStartup) while not self.doQuit.isSet(): QS.read_sound() wx.CallAfter(application.OnReadSound) QS.close_sound() def stop(self): """Set a flag to indicate that the sound thread should end.""" self.doQuit.set() class FrequencyDisplay(wx.lib.stattext.GenStaticText): """Create a frequency display widget.""" def __init__(self, frame, gbs, width, height): wx.lib.stattext.GenStaticText.__init__(self, frame, -1, '3', style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE|wx.RAISED_BORDER) border = 4 for points in range(30, 6, -1): font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(font) w, h = self.GetTextExtent('333 444 555 Hz') if w < width and h < height - border * 2: break self.SetSizeHints(w, h, w * 5, h) self.height = h self.points = points border = self.border = (height - self.height) / 2 self.height_and_border = h + border * 2 self.SetBackgroundColour(conf.color_freq) gbs.Add(self, (0, 0), (1, 3), flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=border) def Clip(self, clip): """Change color to indicate clipping.""" if clip: self.SetBackgroundColour('deep pink') else: self.SetBackgroundColour(conf.color_freq) def Display(self, freq): """Set the frequency to be displayed.""" freq = int(freq) if freq >= 0: t = str(freq) minus = '' else: t = str(-freq) minus = '- ' l = len(t) if l > 9: txt = "%s%s %s %s %s" % (minus, t[0:-9], t[-9:-6], t[-6:-3], t[-3:]) elif l > 6: txt = "%s%s %s %s" % (minus, t[0:-6], t[-6:-3], t[-3:]) elif l > 3: txt = "%s%s %s" % (minus, t[0:-3], t[-3:]) else: txt = minus + t self.SetLabel('%s Hz' % txt) class SliderBoxV(wx.BoxSizer): """A vertical box containing a slider and a text heading""" # Note: A vertical wx slider has the max value at the bottom. This is # reversed for this control. def __init__(self, parent, text, init, themax, handler, display=False): wx.BoxSizer.__init__(self, wx.VERTICAL) self.slider = wx.Slider(parent, -1, init, 0, themax, style=wx.SL_VERTICAL) self.slider.Bind(wx.EVT_SCROLL, handler) sw, sh = self.slider.GetSize() self.text = text self.themax = themax if display: # Display the slider value when it is thumb'd self.text_ctrl = wx.StaticText(parent, -1, str(themax), style=wx.ALIGN_CENTER) w1, h1 = self.text_ctrl.GetSize() # Measure size with max number self.text_ctrl.SetLabel(text) w2, h2 = self.text_ctrl.GetSize() # Measure size with text self.width = max(w1, w2, sw) self.text_ctrl.SetSizeHints(self.width, -1, self.width) self.slider.Bind(wx.EVT_SCROLL_THUMBTRACK, self.Change) self.slider.Bind(wx.EVT_SCROLL_THUMBRELEASE, self.ChangeDone) else: self.text_ctrl = wx.StaticText(parent, -1, text) w2, h2 = self.text_ctrl.GetSize() # Measure size with text self.width = max(w2, sw) self.Add(self.text_ctrl, 0, wx.ALIGN_CENTER) self.Add(self.slider, 1, wx.ALIGN_CENTER) def Change(self, event): event.Skip() self.text_ctrl.SetLabel(str(self.themax - self.slider.GetValue())) def ChangeDone(self, event): event.Skip() self.text_ctrl.SetLabel(self.text) def GetValue(self): return self.themax - self.slider.GetValue() def SetValue(self, value): # Set slider visual position; does not call handler self.slider.SetValue(self.themax - value) # Start of our button classes. They are compatible with wxPython GenButton # buttons. Use the usual methods for access: # GetLabel(self), SetLabel(self, label): Get and set the label # Enable(self, flag), Disable(self), IsEnabled(self): Enable / Disable # GetValue(self), SetValue(self, value): Get / Set check button state True / False # SetIndex(self, index): For cycle buttons, set the label from its index class QuiskButtons: """Base class for special buttons.""" button_bezel = 3 # size of button bezel in pixels def InitButtons(self, text): self.SetBezelWidth(self.button_bezel) self.SetBackgroundColour(conf.color_btn) self.SetUseFocusIndicator(False) self.font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) if text: w, h = self.GetTextExtent(text) else: w, h = self.GetTextExtent("OK") self.Disable() # create a size for null text, but Disable() w += self.button_bezel * 2 + self.GetCharWidth() h = h * 12 / 10 h += self.button_bezel * 2 self.SetSizeHints(w, h, w * 6, h, 1, 1) def OnKeyDown(self, event): pass def OnKeyUp(self, event): pass class QuiskPushbutton(QuiskButtons, wx.lib.buttons.GenButton): """A plain push button widget.""" def __init__(self, parent, command, text, use_right=False): wx.lib.buttons.GenButton.__init__(self, parent, -1, text) self.command = command self.Bind(wx.EVT_BUTTON, self.OnButton) self.InitButtons(text) self.direction = 1 if use_right: self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) def OnButton(self, event): if self.command: self.command(event) def OnRightDown(self, event): self.direction = -1 self.OnLeftDown(event) def OnRightUp(self, event): self.OnLeftUp(event) self.direction = 1 class QuiskRepeatbutton(QuiskButtons, wx.lib.buttons.GenButton): """A push button that repeats when held down.""" def __init__(self, parent, command, text, up_command=None, use_right=False): wx.lib.buttons.GenButton.__init__(self, parent, -1, text) self.command = command self.up_command = up_command self.timer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.OnTimer) self.Bind(wx.EVT_BUTTON, self.OnButton) self.InitButtons(text) self.repeat_state = 0 # repeater button inactive self.direction = 1 if use_right: self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) def SendCommand(self, command): if command: event = wx.PyEvent() event.SetEventObject(self) command(event) def OnLeftDown(self, event): if self.IsEnabled(): self.shift = event.ShiftDown() self.control = event.ControlDown() self.SendCommand(self.command) self.repeat_state = 1 # first button push self.timer.Start(milliseconds=300, oneShot=True) wx.lib.buttons.GenButton.OnLeftDown(self, event) def OnLeftUp(self, event): if self.IsEnabled(): self.SendCommand(self.up_command) self.repeat_state = 0 self.timer.Stop() wx.lib.buttons.GenButton.OnLeftUp(self, event) def OnRightDown(self, event): if self.IsEnabled(): self.shift = event.ShiftDown() self.control = event.ControlDown() self.direction = -1 self.OnLeftDown(event) def OnRightUp(self, event): if self.IsEnabled(): self.OnLeftUp(event) self.direction = 1 def OnTimer(self, event): if self.repeat_state == 1: # after first push, turn on repeats self.timer.Start(milliseconds=150, oneShot=False) self.repeat_state = 2 if self.repeat_state: # send commands until button is released self.SendCommand(self.command) def OnButton(self, event): pass # button command not used class QuiskCheckbutton(QuiskButtons, wx.lib.buttons.GenToggleButton): """A button that pops up and down, and changes color with each push.""" # Check button; get the checked state with self.GetValue() def __init__(self, parent, command, text, color=None): wx.lib.buttons.GenToggleButton.__init__(self, parent, -1, text) self.InitButtons(text) self.Bind(wx.EVT_BUTTON, self.OnButton) self.button_down = 0 # used for radio buttons self.command = command if color is None: self.color = conf.color_check_btn else: self.color = color def SetValue(self, value, do_cmd=False): wx.lib.buttons.GenToggleButton.SetValue(self, value) self.button_down = value if value: self.SetBackgroundColour(self.color) else: self.SetBackgroundColour(conf.color_btn) if do_cmd and self.command: event = wx.PyEvent() event.SetEventObject(self) self.command(event) def OnButton(self, event): if self.GetValue(): self.SetBackgroundColour(self.color) else: self.SetBackgroundColour(conf.color_btn) if self.command: self.command(event) class QuiskCycleCheckbutton(QuiskCheckbutton): """A button that cycles through its labels with each push. The button is up for labels[0], down for all other labels. Change to the next label for each push. If you call SetLabel(), the label must be in the list. The self.index is the index of the current label. """ def __init__(self, parent, command, labels, color=None, is_radio=False): self.labels = list(labels) # Be careful if you change this list self.index = 0 # index of selected label 0, 1, ... self.direction = 0 # 1 for up, -1 for down, 0 for no change to index self.is_radio = is_radio # Is this a radio cycle button? if color is None: color = conf.color_cycle_btn QuiskCheckbutton.__init__(self, parent, command, labels[0], color) self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) def SetLabel(self, label, do_cmd=False): self.index = self.labels.index(label) QuiskCheckbutton.SetLabel(self, label) QuiskCheckbutton.SetValue(self, self.index) if do_cmd and self.command: event = wx.PyEvent() event.SetEventObject(self) self.command(event) def SetIndex(self, index, do_cmd=False): self.index = index QuiskCheckbutton.SetLabel(self, self.labels[index]) QuiskCheckbutton.SetValue(self, index) if do_cmd and self.command: event = wx.PyEvent() event.SetEventObject(self) self.command(event) def OnButton(self, event): if not self.is_radio or self.button_down: self.direction = 1 self.index += 1 if self.index >= len(self.labels): self.index = 0 self.SetIndex(self.index) else: self.direction = 0 if self.command: self.command(event) def OnRightDown(self, event): # Move left in the list of labels if not self.is_radio or self.GetValue(): self.index -= 1 if self.index < 0: self.index = len(self.labels) - 1 self.SetIndex(self.index) self.direction = -1 if self.command: self.command(event) class RadioButtonGroup: """This class encapsulates a group of radio buttons. This class is not a button! The "labels" is a list of labels for the toggle buttons. An item of labels can be a list/tuple, and the corresponding button will be a cycle button. """ def __init__(self, parent, command, labels, default): self.command = command self.buttons = [] self.button = None for text in labels: if type(text) in (ListType, TupleType): b = QuiskCycleCheckbutton(parent, self.OnButton, text, is_radio=True) for t in text: if t == default and self.button is None: b.SetLabel(t) self.button = b else: b = QuiskCheckbutton(parent, self.OnButton, text) if text == default and self.button is None: b.SetValue(True) self.button = b self.buttons.append(b) def SetLabel(self, label, do_cmd=False): self.button = None for b in self.buttons: if self.button is not None: b.SetValue(False) elif isinstance(b, QuiskCycleCheckbutton): try: index = b.labels.index(label) except ValueError: b.SetValue(False) continue else: b.SetIndex(index) self.button = b b.SetValue(True) elif b.GetLabel() == label: b.SetValue(True) self.button = b else: b.SetValue(False) if do_cmd and self.command and self.button: event = wx.PyEvent() event.SetEventObject(self.button) self.command(event) def GetButtons(self): return self.buttons def OnButton(self, event): win = event.GetEventObject() for b in self.buttons: if b is win: self.button = b b.SetValue(True) else: b.SetValue(False) if self.command: self.command(event) def GetLabel(self): if not self.button: return None return self.button.GetLabel() def GetSelectedButton(self): # return the selected button return self.button class ConfigScreen(wx.ScrolledWindow): """Display the configuration and status screen.""" def __init__(self, parent, width, fft_size): wx.ScrolledWindow.__init__(self, parent, pos = (0, 0), size = (width, 100), style = wx.VSCROLL | wx.NO_BORDER) self.SetBackgroundColour(conf.color_graph) self.Bind(wx.EVT_PAINT, self.OnPaint) self.width = width self.setscroll = True self.fft_size = fft_size self.interupts = 0 self.read_error = -1 self.write_error = -1 self.underrun_error = -1 self.fft_error = -1 self.latencyCapt = -1 self.latencyPlay = -1 self.y_scale = 0 self.y_zero = 0 self.rate_min = -1 self.rate_max = -1 self.chan_min = -1 self.chan_max = -1 self.mic_max_display = 0 self.w_phase = None self.err_msg = "No response" self.msg1 = "" self.tabstops = [1] self.tabstops.append(self.tabstops[-1] + 18) self.tabstops.append(self.tabstops[-1] + 2) self.tabstops.append(self.tabstops[-1] + 20) self.tabstops.append(self.tabstops[-1] + 2) self.tabstops.append(self.tabstops[-1] + 17) self.tabstops.append(self.tabstops[-1] + 2) self.tabstops.append(self.tabstops[-1] + 17) self.tabstops.append(self.tabstops[-1] + 2) points = 24 while points > 4: self.font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) charx = self.charx = self.GetCharWidth() chary = self.chary = self.GetCharHeight() if self.tabstops[-1] * charx < width: break points -= 2 for i in range(len(self.tabstops)): self.tabstops[i] *= charx self.dy = chary # line spacing def OnPaint(self, event): dc = wx.PaintDC(self) dc.SetFont(self.font) dc.SetTextForeground('Black') x0 = self.tabstops[0] x, y = self.GetViewStart() self.y = -y + self.dy # blank line at top self.row = 1 p = conf.name_of_sound_play if p: p = "Output to " + p else: p = "Output to (None)" self.MakeRow(dc, 'Interrupts', self.interupts, p, None, None, None, 'Play rate', conf.playback_rate) self.MakeRow(dc, 'Minimum rate', self.rate_min, 'Maximum rate', self.rate_max, 'Min channels', self.chan_min, 'Max channels', self.chan_max) self.MakeRow(dc, 'Capture errors', self.read_error, 'Playback errors', self.write_error, 'Underrun errors', self.underrun_error, 'FFT errors', self.fft_error) self.MakeRow(dc, 'Capture latency', self.latencyCapt, 'Playback latency', self.latencyPlay, 'Total latency', self.latencyCapt + self.latencyPlay, 'FFT points', self.fft_size) self.y += self.dy * 5 / 10 # extra half line if self.err_msg: # Error message dc.SetTextForeground('Red') dc.DrawText(self.err_msg, x0, self.y) # fill='#F00') dc.SetTextForeground('Black') self.y += self.dy if self.msg1: dc.DrawText(self.msg1, x0, self.y) self.y += self.dy t = "Capture rate %d %s" % (application.sample_rate, application.config_text) if application.sound_error: dc.SetTextForeground('Red') dc.DrawText(t, x0, self.y) dc.SetTextForeground('Black') else: dc.DrawText(t, x0, self.y) self.y += self.dy if conf.config_file_exists: t = "Using configuration file %s" % conf.config_file_path else: t = "Configuration file %s was not found" % conf.config_file_path dc.DrawText(t, x0, self.y) self.y += self.dy name = conf.microphone_name if name: if self.mic_max_display > -0.1: t = "Microphone %s maximum level %3.0f db CLIP" % (name, self.mic_max_display) dc.SetTextForeground('Red') dc.DrawText(t, x0, self.y) dc.SetTextForeground('Black') else: t = "Microphone %s maximum level %3.0f db" % (name, self.mic_max_display) dc.DrawText(t, x0, self.y) else: t = "The microphone is not used (null name)." dc.DrawText(t, x0, self.y) self.height = self.y + 2 * self.dy if self.setscroll: self.setscroll = False sp = self.chary # Make controls # Button for phase adjust dialog t = wx.StaticText(self, -1, "Sound card phase", pos=(x0, self.height)) x, y = t.GetSizeTuple() self.phase = wx.Button(self, -1, "Adjust...") self.Bind(wx.EVT_BUTTON, self.OnBtnPhase, self.phase) x1, y1 = self.phase.GetSizeTuple() yoff = (y1 - y) / 2 self.phase.SetPosition((x0 + x + sp, self.height - yoff)) # Choice (combo) box for decimation lst = Hardware.VarDecimGetChoices() if lst: txt = Hardware.VarDecimGetLabel() x2 = self.width / 2 t = wx.StaticText(self, -1, txt, pos=(x2, self.height)) x, y = t.GetSizeTuple() c = wx.Choice(self, -1, pos=(x2 + x + sp, self.height - yoff), choices=lst) self.Bind(wx.EVT_CHOICE, application.OnBtnDecimation, c) index = Hardware.VarDecimGetIndex() c.SetSelection(index) self.height += y1 # The height is now known; set scroll size self.SetScrollbars(1, 1, self.width, self.height + 2 * self.dy) def MakeRow(self, dc, *args): y = self.y for col in range(len(args)): x = self.tabstops[col] t = args[col] if t is not None: t = str(t) if col % 2 == 1: w, h = dc.GetTextExtent(t) x -= w dc.DrawText(t, x, y) self.row += 1 self.y += self.dy def OnGraphData(self, data=None): (self.rate_min, self.rate_max, sample_rate, self.chan_min, self.chan_max, self.msg1, self.unused, self.err_msg, self.read_error, self.write_error, self.underrun_error, self.latencyCapt, self.latencyPlay, self.interupts, self.fft_error, self.mic_max_display, self.data_poll_usec ) = QS.get_state() self.mic_max_display = 20.0 * math.log10((self.mic_max_display + 1) / 32767.0) self.Refresh() def ChangeYscale(self, y_scale): pass def ChangeYzero(self, y_zero): pass def OnIdle(self, event): pass def SetTxFreq(self, freq): pass def OnBtnPhase(self, event): application.screenBtnGroup.SetLabel('Graph', do_cmd=True) if self.w_phase: self.w_phase.Raise() else: self.w_phase = QAdjustPhase(self, self.width) def OnPhaseClose(self, event): self.w_phase.Destroy() self.w_phase = None class GraphDisplay(wx.Window): """Display the FFT graph within the graph screen.""" def __init__(self, parent, x, y, graph_width, height, chary): wx.Window.__init__(self, parent, pos = (x, y), size = (graph_width, height), style = wx.NO_BORDER) self.parent = parent self.chary = chary self.graph_width = graph_width self.line = [(0, 0), (1,1)] # initial fake graph data self.SetBackgroundColour(conf.color_graph) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown) self.Bind(wx.EVT_RIGHT_DOWN, parent.OnRightDown) self.Bind(wx.EVT_LEFT_UP, parent.OnLeftUp) self.Bind(wx.EVT_MOTION, parent.OnMotion) self.Bind(wx.EVT_MOUSEWHEEL, parent.OnWheel) self.tune_x = graph_width / 2 self.scale = 20 # pixels per 10 dB self.height = 10 self.y_min = 1000 self.y_max = 0 self.max_height = application.screen_height self.tuningPen = wx.Pen('Red', 1) self.fltrPen = wx.Pen(conf.color_bandwidth, 1) # WB4JFI ADD filter shadow self.fltrBrush = wx.Brush(conf.color_bandwidth, wx.SOLID) # WB4JFI ADD filter shadow self.fltr_disp_start = -100 # WB4JFI ADD filter shadow self.fltr_disp_size = 100 # WB4JFI ADD filter shadow self.fltr_disp_fill = -99 # WB4JFI ADD filter shadow self.fltr_disp_show = 0 # WB4JFI ADD filter shadow self.backgroundPen = wx.Pen(self.GetBackgroundColour(), 1) self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID) def OnPaint(self, event): #print 'GraphDisplay', self.GetUpdateRegion().GetBox() dc = wx.PaintDC(self) dc.SetPen(wx.BLACK_PEN) x = self.tune_x # setx x to tune freq horizontal position self.UpdateFilterDisplay() # WB4JFI ADD filter shadow if self.fltr_disp_show == 3: # WB4JFI ADD filter shadow dc.SetPen(self.fltrPen) # WB4JFI ADD filter shadow dc.SetBrush(self.fltrBrush) # WB4JFI ADD filter shadow dc.DrawRectangle(x + self.fltr_disp_start, 0, # WB4JFI ADD filter shadow self.fltr_disp_size, self.max_height) # WB4JFI ADD filter shadow dc.FloodFill(x + self.fltr_disp_fill, 1, conf.color_bandwidth, 1) # WB4JFI ADD filter shadow dc.SetPen(wx.BLACK_PEN) dc.DrawLines(self.line) # x = self.tune_x # WB4JFI moved up for filter shadow dc.SetPen(self.tuningPen) # dc.DrawLines(self.line) # x = self.tune_x # dc.SetPen(self.tuningPen) dc.DrawLine(x, 0, x, self.max_height) if not self.parent.in_splitter: dc.SetPen(self.horizPen) chary = self.chary y = self.zeroDB for i in range(0, -99999, -10): if y >= chary / 2: dc.DrawLine(0, y, self.graph_width, y) # y line y = y + self.scale if y > self.height: break def SetHeight(self, height): self.height = height self.SetSize((self.graph_width, height)) def OnGraphData(self, data): line = [] x = 0 y_min = 1000 y_max = 0 for y in data: # y is in dB, -130 to 0 y = self.zeroDB - int(y * self.scale / 10.0 + 0.5) if y > y_max: y_max = y if y < y_min: y_min = y line.append((x, y)) x = x + 1 ymax = max(y_max, self.y_max) ymin = min(y_min, self.y_min) rect = wx.Rect(0, ymin, 1000, ymax - ymin) self.y_min = y_min self.y_max = y_max self.line = line self.Refresh() #rect=rect) def UpdateFilterDisplay(self): # WB4JFI ADD - Update filter display if application.fltr_display: # WB4JFI, check if OK to display filter self.fltr_disp_show = self.fltr_disp_show | 1 # set bit 0, show OK from OUTSIDE else: # otherwise, OUTSIDE don't display self.fltr_disp_show = self.fltr_disp_show & 2 # by clearing bit 0 if application.screen == application.graph or application.screen == application.waterfall: self.fltr_disp_show = self.fltr_disp_show | 2 # bit 1 shows proper screen to display else: self.fltr_disp_show = self.fltr_disp_show & 1 # clear bit 1 to indicate not proper display self.filter_pixels = (application.filterbw / (application.sample_rate /application.data_width) + 0.5) if self.filter_pixels < 2: # if calculated less than 2 pixels self.filter_pixels = 2 # make it at least 2 pixels wide if application.mode == 'LSB' or application.mode == 'CWL': # if filter on lower sideband... self.fltr_disp_start = -(self.filter_pixels - 1) # set start to be at lower side self.fltr_disp_size = self.filter_pixels # and set rectangle size self.fltr_disp_fill = self.fltr_disp_start # start filling at rectangle start if application.mode == 'USB' or application.mode == 'CWU': # if filter on upper sideband... self.fltr_disp_start = 0 # start is at tuning freq self.fltr_disp_size = self.filter_pixels # set rectangle size self.fltr_disp_fill = 1 # start filling at beginning of tune if application.mode == 'AM' or application.mode == 'FM': # if filter is centered at tune freq self.fltr_disp_start = -((self.filter_pixels - 1) /2) # set start at 1/2 bandidth self.fltr_disp_size = self.filter_pixels # set size to cover both sidebands self.fltr_disp_fill = self.filter_pixels # start filling at lower edge # # WB4JFI - end of filter add def SetTuningLine(self, x): dc = wx.ClientDC(self) dc.SetPen(self.backgroundPen) dc.DrawLine(self.tune_x, 0, self.tune_x, self.max_height) dc.SetPen(self.tuningPen) dc.DrawLine(x, 0, x, self.max_height) self.tune_x = x class GraphScreen(wx.Window): """Display the graph screen X and Y axis, and create a graph display.""" def __init__(self, parent, data_width, graph_width, in_splitter=0): wx.Window.__init__(self, parent, pos = (0, 0)) self.in_splitter = in_splitter # Are we in the top of a splitter window? if in_splitter: self.y_scale = conf.waterfall_graph_y_scale self.y_zero = conf.waterfall_graph_y_zero else: self.y_scale = conf.graph_y_scale self.y_zero = conf.graph_y_zero self.VFO = 0 self.WheelMod = 50 # Round frequency when using mouse wheel self.txFreq = 0 self.sample_rate = application.sample_rate self.data_width = data_width self.graph_width = graph_width self.doResize = False self.pen_tick = wx.Pen("Black", 1, wx.SOLID) self.font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) w = self.GetCharWidth() * 14 / 10 h = self.GetCharHeight() self.charx = w self.chary = h self.tick = max(2, h * 3 / 10) self.originX = w * 5 self.offsetY = h + self.tick self.width = self.originX + self.graph_width + self.tick + self.charx * 2 self.height = application.screen_height * 3 / 10 self.x0 = self.originX + self.graph_width / 2 # center of graph self.tuningX = self.x0 self.originY = 10 self.zeroDB = 10 # y location of zero dB; may be above the top of the graph self.scale = 10 self.SetSize((self.width, self.height)) self.SetSizeHints(self.width, 1, self.width) self.SetBackgroundColour(conf.color_graph) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_MOTION, self.OnMotion) self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) self.MakeDisplay() def MakeDisplay(self): self.display = GraphDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) self.display.zeroDB = self.zeroDB def OnPaint(self, event): dc = wx.PaintDC(self) if not self.in_splitter: dc.SetFont(self.font) self.MakeYTicks(dc) self.MakeXTicks(dc) def OnIdle(self, event): if self.doResize: self.ResizeGraph() def OnSize(self, event): self.doResize = True event.Skip() def ResizeGraph(self): """Change the height of the graph. Changing the width interactively is not allowed because the FFT size is fixed. Call after changing the zero or scale to recalculate the X and Y axis marks. """ w, h = self.GetClientSize() if self.in_splitter: # Splitter window has no X axis scale self.height = h self.originY = h else: self.height = h - self.chary # Leave space for X scale self.originY = self.height - self.offsetY self.MakeYScale() self.display.SetHeight(self.originY) self.display.scale = self.scale self.doResize = False self.Refresh() def ChangeYscale(self, y_scale): self.y_scale = y_scale self.doResize = True def ChangeYzero(self, y_zero): self.y_zero = y_zero self.doResize = True def MakeYScale(self): chary = self.chary scale = (self.originY - chary) * 10 / (self.y_scale + 20) # Number of pixels per 10 dB scale = max(1, scale) q = (self.originY - chary ) / scale / 2 zeroDB = chary + q * scale - self.y_zero * scale / 10 if zeroDB > chary: zeroDB = chary self.scale = scale self.zeroDB = zeroDB self.display.zeroDB = self.zeroDB QS.record_graph(self.originX, self.zeroDB, self.scale) def MakeYTicks(self, dc): chary = self.chary x1 = self.originX - self.tick * 3 # left of tick mark x2 = self.originX - 1 # x location of y axis x3 = self.originX + self.graph_width # end of graph data dc.SetPen(self.pen_tick) dc.DrawLine(x2, 0, x2, self.originY + 1) # y axis y = self.zeroDB for i in range(0, -99999, -10): if y >= chary / 2: dc.SetPen(self.pen_tick) dc.DrawLine(x1, y, x2, y) # y tick t = `i` w, h = dc.GetTextExtent(t) dc.DrawText(`i`, x1 - w, y - h / 2) # y text y = y + self.scale if y > self.originY: break def MakeXTicks(self, dc): originY = self.originY x3 = self.originX + self.graph_width # end of fft data charx , z = dc.GetTextExtent('-30000XX') tick0 = self.tick tick1 = tick0 * 2 tick2 = tick0 * 3 # Draw the X axis dc.SetPen(self.pen_tick) dc.DrawLine(self.originX, originY, x3, originY) # Draw the band plan colors below the X axis x = self.originX f = float(x - self.x0) * self.sample_rate / self.data_width c = None y = originY + 1 for freq, color in conf.BandPlan: freq -= self.VFO if f < freq: xend = int(self.x0 + float(freq) * self.data_width / self.sample_rate + 0.5) if c is not None: dc.SetPen(wx.TRANSPARENT_PEN) dc.SetBrush(wx.Brush(c)) dc.DrawRectangle(x, y, min(x3, xend) - x, tick0) # x axis if xend >= x3: break x = xend f = freq c = color stick = 1000 # small tick in Hertz mtick = 5000 # medium tick ltick = 10000 # large tick # check the width of the frequency label versus frequency span df = charx * self.sample_rate / self.data_width if df < 5000: tfreq = 5000 # tick frequency for labels elif df < 10000: tfreq = 10000 elif df < 20000: tfreq = 20000 elif df < 50000: tfreq = 50000 stick = 5000 mtick = 10000 ltick = 50000 else: tfreq = 100000 stick = 5000 mtick = 10000 ltick = 50000 # Draw the X axis ticks and frequency in kHz dc.SetPen(self.pen_tick) freq1 = self.VFO - self.sample_rate / 2 freq1 = (freq1 / stick) * stick freq2 = freq1 + self.sample_rate + stick + 1 y_end = 0 for f in range (freq1, freq2, stick): x = self.x0 + int(float(f - self.VFO) / self.sample_rate * self.data_width) if self.originX <= x <= x3: if f % ltick is 0: # large tick dc.DrawLine(x, originY, x, originY + tick2) elif f % mtick is 0: # medium tick dc.DrawLine(x, originY, x, originY + tick1) else: # small tick dc.DrawLine(x, originY, x, originY + tick0) if f % tfreq is 0: # place frequency label t = str(f/1000) w, h = dc.GetTextExtent(t) dc.DrawText(t, x - w / 2, originY + tick2) y_end = originY + tick2 + h if y_end: # mark the center of the display dc.DrawLine(self.x0, y_end, self.x0, application.screen_height) def OnGraphData(self, data): i1 = (self.data_width - self.graph_width) / 2 i2 = i1 + self.graph_width self.display.OnGraphData(data[i1:i2]) def SetVFO(self, vfo): self.VFO = vfo self.doResize = True def SetTxFreq(self, freq): self.txFreq = freq x = self.x0 + int(float(freq) / self.sample_rate * self.data_width) self.display.SetTuningLine(x - self.originX) self.tuningX = x def GetMousePosition(self, event): """For mouse clicks in our display, translate to our screen coordinates.""" mouse_x, mouse_y = event.GetPositionTuple() win = event.GetEventObject() if win is not self: x, y = win.GetPositionTuple() mouse_x += x mouse_y += y return mouse_x, mouse_y def OnRightDown(self, event): mouse_x, mouse_y = self.GetMousePosition(event) freq = float(mouse_x - self.x0) * self.sample_rate / self.data_width freq = int(freq) if self.VFO > 0: vfo = self.VFO + freq vfo = (vfo + 5000) / 10000 * 10000 # round to even number tune = freq + self.VFO - vfo self.ChangeHwFrequency(tune, vfo, 'MouseBtn3', event) def OnLeftDown(self, event): mouse_x, mouse_y = self.GetMousePosition(event) self.mouse_x = mouse_x if mouse_y > self.originY: # click below X axis self.mouse_origin = self.tuningX else: # click above X axis self.mouse_origin = mouse_x freq = float(mouse_x - self.x0) * self.sample_rate / self.data_width freq = int(freq) self.ChangeHwFrequency(freq, self.VFO, 'MouseBtn1', event) self.CaptureMouse() def OnLeftUp(self, event): if self.HasCapture(): self.ReleaseMouse() def OnMotion(self, event): if event.Dragging() and event.LeftIsDown(): mouse_x, mouse_y = self.GetMousePosition(event) if conf.mouse_tune_method: # Mouse motion changes the VFO frequency x = (mouse_x - self.mouse_x) # Thanks to VK6JBL self.mouse_x = mouse_x freq = x * self.sample_rate / self.data_width freq = int(freq) self.ChangeHwFrequency(self.txFreq, self.VFO - freq, 'MouseMotion', event) else: # Mouse motion changes the tuning frequency # Frequency changes more rapidly for higher mouse Y position speed = max(10, self.originY - mouse_y) / float(self.originY) x = (mouse_x - self.mouse_x) self.mouse_x = mouse_x freq = speed * x * self.sample_rate / self.data_width freq = int(freq) self.ChangeHwFrequency(self.txFreq + freq, self.VFO, 'MouseMotion', event) def OnWheel(self, event): wm = self.WheelMod # Round frequency when using mouse wheel tune = self.txFreq + wm * event.GetWheelRotation() / event.GetWheelDelta() if tune >= 0: tune = tune / wm * wm else: # tune can be negative when the VFO is zero tune = - (- tune / wm * wm) self.ChangeHwFrequency(tune, self.VFO, 'MouseWheel', event) def ChangeHwFrequency(self, tune, vfo, source, event): application.ChangeHwFrequency(tune, vfo, source, event=event) class WaterfallDisplay(wx.Window): """Create a waterfall display within the waterfall screen.""" def __init__(self, parent, x, y, graph_width, height, margin): wx.Window.__init__(self, parent, pos = (x, y), size = (graph_width, height), style = wx.NO_BORDER) self.parent = parent self.graph_width = graph_width self.margin = margin self.height = 10 self.sample_rate = application.sample_rate self.SetBackgroundColour('Black') self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown) self.Bind(wx.EVT_RIGHT_DOWN, parent.OnRightDown) self.Bind(wx.EVT_LEFT_UP, parent.OnLeftUp) self.Bind(wx.EVT_MOTION, parent.OnMotion) self.Bind(wx.EVT_MOUSEWHEEL, parent.OnWheel) self.tune_x = graph_width / 2 self.tuningPen = wx.Pen('White', 3) self.marginPen = wx.Pen(conf.color_graph, 1) # Size of top faster scroll region is (top_key + 2) * (top_key - 1) / 2 self.top_key = 8 self.top_size = (self.top_key + 2) * (self.top_key - 1) / 2 # Make the palette pal2 = conf.waterfallPalette red = [] green = [] blue = [] n = 0 for i in range(256): if i > pal2[n+1][0]: n = n + 1 red.append((i - pal2[n][0]) * (long)(pal2[n+1][1] - pal2[n][1]) / (long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][1]) green.append((i - pal2[n][0]) * (long)(pal2[n+1][2] - pal2[n][2]) / (long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][2]) blue.append((i - pal2[n][0]) * (long)(pal2[n+1][3] - pal2[n][3]) / (long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][3]) self.red = red self.green = green self.blue = blue bmp = wx.EmptyBitmap(0, 0) bmp.x_origin = 0 self.bitmaps = [bmp] * application.screen_height def OnPaint(self, event): dc = wx.PaintDC(self) y = 0 dc.SetPen(self.marginPen) x_origin = int(float(self.VFO) / self.sample_rate * self.data_width + 0.5) for i in range(0, self.margin): dc.DrawLine(0, y, self.graph_width, y) y += 1 index = 0 if conf.waterfall_scroll_mode: # Draw the first few lines multiple times for i in range(self.top_key, 1, -1): b = self.bitmaps[index] x = b.x_origin - x_origin for j in range(0, i): dc.DrawBitmap(b, x, y) y += 1 index += 1 while y < self.height: b = self.bitmaps[index] x = b.x_origin - x_origin dc.DrawBitmap(b, x, y) y += 1 index += 1 dc.SetPen(self.tuningPen) dc.SetLogicalFunction(wx.XOR) dc.DrawLine(self.tune_x, 0, self.tune_x, self.height) def SetHeight(self, height): self.height = height self.SetSize((self.graph_width, height)) def OnGraphData(self, data, y_zero, y_scale): #T('graph start') row = '' # Make a new row of pixels for a one-line image for x in data: # x is -130 to 0, or so (dB) l = int((x + y_zero / 3 + 100) * y_scale / 10) l = max(l, 0) l = min(l, 255) row = row + "%c%c%c" % (chr(self.red[l]), chr(self.green[l]), chr(self.blue[l])) #T('graph string') bmp = wx.BitmapFromBuffer(len(row) / 3, 1, row) bmp.x_origin = int(float(self.VFO) / self.sample_rate * self.data_width + 0.5) self.bitmaps.insert(0, bmp) del self.bitmaps[-1] self.ScrollWindow(0, 1, None) self.Refresh(False, (0, 0, self.graph_width, self.top_size + self.margin)) #T('graph end') def SetTuningLine(self, x): dc = wx.ClientDC(self) dc.SetPen(self.tuningPen) dc.SetLogicalFunction(wx.XOR) dc.DrawLine(self.tune_x, 0, self.tune_x, self.height) dc.DrawLine(x, 0, x, self.height) self.tune_x = x class WaterfallScreen(wx.SplitterWindow): """Create a splitter window with a graph screen and a waterfall screen""" def __init__(self, frame, width, data_width, graph_width): self.y_scale = conf.waterfall_y_scale self.y_zero = conf.waterfall_y_zero wx.SplitterWindow.__init__(self, frame) self.SetSizeHints(width, -1, width) self.SetMinimumPaneSize(1) self.SetSize((width, conf.waterfall_graph_size + 100)) # be able to set sash size self.pane1 = GraphScreen(self, data_width, graph_width, 1) self.pane2 = WaterfallPane(self, data_width, graph_width) self.SplitHorizontally(self.pane1, self.pane2, conf.waterfall_graph_size) def OnIdle(self, event): self.pane1.OnIdle(event) self.pane2.OnIdle(event) def SetTxFreq(self, freq): self.pane1.SetTxFreq(freq) self.pane2.SetTxFreq(freq) def SetVFO(self, vfo): self.pane1.SetVFO(vfo) self.pane2.SetVFO(vfo) def ChangeYscale(self, y_scale): # Test if the shift key is down if wx.GetKeyState(wx.WXK_SHIFT): # Set graph screen self.pane1.ChangeYscale(y_scale) else: # Set waterfall screen self.y_scale = y_scale self.pane2.ChangeYscale(y_scale) def ChangeYzero(self, y_zero): # Test if the shift key is down if wx.GetKeyState(wx.WXK_SHIFT): # Set graph screen self.pane1.ChangeYzero(y_zero) else: # Set waterfall screen self.y_zero = y_zero self.pane2.ChangeYzero(y_zero) def OnGraphData(self, data): self.pane1.OnGraphData(data) self.pane2.OnGraphData(data) class WaterfallPane(GraphScreen): """Create a waterfall screen with an X axis and a waterfall display.""" def __init__(self, frame, data_width, graph_width): GraphScreen.__init__(self, frame, data_width, graph_width) self.y_scale = conf.waterfall_y_scale self.y_zero = conf.waterfall_y_zero self.oldVFO = self.VFO def MakeDisplay(self): self.display = WaterfallDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) self.display.VFO = self.VFO self.display.data_width = self.data_width def SetVFO(self, vfo): GraphScreen.SetVFO(self, vfo) self.display.VFO = vfo if self.oldVFO != vfo: self.oldVFO = vfo self.Refresh() def MakeYTicks(self, dc): pass def ChangeYscale(self, y_scale): self.y_scale = y_scale def ChangeYzero(self, y_zero): self.y_zero = y_zero def OnGraphData(self, data): i1 = (self.data_width - self.graph_width) / 2 i2 = i1 + self.graph_width self.display.OnGraphData(data[i1:i2], self.y_zero, self.y_scale) class ScopeScreen(wx.Window): """Create an oscilloscope screen (mostly used for debug).""" def __init__(self, parent, width, data_width, graph_width): wx.Window.__init__(self, parent, pos = (0, 0), size=(width, -1), style = wx.NO_BORDER) self.SetBackgroundColour(conf.color_graph) self.font = wx.Font(16, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_PAINT, self.OnPaint) self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID) self.y_scale = conf.scope_y_scale self.y_zero = conf.scope_y_zero self.running = 1 self.doResize = False self.width = width self.height = 100 self.originY = self.height / 2 self.data_width = data_width self.graph_width = graph_width w = self.charx = self.GetCharWidth() h = self.chary = self.GetCharHeight() tick = max(2, h * 3 / 10) self.originX = w * 3 self.width = self.originX + self.graph_width + tick + self.charx * 2 self.line = [(0,0), (1,1)] # initial fake graph data def OnIdle(self, event): if self.doResize: self.ResizeGraph() def OnSize(self, event): self.doResize = True event.Skip() def ResizeGraph(self, event=None): # Change the height of the graph. Changing the width interactively is not allowed. w, h = self.GetClientSize() self.height = h self.originY = h / 2 self.doResize = False self.Refresh() def OnPaint(self, event): dc = wx.PaintDC(self) dc.SetFont(self.font) self.MakeYTicks(dc) self.MakeXTicks(dc) self.MakeText(dc) dc.SetPen(wx.BLACK_PEN) dc.DrawLines(self.line) def MakeYTicks(self, dc): chary = self.chary originX = self.originX x3 = self.x3 = originX + self.graph_width # end of graph data dc.SetPen(wx.BLACK_PEN) dc.DrawLine(originX, 0, originX, self.originY * 3) # y axis # Find the size of the Y scale markings themax = 2.5e9 * 10.0 ** - ((160 - self.y_scale) / 50.0) # value at top of screen themax = int(themax) l = [] for j in (5, 6, 7, 8): for i in (1, 2, 5): l.append(i * 10 ** j) for yvalue in l: n = themax / yvalue + 1 # Number of lines ypixels = self.height / n if n < 20: break dc.SetPen(self.horizPen) for i in range(1, 1000): y = self.originY - ypixels * i if y < chary: break # Above axis dc.DrawLine(originX, y, x3, y) # y line # Below axis y = self.originY + ypixels * i dc.DrawLine(originX, y, x3, y) # y line self.yscale = float(ypixels) / yvalue self.yvalue = yvalue def MakeXTicks(self, dc): originY = self.originY x3 = self.x3 # Draw the X axis dc.SetPen(wx.BLACK_PEN) dc.DrawLine(self.originX, originY, x3, originY) # Find the size of the X scale markings in microseconds for i in (20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000): xscale = i # X scale in microseconds if application.sample_rate * xscale * 0.000001 > self.width / 30: break # Draw the X lines dc.SetPen(self.horizPen) for i in range(1, 999): x = int(self.originX + application.sample_rate * xscale * 0.000001 * i + 0.5) if x > x3: break dc.DrawLine(x, 0, x, self.height) # x line self.xscale = xscale def MakeText(self, dc): if self.running: t = " RUN" else: t = " STOP" if self.xscale >= 1000: t = "%s X: %d millisec/div" % (t, self.xscale) else: t = "%s X: %d microsec/div" % (t, self.xscale) yt = `self.yvalue` t = "%s Y: %sE%d/div" % (t, yt[0], len(yt) - 1) dc.DrawText(t, self.originX, self.height - self.chary) def OnGraphData(self, data): if not self.running: return # Preserve data line = [] x = self.originX ymax = self.height for y in data: # y is raw samples 0 to 2**31-1 y = self.originY + int(y * self.yscale + 0.5) if y > ymax: y = ymax elif y < 0: y = 0 line.append((x, y)) x = x + 1 if x > self.x3: break self.line = line self.Refresh() def ChangeYscale(self, y_scale): self.y_scale = y_scale self.doResize = True def ChangeYzero(self, y_zero): self.y_zero = y_zero def SetTxFreq(self, freq): pass class FilterScreen(GraphScreen): """Create a graph of the receive filter response.""" def __init__(self, parent, data_width, graph_width): GraphScreen.__init__(self, parent, data_width, graph_width) self.y_scale = conf.filter_y_scale self.y_zero = conf.filter_y_zero self.VFO = 0 self.txFreq = 0 self.data = [] self.sample_rate = QS.get_filter_rate() def NewFilter(self): self.data = QS.get_filter() def OnGraphData(self, data): GraphScreen.OnGraphData(self, self.data) def ChangeHwFrequency(self, tune, vfo, source, event): self.SetTxFreq(tune) application.freqDisplay.Display(tune) class HelpScreen(wx.html.HtmlWindow): """Create the screen for the Help button.""" def __init__(self, parent, width, height): wx.html.HtmlWindow.__init__(self, parent, -1, size=(width, height)) self.y_scale = 0 self.y_zero = 0 if "gtk2" in wx.PlatformInfo: self.SetStandardFonts() self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22]) # read in text from file help.html in the directory of this module self.LoadFile('help.html') def OnGraphData(self, data): pass def ChangeYscale(self, y_scale): pass def ChangeYzero(self, y_zero): pass def OnIdle(self, event): pass def SetTxFreq(self, freq): pass class QMainFrame(wx.Frame): """Create the main top-level window.""" def __init__(self, width, height): fp = open('__init__.py') # Read in the title title = fp.readline().strip()[1:] fp.close() wx.Frame.__init__(self, None, -1, title, wx.DefaultPosition, (width, height), wx.DEFAULT_FRAME_STYLE, 'MainFrame') self.SetBackgroundColour(conf.color_bg) self.Bind(wx.EVT_CLOSE, self.OnBtnClose) def OnBtnClose(self, event): application.OnBtnClose(event) self.Destroy() class QAdjustPhase(wx.Frame): """Create a window with amplitude and phase adjustment controls""" f_ampl = "Amplitude adjustment %.6f" f_phase = "Phase adjustment %.6f" def __init__(self, parent, width): wx.Frame.__init__(self, application.main_frame, -1, "Adjust Sound Card Amplitude and Phase", pos=(50, 100)) self.Bind(wx.EVT_CLOSE, parent.OnPhaseClose) self.ampl, self.phase = application.GetAmplPhase() self.MakeControls(width) self.Show() def MakeControls(self, width): # Make controls for phase/amplitude adjustment chary = self.GetCharHeight() y = chary * 3 / 10 self.t_ampl = wx.StaticText(self, -1, self.f_ampl % self.ampl, pos=(0, y)) y += self.t_ampl.GetSizeTuple()[1] scale = width * 4 / 10 self.scale = float(scale) self.ampl1 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.ampl1.GetSizeTuple()[1] self.ampl2 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.ampl2.GetSizeTuple()[1] self.t_phase = wx.StaticText(self, -1, self.f_phase % self.phase, pos=(0, y)) y += self.t_phase.GetSizeTuple()[1] self.phase1 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.phase1.GetSizeTuple()[1] self.phase2 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.phase2.GetSizeTuple()[1] self.SetSizeHints(width, y, width, y) # no change in size self.ampl1.Bind(wx.EVT_SCROLL, self.OnAmpl1) self.ampl2.Bind(wx.EVT_SCROLL, self.OnAmpl1) self.phase1.Bind(wx.EVT_SCROLL, self.OnAmpl1) self.phase2.Bind(wx.EVT_SCROLL, self.OnAmpl1) def OnAmpl1(self, event): s2 = self.scale * 10.0 # maximum 0.10 change s1 = s2 * 20.0 # smaller maximum change ampl = self.ampl + self.ampl1.GetValue() / s1 + self.ampl2.GetValue() / s2 self.t_ampl.SetLabel(self.f_ampl % ampl) phase = self.phase + self.phase1.GetValue() / s1 + self.phase2.GetValue() / s2 self.t_phase.SetLabel(self.f_phase % phase) application.SetAmplPhase(ampl, phase) class Spacer(wx.Window): """Create a bar between the graph screen and the controls""" def __init__(self, parent): wx.Window.__init__(self, parent, pos = (0, 0), size=(-1, 6), style = wx.NO_BORDER) self.Bind(wx.EVT_PAINT, self.OnPaint) r, g, b = parent.GetBackgroundColour().Get() dark = (r * 7 / 10, g * 7 / 10, b * 7 / 10) light = (r + (255 - r) * 5 / 10, g + (255 - g) * 5 / 10, b + (255 - b) * 5 / 10) self.dark_pen = wx.Pen(dark, 1, wx.SOLID) self.light_pen = wx.Pen(light, 1, wx.SOLID) self.width = application.screen_width def OnPaint(self, event): dc = wx.PaintDC(self) w = self.width dc.SetPen(self.dark_pen) dc.DrawLine(0, 0, w, 0) dc.DrawLine(0, 1, w, 1) dc.DrawLine(0, 2, w, 2) dc.SetPen(self.light_pen) dc.DrawLine(0, 3, w, 3) dc.DrawLine(0, 4, w, 4) dc.DrawLine(0, 5, w, 5) class App(wx.App): """Class representing the application.""" freq60 = (5330500, 5346500, 5366500, 5371500, 5403500) StateNames = [ # Names of state attributes to save and restore 'bandState', 'bandAmplPhase', 'lastBand', 'VFO', 'txFreq', 'mode', ] def __init__(self): global application application = self self.init_path = None if sys.stdout.isatty(): wx.App.__init__(self, redirect=False) else: wx.App.__init__(self, redirect=True) def QuiskPushbutton(self, *args, **kw): # Make our buttons available to widget files return QuiskPushbutton(*args, **kw) def QuiskRepeatbutton(self, *args, **kw): return QuiskRepeatbutton(*args, **kw) def QuiskCheckbutton(self, *args, **kw): return QuiskCheckbutton(*args, **kw) def QuiskCycleCheckbutton(self, *args, **kw): return QuiskCycleCheckbutton(*args, **kw) def RadioButtonGroup(self, *args, **kw): return RadioButtonGroup(*args, **kw) def OnInit(self): """Perform most initialization of the app here (called by wxPython on startup).""" wx.lib.colourdb.updateColourDB() # Add additional color names global conf # conf is the module for all configuration data import quisk_conf_defaults as conf cpath = argv_options.config_file_path # Get config file path if not cpath: cpath = os.path.expanduser('~/.quisk_conf.py') # Default path setattr(conf, 'config_file_path', cpath) if os.path.isfile(cpath): # See if the user has a config file setattr(conf, 'config_file_exists', True) d = {} d.update(conf.__dict__) # make items from conf available execfile(cpath, d) # execute the user's config file for k, v in d.items(): # add user's config items to conf if k[0] != '_': # omit items starting with '_' setattr(conf, k, v) else: setattr(conf, 'config_file_exists', False) if conf.invertSpectrum: QS.invert_spectrum(1) self.bandState = {} self.bandState.update(conf.bandState) self.bandAmplPhase = conf.bandAmplPhase # Open hardware file global Hardware if hasattr(conf, "Hardware"): # Hardware defined in config file Hardware = conf.Hardware(self, conf) else: Hardware = conf.quisk_hardware.Hardware(self, conf) if Hardware.VarDecimGetChoices(): # Hardware can change the decimation. self.sample_rate = Hardware.VarDecimSet() # Get the sample rate. else: # Use the sample rate from the config file. self.sample_rate = conf.sample_rate if not hasattr(conf, 'playback_rate'): if conf.use_sdriq or conf.use_rx_udp: conf.playback_rate = 48000 else: conf.playback_rate = conf.sample_rate self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow self.smeter_db_count = 0 # average the S-meter self.smeter_db_sum = 0 self.smeter_db = 0 self.smeter_sunits = -87.0 self.timer = time.time() # A seconds clock self.heart_time0 = self.timer # timer to call HeartBeat at intervals self.smeter_db_time0 = self.timer self.smeter_sunits_time0 = self.timer self.band_up_down = 0 # Are band Up/Down buttons in use? self.lastBand = 'Audio' self.VFO = 0 self.ritFreq = 0 self.txFreq = 0 self.screen = None self.audio_volume = 0.0 # Set output volume, 0.0 to 1.0 self.sidetone_volume = 0.0 # Set sidetone volume, 0.0 to 1.0 self.sound_error = 0 self.sound_thread = None self.mode = conf.default_mode self.bottom_widgets = None self.color_list = None self.color_index = 0 self.filterbw = 0 # WB4JFI Added for filter display self.fltr_display = conf.filter_display # WB4JFI added filter display 1=OK, 0=do not display dc = wx.ScreenDC() # get the screen size (self.screen_width, self.screen_height) = dc.GetSizeTuple() del dc self.Bind(wx.EVT_IDLE, self.OnIdle) self.Bind(wx.EVT_QUERY_END_SESSION, self.OnEndSession) # Save and restore program state if conf.persistent_state: self.init_path = os.path.join(os.path.dirname(cpath), '.quisk_init.pkl') try: fp = open(self.init_path, "rb") d = pickle.load(fp) fp.close() for k, v in d.items(): if k in self.StateNames: if k == 'bandState': self.bandState.update(v) else: setattr(self, k, v) except: pass #traceback.print_exc() for k, (vfo, tune, mode) in self.bandState.items(): # Historical: fix bad frequencies try: f1, f2 = conf.BandEdge[k] if not f1 <= vfo + tune <= f2: self.bandState[k] = conf.bandState[k] except KeyError: pass # Find the data width from a list of prefered sizes; it is the width of returned graph data. # The graph_width is the width of data_width that is displayed. width = self.screen_width * conf.graph_width percent = conf.display_fraction # display central fraction of total width percent = int(percent * 100.0 + 0.4) width = width * 100 / percent for x in fftPreferedSizes: if x > width: self.data_width = x break else: self.data_width = fftPreferedSizes[-1] self.graph_width = self.data_width * percent / 100 if self.graph_width % 2 == 1: # Both data_width and graph_width are even numbers self.graph_width += 1 # The FFT size times the average_count controls the graph refresh rate factor = float(self.sample_rate) / conf.graph_refresh / self.data_width ifactor = int(factor + 0.5) if conf.fft_size_multiplier >= ifactor: # Use large FFT and average count 1 fft_mult = ifactor average_count = 1 elif conf.fft_size_multiplier > 0: # Specified fft_size_multiplier fft_mult = conf.fft_size_multiplier average_count = int(factor / fft_mult + 0.5) if average_count < 1: average_count = 1 else: # Calculate the split between fft size and average if self.sample_rate <= 240000: maxfft = 8000 # Maximum fft size else: maxfft = 15000 fft1 = maxfft / self.data_width if fft1 >= ifactor: fft_mult = ifactor average_count = 1 else: av1 = int(factor / fft1 + 0.5) if av1 < 1: av1 = 1 err1 = factor / (fft1 * av1) av2 = av1 + 1 fft2 = int(factor / av2 + 0.5) err2 = factor / (fft2 * av2) if 0.9 < err1 < 1.1 or abs(1.0 - err1) <= abs(1.0 - err2): fft_mult = fft1 average_count = av1 else: fft_mult = fft2 average_count = av2 self.fft_size = self.data_width * fft_mult # Record the basic application parameters QS.record_app(self, conf, self.data_width, self.fft_size, average_count, self.sample_rate) #print 'FFT size %d, FFT mult %d, average_count %d' % ( # self.fft_size, self.fft_size / self.data_width, average_count) #print 'Refresh %.2f Hz' % (float(self.sample_rate) / self.fft_size / average_count) QS.record_graph(0, 0, 1.0) self.width = self.screen_width * 8 / 10 self.height = self.screen_height * 5 / 10 self.main_frame = frame = QMainFrame(self.width, self.height) self.SetTopWindow(frame) # Make all the screens and hide all but one self.graph = GraphScreen(frame, self.data_width, self.graph_width) self.screen = self.graph width = self.graph.width button_width = width # try to estimate the final button width self.config_screen = ConfigScreen(frame, width, self.fft_size) self.config_screen.Hide() self.waterfall = WaterfallScreen(frame, width, self.data_width, self.graph_width) self.waterfall.Hide() self.scope = ScopeScreen(frame, width, self.data_width, self.graph_width) self.scope.Hide() self.filter_screen = FilterScreen(frame, self.data_width, self.graph_width) self.filter_screen.Hide() self.help_screen = HelpScreen(frame, width, self.screen_height / 10) self.help_screen.Hide() frame.SetSizeHints(width, 100) # Make a vertical box to hold all the screens and the bottom box vertBox = self.vertBox = wx.BoxSizer(wx.VERTICAL) frame.SetSizer(vertBox) # Add the screens vertBox.Add(self.config_screen, 1) vertBox.Add(self.graph, 1) vertBox.Add(self.waterfall, 1) vertBox.Add(self.scope, 1) vertBox.Add(self.filter_screen, 1) vertBox.Add(self.help_screen, 1) # Add the spacer vertBox.Add(Spacer(frame), 0, wx.EXPAND) # Add the bottom box hBoxA = wx.BoxSizer(wx.HORIZONTAL) vertBox.Add(hBoxA, 0, wx.EXPAND) # End of vertical box. Add items to the horizontal box. # Add two sliders on the left margin = 3 self.sliderVol = SliderBoxV(frame, 'Vol', 300, 1000, self.ChangeVolume) button_width -= self.sliderVol.width + margin * 2 self.ChangeVolume() # set initial volume level hBoxA.Add(self.sliderVol, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) if Hardware.use_sidetone: self.sliderSto = SliderBoxV(frame, 'STo', 300, 1000, self.ChangeSidetone) button_width -= self.sliderSto.width + margin * 2 self.ChangeSidetone() hBoxA.Add(self.sliderSto, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) # Add the sizer for the middle gap = 2 gbs = wx.GridBagSizer(gap, gap) self.gbs = gbs button_width -= gap * 15 hBoxA.Add(gbs, 1, wx.EXPAND, 0) gbs.SetEmptyCellSize((5, 5)) button_width -= 5 for i in range(0, 6) + range(7, 13): gbs.AddGrowableCol(i) # Add two sliders on the right self.sliderYs = SliderBoxV(frame, 'Ys', 0, 160, self.ChangeYscale, True) button_width -= self.sliderYs.width + margin * 2 hBoxA.Add(self.sliderYs, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) self.sliderYz = SliderBoxV(frame, 'Yz', 0, 160, self.ChangeYzero, True) button_width -= self.sliderYz.width + margin * 2 hBoxA.Add(self.sliderYz, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) button_height = self.MakeButtons(frame, gbs) button_width /= 12 # This is our estimate of the final button size self.MakeTopRow(frame, gbs, button_width, button_height) if conf.quisk_widgets: self.bottom_widgets = conf.quisk_widgets.BottomWidgets(self, Hardware, conf, frame, gbs, vertBox) if QS.open_key(conf.key_method): print 'open_key failed for name "%s"' % conf.key_method if hasattr(conf, 'mixer_settings'): for dev, numid, value in conf.mixer_settings: err_msg = QS.mixer_set(dev, numid, value) if err_msg: print "Mixer", err_msg # Create transmit audio filters if conf.microphone_name: filtI, filtQ = self.MakeFilterCoef(conf.mic_sample_rate, 540, 2500, 1550) QS.set_tx_filters(filtI, filtQ, ()) # Open the hardware. This must be called before open_sound(). self.config_text = Hardware.open() if not self.config_text: self.config_text = "Missing config_text" QS.capt_channels (conf.channel_i, conf.channel_q) QS.play_channels (conf.channel_i, conf.channel_q) QS.micplay_channels (conf.mic_play_chan_I, conf.mic_play_chan_Q) # Note: Subsequent calls to set channels must not name a higher channel number. # Normally, these calls are only used to reverse the channels. QS.open_sound(conf.name_of_sound_capt, conf.name_of_sound_play, self.sample_rate, conf.data_poll_usec, conf.latency_millisecs, conf.microphone_name, conf.tx_ip, conf.tx_audio_port, conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q, conf.mic_out_volume, conf.name_of_mic_play, conf.mic_playback_rate) tune, vfo = Hardware.ReturnFrequency() # Request initial frequency to set band if tune is not None: for band, (f1, f2) in conf.BandEdge.items(): if f1 <= tune <= f2: # Change to the correct band based on frequency self.lastBand = band break self.bandBtnGroup.SetLabel(self.lastBand, do_cmd=True) self.ChangeHwFrequency(None, None) # Request initial VFO and tuning # Note: The filter rate is not valid until after the call to open_sound(). # Create FM audio filter frate = QS.get_filter_rate() # filter rate filtI, filtQ = self.MakeFmFilterCoef(frate, 600, 340, 2800) QS.set_fm_filters(filtI) # Record filter rate for the filter screen self.filter_screen.sample_rate = frate #if info[8]: # error message # self.sound_error = 1 # self.config_screen.err_msg = info[8] # print info[8] if self.sound_error: self.screenBtnGroup.SetLabel('Config', do_cmd=True) frame.Show() else: self.screenBtnGroup.SetLabel(conf.default_screen, do_cmd=True) frame.Show() self.Yield() self.sound_thread = SoundThread() self.sound_thread.start() return True def OnIdle(self, event): if self.screen: self.screen.OnIdle(event) def OnEndSession(self, event): event.Skip() self.OnBtnClose() def OnBtnClose(self, event): if self.sound_thread: self.sound_thread.stop() for i in range(0, 20): if threading.activeCount() == 1: break time.sleep(0.1) def OnExit(self): QS.close_rx_udp() Hardware.close() if self.init_path: # save current program state d = {} for n in self.StateNames: d[n] = getattr(self, n) try: fp = open(self.init_path, "wb") pickle.dump(d, fp) fp.close() except: pass #traceback.print_exc() def MakeTopRow(self, frame, gbs, button_width, button_height): # Down button b_down = QuiskRepeatbutton(frame, self.OnBtnDownBand, "Down", self.OnBtnUpDnBandDone) gbs.Add(b_down, (0, 4), flag=wx.ALIGN_CENTER) # RIT button self.ritButton = QuiskCheckbutton(frame, self.OnBtnRit, "RIT") gbs.Add(self.ritButton, (0, 7), flag=wx.ALIGN_CENTER) # Up button b_up = QuiskRepeatbutton(frame, self.OnBtnUpBand, "Up", self.OnBtnUpDnBandDone) gbs.Add(b_up, (0, 5), flag=wx.ALIGN_CENTER) bw, bh = b_down.GetMinSize() # make top row buttons the same size bw = (bw + button_width) / 2 bh = max(bh, button_height) b_down.SetSizeHints(bw, bh, bw * 5, bh) b_up.SetSizeHints(bw, bh, bw * 5, bh) self.ritButton.SetSizeHints(bw, bh, bw * 5, bh) # RIT slider self.ritScale = wx.Slider(frame, -1, self.ritFreq, -2000, 2000, size=(-1, -1), style=wx.SL_LABELS) self.ritScale.Bind(wx.EVT_SCROLL, self.OnRitScale) gbs.Add(self.ritScale, (0, 8), (1, 3), flag=wx.EXPAND) sw, sh = self.ritScale.GetSize() # Frequency display h = max(bh, sh) # larger of button and slider height self.freqDisplay = FrequencyDisplay(frame, gbs, button_width * 3, h) self.freqDisplay.Display(self.txFreq + self.VFO) # Frequency entry e = wx.TextCtrl(frame, -1, '', style=wx.TE_PROCESS_ENTER|wx.RAISED_BORDER) w, h = e.GetTextExtent('33333333') h = self.freqDisplay.height e.SetSizeHints(w, h, w * 2, h) e.SetBackgroundColour(conf.color_entry) gbs.Add(e, (0, 3), flag= wx.EXPAND | wx.TOP | wx.BOTTOM, border=self.freqDisplay.border) frame.Bind(wx.EVT_TEXT_ENTER, self.FreqEntry, source=e) # S-meter self.smeter = t = wx.lib.stattext.GenStaticText(frame, -1, '', style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE|wx.RAISED_BORDER) for points in range(20, 6, -1): font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) t.SetFont(font) w, h = t.GetTextExtent("ZZS 9 -100.00 dBZZ") if w < button_width * 2: break h = h * 12 / 10 t.SetSizeHints(w, h, -1, h) border = (self.freqDisplay.height_and_border - h) / 2 t.SetBackgroundColour(conf.color_freq) gbs.Add(t, (0, 11), (1, 2), flag=wx.ALIGN_CENTER|wx.EXPAND| wx.TOP | wx.BOTTOM, border=border) def MakeButtons(self, frame, gbs): all_buttons = [] # There are six columns, a small gap column, and then six more columns flag = wx.EXPAND ### Left bank of buttons self.bandBtnGroup = RadioButtonGroup(frame, self.OnBtnBand, conf.bandLabels, None) btns = self.bandBtnGroup.buttons all_buttons += btns i = 0 n1 = len(conf.bandLabels) / 2 n2 = len(conf.bandLabels) - n1 for col in range(0, n1): gbs.Add(btns[i], (1, col), flag=flag) i += 1 for col in range(0, n2): gbs.Add(btns[i], (2, col), flag=flag) i += 1 # Mute, AGC buttons = [] b = QuiskCheckbutton(frame, self.OnBtnMute, text='Mute') buttons.append(b) b = QuiskCycleCheckbutton(frame, self.OnBtnAGC, ('AGC', 'AGC 1', 'AGC 2')) buttons.append(b) b.SetLabel('AGC 1', True) b = QuiskCheckbutton(frame, self.OnBtnNB, text='') buttons.append(b) try: labels = Hardware.rf_gain_labels except: labels = () if labels: self.BtnRfGain = QuiskCycleCheckbutton(frame, Hardware.OnButtonRfGain, labels) buttons.append(self.BtnRfGain) else: b = QuiskCheckbutton(frame, None, text='') buttons.append(b) self.BtnRfGain = None #b = QuiskRepeatbutton(frame, self.OnBtnColor, '', use_right=True) if conf.add_fdx_button: b = QuiskCheckbutton(frame, self.OnBtnFDX, 'FDX', color=conf.color_test) else: b = QuiskCheckbutton(frame, None, text='') buttons.append(b) b = QuiskCheckbutton(frame, self.OnBtnTest1, 'Test 1', color=conf.color_test) buttons.append(b) all_buttons += buttons for col in range(0, 6): gbs.Add(buttons[col], (3, col), flag=flag) ### Right bank of buttons labels = [('CWL', 'CWU'), ('LSB', 'USB'), 'AM', 'FM', conf.add_extern_demod, ''] if conf.add_imd_button: labels[-1] = ('IMD', 'IMD -3dB', 'IMD -6dB') self.modeButns = RadioButtonGroup(frame, self.OnBtnMode, labels, None) btns = self.modeButns.GetButtons() all_buttons += btns btns[-1].color = conf.color_test for col in range(0, 6): gbs.Add(btns[col], (1, col + 7), flag=flag) labels = ('0',) * 6 self.filterButns = RadioButtonGroup(frame, self.OnBtnFilter, labels, None) btns = self.filterButns.GetButtons() all_buttons += btns for col in range(0, 6): gbs.Add(btns[col], (2, col + 7), flag=flag) labels = ('Graph', 'WFall', ('Scope', 'Scope'), 'Config', 'RX Filter', 'Help') self.screenBtnGroup = RadioButtonGroup(frame, self.OnBtnScreen, labels, conf.default_screen) btns = self.screenBtnGroup.GetButtons() all_buttons += btns for col in range(0, 6): gbs.Add(btns[col], (3, col + 7), flag=flag) bw = bh = 0 for b in all_buttons: # find the largest button size w, h = b.GetMinSize() bw = max(bw, w) bh = max(bh, h) for b in all_buttons: # set all buttons to the same size b.SetSizeHints(bw, bh, bw * 5, bh) return bh # return the button height def NewSmeter(self): #avg_seconds = 5.0 # seconds for S-meter average avg_seconds = 1.0 self.smeter_db_count += 1 # count for average x = QS.get_smeter() self.smeter_db_sum += x # sum for average if self.timer - self.smeter_db_time0 > avg_seconds: # average time reached self.smeter_db = self.smeter_db_sum / self.smeter_db_count self.smeter_db_count = self.smeter_db_sum = 0 self.smeter_db_time0 = self.timer if self.smeter_sunits < x: # S-meter moves to peak value self.smeter_sunits = x else: # S-meter decays at this time constant self.smeter_sunits -= (self.smeter_sunits - x) * (self.timer - self.smeter_sunits_time0) self.smeter_sunits_time0 = self.timer s = self.smeter_sunits / 6.0 # change to S units; 6db per S unit s += Hardware.correct_smeter # S-meter correction for the gain, band, etc. if s >= 9.5: s = (s - 9.0) * 6 t = "S9 + %.0f %.2f dB" % (s, self.smeter_db) else: t = "S %.0f %.2f dB" % (s, self.smeter_db) self.smeter.SetLabel(t) def MakeFilterButtons(self, *args): # Change the filter selections depending on the mode: CW, SSB, etc. i = 0 for b in self.filterButns.GetButtons(): b.SetLabel(str(args[i])) b.Refresh() i += 1 def MakeFilterCoef(self, rate, N, bw, center): """Make an I/Q filter with rectangular passband.""" K = bw * N / rate filtI = [] filtQ = [] pi = math.pi sin = math.sin cos = math.cos tune = 2. * pi * center / rate for k in range(-N/2, N/2 + 1): # Make a lowpass filter if k == 0: z = float(K) / N else: z = 1.0 / N * sin(pi * k * K / N) / sin(pi * k / N) # Apply a windowing function if 1: # Blackman window w = 0.42 + 0.5 * cos(2. * pi * k / N) + 0.08 * cos(4. * pi * k / N) elif 0: # Hamming w = 0.54 + 0.46 * cos(2. * pi * k / N) elif 0: # Hanning w = 0.5 + 0.5 * cos(2. * pi * k / N) else: w = 1 z *= w # Make a bandpass filter by tuning the low pass filter to new center frequency. # Make two quadrature filters. if tune: z *= 2.0 * cmath.exp(-1j * tune * k) filtI.append(z.real) filtQ.append(z.imag) else: filtI.append(z) filtQ.append(z) return filtI, filtQ def MakeFmFilterCoef(self, rate, N, f1, f2): """Make an audio filter with FM de-emphasis; remove CTCSS tones.""" bw = f2 - f1 center = (f1 + f2) / 2 N2 = N / 2 # Half the number of points K2 = bw * N / rate / 2 # Half the bandwidth in points filtI = [] filtQ = [] passb = [0] * (N + 1) # desired passband response idft = [0] * (N + 1) # inverse DFT of desired passband pi = math.pi sin = math.sin cos = math.cos tune = 2. * pi * center / rate # indexing can be from - N2 thru + N2 inclusive; total points is 2 * N2 + 1 # indexing can be from 0 thru 2 * N2 inclusive; total points is 2 * N2 + 1 for j in range(-K2, K2 + 1): # Filter shape is -6 bB per octave jj = j + N2 freq = center - bw / 2.0 * float(j) / K2 passb[jj] = float(center) / freq * 0.3 for k in range(-N2 + 1, N2 + 1): # Take inverse DFT of passband response kk = k + N2 x = 0 + 0J for m in range(-N2, N2 + 1): mm = m + N2 if passb[mm]: x += passb[mm] * cmath.exp(1J * 2.0 * pi * m * k / N) x /= N idft[kk] = x idft[0] = idft[-1] # this value is missing for k in range(-N2, N2 + 1): kk = k + N2 z = idft[kk] # Apply a windowing function if 1: # Blackman window w = 0.42 + 0.5 * cos(2. * pi * k / N) + 0.08 * cos(4. * pi * k / N) elif 0: # Hamming w = 0.54 + 0.46 * cos(2. * pi * k / N) elif 0: # Hanning w = 0.5 + 0.5 * cos(2. * pi * k / N) else: w = 1 z *= w # Make a bandpass filter by tuning the low pass filter to new center frequency. # Make two quadrature filters. if tune: z *= 2.0 * cmath.exp(-1j * tune * k) filtI.append(z.real) filtQ.append(z.imag) else: filtI.append(z.real) filtQ.append(z.real) return filtI, filtQ def OnBtnFilter(self, event, bw=None): if event is None: # called by application self.filterButns.SetLabel(str(bw)) else: # called by button btn = event.GetEventObject() bw = int(btn.GetLabel()) self.filterbw = int(self.filterButns.GetLabel()) # WB4JFI ADD - udate filter bandwidth application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp mode = self.mode if mode in ("CWL", "CWU"): N = 1000 center = max(conf.cwTone, bw/2) elif mode in ('LSB', 'USB'): N = 540 center = 300 + bw / 2 else: # AM and FM N = 140 center = 0 frate = QS.get_filter_rate() filtI, filtQ = self.MakeFilterCoef(frate, N, bw, center) QS.set_filters(filtI, filtQ, bw) if self.screen is self.filter_screen: self.screen.NewFilter() def OnBtnScreen(self, event, name=None): if event is not None: win = event.GetEventObject() name = win.GetLabel() self.screen.Hide() if name == 'Config': self.screen = self.config_screen elif name == 'Graph': self.screen = self.graph self.screen.SetTxFreq(self.txFreq) self.freqDisplay.Display(self.VFO + self.txFreq) application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp elif name == 'WFall': self.screen = self.waterfall self.screen.SetTxFreq(self.txFreq) self.freqDisplay.Display(self.VFO + self.txFreq) sash = self.screen.GetSashPosition() application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp elif name == 'Scope': if win.direction: # Another push on the same button self.scope.running = 1 - self.scope.running # Toggle run state else: # Initial push of button self.scope.running = 1 self.screen = self.scope elif name == 'RX Filter': self.screen = self.filter_screen self.screen.SetTxFreq(self.screen.txFreq) self.freqDisplay.Display(self.screen.txFreq) self.screen.NewFilter() elif name == 'Help': self.screen = self.help_screen self.screen.Show() self.vertBox.Layout() # This destroys the initialized sash position! self.sliderYs.SetValue(self.screen.y_scale) self.sliderYz.SetValue(self.screen.y_zero) if name == 'WFall': self.screen.SetSashPosition(sash) def ChangeYscale(self, event): self.screen.ChangeYscale(self.sliderYs.GetValue()) def ChangeYzero(self, event): self.screen.ChangeYzero(self.sliderYz.GetValue()) def OnBtnMute(self, event): btn = event.GetEventObject() if btn.GetValue(): QS.set_volume(0) else: QS.set_volume(self.audio_volume) def OnBtnDecimation(self, event): i = event.GetSelection() rate = Hardware.VarDecimSet(i) if rate != self.sample_rate: self.sample_rate = rate self.graph.sample_rate = rate self.waterfall.pane1.sample_rate = rate self.waterfall.pane2.sample_rate = rate self.waterfall.pane2.display.sample_rate = rate average_count = float(rate) / conf.graph_refresh / self.fft_size average_count = int(average_count + 0.5) average_count = max (1, average_count) QS.change_rate(rate, average_count) tune = self.txFreq vfo = self.VFO self.txFreq = self.VFO = -1 # demand change self.ChangeHwFrequency(tune, vfo, 'NewDecim') def ChangeVolume(self, event=None): # Caution: event can be None value = self.sliderVol.GetValue() # Simulate log taper pot x = (10.0 ** (float(value) * 0.003000434077) - 1) / 1000.0 self.audio_volume = x # audio_volume is 0 to 1.000 QS.set_volume(x) def ChangeSidetone(self, event=None): # Caution: event can be None value = self.sliderSto.GetValue() # Simulate log taper pot x = (10.0 ** (float(value) * 0.003) - 1) / 1000.0 self.sidetone_volume = x QS.set_sidetone(x, self.ritFreq, conf.keyupDelay) def OnRitScale(self, event=None): # Called when the RIT slider is moved # Caution: event can be None if self.ritButton.GetValue(): value = self.ritScale.GetValue() value = int(value) self.ritFreq = value QS.set_tune(self.txFreq + self.ritFreq, self.txFreq) QS.set_sidetone(self.sidetone_volume, self.ritFreq, conf.keyupDelay) def OnBtnRit(self, event=None): # Called when the RIT check button is pressed # Caution: event can be None if self.ritButton.GetValue(): self.ritFreq = self.ritScale.GetValue() else: self.ritFreq = 0 QS.set_tune(self.txFreq + self.ritFreq, self.txFreq) QS.set_sidetone(self.sidetone_volume, self.ritFreq, conf.keyupDelay) def SetRit(self, freq): if freq: self.ritButton.SetValue(1) else: self.ritButton.SetValue(0) self.ritScale.SetValue(freq) self.OnBtnRit() def OnBtnFDX(self, event): btn = event.GetEventObject() if btn.GetValue(): QS.set_fdx(1) else: QS.set_fdx(0) def OnBtnTest1(self, event): btn = event.GetEventObject() if btn.GetValue(): QS.add_tone(10000) else: QS.add_tone(0) def OnBtnTest2(self, event): return def OnBtnColor(self, event): if not self.color_list: clist = wx.lib.colourdb.getColourInfoList() self.color_list = [(0, clist[0][0])] self.color_index = 0 for i in range(1, len(clist)): if self.color_list[-1][1].replace(' ', '') != clist[i][0].replace(' ', ''): #if 'BLUE' in clist[i][0]: self.color_list.append((i, clist[i][0])) else: btn = event.GetEventObject() if btn.shift: del self.color_list[self.color_index] else: self.color_index += btn.direction if self.color_index >= len(self.color_list): self.color_index = 0 elif self.color_index < 0: self.color_index = len(self.color_list) -1 color = self.color_list[self.color_index][1] print self.color_index, color self.main_frame.SetBackgroundColour(color) self.main_frame.Refresh() self.screen.Refresh() def OnBtnAGC(self, event): btn = event.GetEventObject() # Set AGC: agcInUse, agcAttack, agcRelease if btn.index == 1: QS.set_agc(1, 1.0, 0.01) elif btn.index == 2: QS.set_agc(2, 1.0, 0.1) else: QS.set_agc(0, 0, 0) def OnBtnNB(self, event): pass def FreqEntry(self, event): freq = event.GetString() if not freq: return try: if '.' in freq: freq = int(float(freq) * 1E6 + 0.1) else: freq = int(freq) except ValueError: win = event.GetEventObject() win.Clear() win.AppendText("Error") else: for band, (f1, f2) in conf.BandEdge.items(): if f1 <= freq <= f2: # Change to the correct band based on frequency self.bandBtnGroup.SetLabel(band, do_cmd=True) break tune = freq % 10000 vfo = freq - tune self.ChangeHwFrequency(tune, vfo, 'FreqEntry') def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None): """Change the VFO and tuning frequencies, and notify the hardware. tune: the new tuning frequency in +- sample_rate/2; vfo: the new vfo frequency in Hertz; this is the RF frequency at zero Hz audio source: a string indicating the source or widget requesting the change; band: if source is "BtnBand", the band requested; event: for a widget, the event (used to access control/shift key state). Try to update the hardware by calling Hardware.ChangeFrequency(). The hardware will reply with the updated frequencies which may be different from those requested; use and display the returned tune and vfo. If tune or vfo is None, query the hardware for the current frequency. """ if tune is None or vfo is None: tune, vfo = Hardware.ReturnFrequency() if tune is None or vfo is None: # hardware did not change the frequency return else: tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event) tune -= vfo change = 0 if tune != self.txFreq: change = 1 self.txFreq = tune self.screen.SetTxFreq(self.txFreq) QS.set_tune(tune + self.ritFreq, tune) if vfo != self.VFO: change = 1 self.VFO = vfo self.graph.SetVFO(vfo) self.waterfall.SetVFO(vfo) if change: self.freqDisplay.Display(self.txFreq + self.VFO) def DisplayVFO(self, vfo, tune=None): """Change the frequencies internally and display the screen, but do not update the hardware. vfo: the new vfo frequency in Hertz; tune: the new tuning frequency in +- sample_rate/2. """ self.VFO = vfo self.graph.SetVFO(vfo) self.waterfall.SetVFO(vfo) if tune is not None: self.txFreq = tune self.screen.SetTxFreq(tune) self.freqDisplay.Display(self.txFreq + self.VFO) def OnBtnMode(self, event, mode=None): if event is None: # called by application self.modeButns.SetLabel(mode) else: # called by button mode = self.modeButns.GetLabel() Hardware.ChangeMode(mode) self.mode = mode if mode in ('CWL', 'CWU'): if mode == 'CWL': QS.set_rx_mode(0) self.SetRit(conf.cwTone) else: # CWU QS.set_rx_mode(1) self.SetRit(-conf.cwTone) self.MakeFilterButtons(200, 300, 400, 500, 1000, 3000) self.OnBtnFilter(None, 1000) elif mode in ('LSB', 'USB'): if mode == 'LSB': QS.set_rx_mode(2) # LSB else: QS.set_rx_mode(3) # USB self.SetRit(0) self.MakeFilterButtons(1800, 2000, 2200, 2500, 2800, 3300) self.OnBtnFilter(None, 2800) elif mode == 'AM': QS.set_rx_mode(4) self.SetRit(0) self.MakeFilterButtons(4000, 5000, 6000, 7000, 8000, 9000) self.OnBtnFilter(None, 6000) elif mode == 'FM': QS.set_rx_mode(5) self.SetRit(0) self.MakeFilterButtons(10000, 12000, 15000, 25000, 35000, 45000) self.OnBtnFilter(None, 12000) elif mode[0:3] == 'IMD': QS.set_rx_mode(10 + self.modeButns.GetSelectedButton().index) # 10, 11, 12 self.SetRit(0) self.MakeFilterButtons(1800, 2000, 2200, 2500, 2800, 3300) self.OnBtnFilter(None, 2800) elif mode == conf.add_extern_demod: # External demodulation QS.set_rx_mode(6) self.SetRit(0) self.MakeFilterButtons(10000, 12000, 15000, 25000, 35000, 45000) self.OnBtnFilter(None, 12000) application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp def OnBtnBand(self, event): band = self.lastBand # former band in use try: f1, f2 = conf.BandEdge[band] if f1 <= self.VFO + self.txFreq <= f2: self.bandState[band] = (self.VFO, self.txFreq, self.mode) except KeyError: pass btn = event.GetEventObject() band = btn.GetLabel() # new band self.lastBand = band try: vfo, tune, mode = self.bandState[band] except KeyError: vfo, tune, mode = (0, 0, 'LSB') if band == '60': freq = vfo + tune if btn.direction: vfo = self.VFO if 5100000 < vfo < 5600000: if btn.direction > 0: # Move up for f in self.freq60: if f > vfo + self.txFreq: freq = f break else: freq = self.freq60[0] else: # move down l = list(self.freq60) l.reverse() for f in l: if f < vfo + self.txFreq: freq = f break else: freq = self.freq60[-1] half = self.sample_rate / 2 * self.graph_width / self.data_width while freq - vfo <= -half + 1000: vfo -= 10000 while freq - vfo >= +half - 5000: vfo += 10000 tune = freq - vfo elif band == 'Time': vfo, tune, mode = conf.bandTime[btn.index] self.OnBtnMode(None, mode) ampl, phase = self.GetAmplPhase() QS.set_ampl_phase(ampl, phase) self.txFreq = self.VFO = -1 # demand change self.ChangeHwFrequency(tune, vfo, 'BtnBand', band=band) Hardware.ChangeBand(band) def OnBtnDownBand(self, event): self.band_up_down = 1 self.DisplayVFO(self.VFO - 10000, self.txFreq + 10000) def OnBtnUpBand(self, event): self.band_up_down = 1 self.DisplayVFO(self.VFO + 10000, self.txFreq - 10000) def OnBtnUpDnBandDone(self, event): self.band_up_down = 0 tune = self.txFreq vfo = self.VFO self.txFreq = self.VFO = 0 # Force an update self.ChangeHwFrequency(tune, vfo, 'BtnUpDown') def GetAmplPhase(self): if self.bandAmplPhase.has_key("panadapter"): return self.bandAmplPhase["panadapter"] try: return self.bandAmplPhase[self.lastBand] except KeyError: return (0.0, 0.0) def SetAmplPhase(self, ampl, phase): if self.bandAmplPhase.has_key("panadapter"): self.bandAmplPhase["panadapter"] = (ampl, phase) else: self.bandAmplPhase[self.lastBand] = (ampl, phase) QS.set_ampl_phase(ampl, phase) def PostStartup(self): # called once after sound attempts to start self.config_screen.OnGraphData(None) # update config in case sound is not running def OnReadSound(self): # called at frequent intervals self.timer = time.time() if self.screen == self.scope: data = QS.get_graph(0) # get raw data if data: self.scope.OnGraphData(data) # Send message to draw new data return 1 # we got new scope data else: data = QS.get_graph(1) # get FFT data if data: #T('') self.NewSmeter() # update the S-meter if self.screen == self.graph: self.waterfall.OnGraphData(data) # save waterfall data self.graph.OnGraphData(data) # Send message to draw new data else: self.screen.OnGraphData(data) # Send message to draw new data #T('graph data') #application.Yield() #T('Yield') return 1 # We got new graph/scope data if QS.get_overrange(): self.clip_time0 = self.timer self.freqDisplay.Clip(1) if self.clip_time0: if self.timer - self.clip_time0 > 1.0: self.clip_time0 = 0 self.freqDisplay.Clip(0) if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks self.heart_time0 = self.timer Hardware.HeartBeat() if not self.band_up_down: self.ChangeHwFrequency(None, None) # poll for changed frequency def main(): """If quisk is installed as a package, you can run it with quisk.main().""" App() application.MainLoop() if __name__ == '__main__': main() charleston-1.0/filter_display/3.4.7/filter_display_quisk_conf_defaults.py0000644000175000017500000004720111425733435025430 0ustar tfoxtfox# Please do not change the configuration file quisk_conf_defaults.py. # Instead copy one of the other quisk_conf_*.py files to your own # .quisk_conf.py and make changes there. For a normal sound card # configuration, copy quisk_conf_model.py to your .quisk_conf.py. # # Quisk imports quisk_conf_defaults to set its configuration. # If you have a configuration file, it then overwrites the defaults # with your parameters. Your configuration file must be named # ~/.quisk_conf.py, where "~" means your home directory. Or # you may specify a different name with the -c or --config command # line option. Try --help. Check the config screen to make sure that # the correct configuration file is in use. # # The Quisk receiver can use a high quality sound card for capture and playback, # or it can use the SDR-IQ by RfSpace for capture and a lower quality # sound card for playback. Quisk can also be used as a panadapter. # Quisk can control some rigs. See quisk_hardware_*.py. If you have a rig # to control, copy one of the quisk_hardware_*.py files to your own file named # quisk_hardware.py, and edit that file. If there is no quisk_hardware.py, then # quisk_hardware_model.py is used instead. # Import the default Hardware module. You can import a different module in # your .quisk_conf.py. import quisk_hardware_model as quisk_hardware # Module for additional widgets (advanced usage). quisk_widgets = None # Select the default screen when Quisk starts: default_screen = 'Graph' #default_screen = 'WFall' #default_screen = 'Config' # The width of the graph data as a fraction of the screen size. This # will be adjusted by Quisk to accommodate preferred FFT sizes. It can # not be changed once Quisk starts. It can not be made too small because # of the space needed for all the buttons. graph_width = 0.8 # Select the default mode when Quisk starts (overruled by persistent_state): # default_mode = 'FM' default_mode = 'USB' # Select the way the waterfall screen scrolls: # waterfall_scroll_mode = 0 # scroll at a constant rate. waterfall_scroll_mode = 1 # scroll faster at the top so that a new signal appears sooner. # Select the initial size in pixels (minimum 1) of the graph at the top of the waterfall. waterfall_graph_size = 80 # These are the initial values for the Y-scale and Y-zero sliders for each screen. # The sliders go from zero to 160. graph_y_scale = 100 graph_y_zero = 0 waterfall_y_scale = 80 waterfall_y_zero = 40 waterfall_graph_y_scale = 100 waterfall_graph_y_zero = 60 scope_y_scale = 80 scope_y_zero = 0 # Currently doesn't do anything filter_y_scale = 90 filter_y_zero = 0 filter_display = 0 # Quisk can save its current state in a file on exit, and restore it when you restart. # State includes band, frequency and mode, but not every item of state (not screen). # The file is .quisk_init.pkl in the same directory as your config file. If this file # becomes corrupted, just delete it and it will be reconstructed. #persistent_state = False persistent_state = True # This converts from dB to S-units for the S-meter (it is in S-units). correct_smeter = 15.5 # This is the fraction of spectrum to display from zero to one. It is needed if # the passband edges are not valid. Use 0.85 for the SDR-IQ. display_fraction = 1.00 # Define colors used by all widgets in wxPython colour format: color_bg = 'light steel blue' # Lower screen background color_graph = 'lemonchiffon1' # Graph background color_gl = 'grey' # Lines on the graph color_btn = 'steelblue2' # button color color_check_btn = 'yellow2' # color of a check button when it is checked color_cycle_btn = 'goldenrod3' # color of a cycle button when it is checked color_test = 'hot pink' # color of a button used for test (turn off for tx) color_freq = 'lightcyan1' # background color of frequency and s-meter color_entry = color_freq # frequency entry box color_bandwidth = 'lemonchiffon2' # audio bandwidth on spectrum display # WB4JFI ADD filter shadow # These are the palettes for the waterfall. The one used is named waterfallPallette, # so to use a different one, overwrite this name in your .quisk_conf.py. waterfallPalette = ( ( 0, 0, 0, 0), ( 36, 85, 0, 255), ( 73, 153, 0, 255), (109, 255, 0, 128), (146, 255, 119, 0), (182, 85, 255, 100), (219, 255, 255, 0), (255, 255, 255, 255) ) digipanWaterfallPalette = ( ( 0, 0, 0, 0), ( 32, 0, 0, 62), ( 64, 0, 0, 126), ( 96, 145, 142, 96), (128, 181, 184, 48), (160, 223, 226, 105), (192, 254, 254, 4), (255, 255, 58, 0) ) # Quisk can access your sound card through PortAudio or through ALSA drivers. # In PortAudio, soundcards have an index number 0, 1, 2, ... and a name. # The name can be something like "HDA NVidia: AD198x Analog (hw:0,0)" or # "surround41". In Quisk, all PortAudio device names start with "portaudio". # A device name like "portaudio#6" directly specifies the index. A name like # "portaudio:text" means to search for "text" in all available devices. And # there is a default device "portaudiodefault". So these portaudio names are useful: #name_of_sound_capt = "portaudio:(hw:0,0)" # First sound card #name_of_sound_capt = "portaudio:(hw:1,0)" # Second sound card, etc. #name_of_sound_capt = "portaudio#1" # Directly specified index #name_of_sound_capt = "portaudiodefault" # May give poor performance on capture # In ALSA, soundcards have these names. The "hw" devices are the raw # hardware devices, and should be used for soundcard capture. #name_of_sound_capt = "hw:0" # First sound card #name_of_sound_capt = "hw:1" # Second sound card, etc. #name_of_sound_capt = "plughw" #name_of_sound_capt = "plughw:1" #name_of_sound_capt = "default" # Normally you would capture and play on the same soundcard to avoid problems with the # two clocks running at slightly different rates. But you can define name_of_sound_play # to play back on a different device. Define this as the empty string "" to turn off # play (for a panadapter). # # For the SDR-IQ the soundcard is not used for capture; it only plays back audio. # Playback is always 48 kHz stereo. # Configuration for soundcard capture and playback use_sdriq = 0 # Get ADC samples from SDR-IQ is not used use_rx_udp = 0 # Get ADC samples from UDP is not used sample_rate = 48000 # ADC hardware sample rate in Hertz name_of_sound_capt = "hw:0" # Name of soundcard capture hardware device. name_of_sound_play = name_of_sound_capt # Use the same device for play back #name_of_sound_play = "" # Panadapter: Do not play channel_i = 0 # Soundcard index of in-phase channel: 0, 1, 2, ... channel_q = 1 # Soundcard index of quadrature channel: 0, 1, 2, ... # If you use a soundcard with Ethernet control of the VFO, set these parameters: rx_ip = "" # Receiver IP address for VFO control # If you use an SDR-IQ for capture, set these parameters: # import quisk_hardware_sdriq as quisk_hardware # Use different hardware file # use_sdriq = 1 # Capture device is the SDR-IQ # sdriq_name = "/dev/ft2450" # Name of the SDR-IQ device to open # sdriq_clock = 66666667.0 # actual sample rate (66666667 nominal) # sdriq_decimation = 500 # Must be 360, 500, 600, or 1250 # sample_rate = int(float(sdriq_clock) / sdriq_decimation + 0.5) # Don't change this # name_of_sound_capt = "" # We do not capture from the soundcard # name_of_sound_play = "hw:0" # Play back on this soundcard # playback_rate = 48000 # Radio sound play rate, default 48000 # channel_i = 0 # Soundcard index of left channel # channel_q = 1 # Soundcard index of right channel # display_fraction = 0.85 # The edges of the full bandwidth are not valid # If you receive ADC samples from a UDP port, set these parameters: # import quisk_hardware_n2adr as quisk_hardware # Use different hardware file # use_rx_udp = 1 # Get ADC samples from UDP # rx_udp_ip = "192.168.1.91" # Sample source IP address # rx_udp_port = 0xBC77 # Sample source UDP port # rx_udp_clock = 122880000 # ADC sample rate in Hertz # rx_udp_decimation = 8 * 8 * 8 # Decimation from clock to UDP sample rate # The allowable decimations are 8 times 8 times (2 or 4 or 8) times (1 or 5). # So you could enter 8 * 8 * one of (2, 4, 5, 8, 10, 20, 40). # These decimations result in a sample rate of 48 to 960 kHz. # sample_rate = int(float(rx_udp_clock) /rx_udp_decimation + 0.5) # Don't change this # name_of_sound_capt = "" # We do not capture from the soundcard # name_of_sound_play = "hw:0" # Play back on this soundcard # playback_rate = 48000 # Radio sound play rate, default 48000 # This is the received radio sound playback rate. The default will # be 48 kHz for the SDR-IQ and UDP port samples, and sample_rate for sound # card capture. Set it yourself for other rates or hardware. # playback_rate = 48000 # If you use quisk_hardware_fixed.py, this is the fixed VFO frequency in Hertz fixed_vfo_freq = 7056000 # This determines what happens when you tune by dragging the mouse. The correct # choice depends on how your hardware performs tuning. You may want to use a # custom hardware file with a custom ChangeFrequency() method too. mouse_tune_method = 0 # The Quisk tune frequency changes and the VFO frequency is unchanged. #mouse_tune_method = 1 # The Quisk tune frequency is unchanged and the VFO changes. # This is the CW tone frequency in Hertz cwTone = 600 # If you use the microphone feature, the mic_channel_I and Q are the two capture # microphone channels. Quisk uses a monophonic mic, so audio is taken from the I # channel, and the Q channel is (currently) ignored. It is OK to set the same # channel number for both, and this is necessary for a USB mono mic. If you # change the sample rate, you will need to change the C code to use different filters. # Mic samples can be sent to an Ethernet device (use tx_ip and name_of_mic_play = "") # or to a sound card (use name_of_mic_play="hw:1" or other device). # If mic samples are sent to a sound card for Tx, the samples are tuned to the audio # transmit frequency, and are set to zero unless the key is down. # If there is no mic (microphone_name = ""), it is still possible to transmit CW, # and you should set mic_playback_rate to the I/Q receive capture rate. # Microphone capture: microphone_name = "" # Name of microphone capture device (or "hw:1") mic_sample_rate = 48000 # Microphone capture sample rate in Hertz, must be 48000 mic_channel_I = 0 # Soundcard index of mic capture audio channel mic_channel_Q = 0 # Soundcard index of ignored capture channel # Microphone samples sent to soundcard: name_of_mic_play = "" # Name of play device if mic I/Q is sent to a sound card mic_playback_rate = 48000 # Playback rate must be a multiple 1, 2, ... of mic_sample_rate mic_play_chan_I = 0 # Soundcard index of mic I play channel mic_play_chan_Q = 1 # Soundcard index of mic Q play channel mic_out_volume = 1.0 # Microphone output volume (after all processing) as a fraction 0.0 to 1.0 # Microphone samples sent to UDP: tx_ip = "" # Transmit IP address for mic sent to UDP (or "192.168.2.195") tx_audio_port = 0 # UDP port for mic samples (or 0x553B) # If your mixing scheme inverts the RF spectrum, set this option to un-invert it invertSpectrum = 0 # Use "amixer -c 1 contents" to get a list of mixer controls and their numid's for # card 1 (or "-c 0" for card 0). Then make a list of (device_name, numid, value) # for each control you need to set. The sample settings are for my USB microphone. #mixer_settings = [ # ("hw:1", 2, 0.80), # numid of microphone volume control, volume 0.0 to 1.0; # ("hw:1", 1, 1.0) # numid of capture on/off control, turn on with 1.0; # ] # If you want Quisk to add a button to generate a 2-tone IMD test signal, # set this to 1. This feature requires the microphone to work. add_imd_button = 0 # If you want Quisk to add a full duplex button (transmit and receive at the # same time), set this to 1. add_fdx_button = 0 # If you want to write your own I/Q filter and demodulation module, set # this to the name of the button to add, and change extdemod.c. # add_extern_demod = "WFM" add_extern_demod = "" # This is the data used to draw colored lines on the frequency X axis to # indicate CW and Phone sub-bands. You can make it anything you want. # These are the colors used for sub-bands: CW = '#FF4444' # General class CW eCW = '#FF8888' # Extra class CW Phone = '#4444FF' # General class phone ePhone = '#8888FF' # Extra class phone # ARRL band plan special frequencies Data = '#FF9900' DxData = '#CC6600' RTTY = '#FF9900' SSTV = '#FFFF00' AM = '#00FF00' Packet = '#00FFFF' Beacons = '#66FF66' Satellite = '#22AA88' Repeater = '#AA00FF' Simplex = '#00FF44' Other = '#888888' # Colors start at the indicated frequency and continue until the # next frequency. The special color "None" turns off color. BandPlan = [ # 160 meters [ 1800000, Data], [ 1809000, Other], [ 1811000, CW], [ 1843000, Phone], [ 1908000, Other], [ 1912000, Phone], [ 1995000, Other], [ 2000000, None], # 80 meters [ 3500000, eCW], [ 3525000, CW], [ 3570000, Data], [ 3589000, DxData], [ 3591000, Data], [ 3600000, ePhone], [ 3790000, Other], [ 3800000, Phone], [ 3844000, SSTV], [ 3846000, Phone], [ 3880000, AM], [ 3890000, Phone], [ 4000000, None], # 60 meters [ 5330600, Phone], [ 5333400, None], [ 5346600, Phone], [ 5349400, None], [ 5366600, Phone], [ 5369400, None], [ 5371600, Phone], [ 5374400, None], [ 5403600, Phone], [ 5406400, None], # 40 meters [ 7000000, eCW], [ 7025000, CW], [ 7039000, DxData], [ 7041000, CW], [ 7080000, Data], [ 7125000, ePhone], [ 7170000, SSTV], [ 7172000, ePhone], [ 7175000, Phone], [ 7285000, AM], [ 7295000, Phone], [ 7300000, None], # 30 meters [10100000, CW], [10130000, RTTY], [10140000, Packet], [10150000, None], # 20 meters [14000000, eCW], [14025000, CW], [14070000, RTTY], [14095000, Packet], [14099500, Other], [14100500, Packet], [14112000, CW], [14150000, ePhone], [14225000, Phone], [14229000, SSTV], [14231000, Phone], [14281000, AM], [14291000, Phone], [14350000, None], # 17 meters [18068000, CW], [18100000, RTTY], [18105000, Packet], [18110000, Phone], [18168000, None], # 15 meters [21000000, eCW], [21025000, CW], [21070000, RTTY], [21110000, CW], [21200000, ePhone], [21275000, Phone], [21339000, SSTV], [21341000, Phone], [21450000, None], # 12 meters [24890000, CW], [24920000, RTTY], [24925000, Packet], [24930000, Phone], [24990000, None], # 10 meters [28000000, CW], [28070000, RTTY], [28150000, CW], [28200000, Beacons], [28300000, Phone], [28679000, SSTV], [28681000, Phone], [29000000, AM], [29200000, Phone], [29300000, Satellite], [29520000, Repeater], [29590000, Simplex], [29610000, Repeater], [29700000, None], # 6 meters [50000000, Beacons], [50100000, Phone], [54000000, None], ] # For each band, this dictionary gives the lower and upper band edges. Frequencies # outside these limits will not be remembered as the last frequency in the band. BandEdge = { '160':( 1800000, 2000000), '80' :( 3500000, 4000000), '60' :( 5300000, 5430000), '40' :( 7000000, 7300000), '30' :(10100000, 10150000), '20' :(14000000, 14350000), '17' :(18068000, 18168000), '15' :(21000000, 21450000), '12' :(24890000, 24990000), '10' :(28000000, 29700000), '6' :(50000000, 54000000), } # For each band, this dictionary gives the initial center frequency, tuning # frequency as an offset from the center frequency, and the mode. This is # no longer too useful because the persistent_state feature saves and then # overwrites these values anyway. bandState = {'Audio':(0, 0, 'LSB'), '160':( 1890000, -10000, 'LSB'), '80' :( 3660000, -10000, 'LSB'), '60' :( 5370000, 1500, 'USB'), '40' :( 7180000, -5000, 'LSB'), '30':(10120000, -10000, 'CWL'), '20' :(14200000, -10000, 'USB'), '17' :(18120000, 10000, 'USB'), '15':(21250000, -10000, 'USB'), '12' :(24940000, 10000, 'USB'), '10' :(28400000, -10000, 'USB'), 'Time':( 5000000, 0, 'AM'), '6' :(50040000, 10000, 'USB'), } # For the Time band, this is the center frequency, tuning frequency and mode: bandTime = [ ( 2500000-10000, 10000, 'AM'), ( 3330000-10000, 10000, 'AM'), ( 5000000-10000, 10000, 'AM'), ( 7335000-10000, 10000, 'AM'), (10000000-10000, 10000, 'AM'), (14670000-10000, 10000, 'AM'), (15000000-10000, 10000, 'AM'), (20000000-10000, 10000, 'AM'), ] # This is the list of band buttons that Quisk displays, and it should have # a length of 12. Empty buttons can have a null string "" label. # Note that the 60 meter band and the Time band have buttons that support # multiple presses. bandLabels = ['Audio', '160', '80', ('60',) * 5, '40', '30', '20', '17', '15', '12', '10', ('Time',) * len(bandTime)] # For each band, this dictionary gives the amplitude and phase corrections for # sound card data. The corrections are small floating point numbers between # about +/- 0.200000. A correction of (0.0, 0.0) means no correction. # Use the button on the configuration screen to add data. The corrections # are saved by the persistent_state feature, so you should not enter them # in your config file. # If you use Quisk as a panadapter, the corrections will not depend on the band. # In that case create a band "panadapter" in your config file, and all corrections # will be read/written to that band. bandAmplPhase = {'40':(0.0, 0.0)} #bandAmplPhase = {'panadapter':(0.0, 0.0)} # The program polls the soundcard or SDR-IQ for data every data_poll_usec microseconds. # A lower time reduces latency; a higher time is less taxing on the hardware. data_poll_usec = 5000 # poll time in microseconds # The fft_size is the width of the data on the screen (about 800 to # 1200 pixels) times the fft_size_multiplier. Multiple FFTs are averaged # together to achieve your graph refresh rate. If fft_size_multiplier is # too small you will get many fft errors. You can specify fft_size_multiplier, # or enter a large number (use 9999) to maximize it, or enter zero to let # quisk calculate it for you. Look for fft_size_multiplier in quisk.py. # If your hardware can change the decimation, there are further compilcations. # The FFT size is fixed, and only the average count can change to adjust the # refresh rate. fft_size_multiplier = 0 # The graph_refresh is the frequency at which the graph is updated, # and should be about 5 to 10 Hertz. Higher rates require more processor power. graph_refresh = 7 # update the graph at this rate in Hertz # latency_millisecs determines how many samples are in the soundcard play buffer. # A larger number makes it less likely that you will run out of samples to play, # but increases latency. It is OK to suffer a certain number of play buffer # underruns in order to get lower latency. latency_millisecs = 150 # latency time in milliseconds # Select the method to test the state of the key; see is_key_down.c key_method = "" # No keying, or internal method # key_method = "/dev/parport0" # Use the named parallel port # key_method = "/dev/ttyS0" # Use the named serial port # key_method = "192.168.1.44" # Use UDP from this address # If you are using keying, key-down throws away the current capture buffer # and starts a sidetone with a rise time of 5 milliseconds. For # key-up, the sidetone is ended with a fall time of 5 milliseconds, then # a silent period starts, then normal audio starts with a rise time of # 5 milliseconds. The length of the silent period is given by keyupDelay, # but will be at least the time necessary to collect enough samples to # refill the filters. A larger keyupDelay may be needed to accomodate # antenna switching or other requirement of your hardware. keyupDelay = 23 # extra milliseconds silence on key up charleston-1.0/filter_display/3.4.7/filter_mods_3.4.7.txt0000644000175000017500000001426111425736722021537 0ustar tfoxtfox ############################ version 3.4.7 FILTER SHADOW ADDITIONS ################### FILE QUISK.PY CHANGES FOR FILTER SHADOW: 1. Added the following lines at beginning of file. # NOTE: THIS FILE HAD BEEN MODIFIED BY WB4JFI TO ADD A FILTER SHADOW # ON THE DISPLAY TO INDICATE FILTER BANDWIDTH. LINES WITH # "WB4JFI" IN THE COMMENTS HAVE BEEN ADDED TO James # Ahlstrom's ORIGINAL CODE. DO NOT bug HIM ABOUT IT!! 2. IN: class GraphDisplay(wx.Window): def __init__(self, parent, x, y, graph_width, height, chary): ADDED FOLLOWING LINES NEAR 640 #line 655 reads: self.tuningPen = wx.Pen('Red', 1) self.fltrPen = wx.Pen(conf.color_bandwidth, 1) # WB4JFI ADD filter shadow self.fltrBrush = wx.Brush(conf.color_bandwidth, wx.SOLID) # WB4JFI ADD filter shadow self.fltr_disp_start = -100 # WB4JFI ADD filter shadow self.fltr_disp_size = 100 # WB4JFI ADD filter shadow self.fltr_disp_fill = -99 # WB4JFI ADD filter shadow self.fltr_disp_show = 0 # WB4JFI ADD filter shadow #old line 656 reads: self.backgroundPen = wx.Pen(self.GetBackgroundColour(), 1) 3. IN: class GraphDisplay(wx.Window): def OnPaint(self, event): ADDED AND CHANGED LINES TO: # line 667 reads: dc = wx.PaintDC(self) x = self.tune_x # setx x to tune freq horizontal position self.UpdateFilterDisplay() # WB4JFI ADD filter shadow if self.fltr_disp_show == 3: # WB4JFI ADD filter shadow dc.SetPen(self.fltrPen) # WB4JFI ADD filter shadow dc.SetBrush(self.fltrBrush) # WB4JFI ADD filter shadow dc.DrawRectangle(x + self.fltr_disp_start, 0, # WB4JFI ADD filter shadow self.fltr_disp_size, self.max_height) # WB4JFI ADD filter shadow dc.FloodFill(x + self.fltr_disp_fill, 1, conf.color_bandwidth, 1) # WB4JFI ADD filter shadow dc.SetPen(wx.BLACK_PEN) dc.DrawLines(self.line) # x = self.tune_x # WB4JFI moved up for filter shadow dc.SetPen(self.tuningPen) #old line 668: dc.DrawLine(x, 0, x, self.max_height) 4. IN: class GraphDisplay(wx.Window): add the following def section after line 716: # line 716 reads: self.Refresh() #rect=rect) def UpdateFilterDisplay(self): # WB4JFI ADD - Update filter display if application.fltr_display: # WB4JFI, check if OK to display filter self.fltr_disp_show = self.fltr_disp_show | 1 # set bit 0, show OK from OUTSIDE else: # otherwise, OUTSIDE don't display self.fltr_disp_show = self.fltr_disp_show & 2 # by clearing bit 0 if application.screen == application.graph or application.screen == application.waterfall: self.fltr_disp_show = self.fltr_disp_show | 2 # bit 1 shows proper screen to display else: self.fltr_disp_show = self.fltr_disp_show & 1 # clear bit 1 to indicate not proper display self.filter_pixels = (application.filterbw / (application.sample_rate /application.data_width) + 0.5) if self.filter_pixels < 2: # if calculated less than 2 pixels self.filter_pixels = 2 # make it at least 2 pixels wide if application.mode == 'LSB' or application.mode == 'CWL': # if filter on lower sideband... self.fltr_disp_start = -(self.filter_pixels - 1) # set start to be at lower side self.fltr_disp_size = self.filter_pixels # and set rectangle size self.fltr_disp_fill = self.fltr_disp_start # start filling at rectangle start if application.mode == 'USB' or application.mode == 'CWU': # if filter on upper sideband... self.fltr_disp_start = 0 # start is at tuning freq self.fltr_disp_size = self.filter_pixels # set rectangle size self.fltr_disp_fill = 1 # start filling at beginning of tune if application.mode == 'AM' or application.mode == 'FM': # if filter is centered at tune freq self.fltr_disp_start = -((self.filter_pixels - 1) /2) # set start at 1/2 bandidth self.fltr_disp_size = self.filter_pixels # set size to cover both sidebands self.fltr_disp_fill = self.filter_pixels # start filling at lower edge # # WB4JFI - end of filter add # previous line 717: def SetTuningLine(self, x): 5. IN: class App(wx.App): def OnInit(self): Add the following lines after line 1518 # line 1518 reads: self.color_index = 0 self.filterbw = 0 # WB4JFI Added for filter display self.fltr_display = conf.filter_display # WB4JFI added filter display 1=OK, 0=do not display # old line 1519: dc = wx.ScreenDC() # get the screen size 6. IN: class App(wx.App): def OnBtnFilter(self, event, bw=None): Add the following lines after line 2004: # line 2004 reads: bw = int(btn.GetLabel()) self.filterbw = int(self.filterButns.GetLabel()) # WB4JFI ADD - udate filter bandwidth application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp # old line 2005: mode = self.mode 7. IN: def OnBtnScreen(self, event, name=None): Add the following line after line 2032: # line 2032 reads: self.freqDisplay.Display(self.VFO + self.txFreq) application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp # old line 2033: elif name == 'WFall': 8. IN: def OnBtnScreen(self, event, name=None): Add the following line after line 2039: # line 2039 reads: sash = self.screen.GetSashPosition() application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp old line 2040: elif name == 'Scope': 9. IN: def OnBtnMode(self, event, mode=None): Add the following line after line 2284: # Line 2284 reads: self.OnBtnFilter(None, 12000) application.graph.display.UpdateFilterDisplay() # WB4JFI ADD - update the filter disp # old line 2285: def OnBtnBand(self, event): 10. IN FILE quisk_conf_defaults.py, Add the following line after 62: # Line 62 reads: filter_y_zero = 0 filter_display = 0 # old line 63 is a blank line 11 IN FILE quisk_conf_defaults.py, Add the following line after line 87: # line 87 reads: color_entry = color_freq # frequency entry box color_bandwidth = 'lemonchiffon2' # audio bandwidth on spectrum display # WB4JFI ADD filter shadow # old line 88 is blank line charleston-1.0/filter_display/3.4.7/original_quisk_conf_defaults_3.4.7.py0000644000175000017500000004701211424052112024734 0ustar tfoxtfox# Please do not change the configuration file quisk_conf_defaults.py. # Instead copy one of the other quisk_conf_*.py files to your own # .quisk_conf.py and make changes there. For a normal sound card # configuration, copy quisk_conf_model.py to your .quisk_conf.py. # # Quisk imports quisk_conf_defaults to set its configuration. # If you have a configuration file, it then overwrites the defaults # with your parameters. Your configuration file must be named # ~/.quisk_conf.py, where "~" means your home directory. Or # you may specify a different name with the -c or --config command # line option. Try --help. Check the config screen to make sure that # the correct configuration file is in use. # # The Quisk receiver can use a high quality sound card for capture and playback, # or it can use the SDR-IQ by RfSpace for capture and a lower quality # sound card for playback. Quisk can also be used as a panadapter. # Quisk can control some rigs. See quisk_hardware_*.py. If you have a rig # to control, copy one of the quisk_hardware_*.py files to your own file named # quisk_hardware.py, and edit that file. If there is no quisk_hardware.py, then # quisk_hardware_model.py is used instead. # Import the default Hardware module. You can import a different module in # your .quisk_conf.py. import quisk_hardware_model as quisk_hardware # Module for additional widgets (advanced usage). quisk_widgets = None # Select the default screen when Quisk starts: default_screen = 'Graph' #default_screen = 'WFall' #default_screen = 'Config' # The width of the graph data as a fraction of the screen size. This # will be adjusted by Quisk to accommodate preferred FFT sizes. It can # not be changed once Quisk starts. It can not be made too small because # of the space needed for all the buttons. graph_width = 0.8 # Select the default mode when Quisk starts (overruled by persistent_state): # default_mode = 'FM' default_mode = 'USB' # Select the way the waterfall screen scrolls: # waterfall_scroll_mode = 0 # scroll at a constant rate. waterfall_scroll_mode = 1 # scroll faster at the top so that a new signal appears sooner. # Select the initial size in pixels (minimum 1) of the graph at the top of the waterfall. waterfall_graph_size = 80 # These are the initial values for the Y-scale and Y-zero sliders for each screen. # The sliders go from zero to 160. graph_y_scale = 100 graph_y_zero = 0 waterfall_y_scale = 80 waterfall_y_zero = 40 waterfall_graph_y_scale = 100 waterfall_graph_y_zero = 60 scope_y_scale = 80 scope_y_zero = 0 # Currently doesn't do anything filter_y_scale = 90 filter_y_zero = 0 # Quisk can save its current state in a file on exit, and restore it when you restart. # State includes band, frequency and mode, but not every item of state (not screen). # The file is .quisk_init.pkl in the same directory as your config file. If this file # becomes corrupted, just delete it and it will be reconstructed. #persistent_state = False persistent_state = True # This converts from dB to S-units for the S-meter (it is in S-units). correct_smeter = 15.5 # This is the fraction of spectrum to display from zero to one. It is needed if # the passband edges are not valid. Use 0.85 for the SDR-IQ. display_fraction = 1.00 # Define colors used by all widgets in wxPython colour format: color_bg = 'light steel blue' # Lower screen background color_graph = 'lemonchiffon1' # Graph background color_gl = 'grey' # Lines on the graph color_btn = 'steelblue2' # button color color_check_btn = 'yellow2' # color of a check button when it is checked color_cycle_btn = 'goldenrod3' # color of a cycle button when it is checked color_test = 'hot pink' # color of a button used for test (turn off for tx) color_freq = 'lightcyan1' # background color of frequency and s-meter color_entry = color_freq # frequency entry box # These are the palettes for the waterfall. The one used is named waterfallPallette, # so to use a different one, overwrite this name in your .quisk_conf.py. waterfallPalette = ( ( 0, 0, 0, 0), ( 36, 85, 0, 255), ( 73, 153, 0, 255), (109, 255, 0, 128), (146, 255, 119, 0), (182, 85, 255, 100), (219, 255, 255, 0), (255, 255, 255, 255) ) digipanWaterfallPalette = ( ( 0, 0, 0, 0), ( 32, 0, 0, 62), ( 64, 0, 0, 126), ( 96, 145, 142, 96), (128, 181, 184, 48), (160, 223, 226, 105), (192, 254, 254, 4), (255, 255, 58, 0) ) # Quisk can access your sound card through PortAudio or through ALSA drivers. # In PortAudio, soundcards have an index number 0, 1, 2, ... and a name. # The name can be something like "HDA NVidia: AD198x Analog (hw:0,0)" or # "surround41". In Quisk, all PortAudio device names start with "portaudio". # A device name like "portaudio#6" directly specifies the index. A name like # "portaudio:text" means to search for "text" in all available devices. And # there is a default device "portaudiodefault". So these portaudio names are useful: #name_of_sound_capt = "portaudio:(hw:0,0)" # First sound card #name_of_sound_capt = "portaudio:(hw:1,0)" # Second sound card, etc. #name_of_sound_capt = "portaudio#1" # Directly specified index #name_of_sound_capt = "portaudiodefault" # May give poor performance on capture # In ALSA, soundcards have these names. The "hw" devices are the raw # hardware devices, and should be used for soundcard capture. #name_of_sound_capt = "hw:0" # First sound card #name_of_sound_capt = "hw:1" # Second sound card, etc. #name_of_sound_capt = "plughw" #name_of_sound_capt = "plughw:1" #name_of_sound_capt = "default" # Normally you would capture and play on the same soundcard to avoid problems with the # two clocks running at slightly different rates. But you can define name_of_sound_play # to play back on a different device. Define this as the empty string "" to turn off # play (for a panadapter). # # For the SDR-IQ the soundcard is not used for capture; it only plays back audio. # Playback is always 48 kHz stereo. # Configuration for soundcard capture and playback use_sdriq = 0 # Get ADC samples from SDR-IQ is not used use_rx_udp = 0 # Get ADC samples from UDP is not used sample_rate = 48000 # ADC hardware sample rate in Hertz name_of_sound_capt = "hw:0" # Name of soundcard capture hardware device. name_of_sound_play = name_of_sound_capt # Use the same device for play back #name_of_sound_play = "" # Panadapter: Do not play channel_i = 0 # Soundcard index of in-phase channel: 0, 1, 2, ... channel_q = 1 # Soundcard index of quadrature channel: 0, 1, 2, ... # If you use a soundcard with Ethernet control of the VFO, set these parameters: rx_ip = "" # Receiver IP address for VFO control # If you use an SDR-IQ for capture, set these parameters: # import quisk_hardware_sdriq as quisk_hardware # Use different hardware file # use_sdriq = 1 # Capture device is the SDR-IQ # sdriq_name = "/dev/ft2450" # Name of the SDR-IQ device to open # sdriq_clock = 66666667.0 # actual sample rate (66666667 nominal) # sdriq_decimation = 500 # Must be 360, 500, 600, or 1250 # sample_rate = int(float(sdriq_clock) / sdriq_decimation + 0.5) # Don't change this # name_of_sound_capt = "" # We do not capture from the soundcard # name_of_sound_play = "hw:0" # Play back on this soundcard # playback_rate = 48000 # Radio sound play rate, default 48000 # channel_i = 0 # Soundcard index of left channel # channel_q = 1 # Soundcard index of right channel # display_fraction = 0.85 # The edges of the full bandwidth are not valid # If you receive ADC samples from a UDP port, set these parameters: # import quisk_hardware_n2adr as quisk_hardware # Use different hardware file # use_rx_udp = 1 # Get ADC samples from UDP # rx_udp_ip = "192.168.1.91" # Sample source IP address # rx_udp_port = 0xBC77 # Sample source UDP port # rx_udp_clock = 122880000 # ADC sample rate in Hertz # rx_udp_decimation = 8 * 8 * 8 # Decimation from clock to UDP sample rate # The allowable decimations are 8 times 8 times (2 or 4 or 8) times (1 or 5). # So you could enter 8 * 8 * one of (2, 4, 5, 8, 10, 20, 40). # These decimations result in a sample rate of 48 to 960 kHz. # sample_rate = int(float(rx_udp_clock) /rx_udp_decimation + 0.5) # Don't change this # name_of_sound_capt = "" # We do not capture from the soundcard # name_of_sound_play = "hw:0" # Play back on this soundcard # playback_rate = 48000 # Radio sound play rate, default 48000 # This is the received radio sound playback rate. The default will # be 48 kHz for the SDR-IQ and UDP port samples, and sample_rate for sound # card capture. Set it yourself for other rates or hardware. # playback_rate = 48000 # If you use quisk_hardware_fixed.py, this is the fixed VFO frequency in Hertz fixed_vfo_freq = 7056000 # This determines what happens when you tune by dragging the mouse. The correct # choice depends on how your hardware performs tuning. You may want to use a # custom hardware file with a custom ChangeFrequency() method too. mouse_tune_method = 0 # The Quisk tune frequency changes and the VFO frequency is unchanged. #mouse_tune_method = 1 # The Quisk tune frequency is unchanged and the VFO changes. # This is the CW tone frequency in Hertz cwTone = 600 # If you use the microphone feature, the mic_channel_I and Q are the two capture # microphone channels. Quisk uses a monophonic mic, so audio is taken from the I # channel, and the Q channel is (currently) ignored. It is OK to set the same # channel number for both, and this is necessary for a USB mono mic. If you # change the sample rate, you will need to change the C code to use different filters. # Mic samples can be sent to an Ethernet device (use tx_ip and name_of_mic_play = "") # or to a sound card (use name_of_mic_play="hw:1" or other device). # If mic samples are sent to a sound card for Tx, the samples are tuned to the audio # transmit frequency, and are set to zero unless the key is down. # If there is no mic (microphone_name = ""), it is still possible to transmit CW, # and you should set mic_playback_rate to the I/Q receive capture rate. # Microphone capture: microphone_name = "" # Name of microphone capture device (or "hw:1") mic_sample_rate = 48000 # Microphone capture sample rate in Hertz, must be 48000 mic_channel_I = 0 # Soundcard index of mic capture audio channel mic_channel_Q = 0 # Soundcard index of ignored capture channel # Microphone samples sent to soundcard: name_of_mic_play = "" # Name of play device if mic I/Q is sent to a sound card mic_playback_rate = 48000 # Playback rate must be a multiple 1, 2, ... of mic_sample_rate mic_play_chan_I = 0 # Soundcard index of mic I play channel mic_play_chan_Q = 1 # Soundcard index of mic Q play channel mic_out_volume = 1.0 # Microphone output volume (after all processing) as a fraction 0.0 to 1.0 # Microphone samples sent to UDP: tx_ip = "" # Transmit IP address for mic sent to UDP (or "192.168.2.195") tx_audio_port = 0 # UDP port for mic samples (or 0x553B) # If your mixing scheme inverts the RF spectrum, set this option to un-invert it invertSpectrum = 0 # Use "amixer -c 1 contents" to get a list of mixer controls and their numid's for # card 1 (or "-c 0" for card 0). Then make a list of (device_name, numid, value) # for each control you need to set. The sample settings are for my USB microphone. #mixer_settings = [ # ("hw:1", 2, 0.80), # numid of microphone volume control, volume 0.0 to 1.0; # ("hw:1", 1, 1.0) # numid of capture on/off control, turn on with 1.0; # ] # If you want Quisk to add a button to generate a 2-tone IMD test signal, # set this to 1. This feature requires the microphone to work. add_imd_button = 0 # If you want Quisk to add a full duplex button (transmit and receive at the # same time), set this to 1. add_fdx_button = 0 # If you want to write your own I/Q filter and demodulation module, set # this to the name of the button to add, and change extdemod.c. # add_extern_demod = "WFM" add_extern_demod = "" # This is the data used to draw colored lines on the frequency X axis to # indicate CW and Phone sub-bands. You can make it anything you want. # These are the colors used for sub-bands: CW = '#FF4444' # General class CW eCW = '#FF8888' # Extra class CW Phone = '#4444FF' # General class phone ePhone = '#8888FF' # Extra class phone # ARRL band plan special frequencies Data = '#FF9900' DxData = '#CC6600' RTTY = '#FF9900' SSTV = '#FFFF00' AM = '#00FF00' Packet = '#00FFFF' Beacons = '#66FF66' Satellite = '#22AA88' Repeater = '#AA00FF' Simplex = '#00FF44' Other = '#888888' # Colors start at the indicated frequency and continue until the # next frequency. The special color "None" turns off color. BandPlan = [ # 160 meters [ 1800000, Data], [ 1809000, Other], [ 1811000, CW], [ 1843000, Phone], [ 1908000, Other], [ 1912000, Phone], [ 1995000, Other], [ 2000000, None], # 80 meters [ 3500000, eCW], [ 3525000, CW], [ 3570000, Data], [ 3589000, DxData], [ 3591000, Data], [ 3600000, ePhone], [ 3790000, Other], [ 3800000, Phone], [ 3844000, SSTV], [ 3846000, Phone], [ 3880000, AM], [ 3890000, Phone], [ 4000000, None], # 60 meters [ 5330600, Phone], [ 5333400, None], [ 5346600, Phone], [ 5349400, None], [ 5366600, Phone], [ 5369400, None], [ 5371600, Phone], [ 5374400, None], [ 5403600, Phone], [ 5406400, None], # 40 meters [ 7000000, eCW], [ 7025000, CW], [ 7039000, DxData], [ 7041000, CW], [ 7080000, Data], [ 7125000, ePhone], [ 7170000, SSTV], [ 7172000, ePhone], [ 7175000, Phone], [ 7285000, AM], [ 7295000, Phone], [ 7300000, None], # 30 meters [10100000, CW], [10130000, RTTY], [10140000, Packet], [10150000, None], # 20 meters [14000000, eCW], [14025000, CW], [14070000, RTTY], [14095000, Packet], [14099500, Other], [14100500, Packet], [14112000, CW], [14150000, ePhone], [14225000, Phone], [14229000, SSTV], [14231000, Phone], [14281000, AM], [14291000, Phone], [14350000, None], # 17 meters [18068000, CW], [18100000, RTTY], [18105000, Packet], [18110000, Phone], [18168000, None], # 15 meters [21000000, eCW], [21025000, CW], [21070000, RTTY], [21110000, CW], [21200000, ePhone], [21275000, Phone], [21339000, SSTV], [21341000, Phone], [21450000, None], # 12 meters [24890000, CW], [24920000, RTTY], [24925000, Packet], [24930000, Phone], [24990000, None], # 10 meters [28000000, CW], [28070000, RTTY], [28150000, CW], [28200000, Beacons], [28300000, Phone], [28679000, SSTV], [28681000, Phone], [29000000, AM], [29200000, Phone], [29300000, Satellite], [29520000, Repeater], [29590000, Simplex], [29610000, Repeater], [29700000, None], # 6 meters [50000000, Beacons], [50100000, Phone], [54000000, None], ] # For each band, this dictionary gives the lower and upper band edges. Frequencies # outside these limits will not be remembered as the last frequency in the band. BandEdge = { '160':( 1800000, 2000000), '80' :( 3500000, 4000000), '60' :( 5300000, 5430000), '40' :( 7000000, 7300000), '30' :(10100000, 10150000), '20' :(14000000, 14350000), '17' :(18068000, 18168000), '15' :(21000000, 21450000), '12' :(24890000, 24990000), '10' :(28000000, 29700000), '6' :(50000000, 54000000), } # For each band, this dictionary gives the initial center frequency, tuning # frequency as an offset from the center frequency, and the mode. This is # no longer too useful because the persistent_state feature saves and then # overwrites these values anyway. bandState = {'Audio':(0, 0, 'LSB'), '160':( 1890000, -10000, 'LSB'), '80' :( 3660000, -10000, 'LSB'), '60' :( 5370000, 1500, 'USB'), '40' :( 7180000, -5000, 'LSB'), '30':(10120000, -10000, 'CWL'), '20' :(14200000, -10000, 'USB'), '17' :(18120000, 10000, 'USB'), '15':(21250000, -10000, 'USB'), '12' :(24940000, 10000, 'USB'), '10' :(28400000, -10000, 'USB'), 'Time':( 5000000, 0, 'AM'), '6' :(50040000, 10000, 'USB'), } # For the Time band, this is the center frequency, tuning frequency and mode: bandTime = [ ( 2500000-10000, 10000, 'AM'), ( 3330000-10000, 10000, 'AM'), ( 5000000-10000, 10000, 'AM'), ( 7335000-10000, 10000, 'AM'), (10000000-10000, 10000, 'AM'), (14670000-10000, 10000, 'AM'), (15000000-10000, 10000, 'AM'), (20000000-10000, 10000, 'AM'), ] # This is the list of band buttons that Quisk displays, and it should have # a length of 12. Empty buttons can have a null string "" label. # Note that the 60 meter band and the Time band have buttons that support # multiple presses. bandLabels = ['Audio', '160', '80', ('60',) * 5, '40', '30', '20', '17', '15', '12', '10', ('Time',) * len(bandTime)] # For each band, this dictionary gives the amplitude and phase corrections for # sound card data. The corrections are small floating point numbers between # about +/- 0.200000. A correction of (0.0, 0.0) means no correction. # Use the button on the configuration screen to add data. The corrections # are saved by the persistent_state feature, so you should not enter them # in your config file. # If you use Quisk as a panadapter, the corrections will not depend on the band. # In that case create a band "panadapter" in your config file, and all corrections # will be read/written to that band. bandAmplPhase = {'40':(0.0, 0.0)} #bandAmplPhase = {'panadapter':(0.0, 0.0)} # The program polls the soundcard or SDR-IQ for data every data_poll_usec microseconds. # A lower time reduces latency; a higher time is less taxing on the hardware. data_poll_usec = 5000 # poll time in microseconds # The fft_size is the width of the data on the screen (about 800 to # 1200 pixels) times the fft_size_multiplier. Multiple FFTs are averaged # together to achieve your graph refresh rate. If fft_size_multiplier is # too small you will get many fft errors. You can specify fft_size_multiplier, # or enter a large number (use 9999) to maximize it, or enter zero to let # quisk calculate it for you. Look for fft_size_multiplier in quisk.py. # If your hardware can change the decimation, there are further compilcations. # The FFT size is fixed, and only the average count can change to adjust the # refresh rate. fft_size_multiplier = 0 # The graph_refresh is the frequency at which the graph is updated, # and should be about 5 to 10 Hertz. Higher rates require more processor power. graph_refresh = 7 # update the graph at this rate in Hertz # latency_millisecs determines how many samples are in the soundcard play buffer. # A larger number makes it less likely that you will run out of samples to play, # but increases latency. It is OK to suffer a certain number of play buffer # underruns in order to get lower latency. latency_millisecs = 150 # latency time in milliseconds # Select the method to test the state of the key; see is_key_down.c key_method = "" # No keying, or internal method # key_method = "/dev/parport0" # Use the named parallel port # key_method = "/dev/ttyS0" # Use the named serial port # key_method = "192.168.1.44" # Use UDP from this address # If you are using keying, key-down throws away the current capture buffer # and starts a sidetone with a rise time of 5 milliseconds. For # key-up, the sidetone is ended with a fall time of 5 milliseconds, then # a silent period starts, then normal audio starts with a rise time of # 5 milliseconds. The length of the silent period is given by keyupDelay, # but will be at least the time necessary to collect enough samples to # refill the filters. A larger keyupDelay may be needed to accomodate # antenna switching or other requirement of your hardware. keyupDelay = 23 # extra milliseconds silence on key up charleston-1.0/filter_display/3.4.7/original_quisk_3.4.7.py0000755000175000017500000025337611424051002022054 0ustar tfoxtfox#! /usr/bin/python # All QUISK software is Copyright (C) 2006-2010 by James C. Ahlstrom. # This free software is licensed for use under the GNU General Public # License (GPL), see http://www.opensource.org. # Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!! """The main program for Quisk, a software defined radio. Usage: python quisk.py [-c | --config config_file_path] This can also be installed as a package and run as quisk.main(). """ # Change to the directory of quisk.py. This is necessary to import Quisk packages # and to load other extension modules that link against _quisk.so. It also helps to # find ./__init__.py and ./help.html. import sys, os os.chdir(os.path.normpath(os.path.dirname(__file__))) import wx, wx.html, wx.lib.buttons, wx.lib.stattext, wx.lib.colourdb import math, cmath, time, traceback import threading, pickle import _quisk as QS from types import * # Command line parsing: be able to specify the config file. from optparse import OptionParser parser = OptionParser() parser.add_option('-c', '--config', dest='config_file_path', help='Specify the configuration file path') argv_options = parser.parse_args()[0] # These FFT sizes have multiple small factors, and are prefered for efficiency: fftPreferedSizes = (416, 448, 480, 512, 576, 640, 672, 704, 768, 800, 832, 864, 896, 960, 1024, 1056, 1120, 1152, 1248, 1280, 1344, 1408, 1440, 1536, 1568, 1600, 1664, 1728, 1760, 1792, 1920, 2016, 2048, 2080, 2112, 2240, 2304, 2400, 2464, 2496, 2560, 2592, 2688, 2816, 2880, 2912) class Timer: """Debug: measure and print times every ptime seconds. Call with msg == '' to start timer, then with a msg to record the time. """ def __init__(self, ptime = 1.0): self.ptime = ptime # frequency to print in seconds self.time0 = 0 # time zero; measure from this time self.time_print = 0 # last time data was printed self.timers = {} # one timer for each msg self.names = [] # ordered list of msg self.heading = 1 # print heading on first use def __call__(self, msg): tm = time.time() if msg: if not self.time0: # Not recording data return if self.timers.has_key(msg): count, average, highest = self.timers[msg] else: self.names.append(msg) count = 0 average = highest = 0.0 count += 1 delta = tm - self.time0 average += delta if highest < delta: highest = delta self.timers[msg] = (count, average, highest) if tm - self.time_print > self.ptime: # time to print results self.time0 = 0 # end data recording, wait for reset self.time_print = tm if self.heading: self.heading = 0 print "count, msg, avg, max (msec)" print "%4d" % count, for msg in self.names: # keep names in order count, average, highest = self.timers[msg] if not count: continue average /= count print " %s %7.3f %7.3f" % (msg, average * 1e3, highest * 1e3), self.timers[msg] = (0, 0.0, 0.0) print else: # reset the time to zero self.time0 = tm # Start timer if not self.time_print: self.time_print = tm ## T = Timer() # Make a timer instance class SoundThread(threading.Thread): """Create a second (non-GUI) thread to read, process and play sound.""" def __init__(self): self.do_init = 1 threading.Thread.__init__(self) self.doQuit = threading.Event() self.doQuit.clear() def run(self): """Read, process, play sound; then notify the GUI thread to check for FFT data.""" if self.do_init: # Open sound using this thread self.do_init = 0 QS.start_sound() wx.CallAfter(application.PostStartup) while not self.doQuit.isSet(): QS.read_sound() wx.CallAfter(application.OnReadSound) QS.close_sound() def stop(self): """Set a flag to indicate that the sound thread should end.""" self.doQuit.set() class FrequencyDisplay(wx.lib.stattext.GenStaticText): """Create a frequency display widget.""" def __init__(self, frame, gbs, width, height): wx.lib.stattext.GenStaticText.__init__(self, frame, -1, '3', style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE|wx.RAISED_BORDER) border = 4 for points in range(30, 6, -1): font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(font) w, h = self.GetTextExtent('333 444 555 Hz') if w < width and h < height - border * 2: break self.SetSizeHints(w, h, w * 5, h) self.height = h self.points = points border = self.border = (height - self.height) / 2 self.height_and_border = h + border * 2 self.SetBackgroundColour(conf.color_freq) gbs.Add(self, (0, 0), (1, 3), flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=border) def Clip(self, clip): """Change color to indicate clipping.""" if clip: self.SetBackgroundColour('deep pink') else: self.SetBackgroundColour(conf.color_freq) def Display(self, freq): """Set the frequency to be displayed.""" freq = int(freq) if freq >= 0: t = str(freq) minus = '' else: t = str(-freq) minus = '- ' l = len(t) if l > 9: txt = "%s%s %s %s %s" % (minus, t[0:-9], t[-9:-6], t[-6:-3], t[-3:]) elif l > 6: txt = "%s%s %s %s" % (minus, t[0:-6], t[-6:-3], t[-3:]) elif l > 3: txt = "%s%s %s" % (minus, t[0:-3], t[-3:]) else: txt = minus + t self.SetLabel('%s Hz' % txt) class SliderBoxV(wx.BoxSizer): """A vertical box containing a slider and a text heading""" # Note: A vertical wx slider has the max value at the bottom. This is # reversed for this control. def __init__(self, parent, text, init, themax, handler, display=False): wx.BoxSizer.__init__(self, wx.VERTICAL) self.slider = wx.Slider(parent, -1, init, 0, themax, style=wx.SL_VERTICAL) self.slider.Bind(wx.EVT_SCROLL, handler) sw, sh = self.slider.GetSize() self.text = text self.themax = themax if display: # Display the slider value when it is thumb'd self.text_ctrl = wx.StaticText(parent, -1, str(themax), style=wx.ALIGN_CENTER) w1, h1 = self.text_ctrl.GetSize() # Measure size with max number self.text_ctrl.SetLabel(text) w2, h2 = self.text_ctrl.GetSize() # Measure size with text self.width = max(w1, w2, sw) self.text_ctrl.SetSizeHints(self.width, -1, self.width) self.slider.Bind(wx.EVT_SCROLL_THUMBTRACK, self.Change) self.slider.Bind(wx.EVT_SCROLL_THUMBRELEASE, self.ChangeDone) else: self.text_ctrl = wx.StaticText(parent, -1, text) w2, h2 = self.text_ctrl.GetSize() # Measure size with text self.width = max(w2, sw) self.Add(self.text_ctrl, 0, wx.ALIGN_CENTER) self.Add(self.slider, 1, wx.ALIGN_CENTER) def Change(self, event): event.Skip() self.text_ctrl.SetLabel(str(self.themax - self.slider.GetValue())) def ChangeDone(self, event): event.Skip() self.text_ctrl.SetLabel(self.text) def GetValue(self): return self.themax - self.slider.GetValue() def SetValue(self, value): # Set slider visual position; does not call handler self.slider.SetValue(self.themax - value) # Start of our button classes. They are compatible with wxPython GenButton # buttons. Use the usual methods for access: # GetLabel(self), SetLabel(self, label): Get and set the label # Enable(self, flag), Disable(self), IsEnabled(self): Enable / Disable # GetValue(self), SetValue(self, value): Get / Set check button state True / False # SetIndex(self, index): For cycle buttons, set the label from its index class QuiskButtons: """Base class for special buttons.""" button_bezel = 3 # size of button bezel in pixels def InitButtons(self, text): self.SetBezelWidth(self.button_bezel) self.SetBackgroundColour(conf.color_btn) self.SetUseFocusIndicator(False) self.font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) if text: w, h = self.GetTextExtent(text) else: w, h = self.GetTextExtent("OK") self.Disable() # create a size for null text, but Disable() w += self.button_bezel * 2 + self.GetCharWidth() h = h * 12 / 10 h += self.button_bezel * 2 self.SetSizeHints(w, h, w * 6, h, 1, 1) def OnKeyDown(self, event): pass def OnKeyUp(self, event): pass class QuiskPushbutton(QuiskButtons, wx.lib.buttons.GenButton): """A plain push button widget.""" def __init__(self, parent, command, text, use_right=False): wx.lib.buttons.GenButton.__init__(self, parent, -1, text) self.command = command self.Bind(wx.EVT_BUTTON, self.OnButton) self.InitButtons(text) self.direction = 1 if use_right: self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) def OnButton(self, event): if self.command: self.command(event) def OnRightDown(self, event): self.direction = -1 self.OnLeftDown(event) def OnRightUp(self, event): self.OnLeftUp(event) self.direction = 1 class QuiskRepeatbutton(QuiskButtons, wx.lib.buttons.GenButton): """A push button that repeats when held down.""" def __init__(self, parent, command, text, up_command=None, use_right=False): wx.lib.buttons.GenButton.__init__(self, parent, -1, text) self.command = command self.up_command = up_command self.timer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.OnTimer) self.Bind(wx.EVT_BUTTON, self.OnButton) self.InitButtons(text) self.repeat_state = 0 # repeater button inactive self.direction = 1 if use_right: self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) def SendCommand(self, command): if command: event = wx.PyEvent() event.SetEventObject(self) command(event) def OnLeftDown(self, event): if self.IsEnabled(): self.shift = event.ShiftDown() self.control = event.ControlDown() self.SendCommand(self.command) self.repeat_state = 1 # first button push self.timer.Start(milliseconds=300, oneShot=True) wx.lib.buttons.GenButton.OnLeftDown(self, event) def OnLeftUp(self, event): if self.IsEnabled(): self.SendCommand(self.up_command) self.repeat_state = 0 self.timer.Stop() wx.lib.buttons.GenButton.OnLeftUp(self, event) def OnRightDown(self, event): if self.IsEnabled(): self.shift = event.ShiftDown() self.control = event.ControlDown() self.direction = -1 self.OnLeftDown(event) def OnRightUp(self, event): if self.IsEnabled(): self.OnLeftUp(event) self.direction = 1 def OnTimer(self, event): if self.repeat_state == 1: # after first push, turn on repeats self.timer.Start(milliseconds=150, oneShot=False) self.repeat_state = 2 if self.repeat_state: # send commands until button is released self.SendCommand(self.command) def OnButton(self, event): pass # button command not used class QuiskCheckbutton(QuiskButtons, wx.lib.buttons.GenToggleButton): """A button that pops up and down, and changes color with each push.""" # Check button; get the checked state with self.GetValue() def __init__(self, parent, command, text, color=None): wx.lib.buttons.GenToggleButton.__init__(self, parent, -1, text) self.InitButtons(text) self.Bind(wx.EVT_BUTTON, self.OnButton) self.button_down = 0 # used for radio buttons self.command = command if color is None: self.color = conf.color_check_btn else: self.color = color def SetValue(self, value, do_cmd=False): wx.lib.buttons.GenToggleButton.SetValue(self, value) self.button_down = value if value: self.SetBackgroundColour(self.color) else: self.SetBackgroundColour(conf.color_btn) if do_cmd and self.command: event = wx.PyEvent() event.SetEventObject(self) self.command(event) def OnButton(self, event): if self.GetValue(): self.SetBackgroundColour(self.color) else: self.SetBackgroundColour(conf.color_btn) if self.command: self.command(event) class QuiskCycleCheckbutton(QuiskCheckbutton): """A button that cycles through its labels with each push. The button is up for labels[0], down for all other labels. Change to the next label for each push. If you call SetLabel(), the label must be in the list. The self.index is the index of the current label. """ def __init__(self, parent, command, labels, color=None, is_radio=False): self.labels = list(labels) # Be careful if you change this list self.index = 0 # index of selected label 0, 1, ... self.direction = 0 # 1 for up, -1 for down, 0 for no change to index self.is_radio = is_radio # Is this a radio cycle button? if color is None: color = conf.color_cycle_btn QuiskCheckbutton.__init__(self, parent, command, labels[0], color) self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) def SetLabel(self, label, do_cmd=False): self.index = self.labels.index(label) QuiskCheckbutton.SetLabel(self, label) QuiskCheckbutton.SetValue(self, self.index) if do_cmd and self.command: event = wx.PyEvent() event.SetEventObject(self) self.command(event) def SetIndex(self, index, do_cmd=False): self.index = index QuiskCheckbutton.SetLabel(self, self.labels[index]) QuiskCheckbutton.SetValue(self, index) if do_cmd and self.command: event = wx.PyEvent() event.SetEventObject(self) self.command(event) def OnButton(self, event): if not self.is_radio or self.button_down: self.direction = 1 self.index += 1 if self.index >= len(self.labels): self.index = 0 self.SetIndex(self.index) else: self.direction = 0 if self.command: self.command(event) def OnRightDown(self, event): # Move left in the list of labels if not self.is_radio or self.GetValue(): self.index -= 1 if self.index < 0: self.index = len(self.labels) - 1 self.SetIndex(self.index) self.direction = -1 if self.command: self.command(event) class RadioButtonGroup: """This class encapsulates a group of radio buttons. This class is not a button! The "labels" is a list of labels for the toggle buttons. An item of labels can be a list/tuple, and the corresponding button will be a cycle button. """ def __init__(self, parent, command, labels, default): self.command = command self.buttons = [] self.button = None for text in labels: if type(text) in (ListType, TupleType): b = QuiskCycleCheckbutton(parent, self.OnButton, text, is_radio=True) for t in text: if t == default and self.button is None: b.SetLabel(t) self.button = b else: b = QuiskCheckbutton(parent, self.OnButton, text) if text == default and self.button is None: b.SetValue(True) self.button = b self.buttons.append(b) def SetLabel(self, label, do_cmd=False): self.button = None for b in self.buttons: if self.button is not None: b.SetValue(False) elif isinstance(b, QuiskCycleCheckbutton): try: index = b.labels.index(label) except ValueError: b.SetValue(False) continue else: b.SetIndex(index) self.button = b b.SetValue(True) elif b.GetLabel() == label: b.SetValue(True) self.button = b else: b.SetValue(False) if do_cmd and self.command and self.button: event = wx.PyEvent() event.SetEventObject(self.button) self.command(event) def GetButtons(self): return self.buttons def OnButton(self, event): win = event.GetEventObject() for b in self.buttons: if b is win: self.button = b b.SetValue(True) else: b.SetValue(False) if self.command: self.command(event) def GetLabel(self): if not self.button: return None return self.button.GetLabel() def GetSelectedButton(self): # return the selected button return self.button class ConfigScreen(wx.ScrolledWindow): """Display the configuration and status screen.""" def __init__(self, parent, width, fft_size): wx.ScrolledWindow.__init__(self, parent, pos = (0, 0), size = (width, 100), style = wx.VSCROLL | wx.NO_BORDER) self.SetBackgroundColour(conf.color_graph) self.Bind(wx.EVT_PAINT, self.OnPaint) self.width = width self.setscroll = True self.fft_size = fft_size self.interupts = 0 self.read_error = -1 self.write_error = -1 self.underrun_error = -1 self.fft_error = -1 self.latencyCapt = -1 self.latencyPlay = -1 self.y_scale = 0 self.y_zero = 0 self.rate_min = -1 self.rate_max = -1 self.chan_min = -1 self.chan_max = -1 self.mic_max_display = 0 self.w_phase = None self.err_msg = "No response" self.msg1 = "" self.tabstops = [1] self.tabstops.append(self.tabstops[-1] + 18) self.tabstops.append(self.tabstops[-1] + 2) self.tabstops.append(self.tabstops[-1] + 20) self.tabstops.append(self.tabstops[-1] + 2) self.tabstops.append(self.tabstops[-1] + 17) self.tabstops.append(self.tabstops[-1] + 2) self.tabstops.append(self.tabstops[-1] + 17) self.tabstops.append(self.tabstops[-1] + 2) points = 24 while points > 4: self.font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) charx = self.charx = self.GetCharWidth() chary = self.chary = self.GetCharHeight() if self.tabstops[-1] * charx < width: break points -= 2 for i in range(len(self.tabstops)): self.tabstops[i] *= charx self.dy = chary # line spacing def OnPaint(self, event): dc = wx.PaintDC(self) dc.SetFont(self.font) dc.SetTextForeground('Black') x0 = self.tabstops[0] x, y = self.GetViewStart() self.y = -y + self.dy # blank line at top self.row = 1 p = conf.name_of_sound_play if p: p = "Output to " + p else: p = "Output to (None)" self.MakeRow(dc, 'Interrupts', self.interupts, p, None, None, None, 'Play rate', conf.playback_rate) self.MakeRow(dc, 'Minimum rate', self.rate_min, 'Maximum rate', self.rate_max, 'Min channels', self.chan_min, 'Max channels', self.chan_max) self.MakeRow(dc, 'Capture errors', self.read_error, 'Playback errors', self.write_error, 'Underrun errors', self.underrun_error, 'FFT errors', self.fft_error) self.MakeRow(dc, 'Capture latency', self.latencyCapt, 'Playback latency', self.latencyPlay, 'Total latency', self.latencyCapt + self.latencyPlay, 'FFT points', self.fft_size) self.y += self.dy * 5 / 10 # extra half line if self.err_msg: # Error message dc.SetTextForeground('Red') dc.DrawText(self.err_msg, x0, self.y) # fill='#F00') dc.SetTextForeground('Black') self.y += self.dy if self.msg1: dc.DrawText(self.msg1, x0, self.y) self.y += self.dy t = "Capture rate %d %s" % (application.sample_rate, application.config_text) if application.sound_error: dc.SetTextForeground('Red') dc.DrawText(t, x0, self.y) dc.SetTextForeground('Black') else: dc.DrawText(t, x0, self.y) self.y += self.dy if conf.config_file_exists: t = "Using configuration file %s" % conf.config_file_path else: t = "Configuration file %s was not found" % conf.config_file_path dc.DrawText(t, x0, self.y) self.y += self.dy name = conf.microphone_name if name: if self.mic_max_display > -0.1: t = "Microphone %s maximum level %3.0f db CLIP" % (name, self.mic_max_display) dc.SetTextForeground('Red') dc.DrawText(t, x0, self.y) dc.SetTextForeground('Black') else: t = "Microphone %s maximum level %3.0f db" % (name, self.mic_max_display) dc.DrawText(t, x0, self.y) else: t = "The microphone is not used (null name)." dc.DrawText(t, x0, self.y) self.height = self.y + 2 * self.dy if self.setscroll: self.setscroll = False sp = self.chary # Make controls # Button for phase adjust dialog t = wx.StaticText(self, -1, "Sound card phase", pos=(x0, self.height)) x, y = t.GetSizeTuple() self.phase = wx.Button(self, -1, "Adjust...") self.Bind(wx.EVT_BUTTON, self.OnBtnPhase, self.phase) x1, y1 = self.phase.GetSizeTuple() yoff = (y1 - y) / 2 self.phase.SetPosition((x0 + x + sp, self.height - yoff)) # Choice (combo) box for decimation lst = Hardware.VarDecimGetChoices() if lst: txt = Hardware.VarDecimGetLabel() x2 = self.width / 2 t = wx.StaticText(self, -1, txt, pos=(x2, self.height)) x, y = t.GetSizeTuple() c = wx.Choice(self, -1, pos=(x2 + x + sp, self.height - yoff), choices=lst) self.Bind(wx.EVT_CHOICE, application.OnBtnDecimation, c) index = Hardware.VarDecimGetIndex() c.SetSelection(index) self.height += y1 # The height is now known; set scroll size self.SetScrollbars(1, 1, self.width, self.height + 2 * self.dy) def MakeRow(self, dc, *args): y = self.y for col in range(len(args)): x = self.tabstops[col] t = args[col] if t is not None: t = str(t) if col % 2 == 1: w, h = dc.GetTextExtent(t) x -= w dc.DrawText(t, x, y) self.row += 1 self.y += self.dy def OnGraphData(self, data=None): (self.rate_min, self.rate_max, sample_rate, self.chan_min, self.chan_max, self.msg1, self.unused, self.err_msg, self.read_error, self.write_error, self.underrun_error, self.latencyCapt, self.latencyPlay, self.interupts, self.fft_error, self.mic_max_display, self.data_poll_usec ) = QS.get_state() self.mic_max_display = 20.0 * math.log10((self.mic_max_display + 1) / 32767.0) self.Refresh() def ChangeYscale(self, y_scale): pass def ChangeYzero(self, y_zero): pass def OnIdle(self, event): pass def SetTxFreq(self, freq): pass def OnBtnPhase(self, event): application.screenBtnGroup.SetLabel('Graph', do_cmd=True) if self.w_phase: self.w_phase.Raise() else: self.w_phase = QAdjustPhase(self, self.width) def OnPhaseClose(self, event): self.w_phase.Destroy() self.w_phase = None class GraphDisplay(wx.Window): """Display the FFT graph within the graph screen.""" def __init__(self, parent, x, y, graph_width, height, chary): wx.Window.__init__(self, parent, pos = (x, y), size = (graph_width, height), style = wx.NO_BORDER) self.parent = parent self.chary = chary self.graph_width = graph_width self.line = [(0, 0), (1,1)] # initial fake graph data self.SetBackgroundColour(conf.color_graph) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown) self.Bind(wx.EVT_RIGHT_DOWN, parent.OnRightDown) self.Bind(wx.EVT_LEFT_UP, parent.OnLeftUp) self.Bind(wx.EVT_MOTION, parent.OnMotion) self.Bind(wx.EVT_MOUSEWHEEL, parent.OnWheel) self.tune_x = graph_width / 2 self.scale = 20 # pixels per 10 dB self.height = 10 self.y_min = 1000 self.y_max = 0 self.max_height = application.screen_height self.tuningPen = wx.Pen('Red', 1) self.backgroundPen = wx.Pen(self.GetBackgroundColour(), 1) self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID) def OnPaint(self, event): #print 'GraphDisplay', self.GetUpdateRegion().GetBox() dc = wx.PaintDC(self) dc.SetPen(wx.BLACK_PEN) dc.DrawLines(self.line) x = self.tune_x dc.SetPen(self.tuningPen) dc.DrawLine(x, 0, x, self.max_height) if not self.parent.in_splitter: dc.SetPen(self.horizPen) chary = self.chary y = self.zeroDB for i in range(0, -99999, -10): if y >= chary / 2: dc.DrawLine(0, y, self.graph_width, y) # y line y = y + self.scale if y > self.height: break def SetHeight(self, height): self.height = height self.SetSize((self.graph_width, height)) def OnGraphData(self, data): line = [] x = 0 y_min = 1000 y_max = 0 for y in data: # y is in dB, -130 to 0 y = self.zeroDB - int(y * self.scale / 10.0 + 0.5) if y > y_max: y_max = y if y < y_min: y_min = y line.append((x, y)) x = x + 1 ymax = max(y_max, self.y_max) ymin = min(y_min, self.y_min) rect = wx.Rect(0, ymin, 1000, ymax - ymin) self.y_min = y_min self.y_max = y_max self.line = line self.Refresh() #rect=rect) def SetTuningLine(self, x): dc = wx.ClientDC(self) dc.SetPen(self.backgroundPen) dc.DrawLine(self.tune_x, 0, self.tune_x, self.max_height) dc.SetPen(self.tuningPen) dc.DrawLine(x, 0, x, self.max_height) self.tune_x = x class GraphScreen(wx.Window): """Display the graph screen X and Y axis, and create a graph display.""" def __init__(self, parent, data_width, graph_width, in_splitter=0): wx.Window.__init__(self, parent, pos = (0, 0)) self.in_splitter = in_splitter # Are we in the top of a splitter window? if in_splitter: self.y_scale = conf.waterfall_graph_y_scale self.y_zero = conf.waterfall_graph_y_zero else: self.y_scale = conf.graph_y_scale self.y_zero = conf.graph_y_zero self.VFO = 0 self.WheelMod = 50 # Round frequency when using mouse wheel self.txFreq = 0 self.sample_rate = application.sample_rate self.data_width = data_width self.graph_width = graph_width self.doResize = False self.pen_tick = wx.Pen("Black", 1, wx.SOLID) self.font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) w = self.GetCharWidth() * 14 / 10 h = self.GetCharHeight() self.charx = w self.chary = h self.tick = max(2, h * 3 / 10) self.originX = w * 5 self.offsetY = h + self.tick self.width = self.originX + self.graph_width + self.tick + self.charx * 2 self.height = application.screen_height * 3 / 10 self.x0 = self.originX + self.graph_width / 2 # center of graph self.tuningX = self.x0 self.originY = 10 self.zeroDB = 10 # y location of zero dB; may be above the top of the graph self.scale = 10 self.SetSize((self.width, self.height)) self.SetSizeHints(self.width, 1, self.width) self.SetBackgroundColour(conf.color_graph) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_MOTION, self.OnMotion) self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) self.MakeDisplay() def MakeDisplay(self): self.display = GraphDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) self.display.zeroDB = self.zeroDB def OnPaint(self, event): dc = wx.PaintDC(self) if not self.in_splitter: dc.SetFont(self.font) self.MakeYTicks(dc) self.MakeXTicks(dc) def OnIdle(self, event): if self.doResize: self.ResizeGraph() def OnSize(self, event): self.doResize = True event.Skip() def ResizeGraph(self): """Change the height of the graph. Changing the width interactively is not allowed because the FFT size is fixed. Call after changing the zero or scale to recalculate the X and Y axis marks. """ w, h = self.GetClientSize() if self.in_splitter: # Splitter window has no X axis scale self.height = h self.originY = h else: self.height = h - self.chary # Leave space for X scale self.originY = self.height - self.offsetY self.MakeYScale() self.display.SetHeight(self.originY) self.display.scale = self.scale self.doResize = False self.Refresh() def ChangeYscale(self, y_scale): self.y_scale = y_scale self.doResize = True def ChangeYzero(self, y_zero): self.y_zero = y_zero self.doResize = True def MakeYScale(self): chary = self.chary scale = (self.originY - chary) * 10 / (self.y_scale + 20) # Number of pixels per 10 dB scale = max(1, scale) q = (self.originY - chary ) / scale / 2 zeroDB = chary + q * scale - self.y_zero * scale / 10 if zeroDB > chary: zeroDB = chary self.scale = scale self.zeroDB = zeroDB self.display.zeroDB = self.zeroDB QS.record_graph(self.originX, self.zeroDB, self.scale) def MakeYTicks(self, dc): chary = self.chary x1 = self.originX - self.tick * 3 # left of tick mark x2 = self.originX - 1 # x location of y axis x3 = self.originX + self.graph_width # end of graph data dc.SetPen(self.pen_tick) dc.DrawLine(x2, 0, x2, self.originY + 1) # y axis y = self.zeroDB for i in range(0, -99999, -10): if y >= chary / 2: dc.SetPen(self.pen_tick) dc.DrawLine(x1, y, x2, y) # y tick t = `i` w, h = dc.GetTextExtent(t) dc.DrawText(`i`, x1 - w, y - h / 2) # y text y = y + self.scale if y > self.originY: break def MakeXTicks(self, dc): originY = self.originY x3 = self.originX + self.graph_width # end of fft data charx , z = dc.GetTextExtent('-30000XX') tick0 = self.tick tick1 = tick0 * 2 tick2 = tick0 * 3 # Draw the X axis dc.SetPen(self.pen_tick) dc.DrawLine(self.originX, originY, x3, originY) # Draw the band plan colors below the X axis x = self.originX f = float(x - self.x0) * self.sample_rate / self.data_width c = None y = originY + 1 for freq, color in conf.BandPlan: freq -= self.VFO if f < freq: xend = int(self.x0 + float(freq) * self.data_width / self.sample_rate + 0.5) if c is not None: dc.SetPen(wx.TRANSPARENT_PEN) dc.SetBrush(wx.Brush(c)) dc.DrawRectangle(x, y, min(x3, xend) - x, tick0) # x axis if xend >= x3: break x = xend f = freq c = color stick = 1000 # small tick in Hertz mtick = 5000 # medium tick ltick = 10000 # large tick # check the width of the frequency label versus frequency span df = charx * self.sample_rate / self.data_width if df < 5000: tfreq = 5000 # tick frequency for labels elif df < 10000: tfreq = 10000 elif df < 20000: tfreq = 20000 elif df < 50000: tfreq = 50000 stick = 5000 mtick = 10000 ltick = 50000 else: tfreq = 100000 stick = 5000 mtick = 10000 ltick = 50000 # Draw the X axis ticks and frequency in kHz dc.SetPen(self.pen_tick) freq1 = self.VFO - self.sample_rate / 2 freq1 = (freq1 / stick) * stick freq2 = freq1 + self.sample_rate + stick + 1 y_end = 0 for f in range (freq1, freq2, stick): x = self.x0 + int(float(f - self.VFO) / self.sample_rate * self.data_width) if self.originX <= x <= x3: if f % ltick is 0: # large tick dc.DrawLine(x, originY, x, originY + tick2) elif f % mtick is 0: # medium tick dc.DrawLine(x, originY, x, originY + tick1) else: # small tick dc.DrawLine(x, originY, x, originY + tick0) if f % tfreq is 0: # place frequency label t = str(f/1000) w, h = dc.GetTextExtent(t) dc.DrawText(t, x - w / 2, originY + tick2) y_end = originY + tick2 + h if y_end: # mark the center of the display dc.DrawLine(self.x0, y_end, self.x0, application.screen_height) def OnGraphData(self, data): i1 = (self.data_width - self.graph_width) / 2 i2 = i1 + self.graph_width self.display.OnGraphData(data[i1:i2]) def SetVFO(self, vfo): self.VFO = vfo self.doResize = True def SetTxFreq(self, freq): self.txFreq = freq x = self.x0 + int(float(freq) / self.sample_rate * self.data_width) self.display.SetTuningLine(x - self.originX) self.tuningX = x def GetMousePosition(self, event): """For mouse clicks in our display, translate to our screen coordinates.""" mouse_x, mouse_y = event.GetPositionTuple() win = event.GetEventObject() if win is not self: x, y = win.GetPositionTuple() mouse_x += x mouse_y += y return mouse_x, mouse_y def OnRightDown(self, event): mouse_x, mouse_y = self.GetMousePosition(event) freq = float(mouse_x - self.x0) * self.sample_rate / self.data_width freq = int(freq) if self.VFO > 0: vfo = self.VFO + freq vfo = (vfo + 5000) / 10000 * 10000 # round to even number tune = freq + self.VFO - vfo self.ChangeHwFrequency(tune, vfo, 'MouseBtn3', event) def OnLeftDown(self, event): mouse_x, mouse_y = self.GetMousePosition(event) self.mouse_x = mouse_x if mouse_y > self.originY: # click below X axis self.mouse_origin = self.tuningX else: # click above X axis self.mouse_origin = mouse_x freq = float(mouse_x - self.x0) * self.sample_rate / self.data_width freq = int(freq) self.ChangeHwFrequency(freq, self.VFO, 'MouseBtn1', event) self.CaptureMouse() def OnLeftUp(self, event): if self.HasCapture(): self.ReleaseMouse() def OnMotion(self, event): if event.Dragging() and event.LeftIsDown(): mouse_x, mouse_y = self.GetMousePosition(event) if conf.mouse_tune_method: # Mouse motion changes the VFO frequency x = (mouse_x - self.mouse_x) # Thanks to VK6JBL self.mouse_x = mouse_x freq = x * self.sample_rate / self.data_width freq = int(freq) self.ChangeHwFrequency(self.txFreq, self.VFO - freq, 'MouseMotion', event) else: # Mouse motion changes the tuning frequency # Frequency changes more rapidly for higher mouse Y position speed = max(10, self.originY - mouse_y) / float(self.originY) x = (mouse_x - self.mouse_x) self.mouse_x = mouse_x freq = speed * x * self.sample_rate / self.data_width freq = int(freq) self.ChangeHwFrequency(self.txFreq + freq, self.VFO, 'MouseMotion', event) def OnWheel(self, event): wm = self.WheelMod # Round frequency when using mouse wheel tune = self.txFreq + wm * event.GetWheelRotation() / event.GetWheelDelta() if tune >= 0: tune = tune / wm * wm else: # tune can be negative when the VFO is zero tune = - (- tune / wm * wm) self.ChangeHwFrequency(tune, self.VFO, 'MouseWheel', event) def ChangeHwFrequency(self, tune, vfo, source, event): application.ChangeHwFrequency(tune, vfo, source, event=event) class WaterfallDisplay(wx.Window): """Create a waterfall display within the waterfall screen.""" def __init__(self, parent, x, y, graph_width, height, margin): wx.Window.__init__(self, parent, pos = (x, y), size = (graph_width, height), style = wx.NO_BORDER) self.parent = parent self.graph_width = graph_width self.margin = margin self.height = 10 self.sample_rate = application.sample_rate self.SetBackgroundColour('Black') self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown) self.Bind(wx.EVT_RIGHT_DOWN, parent.OnRightDown) self.Bind(wx.EVT_LEFT_UP, parent.OnLeftUp) self.Bind(wx.EVT_MOTION, parent.OnMotion) self.Bind(wx.EVT_MOUSEWHEEL, parent.OnWheel) self.tune_x = graph_width / 2 self.tuningPen = wx.Pen('White', 3) self.marginPen = wx.Pen(conf.color_graph, 1) # Size of top faster scroll region is (top_key + 2) * (top_key - 1) / 2 self.top_key = 8 self.top_size = (self.top_key + 2) * (self.top_key - 1) / 2 # Make the palette pal2 = conf.waterfallPalette red = [] green = [] blue = [] n = 0 for i in range(256): if i > pal2[n+1][0]: n = n + 1 red.append((i - pal2[n][0]) * (long)(pal2[n+1][1] - pal2[n][1]) / (long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][1]) green.append((i - pal2[n][0]) * (long)(pal2[n+1][2] - pal2[n][2]) / (long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][2]) blue.append((i - pal2[n][0]) * (long)(pal2[n+1][3] - pal2[n][3]) / (long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][3]) self.red = red self.green = green self.blue = blue bmp = wx.EmptyBitmap(0, 0) bmp.x_origin = 0 self.bitmaps = [bmp] * application.screen_height def OnPaint(self, event): dc = wx.PaintDC(self) y = 0 dc.SetPen(self.marginPen) x_origin = int(float(self.VFO) / self.sample_rate * self.data_width + 0.5) for i in range(0, self.margin): dc.DrawLine(0, y, self.graph_width, y) y += 1 index = 0 if conf.waterfall_scroll_mode: # Draw the first few lines multiple times for i in range(self.top_key, 1, -1): b = self.bitmaps[index] x = b.x_origin - x_origin for j in range(0, i): dc.DrawBitmap(b, x, y) y += 1 index += 1 while y < self.height: b = self.bitmaps[index] x = b.x_origin - x_origin dc.DrawBitmap(b, x, y) y += 1 index += 1 dc.SetPen(self.tuningPen) dc.SetLogicalFunction(wx.XOR) dc.DrawLine(self.tune_x, 0, self.tune_x, self.height) def SetHeight(self, height): self.height = height self.SetSize((self.graph_width, height)) def OnGraphData(self, data, y_zero, y_scale): #T('graph start') row = '' # Make a new row of pixels for a one-line image for x in data: # x is -130 to 0, or so (dB) l = int((x + y_zero / 3 + 100) * y_scale / 10) l = max(l, 0) l = min(l, 255) row = row + "%c%c%c" % (chr(self.red[l]), chr(self.green[l]), chr(self.blue[l])) #T('graph string') bmp = wx.BitmapFromBuffer(len(row) / 3, 1, row) bmp.x_origin = int(float(self.VFO) / self.sample_rate * self.data_width + 0.5) self.bitmaps.insert(0, bmp) del self.bitmaps[-1] self.ScrollWindow(0, 1, None) self.Refresh(False, (0, 0, self.graph_width, self.top_size + self.margin)) #T('graph end') def SetTuningLine(self, x): dc = wx.ClientDC(self) dc.SetPen(self.tuningPen) dc.SetLogicalFunction(wx.XOR) dc.DrawLine(self.tune_x, 0, self.tune_x, self.height) dc.DrawLine(x, 0, x, self.height) self.tune_x = x class WaterfallScreen(wx.SplitterWindow): """Create a splitter window with a graph screen and a waterfall screen""" def __init__(self, frame, width, data_width, graph_width): self.y_scale = conf.waterfall_y_scale self.y_zero = conf.waterfall_y_zero wx.SplitterWindow.__init__(self, frame) self.SetSizeHints(width, -1, width) self.SetMinimumPaneSize(1) self.SetSize((width, conf.waterfall_graph_size + 100)) # be able to set sash size self.pane1 = GraphScreen(self, data_width, graph_width, 1) self.pane2 = WaterfallPane(self, data_width, graph_width) self.SplitHorizontally(self.pane1, self.pane2, conf.waterfall_graph_size) def OnIdle(self, event): self.pane1.OnIdle(event) self.pane2.OnIdle(event) def SetTxFreq(self, freq): self.pane1.SetTxFreq(freq) self.pane2.SetTxFreq(freq) def SetVFO(self, vfo): self.pane1.SetVFO(vfo) self.pane2.SetVFO(vfo) def ChangeYscale(self, y_scale): # Test if the shift key is down if wx.GetKeyState(wx.WXK_SHIFT): # Set graph screen self.pane1.ChangeYscale(y_scale) else: # Set waterfall screen self.y_scale = y_scale self.pane2.ChangeYscale(y_scale) def ChangeYzero(self, y_zero): # Test if the shift key is down if wx.GetKeyState(wx.WXK_SHIFT): # Set graph screen self.pane1.ChangeYzero(y_zero) else: # Set waterfall screen self.y_zero = y_zero self.pane2.ChangeYzero(y_zero) def OnGraphData(self, data): self.pane1.OnGraphData(data) self.pane2.OnGraphData(data) class WaterfallPane(GraphScreen): """Create a waterfall screen with an X axis and a waterfall display.""" def __init__(self, frame, data_width, graph_width): GraphScreen.__init__(self, frame, data_width, graph_width) self.y_scale = conf.waterfall_y_scale self.y_zero = conf.waterfall_y_zero self.oldVFO = self.VFO def MakeDisplay(self): self.display = WaterfallDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) self.display.VFO = self.VFO self.display.data_width = self.data_width def SetVFO(self, vfo): GraphScreen.SetVFO(self, vfo) self.display.VFO = vfo if self.oldVFO != vfo: self.oldVFO = vfo self.Refresh() def MakeYTicks(self, dc): pass def ChangeYscale(self, y_scale): self.y_scale = y_scale def ChangeYzero(self, y_zero): self.y_zero = y_zero def OnGraphData(self, data): i1 = (self.data_width - self.graph_width) / 2 i2 = i1 + self.graph_width self.display.OnGraphData(data[i1:i2], self.y_zero, self.y_scale) class ScopeScreen(wx.Window): """Create an oscilloscope screen (mostly used for debug).""" def __init__(self, parent, width, data_width, graph_width): wx.Window.__init__(self, parent, pos = (0, 0), size=(width, -1), style = wx.NO_BORDER) self.SetBackgroundColour(conf.color_graph) self.font = wx.Font(16, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) self.SetFont(self.font) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_PAINT, self.OnPaint) self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID) self.y_scale = conf.scope_y_scale self.y_zero = conf.scope_y_zero self.running = 1 self.doResize = False self.width = width self.height = 100 self.originY = self.height / 2 self.data_width = data_width self.graph_width = graph_width w = self.charx = self.GetCharWidth() h = self.chary = self.GetCharHeight() tick = max(2, h * 3 / 10) self.originX = w * 3 self.width = self.originX + self.graph_width + tick + self.charx * 2 self.line = [(0,0), (1,1)] # initial fake graph data def OnIdle(self, event): if self.doResize: self.ResizeGraph() def OnSize(self, event): self.doResize = True event.Skip() def ResizeGraph(self, event=None): # Change the height of the graph. Changing the width interactively is not allowed. w, h = self.GetClientSize() self.height = h self.originY = h / 2 self.doResize = False self.Refresh() def OnPaint(self, event): dc = wx.PaintDC(self) dc.SetFont(self.font) self.MakeYTicks(dc) self.MakeXTicks(dc) self.MakeText(dc) dc.SetPen(wx.BLACK_PEN) dc.DrawLines(self.line) def MakeYTicks(self, dc): chary = self.chary originX = self.originX x3 = self.x3 = originX + self.graph_width # end of graph data dc.SetPen(wx.BLACK_PEN) dc.DrawLine(originX, 0, originX, self.originY * 3) # y axis # Find the size of the Y scale markings themax = 2.5e9 * 10.0 ** - ((160 - self.y_scale) / 50.0) # value at top of screen themax = int(themax) l = [] for j in (5, 6, 7, 8): for i in (1, 2, 5): l.append(i * 10 ** j) for yvalue in l: n = themax / yvalue + 1 # Number of lines ypixels = self.height / n if n < 20: break dc.SetPen(self.horizPen) for i in range(1, 1000): y = self.originY - ypixels * i if y < chary: break # Above axis dc.DrawLine(originX, y, x3, y) # y line # Below axis y = self.originY + ypixels * i dc.DrawLine(originX, y, x3, y) # y line self.yscale = float(ypixels) / yvalue self.yvalue = yvalue def MakeXTicks(self, dc): originY = self.originY x3 = self.x3 # Draw the X axis dc.SetPen(wx.BLACK_PEN) dc.DrawLine(self.originX, originY, x3, originY) # Find the size of the X scale markings in microseconds for i in (20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000): xscale = i # X scale in microseconds if application.sample_rate * xscale * 0.000001 > self.width / 30: break # Draw the X lines dc.SetPen(self.horizPen) for i in range(1, 999): x = int(self.originX + application.sample_rate * xscale * 0.000001 * i + 0.5) if x > x3: break dc.DrawLine(x, 0, x, self.height) # x line self.xscale = xscale def MakeText(self, dc): if self.running: t = " RUN" else: t = " STOP" if self.xscale >= 1000: t = "%s X: %d millisec/div" % (t, self.xscale) else: t = "%s X: %d microsec/div" % (t, self.xscale) yt = `self.yvalue` t = "%s Y: %sE%d/div" % (t, yt[0], len(yt) - 1) dc.DrawText(t, self.originX, self.height - self.chary) def OnGraphData(self, data): if not self.running: return # Preserve data line = [] x = self.originX ymax = self.height for y in data: # y is raw samples 0 to 2**31-1 y = self.originY + int(y * self.yscale + 0.5) if y > ymax: y = ymax elif y < 0: y = 0 line.append((x, y)) x = x + 1 if x > self.x3: break self.line = line self.Refresh() def ChangeYscale(self, y_scale): self.y_scale = y_scale self.doResize = True def ChangeYzero(self, y_zero): self.y_zero = y_zero def SetTxFreq(self, freq): pass class FilterScreen(GraphScreen): """Create a graph of the receive filter response.""" def __init__(self, parent, data_width, graph_width): GraphScreen.__init__(self, parent, data_width, graph_width) self.y_scale = conf.filter_y_scale self.y_zero = conf.filter_y_zero self.VFO = 0 self.txFreq = 0 self.data = [] self.sample_rate = QS.get_filter_rate() def NewFilter(self): self.data = QS.get_filter() def OnGraphData(self, data): GraphScreen.OnGraphData(self, self.data) def ChangeHwFrequency(self, tune, vfo, source, event): self.SetTxFreq(tune) application.freqDisplay.Display(tune) class HelpScreen(wx.html.HtmlWindow): """Create the screen for the Help button.""" def __init__(self, parent, width, height): wx.html.HtmlWindow.__init__(self, parent, -1, size=(width, height)) self.y_scale = 0 self.y_zero = 0 if "gtk2" in wx.PlatformInfo: self.SetStandardFonts() self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22]) # read in text from file help.html in the directory of this module self.LoadFile('help.html') def OnGraphData(self, data): pass def ChangeYscale(self, y_scale): pass def ChangeYzero(self, y_zero): pass def OnIdle(self, event): pass def SetTxFreq(self, freq): pass class QMainFrame(wx.Frame): """Create the main top-level window.""" def __init__(self, width, height): fp = open('__init__.py') # Read in the title title = fp.readline().strip()[1:] fp.close() wx.Frame.__init__(self, None, -1, title, wx.DefaultPosition, (width, height), wx.DEFAULT_FRAME_STYLE, 'MainFrame') self.SetBackgroundColour(conf.color_bg) self.Bind(wx.EVT_CLOSE, self.OnBtnClose) def OnBtnClose(self, event): application.OnBtnClose(event) self.Destroy() class QAdjustPhase(wx.Frame): """Create a window with amplitude and phase adjustment controls""" f_ampl = "Amplitude adjustment %.6f" f_phase = "Phase adjustment %.6f" def __init__(self, parent, width): wx.Frame.__init__(self, application.main_frame, -1, "Adjust Sound Card Amplitude and Phase", pos=(50, 100)) self.Bind(wx.EVT_CLOSE, parent.OnPhaseClose) self.ampl, self.phase = application.GetAmplPhase() self.MakeControls(width) self.Show() def MakeControls(self, width): # Make controls for phase/amplitude adjustment chary = self.GetCharHeight() y = chary * 3 / 10 self.t_ampl = wx.StaticText(self, -1, self.f_ampl % self.ampl, pos=(0, y)) y += self.t_ampl.GetSizeTuple()[1] scale = width * 4 / 10 self.scale = float(scale) self.ampl1 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.ampl1.GetSizeTuple()[1] self.ampl2 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.ampl2.GetSizeTuple()[1] self.t_phase = wx.StaticText(self, -1, self.f_phase % self.phase, pos=(0, y)) y += self.t_phase.GetSizeTuple()[1] self.phase1 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.phase1.GetSizeTuple()[1] self.phase2 = wx.Slider(self, -1, 0, -scale, scale, pos=(0, y), size=(width, -1)) y += self.phase2.GetSizeTuple()[1] self.SetSizeHints(width, y, width, y) # no change in size self.ampl1.Bind(wx.EVT_SCROLL, self.OnAmpl1) self.ampl2.Bind(wx.EVT_SCROLL, self.OnAmpl1) self.phase1.Bind(wx.EVT_SCROLL, self.OnAmpl1) self.phase2.Bind(wx.EVT_SCROLL, self.OnAmpl1) def OnAmpl1(self, event): s2 = self.scale * 10.0 # maximum 0.10 change s1 = s2 * 20.0 # smaller maximum change ampl = self.ampl + self.ampl1.GetValue() / s1 + self.ampl2.GetValue() / s2 self.t_ampl.SetLabel(self.f_ampl % ampl) phase = self.phase + self.phase1.GetValue() / s1 + self.phase2.GetValue() / s2 self.t_phase.SetLabel(self.f_phase % phase) application.SetAmplPhase(ampl, phase) class Spacer(wx.Window): """Create a bar between the graph screen and the controls""" def __init__(self, parent): wx.Window.__init__(self, parent, pos = (0, 0), size=(-1, 6), style = wx.NO_BORDER) self.Bind(wx.EVT_PAINT, self.OnPaint) r, g, b = parent.GetBackgroundColour().Get() dark = (r * 7 / 10, g * 7 / 10, b * 7 / 10) light = (r + (255 - r) * 5 / 10, g + (255 - g) * 5 / 10, b + (255 - b) * 5 / 10) self.dark_pen = wx.Pen(dark, 1, wx.SOLID) self.light_pen = wx.Pen(light, 1, wx.SOLID) self.width = application.screen_width def OnPaint(self, event): dc = wx.PaintDC(self) w = self.width dc.SetPen(self.dark_pen) dc.DrawLine(0, 0, w, 0) dc.DrawLine(0, 1, w, 1) dc.DrawLine(0, 2, w, 2) dc.SetPen(self.light_pen) dc.DrawLine(0, 3, w, 3) dc.DrawLine(0, 4, w, 4) dc.DrawLine(0, 5, w, 5) class App(wx.App): """Class representing the application.""" freq60 = (5330500, 5346500, 5366500, 5371500, 5403500) StateNames = [ # Names of state attributes to save and restore 'bandState', 'bandAmplPhase', 'lastBand', 'VFO', 'txFreq', 'mode', ] def __init__(self): global application application = self self.init_path = None if sys.stdout.isatty(): wx.App.__init__(self, redirect=False) else: wx.App.__init__(self, redirect=True) def QuiskPushbutton(self, *args, **kw): # Make our buttons available to widget files return QuiskPushbutton(*args, **kw) def QuiskRepeatbutton(self, *args, **kw): return QuiskRepeatbutton(*args, **kw) def QuiskCheckbutton(self, *args, **kw): return QuiskCheckbutton(*args, **kw) def QuiskCycleCheckbutton(self, *args, **kw): return QuiskCycleCheckbutton(*args, **kw) def RadioButtonGroup(self, *args, **kw): return RadioButtonGroup(*args, **kw) def OnInit(self): """Perform most initialization of the app here (called by wxPython on startup).""" wx.lib.colourdb.updateColourDB() # Add additional color names global conf # conf is the module for all configuration data import quisk_conf_defaults as conf cpath = argv_options.config_file_path # Get config file path if not cpath: cpath = os.path.expanduser('~/.quisk_conf.py') # Default path setattr(conf, 'config_file_path', cpath) if os.path.isfile(cpath): # See if the user has a config file setattr(conf, 'config_file_exists', True) d = {} d.update(conf.__dict__) # make items from conf available execfile(cpath, d) # execute the user's config file for k, v in d.items(): # add user's config items to conf if k[0] != '_': # omit items starting with '_' setattr(conf, k, v) else: setattr(conf, 'config_file_exists', False) if conf.invertSpectrum: QS.invert_spectrum(1) self.bandState = {} self.bandState.update(conf.bandState) self.bandAmplPhase = conf.bandAmplPhase # Open hardware file global Hardware if hasattr(conf, "Hardware"): # Hardware defined in config file Hardware = conf.Hardware(self, conf) else: Hardware = conf.quisk_hardware.Hardware(self, conf) if Hardware.VarDecimGetChoices(): # Hardware can change the decimation. self.sample_rate = Hardware.VarDecimSet() # Get the sample rate. else: # Use the sample rate from the config file. self.sample_rate = conf.sample_rate if not hasattr(conf, 'playback_rate'): if conf.use_sdriq or conf.use_rx_udp: conf.playback_rate = 48000 else: conf.playback_rate = conf.sample_rate self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow self.smeter_db_count = 0 # average the S-meter self.smeter_db_sum = 0 self.smeter_db = 0 self.smeter_sunits = -87.0 self.timer = time.time() # A seconds clock self.heart_time0 = self.timer # timer to call HeartBeat at intervals self.smeter_db_time0 = self.timer self.smeter_sunits_time0 = self.timer self.band_up_down = 0 # Are band Up/Down buttons in use? self.lastBand = 'Audio' self.VFO = 0 self.ritFreq = 0 self.txFreq = 0 self.screen = None self.audio_volume = 0.0 # Set output volume, 0.0 to 1.0 self.sidetone_volume = 0.0 # Set sidetone volume, 0.0 to 1.0 self.sound_error = 0 self.sound_thread = None self.mode = conf.default_mode self.bottom_widgets = None self.color_list = None self.color_index = 0 dc = wx.ScreenDC() # get the screen size (self.screen_width, self.screen_height) = dc.GetSizeTuple() del dc self.Bind(wx.EVT_IDLE, self.OnIdle) self.Bind(wx.EVT_QUERY_END_SESSION, self.OnEndSession) # Save and restore program state if conf.persistent_state: self.init_path = os.path.join(os.path.dirname(cpath), '.quisk_init.pkl') try: fp = open(self.init_path, "rb") d = pickle.load(fp) fp.close() for k, v in d.items(): if k in self.StateNames: if k == 'bandState': self.bandState.update(v) else: setattr(self, k, v) except: pass #traceback.print_exc() for k, (vfo, tune, mode) in self.bandState.items(): # Historical: fix bad frequencies try: f1, f2 = conf.BandEdge[k] if not f1 <= vfo + tune <= f2: self.bandState[k] = conf.bandState[k] except KeyError: pass # Find the data width from a list of prefered sizes; it is the width of returned graph data. # The graph_width is the width of data_width that is displayed. width = self.screen_width * conf.graph_width percent = conf.display_fraction # display central fraction of total width percent = int(percent * 100.0 + 0.4) width = width * 100 / percent for x in fftPreferedSizes: if x > width: self.data_width = x break else: self.data_width = fftPreferedSizes[-1] self.graph_width = self.data_width * percent / 100 if self.graph_width % 2 == 1: # Both data_width and graph_width are even numbers self.graph_width += 1 # The FFT size times the average_count controls the graph refresh rate factor = float(self.sample_rate) / conf.graph_refresh / self.data_width ifactor = int(factor + 0.5) if conf.fft_size_multiplier >= ifactor: # Use large FFT and average count 1 fft_mult = ifactor average_count = 1 elif conf.fft_size_multiplier > 0: # Specified fft_size_multiplier fft_mult = conf.fft_size_multiplier average_count = int(factor / fft_mult + 0.5) if average_count < 1: average_count = 1 else: # Calculate the split between fft size and average if self.sample_rate <= 240000: maxfft = 8000 # Maximum fft size else: maxfft = 15000 fft1 = maxfft / self.data_width if fft1 >= ifactor: fft_mult = ifactor average_count = 1 else: av1 = int(factor / fft1 + 0.5) if av1 < 1: av1 = 1 err1 = factor / (fft1 * av1) av2 = av1 + 1 fft2 = int(factor / av2 + 0.5) err2 = factor / (fft2 * av2) if 0.9 < err1 < 1.1 or abs(1.0 - err1) <= abs(1.0 - err2): fft_mult = fft1 average_count = av1 else: fft_mult = fft2 average_count = av2 self.fft_size = self.data_width * fft_mult # Record the basic application parameters QS.record_app(self, conf, self.data_width, self.fft_size, average_count, self.sample_rate) #print 'FFT size %d, FFT mult %d, average_count %d' % ( # self.fft_size, self.fft_size / self.data_width, average_count) #print 'Refresh %.2f Hz' % (float(self.sample_rate) / self.fft_size / average_count) QS.record_graph(0, 0, 1.0) self.width = self.screen_width * 8 / 10 self.height = self.screen_height * 5 / 10 self.main_frame = frame = QMainFrame(self.width, self.height) self.SetTopWindow(frame) # Make all the screens and hide all but one self.graph = GraphScreen(frame, self.data_width, self.graph_width) self.screen = self.graph width = self.graph.width button_width = width # try to estimate the final button width self.config_screen = ConfigScreen(frame, width, self.fft_size) self.config_screen.Hide() self.waterfall = WaterfallScreen(frame, width, self.data_width, self.graph_width) self.waterfall.Hide() self.scope = ScopeScreen(frame, width, self.data_width, self.graph_width) self.scope.Hide() self.filter_screen = FilterScreen(frame, self.data_width, self.graph_width) self.filter_screen.Hide() self.help_screen = HelpScreen(frame, width, self.screen_height / 10) self.help_screen.Hide() frame.SetSizeHints(width, 100) # Make a vertical box to hold all the screens and the bottom box vertBox = self.vertBox = wx.BoxSizer(wx.VERTICAL) frame.SetSizer(vertBox) # Add the screens vertBox.Add(self.config_screen, 1) vertBox.Add(self.graph, 1) vertBox.Add(self.waterfall, 1) vertBox.Add(self.scope, 1) vertBox.Add(self.filter_screen, 1) vertBox.Add(self.help_screen, 1) # Add the spacer vertBox.Add(Spacer(frame), 0, wx.EXPAND) # Add the bottom box hBoxA = wx.BoxSizer(wx.HORIZONTAL) vertBox.Add(hBoxA, 0, wx.EXPAND) # End of vertical box. Add items to the horizontal box. # Add two sliders on the left margin = 3 self.sliderVol = SliderBoxV(frame, 'Vol', 300, 1000, self.ChangeVolume) button_width -= self.sliderVol.width + margin * 2 self.ChangeVolume() # set initial volume level hBoxA.Add(self.sliderVol, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) if Hardware.use_sidetone: self.sliderSto = SliderBoxV(frame, 'STo', 300, 1000, self.ChangeSidetone) button_width -= self.sliderSto.width + margin * 2 self.ChangeSidetone() hBoxA.Add(self.sliderSto, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) # Add the sizer for the middle gap = 2 gbs = wx.GridBagSizer(gap, gap) self.gbs = gbs button_width -= gap * 15 hBoxA.Add(gbs, 1, wx.EXPAND, 0) gbs.SetEmptyCellSize((5, 5)) button_width -= 5 for i in range(0, 6) + range(7, 13): gbs.AddGrowableCol(i) # Add two sliders on the right self.sliderYs = SliderBoxV(frame, 'Ys', 0, 160, self.ChangeYscale, True) button_width -= self.sliderYs.width + margin * 2 hBoxA.Add(self.sliderYs, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) self.sliderYz = SliderBoxV(frame, 'Yz', 0, 160, self.ChangeYzero, True) button_width -= self.sliderYz.width + margin * 2 hBoxA.Add(self.sliderYz, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) button_height = self.MakeButtons(frame, gbs) button_width /= 12 # This is our estimate of the final button size self.MakeTopRow(frame, gbs, button_width, button_height) if conf.quisk_widgets: self.bottom_widgets = conf.quisk_widgets.BottomWidgets(self, Hardware, conf, frame, gbs, vertBox) if QS.open_key(conf.key_method): print 'open_key failed for name "%s"' % conf.key_method if hasattr(conf, 'mixer_settings'): for dev, numid, value in conf.mixer_settings: err_msg = QS.mixer_set(dev, numid, value) if err_msg: print "Mixer", err_msg # Create transmit audio filters if conf.microphone_name: filtI, filtQ = self.MakeFilterCoef(conf.mic_sample_rate, 540, 2500, 1550) QS.set_tx_filters(filtI, filtQ, ()) # Open the hardware. This must be called before open_sound(). self.config_text = Hardware.open() if not self.config_text: self.config_text = "Missing config_text" QS.capt_channels (conf.channel_i, conf.channel_q) QS.play_channels (conf.channel_i, conf.channel_q) QS.micplay_channels (conf.mic_play_chan_I, conf.mic_play_chan_Q) # Note: Subsequent calls to set channels must not name a higher channel number. # Normally, these calls are only used to reverse the channels. QS.open_sound(conf.name_of_sound_capt, conf.name_of_sound_play, self.sample_rate, conf.data_poll_usec, conf.latency_millisecs, conf.microphone_name, conf.tx_ip, conf.tx_audio_port, conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q, conf.mic_out_volume, conf.name_of_mic_play, conf.mic_playback_rate) tune, vfo = Hardware.ReturnFrequency() # Request initial frequency to set band if tune is not None: for band, (f1, f2) in conf.BandEdge.items(): if f1 <= tune <= f2: # Change to the correct band based on frequency self.lastBand = band break self.bandBtnGroup.SetLabel(self.lastBand, do_cmd=True) self.ChangeHwFrequency(None, None) # Request initial VFO and tuning # Note: The filter rate is not valid until after the call to open_sound(). # Create FM audio filter frate = QS.get_filter_rate() # filter rate filtI, filtQ = self.MakeFmFilterCoef(frate, 600, 340, 2800) QS.set_fm_filters(filtI) # Record filter rate for the filter screen self.filter_screen.sample_rate = frate #if info[8]: # error message # self.sound_error = 1 # self.config_screen.err_msg = info[8] # print info[8] if self.sound_error: self.screenBtnGroup.SetLabel('Config', do_cmd=True) frame.Show() else: self.screenBtnGroup.SetLabel(conf.default_screen, do_cmd=True) frame.Show() self.Yield() self.sound_thread = SoundThread() self.sound_thread.start() return True def OnIdle(self, event): if self.screen: self.screen.OnIdle(event) def OnEndSession(self, event): event.Skip() self.OnBtnClose() def OnBtnClose(self, event): if self.sound_thread: self.sound_thread.stop() for i in range(0, 20): if threading.activeCount() == 1: break time.sleep(0.1) def OnExit(self): QS.close_rx_udp() Hardware.close() if self.init_path: # save current program state d = {} for n in self.StateNames: d[n] = getattr(self, n) try: fp = open(self.init_path, "wb") pickle.dump(d, fp) fp.close() except: pass #traceback.print_exc() def MakeTopRow(self, frame, gbs, button_width, button_height): # Down button b_down = QuiskRepeatbutton(frame, self.OnBtnDownBand, "Down", self.OnBtnUpDnBandDone) gbs.Add(b_down, (0, 4), flag=wx.ALIGN_CENTER) # RIT button self.ritButton = QuiskCheckbutton(frame, self.OnBtnRit, "RIT") gbs.Add(self.ritButton, (0, 7), flag=wx.ALIGN_CENTER) # Up button b_up = QuiskRepeatbutton(frame, self.OnBtnUpBand, "Up", self.OnBtnUpDnBandDone) gbs.Add(b_up, (0, 5), flag=wx.ALIGN_CENTER) bw, bh = b_down.GetMinSize() # make top row buttons the same size bw = (bw + button_width) / 2 bh = max(bh, button_height) b_down.SetSizeHints(bw, bh, bw * 5, bh) b_up.SetSizeHints(bw, bh, bw * 5, bh) self.ritButton.SetSizeHints(bw, bh, bw * 5, bh) # RIT slider self.ritScale = wx.Slider(frame, -1, self.ritFreq, -2000, 2000, size=(-1, -1), style=wx.SL_LABELS) self.ritScale.Bind(wx.EVT_SCROLL, self.OnRitScale) gbs.Add(self.ritScale, (0, 8), (1, 3), flag=wx.EXPAND) sw, sh = self.ritScale.GetSize() # Frequency display h = max(bh, sh) # larger of button and slider height self.freqDisplay = FrequencyDisplay(frame, gbs, button_width * 3, h) self.freqDisplay.Display(self.txFreq + self.VFO) # Frequency entry e = wx.TextCtrl(frame, -1, '', style=wx.TE_PROCESS_ENTER|wx.RAISED_BORDER) w, h = e.GetTextExtent('33333333') h = self.freqDisplay.height e.SetSizeHints(w, h, w * 2, h) e.SetBackgroundColour(conf.color_entry) gbs.Add(e, (0, 3), flag= wx.EXPAND | wx.TOP | wx.BOTTOM, border=self.freqDisplay.border) frame.Bind(wx.EVT_TEXT_ENTER, self.FreqEntry, source=e) # S-meter self.smeter = t = wx.lib.stattext.GenStaticText(frame, -1, '', style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE|wx.RAISED_BORDER) for points in range(20, 6, -1): font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) t.SetFont(font) w, h = t.GetTextExtent("ZZS 9 -100.00 dBZZ") if w < button_width * 2: break h = h * 12 / 10 t.SetSizeHints(w, h, -1, h) border = (self.freqDisplay.height_and_border - h) / 2 t.SetBackgroundColour(conf.color_freq) gbs.Add(t, (0, 11), (1, 2), flag=wx.ALIGN_CENTER|wx.EXPAND| wx.TOP | wx.BOTTOM, border=border) def MakeButtons(self, frame, gbs): all_buttons = [] # There are six columns, a small gap column, and then six more columns flag = wx.EXPAND ### Left bank of buttons self.bandBtnGroup = RadioButtonGroup(frame, self.OnBtnBand, conf.bandLabels, None) btns = self.bandBtnGroup.buttons all_buttons += btns i = 0 n1 = len(conf.bandLabels) / 2 n2 = len(conf.bandLabels) - n1 for col in range(0, n1): gbs.Add(btns[i], (1, col), flag=flag) i += 1 for col in range(0, n2): gbs.Add(btns[i], (2, col), flag=flag) i += 1 # Mute, AGC buttons = [] b = QuiskCheckbutton(frame, self.OnBtnMute, text='Mute') buttons.append(b) b = QuiskCycleCheckbutton(frame, self.OnBtnAGC, ('AGC', 'AGC 1', 'AGC 2')) buttons.append(b) b.SetLabel('AGC 1', True) b = QuiskCheckbutton(frame, self.OnBtnNB, text='') buttons.append(b) try: labels = Hardware.rf_gain_labels except: labels = () if labels: self.BtnRfGain = QuiskCycleCheckbutton(frame, Hardware.OnButtonRfGain, labels) buttons.append(self.BtnRfGain) else: b = QuiskCheckbutton(frame, None, text='') buttons.append(b) self.BtnRfGain = None #b = QuiskRepeatbutton(frame, self.OnBtnColor, '', use_right=True) if conf.add_fdx_button: b = QuiskCheckbutton(frame, self.OnBtnFDX, 'FDX', color=conf.color_test) else: b = QuiskCheckbutton(frame, None, text='') buttons.append(b) b = QuiskCheckbutton(frame, self.OnBtnTest1, 'Test 1', color=conf.color_test) buttons.append(b) all_buttons += buttons for col in range(0, 6): gbs.Add(buttons[col], (3, col), flag=flag) ### Right bank of buttons labels = [('CWL', 'CWU'), ('LSB', 'USB'), 'AM', 'FM', conf.add_extern_demod, ''] if conf.add_imd_button: labels[-1] = ('IMD', 'IMD -3dB', 'IMD -6dB') self.modeButns = RadioButtonGroup(frame, self.OnBtnMode, labels, None) btns = self.modeButns.GetButtons() all_buttons += btns btns[-1].color = conf.color_test for col in range(0, 6): gbs.Add(btns[col], (1, col + 7), flag=flag) labels = ('0',) * 6 self.filterButns = RadioButtonGroup(frame, self.OnBtnFilter, labels, None) btns = self.filterButns.GetButtons() all_buttons += btns for col in range(0, 6): gbs.Add(btns[col], (2, col + 7), flag=flag) labels = ('Graph', 'WFall', ('Scope', 'Scope'), 'Config', 'RX Filter', 'Help') self.screenBtnGroup = RadioButtonGroup(frame, self.OnBtnScreen, labels, conf.default_screen) btns = self.screenBtnGroup.GetButtons() all_buttons += btns for col in range(0, 6): gbs.Add(btns[col], (3, col + 7), flag=flag) bw = bh = 0 for b in all_buttons: # find the largest button size w, h = b.GetMinSize() bw = max(bw, w) bh = max(bh, h) for b in all_buttons: # set all buttons to the same size b.SetSizeHints(bw, bh, bw * 5, bh) return bh # return the button height def NewSmeter(self): #avg_seconds = 5.0 # seconds for S-meter average avg_seconds = 1.0 self.smeter_db_count += 1 # count for average x = QS.get_smeter() self.smeter_db_sum += x # sum for average if self.timer - self.smeter_db_time0 > avg_seconds: # average time reached self.smeter_db = self.smeter_db_sum / self.smeter_db_count self.smeter_db_count = self.smeter_db_sum = 0 self.smeter_db_time0 = self.timer if self.smeter_sunits < x: # S-meter moves to peak value self.smeter_sunits = x else: # S-meter decays at this time constant self.smeter_sunits -= (self.smeter_sunits - x) * (self.timer - self.smeter_sunits_time0) self.smeter_sunits_time0 = self.timer s = self.smeter_sunits / 6.0 # change to S units; 6db per S unit s += Hardware.correct_smeter # S-meter correction for the gain, band, etc. if s >= 9.5: s = (s - 9.0) * 6 t = "S9 + %.0f %.2f dB" % (s, self.smeter_db) else: t = "S %.0f %.2f dB" % (s, self.smeter_db) self.smeter.SetLabel(t) def MakeFilterButtons(self, *args): # Change the filter selections depending on the mode: CW, SSB, etc. i = 0 for b in self.filterButns.GetButtons(): b.SetLabel(str(args[i])) b.Refresh() i += 1 def MakeFilterCoef(self, rate, N, bw, center): """Make an I/Q filter with rectangular passband.""" K = bw * N / rate filtI = [] filtQ = [] pi = math.pi sin = math.sin cos = math.cos tune = 2. * pi * center / rate for k in range(-N/2, N/2 + 1): # Make a lowpass filter if k == 0: z = float(K) / N else: z = 1.0 / N * sin(pi * k * K / N) / sin(pi * k / N) # Apply a windowing function if 1: # Blackman window w = 0.42 + 0.5 * cos(2. * pi * k / N) + 0.08 * cos(4. * pi * k / N) elif 0: # Hamming w = 0.54 + 0.46 * cos(2. * pi * k / N) elif 0: # Hanning w = 0.5 + 0.5 * cos(2. * pi * k / N) else: w = 1 z *= w # Make a bandpass filter by tuning the low pass filter to new center frequency. # Make two quadrature filters. if tune: z *= 2.0 * cmath.exp(-1j * tune * k) filtI.append(z.real) filtQ.append(z.imag) else: filtI.append(z) filtQ.append(z) return filtI, filtQ def MakeFmFilterCoef(self, rate, N, f1, f2): """Make an audio filter with FM de-emphasis; remove CTCSS tones.""" bw = f2 - f1 center = (f1 + f2) / 2 N2 = N / 2 # Half the number of points K2 = bw * N / rate / 2 # Half the bandwidth in points filtI = [] filtQ = [] passb = [0] * (N + 1) # desired passband response idft = [0] * (N + 1) # inverse DFT of desired passband pi = math.pi sin = math.sin cos = math.cos tune = 2. * pi * center / rate # indexing can be from - N2 thru + N2 inclusive; total points is 2 * N2 + 1 # indexing can be from 0 thru 2 * N2 inclusive; total points is 2 * N2 + 1 for j in range(-K2, K2 + 1): # Filter shape is -6 bB per octave jj = j + N2 freq = center - bw / 2.0 * float(j) / K2 passb[jj] = float(center) / freq * 0.3 for k in range(-N2 + 1, N2 + 1): # Take inverse DFT of passband response kk = k + N2 x = 0 + 0J for m in range(-N2, N2 + 1): mm = m + N2 if passb[mm]: x += passb[mm] * cmath.exp(1J * 2.0 * pi * m * k / N) x /= N idft[kk] = x idft[0] = idft[-1] # this value is missing for k in range(-N2, N2 + 1): kk = k + N2 z = idft[kk] # Apply a windowing function if 1: # Blackman window w = 0.42 + 0.5 * cos(2. * pi * k / N) + 0.08 * cos(4. * pi * k / N) elif 0: # Hamming w = 0.54 + 0.46 * cos(2. * pi * k / N) elif 0: # Hanning w = 0.5 + 0.5 * cos(2. * pi * k / N) else: w = 1 z *= w # Make a bandpass filter by tuning the low pass filter to new center frequency. # Make two quadrature filters. if tune: z *= 2.0 * cmath.exp(-1j * tune * k) filtI.append(z.real) filtQ.append(z.imag) else: filtI.append(z.real) filtQ.append(z.real) return filtI, filtQ def OnBtnFilter(self, event, bw=None): if event is None: # called by application self.filterButns.SetLabel(str(bw)) else: # called by button btn = event.GetEventObject() bw = int(btn.GetLabel()) mode = self.mode if mode in ("CWL", "CWU"): N = 1000 center = max(conf.cwTone, bw/2) elif mode in ('LSB', 'USB'): N = 540 center = 300 + bw / 2 else: # AM and FM N = 140 center = 0 frate = QS.get_filter_rate() filtI, filtQ = self.MakeFilterCoef(frate, N, bw, center) QS.set_filters(filtI, filtQ, bw) if self.screen is self.filter_screen: self.screen.NewFilter() def OnBtnScreen(self, event, name=None): if event is not None: win = event.GetEventObject() name = win.GetLabel() self.screen.Hide() if name == 'Config': self.screen = self.config_screen elif name == 'Graph': self.screen = self.graph self.screen.SetTxFreq(self.txFreq) self.freqDisplay.Display(self.VFO + self.txFreq) elif name == 'WFall': self.screen = self.waterfall self.screen.SetTxFreq(self.txFreq) self.freqDisplay.Display(self.VFO + self.txFreq) sash = self.screen.GetSashPosition() elif name == 'Scope': if win.direction: # Another push on the same button self.scope.running = 1 - self.scope.running # Toggle run state else: # Initial push of button self.scope.running = 1 self.screen = self.scope elif name == 'RX Filter': self.screen = self.filter_screen self.screen.SetTxFreq(self.screen.txFreq) self.freqDisplay.Display(self.screen.txFreq) self.screen.NewFilter() elif name == 'Help': self.screen = self.help_screen self.screen.Show() self.vertBox.Layout() # This destroys the initialized sash position! self.sliderYs.SetValue(self.screen.y_scale) self.sliderYz.SetValue(self.screen.y_zero) if name == 'WFall': self.screen.SetSashPosition(sash) def ChangeYscale(self, event): self.screen.ChangeYscale(self.sliderYs.GetValue()) def ChangeYzero(self, event): self.screen.ChangeYzero(self.sliderYz.GetValue()) def OnBtnMute(self, event): btn = event.GetEventObject() if btn.GetValue(): QS.set_volume(0) else: QS.set_volume(self.audio_volume) def OnBtnDecimation(self, event): i = event.GetSelection() rate = Hardware.VarDecimSet(i) if rate != self.sample_rate: self.sample_rate = rate self.graph.sample_rate = rate self.waterfall.pane1.sample_rate = rate self.waterfall.pane2.sample_rate = rate self.waterfall.pane2.display.sample_rate = rate average_count = float(rate) / conf.graph_refresh / self.fft_size average_count = int(average_count + 0.5) average_count = max (1, average_count) QS.change_rate(rate, average_count) tune = self.txFreq vfo = self.VFO self.txFreq = self.VFO = -1 # demand change self.ChangeHwFrequency(tune, vfo, 'NewDecim') def ChangeVolume(self, event=None): # Caution: event can be None value = self.sliderVol.GetValue() # Simulate log taper pot x = (10.0 ** (float(value) * 0.003000434077) - 1) / 1000.0 self.audio_volume = x # audio_volume is 0 to 1.000 QS.set_volume(x) def ChangeSidetone(self, event=None): # Caution: event can be None value = self.sliderSto.GetValue() # Simulate log taper pot x = (10.0 ** (float(value) * 0.003) - 1) / 1000.0 self.sidetone_volume = x QS.set_sidetone(x, self.ritFreq, conf.keyupDelay) def OnRitScale(self, event=None): # Called when the RIT slider is moved # Caution: event can be None if self.ritButton.GetValue(): value = self.ritScale.GetValue() value = int(value) self.ritFreq = value QS.set_tune(self.txFreq + self.ritFreq, self.txFreq) QS.set_sidetone(self.sidetone_volume, self.ritFreq, conf.keyupDelay) def OnBtnRit(self, event=None): # Called when the RIT check button is pressed # Caution: event can be None if self.ritButton.GetValue(): self.ritFreq = self.ritScale.GetValue() else: self.ritFreq = 0 QS.set_tune(self.txFreq + self.ritFreq, self.txFreq) QS.set_sidetone(self.sidetone_volume, self.ritFreq, conf.keyupDelay) def SetRit(self, freq): if freq: self.ritButton.SetValue(1) else: self.ritButton.SetValue(0) self.ritScale.SetValue(freq) self.OnBtnRit() def OnBtnFDX(self, event): btn = event.GetEventObject() if btn.GetValue(): QS.set_fdx(1) else: QS.set_fdx(0) def OnBtnTest1(self, event): btn = event.GetEventObject() if btn.GetValue(): QS.add_tone(10000) else: QS.add_tone(0) def OnBtnTest2(self, event): return def OnBtnColor(self, event): if not self.color_list: clist = wx.lib.colourdb.getColourInfoList() self.color_list = [(0, clist[0][0])] self.color_index = 0 for i in range(1, len(clist)): if self.color_list[-1][1].replace(' ', '') != clist[i][0].replace(' ', ''): #if 'BLUE' in clist[i][0]: self.color_list.append((i, clist[i][0])) else: btn = event.GetEventObject() if btn.shift: del self.color_list[self.color_index] else: self.color_index += btn.direction if self.color_index >= len(self.color_list): self.color_index = 0 elif self.color_index < 0: self.color_index = len(self.color_list) -1 color = self.color_list[self.color_index][1] print self.color_index, color self.main_frame.SetBackgroundColour(color) self.main_frame.Refresh() self.screen.Refresh() def OnBtnAGC(self, event): btn = event.GetEventObject() # Set AGC: agcInUse, agcAttack, agcRelease if btn.index == 1: QS.set_agc(1, 1.0, 0.01) elif btn.index == 2: QS.set_agc(2, 1.0, 0.1) else: QS.set_agc(0, 0, 0) def OnBtnNB(self, event): pass def FreqEntry(self, event): freq = event.GetString() if not freq: return try: if '.' in freq: freq = int(float(freq) * 1E6 + 0.1) else: freq = int(freq) except ValueError: win = event.GetEventObject() win.Clear() win.AppendText("Error") else: for band, (f1, f2) in conf.BandEdge.items(): if f1 <= freq <= f2: # Change to the correct band based on frequency self.bandBtnGroup.SetLabel(band, do_cmd=True) break tune = freq % 10000 vfo = freq - tune self.ChangeHwFrequency(tune, vfo, 'FreqEntry') def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None): """Change the VFO and tuning frequencies, and notify the hardware. tune: the new tuning frequency in +- sample_rate/2; vfo: the new vfo frequency in Hertz; this is the RF frequency at zero Hz audio source: a string indicating the source or widget requesting the change; band: if source is "BtnBand", the band requested; event: for a widget, the event (used to access control/shift key state). Try to update the hardware by calling Hardware.ChangeFrequency(). The hardware will reply with the updated frequencies which may be different from those requested; use and display the returned tune and vfo. If tune or vfo is None, query the hardware for the current frequency. """ if tune is None or vfo is None: tune, vfo = Hardware.ReturnFrequency() if tune is None or vfo is None: # hardware did not change the frequency return else: tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event) tune -= vfo change = 0 if tune != self.txFreq: change = 1 self.txFreq = tune self.screen.SetTxFreq(self.txFreq) QS.set_tune(tune + self.ritFreq, tune) if vfo != self.VFO: change = 1 self.VFO = vfo self.graph.SetVFO(vfo) self.waterfall.SetVFO(vfo) if change: self.freqDisplay.Display(self.txFreq + self.VFO) def DisplayVFO(self, vfo, tune=None): """Change the frequencies internally and display the screen, but do not update the hardware. vfo: the new vfo frequency in Hertz; tune: the new tuning frequency in +- sample_rate/2. """ self.VFO = vfo self.graph.SetVFO(vfo) self.waterfall.SetVFO(vfo) if tune is not None: self.txFreq = tune self.screen.SetTxFreq(tune) self.freqDisplay.Display(self.txFreq + self.VFO) def OnBtnMode(self, event, mode=None): if event is None: # called by application self.modeButns.SetLabel(mode) else: # called by button mode = self.modeButns.GetLabel() Hardware.ChangeMode(mode) self.mode = mode if mode in ('CWL', 'CWU'): if mode == 'CWL': QS.set_rx_mode(0) self.SetRit(conf.cwTone) else: # CWU QS.set_rx_mode(1) self.SetRit(-conf.cwTone) self.MakeFilterButtons(200, 300, 400, 500, 1000, 3000) self.OnBtnFilter(None, 1000) elif mode in ('LSB', 'USB'): if mode == 'LSB': QS.set_rx_mode(2) # LSB else: QS.set_rx_mode(3) # USB self.SetRit(0) self.MakeFilterButtons(1800, 2000, 2200, 2500, 2800, 3300) self.OnBtnFilter(None, 2800) elif mode == 'AM': QS.set_rx_mode(4) self.SetRit(0) self.MakeFilterButtons(4000, 5000, 6000, 7000, 8000, 9000) self.OnBtnFilter(None, 6000) elif mode == 'FM': QS.set_rx_mode(5) self.SetRit(0) self.MakeFilterButtons(10000, 12000, 15000, 25000, 35000, 45000) self.OnBtnFilter(None, 12000) elif mode[0:3] == 'IMD': QS.set_rx_mode(10 + self.modeButns.GetSelectedButton().index) # 10, 11, 12 self.SetRit(0) self.MakeFilterButtons(1800, 2000, 2200, 2500, 2800, 3300) self.OnBtnFilter(None, 2800) elif mode == conf.add_extern_demod: # External demodulation QS.set_rx_mode(6) self.SetRit(0) self.MakeFilterButtons(10000, 12000, 15000, 25000, 35000, 45000) self.OnBtnFilter(None, 12000) def OnBtnBand(self, event): band = self.lastBand # former band in use try: f1, f2 = conf.BandEdge[band] if f1 <= self.VFO + self.txFreq <= f2: self.bandState[band] = (self.VFO, self.txFreq, self.mode) except KeyError: pass btn = event.GetEventObject() band = btn.GetLabel() # new band self.lastBand = band try: vfo, tune, mode = self.bandState[band] except KeyError: vfo, tune, mode = (0, 0, 'LSB') if band == '60': freq = vfo + tune if btn.direction: vfo = self.VFO if 5100000 < vfo < 5600000: if btn.direction > 0: # Move up for f in self.freq60: if f > vfo + self.txFreq: freq = f break else: freq = self.freq60[0] else: # move down l = list(self.freq60) l.reverse() for f in l: if f < vfo + self.txFreq: freq = f break else: freq = self.freq60[-1] half = self.sample_rate / 2 * self.graph_width / self.data_width while freq - vfo <= -half + 1000: vfo -= 10000 while freq - vfo >= +half - 5000: vfo += 10000 tune = freq - vfo elif band == 'Time': vfo, tune, mode = conf.bandTime[btn.index] self.OnBtnMode(None, mode) ampl, phase = self.GetAmplPhase() QS.set_ampl_phase(ampl, phase) self.txFreq = self.VFO = -1 # demand change self.ChangeHwFrequency(tune, vfo, 'BtnBand', band=band) Hardware.ChangeBand(band) def OnBtnDownBand(self, event): self.band_up_down = 1 self.DisplayVFO(self.VFO - 10000, self.txFreq + 10000) def OnBtnUpBand(self, event): self.band_up_down = 1 self.DisplayVFO(self.VFO + 10000, self.txFreq - 10000) def OnBtnUpDnBandDone(self, event): self.band_up_down = 0 tune = self.txFreq vfo = self.VFO self.txFreq = self.VFO = 0 # Force an update self.ChangeHwFrequency(tune, vfo, 'BtnUpDown') def GetAmplPhase(self): if self.bandAmplPhase.has_key("panadapter"): return self.bandAmplPhase["panadapter"] try: return self.bandAmplPhase[self.lastBand] except KeyError: return (0.0, 0.0) def SetAmplPhase(self, ampl, phase): if self.bandAmplPhase.has_key("panadapter"): self.bandAmplPhase["panadapter"] = (ampl, phase) else: self.bandAmplPhase[self.lastBand] = (ampl, phase) QS.set_ampl_phase(ampl, phase) def PostStartup(self): # called once after sound attempts to start self.config_screen.OnGraphData(None) # update config in case sound is not running def OnReadSound(self): # called at frequent intervals self.timer = time.time() if self.screen == self.scope: data = QS.get_graph(0) # get raw data if data: self.scope.OnGraphData(data) # Send message to draw new data return 1 # we got new scope data else: data = QS.get_graph(1) # get FFT data if data: #T('') self.NewSmeter() # update the S-meter if self.screen == self.graph: self.waterfall.OnGraphData(data) # save waterfall data self.graph.OnGraphData(data) # Send message to draw new data else: self.screen.OnGraphData(data) # Send message to draw new data #T('graph data') #application.Yield() #T('Yield') return 1 # We got new graph/scope data if QS.get_overrange(): self.clip_time0 = self.timer self.freqDisplay.Clip(1) if self.clip_time0: if self.timer - self.clip_time0 > 1.0: self.clip_time0 = 0 self.freqDisplay.Clip(0) if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks self.heart_time0 = self.timer Hardware.HeartBeat() if not self.band_up_down: self.ChangeHwFrequency(None, None) # poll for changed frequency def main(): """If quisk is installed as a package, you can run it with quisk.main().""" App() application.MainLoop() if __name__ == '__main__': main() charleston-1.0/setup.py0000644000175000017500000000305411423624124013751 0ustar tfoxtfoxfrom distutils.core import setup, Extension module1 = Extension ('chas_rx1', libraries = [':_quisk.so', 'm', 'usb'], runtime_library_dirs = ['.'], sources = ['chas_rx1.c'], include_dirs = ['.', '..'], ) setup ( name = 'charleston', version = 1.0, description = 'This module adds support for the Charleston RX-1 hardware to QUISK', long_description = """This module adds support for the Charleston Rx-1 SDR receiver. The Rx-1 was designed by Dr. John Schwacke, of MUSC, in Charleston, SC, with additional support provided by AMRAD, and WB4JFI. It consists of a Digilent Nexys2 PFGA board with a special A/D module attached. The A/D includes a preamp with gain control, a LPF (both bypassable), and a TI AFEDRI8201 DDC chip. The '8201 consists of a 12-bit A/D (with a 76.8MHz clock), a frequency converter, a CIC filter, followed by two FIR filters. The output of the '8201 is fed to the FPGA baord, which acts as a FIFO buffer and USB interface to the host computer""", author = 'Terry Fox', author_email = 'tfox@knology.net', url='www.amrad.org', license = "GPL", platforms = ['POSIX'], packages = ['charleston'], package_dir = {'charleston' : '.'}, ext_modules = [module1], classifiers = [ 'Development Status :: 6 - Mature', 'Environment :: X11 Applications', 'Intended Audience :: End Users/Desktop', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Natural Language :: English', 'Operating System :: POSIX', 'Programming Language :: Python', 'Programming Language :: C', 'Topic :: Communications :: Ham Radio', ], ) charleston-1.0/chas_rx1.h0000644000175000017500000000421311423216510014112 0ustar tfoxtfox#include #include #include #include #define DIGILENT_PRODUCT_ID 0x0005 #define DIGILENT_VENDOR_ID 0x1443 usb_dev_handle *dev; // USB device handle for opening device struct usb_device *udev; sem_t iflock; #define NEXSDR_RFPATH_BYPASS 0 #define NEXSDR_RFPATH_LPFLNA 1 #define NEXSDR_LNAGAIN_HIGH 1 #define NEXSDR_LNAGAIN_LOW 0 #define NEXSDR_LNASLOPE_POS 0 #define NEXSDR_LNASLOPE_NEG 1 #define NEXSDR_ADC_RATE 76800000 #define A8201_REG_MCLKMODE 0x00 #define A8201_REG_CIC 0x05 #define A8201_REG_FIR1 0x07 #define A8201_REG_FIR2A 0x08 #define A8201_REG_FIR2B 0x09 #define A8201_REG_INTLV 0x0A #define A8201_REG_DAC 0x0B #define A8201_REG_PGAPD 0x0C #define A8201_PWD 0x01 #define REGADR_FIFO0 0x00 #define REGADR_FIFO1 0x08 #define REGADR_CTRL 0x10 #define REGADR_SPI_CMD_LO 0x1A #define REGADR_SPI_CMD_HI 0x1B #define REGADR_SPI_DAT_LO 0x18 #define REGADR_SPI_DAT_HI 0x19 #define REGADR_SPI_SEND 0x1C #define REGADR_FIFO_OVF 0x20 #define CTRL_RFSW 0x01 #define CTRL_HILO 0x02 #define CTRL_MODE 0x04 #define CTRL_RESET 0x08 #define CTRL_SYNC 0x10 #define CTRL_OVF_RST 0x20 #define CTRL_FIFO0_RST 0x40 #define CTRL_FIFO1_RST 0x80 #define NEXSDR_BLOCK_SIZE 4096 #define NEXSDR_NUM_BUFFERS 16 #define ADC_MULT 65536 static void quisk_start_chas_rx1(void); static void quisk_stop_chas_rx1(void); /* AFEDRI8201 FIR Filter Coefficients (CIC droop compensation and LPF) */ static int fir1[32] = { // parameters for 192ks/s -5,-44,-10,54,34,-66,-76,70,140,-50,-224,-11,312,132,-383,/*-321*/-322,401,580,-324, /*-895*/-896,102,/*1238*/1239,328,-1562,-1063,1785,2307,/*-1717*/-1718, /*-4696*/-4697,/*432*/431,11355,/*17121*/17122 }; /* AFEDRI8201 FIR FIlter Coefficients (LPF) (2nd FIR, these are for 90% filter)*/ static int fir2[63] = { -4,-14,0,15,5,-15,-11,15,18,-11,-26,5,34,6,-39,-21,41,39,-36,-60,23,80,0,-95, -32,101,72,-94,-117,71,160,-28,-196,-33,216,111,-213,-200,179,292,-109,-375,0, 435,147,-459,/*-328*/-329,430,536,-329,-759,138,983,173,-1194,-657,1377,1454, -1519,/*-3082*/-3083,1608,/*10299*/10300,/*14751*/14752 }; charleston-1.0/PKG-INFO0000644000175000017500000000247211430152203013327 0ustar tfoxtfoxMetadata-Version: 1.0 Name: charleston Version: 1.0 Summary: This module adds support for the Charleston RX-1 hardware to QUISK Home-page: www.amrad.org Author: Terry Fox Author-email: tfox@knology.net License: GPL Description: This module adds support for the Charleston Rx-1 SDR receiver. The Rx-1 was designed by Dr. John Schwacke, of MUSC, in Charleston, SC, with additional support provided by AMRAD, and WB4JFI. It consists of a Digilent Nexys2 PFGA board with a special A/D module attached. The A/D includes a preamp with gain control, a LPF (both bypassable), and a TI AFEDRI8201 DDC chip. The '8201 consists of a 12-bit A/D (with a 76.8MHz clock), a frequency converter, a CIC filter, followed by two FIR filters. The output of the '8201 is fed to the FPGA baord, which acts as a FIFO buffer and USB interface to the host computer Platform: POSIX Classifier: Development Status :: 6 - Mature Classifier: Environment :: X11 Applications Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Natural Language :: English Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Programming Language :: C Classifier: Topic :: Communications :: Ham Radio