From 1e18d4b6e4dec8e6d1b2069e3994eb1ed6dab0c4 Mon Sep 17 00:00:00 2001 From: Martin Ewing Date: Sat, 17 May 2014 20:33:52 -0400 Subject: [PATCH] Improvements for RPi, new options --- fft_bench.ipy | 2 +- iq.py | 148 +++++++++++++++++++++++++-------------------- iq_af.py | 134 ++++++++++++++++++++++++++++++---------- iq_dsp.py | 11 ++-- iq_opt.py | 109 +++++++++++++++++++-------------- iq_rtl.py | 5 +- iq_wf.py | 5 +- lcd4_brightness.sh | 3 + pa.py | 2 + try.sh | 8 +++ 10 files changed, 274 insertions(+), 153 deletions(-) create mode 100755 try.sh diff --git a/fft_bench.ipy b/fft_bench.ipy index 9977295..cf63734 100755 --- a/fft_bench.ipy +++ b/fft_bench.ipy @@ -1,5 +1,5 @@ #!/usr/bin/env ipython -# FFT timing benchmarks (requires ipython package) +# FFT timing benchmarks (requires ipython and numpy packages) import math import numpy as np diff --git a/iq.py b/iq.py index 3eeaf65..da870e6 100755 --- a/iq.py +++ b/iq.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # Program iq.py - spectrum displays from quadrature sampled IF data. -# Copyright (C) 2013 Martin Ewing +# Copyright (C) 2013-2014 Martin Ewing # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -27,8 +27,14 @@ # underlying C/C++ libraries PortAudio, SDL, and rtl-sdr. # -# TO DO: -# Document sources of non-std modules +# HISTORY +# 01-04-2014 Initial release (QST article 4/2014) +# 05-17-2014 Improvements for RPi timing, etc. +# Add REV, skip, sp_max/min, v_max/min options + +# Note for directfb use (i.e. without X11/Xorg): +# User must be a member of the following Linux groups: +# adm dialout audio video input (plus user's own group, e.g., pi) import sys,time, threading, os, subprocess import pygame as pg @@ -68,10 +74,11 @@ opt = options.opt # Get option object from options module print "identification:", opt.ident print "source :", opt.source print "waterfall :", opt.waterfall +print "rev i/q :", opt.rev_iq print "sample rate :", opt.sample_rate print "size :", opt.size print "buffers :", opt.buffers -print "taking :", opt.taking +print "skipping :", opt.skip print "hamlib :", opt.hamlib print "hamlib rigtype:", opt.hamlib_rigtype print "hamlib device :", opt.hamlib_device @@ -83,7 +90,9 @@ print "hamlib intvl :", opt.hamlib_interval print "cpu load intvl:", opt.cpu_load_interval print "wf accum. :", opt.waterfall_accumulation print "wf palette :", opt.waterfall_palette -print "max queue dept:", opt.max_queue +print "sp_min, max :", opt.sp_min, opt.sp_max +print "v_min, max :", opt.v_min, opt.v_max +#print "max queue dept:", opt.max_queue print "PCM290x lagfix:", opt.lagfix if opt.lcd4: print "LCD4 brightnes:", opt.lcd4_brightness @@ -113,7 +122,6 @@ class LED(object): """ self.surface = pg.Surface((width, width)) self.wd2 = width/2 - #self.colors = colors return def get_LED_surface(self, color): @@ -132,12 +140,13 @@ class LED(object): class Graticule(object): """ Create a pygame surface with freq / power (dB) grid and units. - input: options, pg font, graticule height, width, line color, and text color + input: options, pg font, graticule height, width, line color, + and text color """ def __init__(self, opt, font, h, w, color_l, color_t): self.opt = opt - self.sp_max = -20 # default max value (dB) - self.sp_min = -120 # default min value + self.sp_max = opt.sp_max #-20 # default max value (dB) + self.sp_min = opt.sp_min #-120 # default min value self.font = font # font to use for text self.h = h # height of graph area self.w = w # width @@ -251,7 +260,7 @@ if opt.lcd4: # setup for directfb (non-X) graphics SCREEN_SIZE = (480,272) # default size for the 4" LCD (480x272) SCREEN_MODE = pg.FULLSCREEN # If we are root, we can set up LCD4 brightness. - brightness = str(min(100, max(0, opt.lcd4_brightness))) # validated string + brightness = str(min(100, max(0, opt.lcd4_brightness))) # validated string # Find path of script (same directory as iq.py) and append brightness value cmd = os.path.join( os.path.split(sys.argv[0])[0], "lcd4_brightness.sh") \ + " %s" % brightness @@ -278,7 +287,7 @@ w_middle = w_spectra/2 # mid point of spectrum x_spectra = (w_main-w_spectra) / 2.0 # x coord. of spectrum on screen h_2d = 2*SCREEN_SIZE[1]/3 if opt.waterfall \ - else SCREEN_SIZE[1] # height of 2d spectrum display + else SCREEN_SIZE[1] # height of 2d spectrum display h_2d -= 25 # compensate for LCD4 overscan? y_2d = 20. # y position of 2d disp. (screen top = 0) @@ -288,22 +297,11 @@ if opt.size > w_spectra: for n in [1024, 512, 256, 128]: if n <= w_spectra: print "*** Size was reset from %d to %d." % (opt.size, n) - opt.size = n # Force size to be 2**k (ok, but may not be best choice) + opt.size = n # Force size to be 2**k (ok, reasonable choice?) break chunk_size = opt.buffers * opt.size # No. samples per chunk (pyaudio callback) chunk_time = float(chunk_size) / opt.sample_rate -# Initialize input mode, RTL or AF -if opt.source=="rtl": # input from RTL dongle - import iq_rtl as rtl - dataIn = rtl.RTL_In(opt) -elif opt.source=='audio': # input from audio card - import iq_af as af - dataIn = af.DataInput(opt) -else: - print "unrecognized mode" - quit_all() - myDSP = dsp.DSP(opt) # Establish DSP logic # Surface for the 2d spectrum @@ -315,17 +313,17 @@ led_urun = LED(10) led_clip = LED(10) # Waterfall geometry -h_wf = SCREEN_SIZE[1]/3 # Height of waterfall (3d spectrum) +h_wf = SCREEN_SIZE[1]/3 # Height of waterfall (3d spectrum) y_wf = y_2d + h_2d # Position just below 2d surface # Surface for waterfall (3d) spectrum surf_wf = pg.Surface((w_spectra, h_wf)) -pg.display.set_caption(opt.ident) # Title for main window +pg.display.set_caption(opt.ident) # Title for main window # Establish fonts for screen text. lgfont = pg.font.SysFont('sans', 16) -lgfont_ht = lgfont.get_linesize() # text height +lgfont_ht = lgfont.get_linesize() # text height medfont = pg.font.SysFont('sans', 12) medfont_ht = medfont.get_linesize() smfont = pg.font.SysFont('mono', 9) @@ -335,9 +333,8 @@ smfont_ht = smfont.get_linesize() wf_pixel_size = (w_spectra/opt.size, h_wf/WF_LINES) # min, max dB for wf palette -v_min = -120 # lower end (dB) -v_max = -20 # higher end -nsteps = 50 # number of distinct colors +v_min, v_max = opt.v_min, opt.v_max # lower/higher end (dB) +nsteps = 50 # number of distinct colors if opt.waterfall: # Instantiate the waterfall and palette data @@ -354,7 +351,8 @@ if opt.hamlib: # Create thread for Hamlib freq. checking. # Helps to even out the loop timing, maybe. - hl_thread = threading.Thread(target=updatefreq, args = (opt.hamlib_interval, rig)) + hl_thread = threading.Thread(target=updatefreq, + args = (opt.hamlib_interval, rig)) hl_thread.daemon = True hl_thread.start() print "Hamlib thread started." @@ -369,7 +367,7 @@ print "CPU monitor thread started." # Create graticule providing 2d graph calibration. mygraticule = Graticule(opt, smfont, h_2d, w_spectra, GRAT_COLOR, GRAT_COLOR_2) -sp_min, sp_max = sp_min_def, sp_max_def = -120, -20 +sp_min, sp_max = sp_min_def, sp_max_def = opt.sp_min, opt.sp_max mygraticule.set_range(sp_min, sp_max) surf_2d_graticule = mygraticule.make() @@ -380,9 +378,23 @@ parms_msg = "Fs = %d Hz; Res. = %.1f Hz;" \ (opt.sample_rate, float(opt.sample_rate)/opt.size, opt.size, w_spectra, float(opt.size*opt.buffers)/opt.sample_rate) wparms, hparms = medfont.size(parms_msg) -parms_matter = pg.Surface((wparms, hparms) )#, flags=pg.SRCALPHA) +parms_matter = pg.Surface((wparms, hparms) ) parms_matter.blit(medfont.render(parms_msg, 1, TCOLOR2), (0,0)) +print "Update interval = %.2f ms" % float(1000*chunk_time) + +# Initialize input mode, RTL or AF +# This starts the input stream, so place it close to start of main loop. +if opt.source=="rtl": # input from RTL dongle + import iq_rtl as rtl + dataIn = rtl.RTL_In(opt) +elif opt.source=='audio': # input from audio card + import iq_af as af + mainqueueLock = af.queueLock # queue and lock only for soundcard + dataIn = af.DataInput(opt) +else: + print "unrecognized mode" + quit_all() # ** MAIN PROGRAM LOOP ** @@ -394,14 +406,14 @@ t_last_data = 0. nframe = 0 t_frame0 = time.time() led_overflow_ct = 0 -print "Update interval = %.2f ms" % float(1000*chunk_time) - +startqueue = True while True: - nframe += 1 # keep track of loops for possible bookkeeping + nframe += 1 # keep track of loop count FWIW # Each time through the main loop, we reconstruct the main screen - surf_main.fill(BGCOLOR) # Erase with background color + + surf_main.fill(BGCOLOR) # Erase with background color # Each time through this loop, we receive an audio chunk, containing # multiple buffers. The buffers have been transformed and the log power @@ -409,8 +421,6 @@ while True: # plotted in the "2d" graph area. After a number of log spectra are # displayed in the "2d" graph, a new line of the waterfall is generated. - #surf_main.blit(top_matter, (10,10)) # static operating info - # Line of text with receiver center freq. if available if opt.hamlib: msg = "%.3f kHz" % rigfreq # take current rigfreq from hamlib thread @@ -444,33 +454,35 @@ while True: surf_main.blit(medfont.render(msg, 1, BLACK, BGCOLOR), (25, y_2d-hh)) surf_main.blit(sled, (10, y_2d-hh)) - if opt.source=='rtl': # Input from RTL-SDR dongle + if opt.source=='rtl': # Input from RTL-SDR dongle iq_data_cmplx = dataIn.ReadSamples(chunk_size) + if opt.rev_iq: # reverse spectrum? + iq_data_cmplx = np.imag(iq_data_cmplx)+1j*np.real(iq_data_cmplx) time.sleep(0.05) # slow down if fast PC stats = [ 0, 0] # for now... else: # Input from audio card # In its separate thread, a chunk of audio data has accumulated. # When ready, pull log power spectrum data out of queue. - while dataIn.dataqueue.qsize() < 2: - time.sleep(0.1 * chunk_time ) - my_in_data_s = dataIn.dataqueue.get(True, 2.0) # block w/timeout - dataIn.dataqueue.task_done() + my_in_data_s = dataIn.get_queued_data() # timeout protected # Convert string of 16-bit I,Q samples to complex floating iq_local = np.fromstring(my_in_data_s,dtype=np.int16).astype('float32') - re_d = np.array(iq_local[1::2]) # right input (I) - im_d = np.array(iq_local[0::2]) # left input (Q) + re_d = np.array(iq_local[1::2]) # right input (I) + im_d = np.array(iq_local[0::2]) # left input (Q) # The PCM290x chip has 1 lag offset of R wrt L channel. Fix, if needed. if opt.lagfix: im_d = np.roll(im_d, 1) # Get some stats (max values) to monitor gain settings, etc. stats = [int(np.amax(re_d)), int(np.amax(im_d))] - iq_data_cmplx = np.array(re_d + im_d*1j) + if opt.rev_iq: # reverse spectrum? + iq_data_cmplx = np.array(im_d + re_d*1j) + else: # normal spectrum + iq_data_cmplx = np.array(re_d + im_d*1j) sp_log = myDSP.GetLogPowerSpectrum(iq_data_cmplx) - if opt.source=='rtl': # Boost rtl spectrum (arbitrary amount) - sp_log += 60 # RTL data were normalized to +/- 1. + if opt.source=='rtl': # Boost rtl spectrum (arbitrary amount) + sp_log += 60 # RTL data were normalized to +/- 1. yscale = float(h_2d)/(sp_max-sp_min) # yscale is screen units per dB # Set the 2d surface to background/graticule. @@ -499,7 +511,8 @@ while True: # This takes cpu time, so don't recompute it too often. (DSP & graphics # are still running.) info_counter = ( info_counter + 1 ) % INFO_CYCLE - if info_counter == 1: # First time through, and every INFO_CYCLE-th time thereafter. + if info_counter == 1: + # First time through, and every INFO_CYCLE-th time thereafter. # Some button labels to show at right of LCD4 window # Add labels for LCD4 buttons. place_buttons = False @@ -515,11 +528,11 @@ while True: # Info comes in 4 phases (0 - 3), cycle among them with if info_phase == 1: lines = [ "KEYBOARD CONTROLS:", - "(R) Reset display; (Q) Quit program", - "Change upper plot dB limit: (U) increase; (u) decrease", - "Change lower plot dB limit: (L) increase; (l) decrease", - "Change WF palette upper limit: (B) increase; (b) decrease", - "Change WF palette lower limit: (D) increase; (d) decrease" ] + "(R) Reset display; (Q) Quit program", + "Change upper plot dB limit: (U) increase; (u) decrease", + "Change lower plot dB limit: (L) increase; (l) decrease", + "Change WF palette upper limit: (B) increase; (b) decrease", + "Change WF palette lower limit: (D) increase; (d) decrease" ] if opt.source=='rtl' or opt.hamlib: lines.append("Change rcvr freq: (rt arrow) increase; (lt arrow) decrease") lines.append(" Use SHIFT for bigger steps") @@ -544,7 +557,7 @@ while True: wh = (0, 0) for il in lines: # Find max line width, height wh = map(max, wh, medfont.size(il)) - help_matter = pg.Surface((wh[0]+24, len(lines)*wh[1]+15) )#, flags=pg.SRCALPHA) + help_matter = pg.Surface((wh[0]+24, len(lines)*wh[1]+15) ) for ix,x in enumerate(lines): help_matter.blit(medfont.render(x, 1, TCOLOR2), (20,ix*wh[1]+15)) @@ -574,11 +587,15 @@ while True: surf_main.blit(live_surface,(20,SCREEN_SIZE[1]-60)) # Check for pygame events - keyboard, etc. + # Note: A key press is not recorded as a PyGame event if you are + # connecting via SSH. In that case, use --sp_min/max and --v_min/max + # command line options to set scales. + for event in pg.event.get(): if event.type == pg.QUIT: quit_all() elif event.type == pg.KEYDOWN: - if info_phase <= 1: # Normal operation (0) or help phase 1 (1) + if info_phase <= 1: # Normal op. (0) or help phase 1 (1) # We usually want left or right shift treated the same! shifted = event.mod & (pg.KMOD_LSHIFT | pg.KMOD_RSHIFT) if event.key == pg.K_q: @@ -624,8 +641,8 @@ while True: if opt.waterfall: v_min, v_max = mywf.reset_range() - # Note that LCD peripheral buttons are Right, Left, Up, Down arrows - # and "Enter". (Same as keyboard buttons) + # Note that LCD peripheral buttons are Right, Left, Up, Down + # arrows and "Enter". (Same as keyboard buttons) elif event.key == pg.K_RIGHT: # right arrow + freq if opt.source=='rtl': @@ -652,13 +669,13 @@ while True: elif event.key == pg.K_DOWN: print "Down" elif event.key == pg.K_RETURN: - info_phase += 1 # Jump to phase 1 or phase 2 overlay - info_counter = 0 # (next time) + info_phase += 1 # Jump to phase 1 or 2 overlay + info_counter = 0 # (next time) - # We can have an alternate set of keyboard (LCD button) responses for each - # "phase" of the on-screen help system. + # We can have an alternate set of keyboard (LCD button) responses + # for each "phase" of the on-screen help system. - elif info_phase == 2: # Listen for info phase 2 keys + elif info_phase == 2: # Listen for info phase 2 keys # Showing 2d spectrum gain/offset adjustments # Note: making graticule is moderately slow. # Do not repeat range changes too quickly! @@ -684,10 +701,10 @@ while True: surf_2d_graticule = mygraticule.make() elif event.key == pg.K_RETURN: info_phase = 3 if opt.waterfall \ - else 0 # Next is phase 3 unless no WF. + else 0 # Next is phase 3 unless no WF. info_counter = 0 - elif info_phase == 3: # Listen for info phase 3 keys + elif info_phase == 3: # Listen for info phase 3 keys # Showing waterfall pallette adjustments # Note: recalculating palette is quite slow. # Do not repeat range changes too quickly! (1 per second max?) @@ -710,7 +727,6 @@ while True: elif event.key == pg.K_RETURN: info_phase = 0 # Turn OFF overlay info_counter = 0 - # Finally, update display for user pg.display.update() diff --git a/iq_af.py b/iq_af.py index e0bd7d2..52bd822 100755 --- a/iq_af.py +++ b/iq_af.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # Program iq_af.py - manage I/Q audio from soundcard using pyaudio -# Copyright (C) 2013 Martin Ewing +# Copyright (C) 2013-2014 Martin Ewing # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,29 +21,73 @@ # Part of the iq.py program. # +# HISTORY +# 01-04-2014 Initial release (QST article) +# 05-17-2014 timing improvements, esp for Raspberry Pi, etc. +# implement 'skip' + import sys, time, threading import Queue import pyaudio as pa -# Global variables (in this module's namespace!) -# globals are required to communicate with callback thread. -led_underrun_ct = 0 # buffer underrun LED -cbcount = 0 -cbqueue = None # will be queue to transmit af data - # CALLBACK ROUTINE # pyaudio callback routine is called when in_data buffer is ready. # See pyaudio and portaudio documentation for details. # Callback may not be called at a uniform rate. -def pa_callback_iqin(in_data, f_c, time_info, status): - global cbcount, cbqueue - global led_underrun_ct +# "skip = N" means "discard every (N+1)th buffer" (N > 0) or +# "only use every (-N+1)th buffer" (N < 0) +# i.e. skip=2 -> discard every 3rd buffer; +# skip=-2 -> use every 3rd buffer. +# (skip=1 and skip=-1 have same effect!) +# skip=0 means take all data. + +# Global variables (in this module's namespace!) +# globals are required to communicate with callback thread. +led_underrun_ct = 0 # buffer underrun LED +cbcount = 0 +MAXQUEUELEN = 32 # Don't use iq-opt for this? +cbqueue = Queue.Queue(MAXQUEUELEN) # will be queue to transmit af data +cbskip_ct = 0 +queueLock = threading.Lock() # protect queue accesses +cbfirst = 1 # Skip this many buffers at start +#err_status = None +#err_time_info = None +def pa_callback_iqin(in_data, f_c, time_info, status): + global cbcount, cbqueue, cbskip, cbskip_ct + #global err_status, err_time_info + global led_underrun_ct, queueLock, cbfirst + + #err_status = status # for debugging in case of hangup + #err_time_info = time_info cbcount += 1 - if status == pa.paAbort: - led_underrun_ct = 1 # signal LED "underrun" + + if status == pa.paInputOverflow: + led_underrun_ct = 1 # signal LED "underrun" (really, overflow) + # Decide if we should skip this buffer or take it. + # First, are we dropping every Nth buffer? + if cbskip > 0: # Yes, we must check cbskip_ct + if cbskip_ct >= cbskip: + cbskip_ct = 0 + return (None, pa.paContinue) # Discard this buffer + else: + cbskip_ct += 1 # OK to process buffer + # Or, are we accepting every Nth buffer? + if cbskip < 0: + if cbskip_ct >= -cbskip: + cbskip_ct = 0 # OK to process buffer + else: + cbskip_ct += 1 + return (None, pa.paContinue) # Discard this buffer + # Having decided to take the current buffer, or cbskip==0, + # send it to main thread. + if cbfirst > 0: + cbfirst -= 1 + return (None, pa.paContinue) # Toss out first N data try: - cbqueue.put_nowait(in_data) # send to queue for iq main to pick up + queueLock.acquire() + cbqueue.put_nowait(in_data) # queue should sync with main thread + queueLock.release() except Queue.Full: print "ERROR: Internal queue is filled. Reconfigure to use less CPU." print "\n\n (Ignore remaining errors!)" @@ -52,33 +96,40 @@ def pa_callback_iqin(in_data, f_c, time_info, status): # END OF CALLBACK ROUTINE class DataInput(object): - """ Set up audio input, optionally using callback mode. + """ Set up audio input with callbacks. """ def __init__(self, opt=None): - global cbqueue - - self.opt = opt # command line options, as parsed. # Initialize pyaudio (A python mapping of PortAudio) # Consult pyaudio documentation. self.audio = pa.PyAudio() # generates lots of warnings. print + self.Restart(opt) + return + + def Restart(self, opt): # Maybe restart after error? + global cbqueue, cbskip + + cbskip = opt.skip + print # set up stereo / 48K IQ input channel. Stream will be started. - if self.opt.index < 0: # Find pyaudio's idea of default index + if opt.index < 0: # Find pyaudio's idea of default index defdevinfo = self.audio.get_default_input_device_info() - print "Default device index is %d; id='%s'"% (defdevinfo['index'], defdevinfo['name']) + print "Default device index is %d; id='%s'"% \ + (defdevinfo['index'], defdevinfo['name']) af_using_index = defdevinfo['index'] else: af_using_index = opt.index # Use user's choice of index devinfo = self.audio.get_device_info_by_index(af_using_index) - print "Using device index %d; id='%s'" % (devinfo['index'], devinfo['name']) + print "Using device index %d; id='%s'" % \ + (devinfo['index'], devinfo['name']) try: # Verify this is a supported mode. support = self.audio.is_format_supported( input_format=pa.paInt16, # 16 bit samples input_channels=2, # 2 channels - rate=self.opt.sample_rate, # typ. 48000 - input_device=af_using_index) # maybe the default device? + rate=opt.sample_rate, # typ. 48000 + input_device=af_using_index) except ValueError as e: print "ERROR self.audio.is_format_supported", e sys.exit() @@ -86,27 +137,44 @@ class DataInput(object): self.afiqstream = self.audio.open( format=pa.paInt16, # 16 bit samples channels=2, # 2 channels - rate=self.opt.sample_rate, # typ. 48000 - frames_per_buffer= self.opt.buffers*opt.size, - input_device_index=af_using_index, # maybe the default device - input=True, # being used for input, not output + rate=opt.sample_rate, # typ. 48000 + frames_per_buffer= opt.buffers * opt.size, + input_device_index=af_using_index, + input=True, # being used for input only stream_callback=pa_callback_iqin ) - - self.dataqueue = Queue.Queue(opt.max_queue) # needs to be "big enough" - cbqueue = self.dataqueue return + def get_queued_data(self): + timeout = 40 + while cbqueue.qsize() < 4: + timeout -= 1 + if timeout <= 0: + print "timeout waiting for queue to become non-empty!" + sys.exit() + time.sleep(.1) + queueLock.acquire() + data = cbqueue.get(True, 4.) # Why addnl timeout set? + queueLock.release() + return data + + def CPU_load(self): + load = self.afiqstream.get_cpu_load() + return load + + def isActive(self): + return self.afiqstream.is_active() + def Start(self): # Start pyaudio stream self.afiqstream.start_stream() - return def Stop(self): # Stop pyaudio stream self.afiqstream.stop_stream() - return - - def Terminate(self): # Stop and release all resources + + def CloseStream(self): self.afiqstream.stop_stream() self.afiqstream.close() + + def Terminate(self): # Stop and release all resources self.audio.terminate() if __name__ == '__main__': diff --git a/iq_dsp.py b/iq_dsp.py index b72567b..7515343 100755 --- a/iq_dsp.py +++ b/iq_dsp.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # Program iq_dsp.py - Compute spectrum from I/Q data. -# Copyright (C) 2013 Martin Ewing +# Copyright (C) 2013-2014 Martin Ewing # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,6 +20,9 @@ # # Part of the iq.py program. +# HISTORY +# 01-04-2014 Initial Release + import math, time import numpy as np import numpy.fft as fft @@ -41,10 +44,6 @@ class DSP(object): def GetLogPowerSpectrum(self, data): size = self.opt.size # size of FFT in I,Q samples. power_spectrum = np.zeros(size) - if self.opt.taking > 0: - nbuf_taking = min(self.opt.taking, self.opt.buffers) # if need to shuck load - else: - nbuf_taking = self.opt.buffers # faster systems # Time-domain analysis: Often we have long normal signals interrupted # by huge wide-band pulses that degrade our power spectrum average. @@ -61,7 +60,7 @@ class DSP(object): # Calculate our current threshold relative to measured median. td_threshold = self.opt.pulse * td_median nbuf_taken = 0 # Actual number of buffers accumulated - for ic in range(nbuf_taking): + for ic in range(self.opt.buffers): td_segment = data[ic*size:(ic+1)*size] td_max = np.amax(np.abs(td_segment)) # Do we have a noise pulse? if td_max < td_threshold: # No, get pwr spectrum etc. diff --git a/iq_opt.py b/iq_opt.py index eaef7a0..13fb103 100755 --- a/iq_opt.py +++ b/iq_opt.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # Program iq_opt.py - Handle program options and command line parameters. -# Copyright (C) 2013 Martin Ewing +# Copyright (C) 2013-2014 Martin Ewing # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,10 +20,16 @@ # # Part of the iq.py program. +# HISTORY +# 01-04-2014 Initial release +# 05-05-2014 Changed options + import optparse -# This module gets command-line options from the invocation of the main program, -# iq.py. +# This module handles command-line options. + +# Note options changed: +# Add "skip", "REV", remove "RPI", "taking", "max_queue" # Set up command line parser. (Use iq.py --help to see a formatted qlisting.) op = optparse.OptionParser() @@ -37,10 +43,12 @@ op.add_option("--LAGFIX", action="store_true", dest="lagfix", help="Special mode to fix PCM290x R/L offset.") op.add_option("--LCD4", action="store_true", dest="lcd4", help='Use 4" LCD instead of large screen') -op.add_option("--RPI", action="store_true", dest="device_rpi", - help="Set up some defaults for Raspberry Pi") +#op.add_option("--RPI", action="store_true", dest="device_rpi", +# help="Set up some defaults for Raspberry Pi") op.add_option("--RTL", action="store_true", dest="source_rtl", help="Set source to RTL-SDR") +op.add_option("--REV", action="store_true", dest="rev_iq", + help="Reverse I & Q to reverse spectrum display") op.add_option("--WATERFALL", action="store_true", dest="waterfall", help="Use waterfall display.") @@ -61,8 +69,9 @@ op.add_option("--index", action="store", type="int", dest="index", "selects default input device.") op.add_option("--lcd4_brightness", action="store", type="int", dest="lcd4_brightness", help="LCD4 display brightness 0 - 100") -op.add_option("--max_queue", action="store", type="int", dest="max_queue", - help="Real-time queue depth") +# Now set as constant in iq_af.py +#op.add_option("--max_queue", action="store", type="int", dest="max_queue", +# help="Real-time queue depth") op.add_option("--n_buffers", action="store", type="int", dest="buffers", help="Number of FFT buffers in 'chunk', default 12") op.add_option("--pulse_clip", action="store", type="int", dest="pulse", @@ -73,8 +82,19 @@ op.add_option("--rtl_gain", action="store", type="int", dest="rtl_gain", help="RTL_SDR gain, default 0.") op.add_option("--size", action="store", type="int", dest="size", help="size of FFT. Default is 512.") -op.add_option("--take", action="store", type="int", dest="taking", - help="No. of buffers to take per chunk, must be <= buffers.") +#op.add_option("--take", action="store", type="int", dest="taking", +# help="No. of buffers to take per chunk, must be <= buffers.") +op.add_option("--skip", action="store", type="int", dest="skip", + help="Skipping input data parameter >= 0") +op.add_option("--sp_min", action="store", type="int", dest="sp_min", + help="spectrum level, low end, dB") +op.add_option("--sp_max", action="store", type="int", dest="sp_max", + help="spectrum level, hi end, dB") +op.add_option("--v_min", action="store", type="int", dest="v_min", + help="palette level, low end, dB") +op.add_option("--v_max", action="store", type="int", dest="v_max", + help="palette level, hi end, dB") + op.add_option("--waterfall_acc", action="store", type="int", dest="waterfall_accumulation", help="No. of spectra per waterfall line") op.add_option("--waterfall_palette", action="store", type="int", dest="waterfall_palette", @@ -82,54 +102,53 @@ op.add_option("--waterfall_palette", action="store", type="int", dest="waterfall # The following are the default values which are used if not specified in the # command line. You may want to edit them to be close to your normal operating needs. +DEF_SAMPLE_RATE = 48000 op.set_defaults( - buffers = 12, # no. buffers in sample chunk (RPi-40) - cpu_load_interval = 3.0, # cycle time for CPU monitor thread - device = None, # Possibly "BBB" or "RPI" (set up appropriately) - fullscreen = False, # Use full screen mode? (if not LCD4) - hamlib = False, # Using Hamlib? T/F (RPi-False) - hamlib_device = "/dev/ttyUSB0", # Device address for Hamlib I/O - hamlib_interval = 1.0, # Wait between hamlib freq. checks (secs) - hamlib_rigtype = 229, # Elecraft K3/KX3. - index = -1, # index of audio device (-1 use default) - lagfix = False, # Fix up PCM 290x bug - lcd4 = False, # default large screen - lcd4_brightness = 75, # brightness 0 - 100 - max_queue = 30, # max depth of queue from audio callback - pulse = 10, # pulse clip threshold - rtl_frequency = 146.e6, # RTL center freq. Hz - rtl_gain = 0, # auto - sample_rate = 48000, # (stereo) frames/second (Hz) (RTL up to 2048000) - size = 384, # size of FFT --> freq. resolution (RPi-256) - source_rtl = False, # Use sound card, not RTL-SDR input - taking = -1, # 0 < taking < buffers to cut cpu load, -1=all - waterfall = False, # Using waterfall? T/F - waterfall_accumulation = 4, # No. of spectra per waterfall line - waterfall_palette = 2 # choose a waterfall color scheme - ) + buffers = 12, # no. buffers in sample chunk (RPi-40) + cpu_load_interval = 3.0, # cycle time for CPU monitor thread + fullscreen = False, # Use full screen mode? (if not LCD4) + hamlib = False, # Using Hamlib? T/F (RPi-False) + hamlib_device = "/dev/ttyUSB0", # Device address for Hamlib I/O + hamlib_interval = 1.0, # Wait between hamlib freq. checks (secs) + hamlib_rigtype = 229, # Elecraft K3/KX3. + index = -1, # index of audio device (-1 use default) + lagfix = False, # Fix up PCM 290x bug + lcd4 = False, # default large screen + lcd4_brightness = 75, # brightness 0 - 100 +# max_queue = 30, # max depth of queue from audio callback + pulse = 10, # pulse clip threshold + rev_iq = False, # Reverse I & Q + rtl_frequency = 146.e6, # RTL center freq. Hz + rtl_gain = 0, # auto + sample_rate = DEF_SAMPLE_RATE, # (stereo) frames/second (Hz) + size = 384, # size of FFT --> freq. resolution (RPi-256) + skip = 0, # if not =0, skip some input data + source_rtl = False, # Use sound card, not RTL-SDR input + sp_min =-120, # dB relative to clipping, at bottom of grid + sp_max =-20, # dB relative to clipping, at top of grid + v_min =-120, # palette starts at this level + v_max =-20, # palette ends at this level +# taking = -1, # 0 < taking < buffers to cut cpu load, -1=all + waterfall = False, # Using waterfall? T/F + waterfall_accumulation = 4, # No. of spectra per waterfall line + waterfall_palette = 2 # choose a waterfall color scheme + ) opt, args = op.parse_args() # This is an "option" that the user can't change. -opt.ident = "IQ.PY v. 0.30 de AA6E" +opt.ident = "IQ.PY v. 0.40 de AA6E" # --RTL option forces source=rtl, but normally source=audio opt.source = "rtl" if opt.source_rtl else "audio" -if opt.device_rpi: - # adjust to comfortable settings for Raspberry Pi - opt.buffers = 15 - opt.taking = 4 # reduce CPU load (to 4/15 of max.) - opt.size = 256 +# Change default Freq for RTL to an appropriate (legal) value (tnx KF3EB) +# However, do not override user's --rate setting, if present. +if opt.source_rtl and (opt.sample_rate == DEF_SAMPLE_RATE): + opt.sample_rate = 1024000 # Main module will use: options.opt to pick up this 'opt' instance. if __name__ == '__main__': print 'debug' - # Print the variables in opt. Opt is a weird thing, not a dictionary. - #print dir(opt) - for x in dir(opt): - if x[0] != "_" and x.find("read_") < 0 and x != "ensure_value": - y = eval("opt."+x) - print x, "=", y, type(y) diff --git a/iq_rtl.py b/iq_rtl.py index 4f85fb4..2f59495 100755 --- a/iq_rtl.py +++ b/iq_rtl.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # Program iq_rtl.py - Manage input from RTL_SDR dongle. -# Copyright (C) 2013 Martin Ewing +# Copyright (C) 2013-2014 Martin Ewing # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,6 +20,9 @@ # # Part of the iq.py program. +# HISTORY +# 01-04-2014 Initial release + import rtlsdr class RTL_In(object): diff --git a/iq_wf.py b/iq_wf.py index fe84a80..cf503df 100755 --- a/iq_wf.py +++ b/iq_wf.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # Program iq_wf.py - Create waterfall spectrum display. -# Copyright (C) 2013 Martin Ewing +# Copyright (C) 2013-2014 Martin Ewing # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,6 +20,9 @@ # # Part of the iq.py program. +# HISTORY +# 01-04-2014 Initial release + import pygame as pg import numpy as np import math, sys diff --git a/lcd4_brightness.sh b/lcd4_brightness.sh index 88d23af..e671320 100755 --- a/lcd4_brightness.sh +++ b/lcd4_brightness.sh @@ -1,4 +1,7 @@ #! /bin/bash + +# Applies to BeagleBone Black with LCD4 or compatible display. + # Set LCD4 brightness 0-100 from command line. # Insist on being root if [[ $EUID -ne 0 ]]; then diff --git a/pa.py b/pa.py index 45c40a1..5235fcf 100755 --- a/pa.py +++ b/pa.py @@ -4,6 +4,8 @@ # This program prints out your system's audio input configuration as seen # by pyaudio (PortAudio). +# Copyright 2013-2014 Martin Ewing + import pyaudio as pa print """First, you will receive a number of ALSA warnings about unknown PCM cards, etc. diff --git a/try.sh b/try.sh new file mode 100755 index 0000000..ff377fd --- /dev/null +++ b/try.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Audio test, Raspberry Pi, iMic soundcard, USB 1.1 ~85% cpu load +# Use 'nice -20 ...' when running at highest CPU utilization. +python iq.py --rate=48000 --size=384 --index=1 --skip=-1 --n_buffers=6 --WATERFALL --sp_min=-90 --sp_max=0 --v_min=-90 --v_max=0 + +# RTL Test, Raspberry Pi +#python iq.py --RTL --WATERFALL --rtl_gain=0 --n_buffers=12 --size=384 --REV