kopia lustrzana https://git.code.sf.net/p/tinypythonpanadapter/code
commit
37f36cfc86
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env ipython
|
||||
# FFT timing benchmarks (requires ipython package)
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
for n in [224, 256, 257, 288, 320, 384, 448, 512, 513, 576]:
|
||||
print "Size =", n,
|
||||
%timeit np.fft.fft(np.random.random(n))
|
||||
|
|
@ -0,0 +1,719 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Program iq.py - spectrum displays from quadrature sampled IF data.
|
||||
# Copyright (C) 2013 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
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Contact the author by e-mail: aa6e@arrl.net
|
||||
#
|
||||
# Our goal is to display a zero-centered spectrum and waterfall on small
|
||||
# computers, such as the BeagleBone Black or the Raspberry Pi,
|
||||
# spanning up to +/- 48 kHz (96 kHz sampling) with input from audio card
|
||||
# or +/- 1.024 MHz from RTL dongle.
|
||||
#
|
||||
# We use pyaudio, pygame, and pyrtlsdr Python libraries, which depend on
|
||||
# underlying C/C++ libraries PortAudio, SDL, and rtl-sdr.
|
||||
#
|
||||
|
||||
# TO DO:
|
||||
# Document sources of non-std modules
|
||||
|
||||
import sys,time, threading, os, subprocess
|
||||
import pygame as pg
|
||||
import numpy as np
|
||||
import iq_dsp as dsp
|
||||
import iq_wf as wf
|
||||
import iq_opt as options
|
||||
|
||||
# Some colors in PyGame style
|
||||
BLACK = ( 0, 0, 0)
|
||||
WHITE = (255, 255, 255)
|
||||
GREEN = ( 0, 255, 0)
|
||||
BLUE = ( 0, 0, 255)
|
||||
RED = (255, 0, 0)
|
||||
YELLOW = (192, 192, 0)
|
||||
DARK_RED = (128, 0, 0)
|
||||
LITE_RED = (255, 100, 100)
|
||||
BGCOLOR = (255, 230, 200)
|
||||
BLUE_GRAY= (100, 100, 180)
|
||||
ORANGE = (255, 150, 0)
|
||||
GRAY = (192, 192, 192)
|
||||
# RGBA colors - with alpha
|
||||
TRANS_YELLOW = (255,255,0,150)
|
||||
|
||||
# Adjust for best graticule color depending on display gamma, resolution, etc.
|
||||
GRAT_COLOR = DARK_RED # Color of graticule (grid)
|
||||
GRAT_COLOR_2 = WHITE # Color of graticule text
|
||||
TRANS_OVERLAY = TRANS_YELLOW # for info overlay
|
||||
TCOLOR2 = ORANGE # text color on info screen
|
||||
|
||||
INFO_CYCLE = 8 # Display frames per help info update
|
||||
|
||||
opt = options.opt # Get option object from options module
|
||||
|
||||
|
||||
# print list of parameters to console.
|
||||
print "identification:", opt.ident
|
||||
print "source :", opt.source
|
||||
print "waterfall :", opt.waterfall
|
||||
print "sample rate :", opt.sample_rate
|
||||
print "size :", opt.size
|
||||
print "buffers :", opt.buffers
|
||||
print "taking :", opt.taking
|
||||
print "hamlib :", opt.hamlib
|
||||
print "hamlib rigtype:", opt.hamlib_rigtype
|
||||
print "hamlib device :", opt.hamlib_device
|
||||
print "rtl frequency :", opt.rtl_frequency
|
||||
print "rtl gain :", opt.rtl_gain
|
||||
print "pulse :", opt.pulse
|
||||
print "fullscreen :", opt.fullscreen
|
||||
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 "PCM290x lagfix:", opt.lagfix
|
||||
if opt.lcd4:
|
||||
print "LCD4 brightnes:", opt.lcd4_brightness
|
||||
|
||||
def quit_all():
|
||||
""" Quit pygames and close std outputs somewhat gracefully.
|
||||
Minimize console error messages.
|
||||
"""
|
||||
pg.quit()
|
||||
try:
|
||||
sys.stdout.close()
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
sys.stderr.close()
|
||||
except:
|
||||
pass
|
||||
sys.exit()
|
||||
|
||||
class LED(object):
|
||||
""" Make an LED indicator surface in pygame environment.
|
||||
Does not include title
|
||||
"""
|
||||
def __init__(self, width):
|
||||
""" width = pixels width (& height)
|
||||
colors = dictionary with color_values and PyGame Color specs
|
||||
"""
|
||||
self.surface = pg.Surface((width, width))
|
||||
self.wd2 = width/2
|
||||
#self.colors = colors
|
||||
return
|
||||
|
||||
def get_LED_surface(self, color):
|
||||
""" Set LED surface to requested color
|
||||
Return square surface ready to blit
|
||||
"""
|
||||
self.surface.fill(BGCOLOR)
|
||||
# Always make full-size black circle with no fill.
|
||||
pg.draw.circle(self.surface,BLACK,(self.wd2,self.wd2),self.wd2,2)
|
||||
if color == None:
|
||||
return self.surface
|
||||
# Make inset filled color circle.
|
||||
pg.draw.circle(self.surface,color,(self.wd2,self.wd2),self.wd2-2,0)
|
||||
return self.surface
|
||||
|
||||
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
|
||||
"""
|
||||
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.font = font # font to use for text
|
||||
self.h = h # height of graph area
|
||||
self.w = w # width
|
||||
self.color_l = color_l # color for lines
|
||||
self.color_t = color_t # color for text
|
||||
self.surface = pg.Surface((self.w, self.h))
|
||||
return
|
||||
|
||||
def make(self):
|
||||
""" Make or re-make the graticule.
|
||||
Returns pygame surface
|
||||
"""
|
||||
self.surface.fill(BLACK)
|
||||
# yscale is screen units per dB
|
||||
yscale = float(self.h)/(self.sp_max-self.sp_min)
|
||||
# Define vertical dB scale - draw line each 10 dB.
|
||||
for attn in range(self.sp_min, self.sp_max, 10):
|
||||
yattn = ((attn - self.sp_min) * yscale) + 3.
|
||||
yattnflip = self.h - yattn # screen y coord increases downward
|
||||
# Draw a single line, dark red.
|
||||
pg.draw.line(self.surface, self.color_l, (0, yattnflip),
|
||||
(self.w, yattnflip))
|
||||
# Render and blit the dB value at left, just above line
|
||||
self.surface.blit(self.font.render("%3d" % attn, 1, self.color_t),
|
||||
(5, yattnflip-12))
|
||||
|
||||
# add unit (dB) to topmost label
|
||||
ww, hh = self.font.size("%3d" % attn)
|
||||
self.surface.blit(self.font.render("dB", 1, self.color_t),
|
||||
(5+ww, yattnflip-12))
|
||||
|
||||
# Define freq. scale - draw vert. line at convenient intervals
|
||||
frq_range = float(self.opt.sample_rate)/1000. # kHz total bandwidth
|
||||
xscale = self.w/frq_range # pixels/kHz x direction
|
||||
srate2 = frq_range/2 # plus or minus kHz
|
||||
# Choose the best tick that will work with RTL or sound cards.
|
||||
for xtick_max in [ 800, 400, 200, 100, 80, 40, 20, 10 ]:
|
||||
if xtick_max < srate2:
|
||||
break
|
||||
ticks = [ -xtick_max, -xtick_max/2, 0, xtick_max/2, xtick_max ]
|
||||
for offset in ticks:
|
||||
x = offset*xscale + self.w/2
|
||||
pg.draw.line(self.surface, self.color_l, (x, 0), (x, self.h))
|
||||
fmt = "%d kHz" if offset == 0 else "%+3d"
|
||||
self.surface.blit(self.font.render(fmt % offset, 1, self.color_t),
|
||||
(x+2, 0))
|
||||
return self.surface
|
||||
|
||||
def set_range(self, sp_min, sp_max):
|
||||
""" Set desired range for vertical scale in dB, min. and max.
|
||||
0 dB is maximum theoretical response for 16 bit sampling.
|
||||
Lines are always drawn at 10 dB intervals.
|
||||
"""
|
||||
if not sp_max > sp_min:
|
||||
print "Invalid dB scale setting requested!"
|
||||
quit_all()
|
||||
self.sp_max = sp_max
|
||||
self.sp_min = sp_min
|
||||
return
|
||||
|
||||
# THREAD: Hamlib, checking Rx frequency, and changing if requested.
|
||||
if opt.hamlib:
|
||||
import Hamlib
|
||||
rigfreq_request = None
|
||||
rigfreq = 7.0e6 # something reasonable to start
|
||||
def updatefreq(interval, rig):
|
||||
""" Read/set rig frequency via Hamlib.
|
||||
Interval defines repetition time (float secs)
|
||||
Return via global variable rigfreq (float kHz)
|
||||
To be run as thread.
|
||||
(All Hamlib I/O is done through this thread.)
|
||||
"""
|
||||
global rigfreq, rigfreq_request
|
||||
rigfreq = float(rig.get_freq()) * 0.001 # freq in kHz
|
||||
while True: # forever!
|
||||
# With KX3 @ 38.4 kbs, get_freq takes 100-150 ms to complete
|
||||
# If a new vfo setting is desired, we will have rigfreq_request
|
||||
# set to the new frequency, otherwise = None.
|
||||
if rigfreq_request: # ordering of loop speeds up freq change
|
||||
if rigfreq_request != rigfreq:
|
||||
rig.set_freq(rigfreq_request*1000.)
|
||||
rigfreq_request = None
|
||||
rigfreq = float(rig.get_freq()) * 0.001 # freq in kHz
|
||||
time.sleep(interval)
|
||||
|
||||
# THREAD: CPU load checking, monitoring cpu stats.
|
||||
cpu_usage = [0., 0., 0.]
|
||||
def cpu_load(interval):
|
||||
""" Check CPU user and system time usage, along with load average.
|
||||
User & system reported as fraction of wall clock time in
|
||||
global variable cpu_usage.
|
||||
Interval defines sleep time between checks (float secs).
|
||||
To be run as thread.
|
||||
"""
|
||||
global cpu_usage
|
||||
times_store = np.array(os.times())
|
||||
# Will return: fraction usr time, sys time, and 1-minute load average
|
||||
cpu_usage = [0., 0., os.getloadavg()[0]]
|
||||
while True:
|
||||
time.sleep(interval)
|
||||
times = np.array(os.times())
|
||||
dtimes = times - times_store # difference since last loop
|
||||
usr = dtimes[0]/dtimes[4] # fraction, 0 - 1
|
||||
sys = dtimes[1]/dtimes[4]
|
||||
times_store = times
|
||||
cpu_usage = [usr, sys, os.getloadavg()[0]]
|
||||
|
||||
# Screen setup parameters
|
||||
|
||||
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
|
||||
# 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
|
||||
# (The subprocess script is a no-op if we are not root.)
|
||||
subprocess.call(cmd, shell=True) # invoke shell script
|
||||
else:
|
||||
SCREEN_MODE = pg.FULLSCREEN if opt.fullscreen else 0
|
||||
SCREEN_SIZE = (640, 512) if opt.waterfall \
|
||||
else (640,310) # NB: graphics may not scale well
|
||||
WF_LINES = 50 # How many lines to use in the waterfall
|
||||
|
||||
# Initialize pygame (pg)
|
||||
# We should not use pg.init(), because we don't want pg audio functions.
|
||||
pg.display.init()
|
||||
pg.font.init()
|
||||
|
||||
# Define the main window surface
|
||||
surf_main = pg.display.set_mode(SCREEN_SIZE, SCREEN_MODE)
|
||||
w_main = surf_main.get_width()
|
||||
|
||||
# derived parameters
|
||||
w_spectra = w_main-10 # Allow a small margin, left and right
|
||||
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
|
||||
h_2d -= 25 # compensate for LCD4 overscan?
|
||||
y_2d = 20. # y position of 2d disp. (screen top = 0)
|
||||
|
||||
# NB: transform size must be <= w_spectra. I.e., need at least one
|
||||
# pixel of width per data point. Otherwise, waterfall won't work, etc.
|
||||
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)
|
||||
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
|
||||
surf_2d = pg.Surface((w_spectra, h_2d)) # Initialized to black
|
||||
surf_2d_graticule = pg.Surface((w_spectra, h_2d)) # to hold fixed graticule
|
||||
|
||||
# define two LED widgets
|
||||
led_urun = LED(10)
|
||||
led_clip = LED(10)
|
||||
|
||||
# Waterfall geometry
|
||||
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
|
||||
|
||||
# Establish fonts for screen text.
|
||||
lgfont = pg.font.SysFont('sans', 16)
|
||||
lgfont_ht = lgfont.get_linesize() # text height
|
||||
medfont = pg.font.SysFont('sans', 12)
|
||||
medfont_ht = medfont.get_linesize()
|
||||
smfont = pg.font.SysFont('mono', 9)
|
||||
smfont_ht = smfont.get_linesize()
|
||||
|
||||
# Define the size of a unit pixel in the waterfall
|
||||
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
|
||||
|
||||
if opt.waterfall:
|
||||
# Instantiate the waterfall and palette data
|
||||
mywf = wf.Wf(opt, v_min, v_max, nsteps, wf_pixel_size)
|
||||
|
||||
if opt.hamlib:
|
||||
import Hamlib
|
||||
# start up Hamlib rig connection
|
||||
Hamlib.rig_set_debug (Hamlib.RIG_DEBUG_NONE)
|
||||
rig = Hamlib.Rig(opt.hamlib_rigtype)
|
||||
rig.set_conf ("rig_pathname",opt.hamlib_device)
|
||||
rig.set_conf ("retry","5")
|
||||
rig.open ()
|
||||
|
||||
# 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.daemon = True
|
||||
hl_thread.start()
|
||||
print "Hamlib thread started."
|
||||
else:
|
||||
print "Hamlib not requested."
|
||||
|
||||
# Create thread for cpu load monitor
|
||||
lm_thread = threading.Thread(target=cpu_load, args = (opt.cpu_load_interval,))
|
||||
lm_thread.daemon = True
|
||||
lm_thread.start()
|
||||
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
|
||||
mygraticule.set_range(sp_min, sp_max)
|
||||
surf_2d_graticule = mygraticule.make()
|
||||
|
||||
# Pre-formatx "static" text items to save time in real-time loop
|
||||
# Useful operating parameters
|
||||
parms_msg = "Fs = %d Hz; Res. = %.1f Hz;" \
|
||||
" chans = %d; width = %d px; acc = %.3f sec" % \
|
||||
(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.blit(medfont.render(parms_msg, 1, TCOLOR2), (0,0))
|
||||
|
||||
|
||||
# ** MAIN PROGRAM LOOP **
|
||||
|
||||
run_flag = True # set false to suspend for help screen etc.
|
||||
info_phase = 0 # > 0 --> show info overlay
|
||||
info_counter = 0
|
||||
tloop = 0.
|
||||
t_last_data = 0.
|
||||
nframe = 0
|
||||
t_frame0 = time.time()
|
||||
led_overflow_ct = 0
|
||||
print "Update interval = %.2f ms" % float(1000*chunk_time)
|
||||
|
||||
while True:
|
||||
|
||||
nframe += 1 # keep track of loops for possible bookkeeping
|
||||
|
||||
# Each time through the main loop, we reconstruct the main screen
|
||||
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
|
||||
# spectra from each buffer will be provided in sp_log, which will be
|
||||
# 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
|
||||
elif opt.source=='rtl':
|
||||
msg = "%.3f MHz" % (dataIn.rtl.get_center_freq()/1.e6)
|
||||
if opt.hamlib or (opt.source=='rtl'):
|
||||
# Center it and blit just above 2d display
|
||||
ww, hh = lgfont.size(msg)
|
||||
surf_main.blit(lgfont.render(msg, 1, BLACK, BGCOLOR),
|
||||
(w_middle + x_spectra - ww/2, y_2d-hh))
|
||||
|
||||
# show overflow & underrun indicators (for audio, not rtl)
|
||||
if opt.source=='audio':
|
||||
if af.led_underrun_ct > 0: # underflow flag in af module
|
||||
sled = led_urun.get_LED_surface(RED)
|
||||
af.led_underrun_ct -= 1 # count down to extinguish
|
||||
else:
|
||||
sled = led_urun.get_LED_surface(None) #off!
|
||||
msg = "Buffer underrun"
|
||||
ww, hh = medfont.size(msg)
|
||||
ww1 = SCREEN_SIZE[0]-ww-10
|
||||
surf_main.blit(medfont.render(msg, 1, BLACK, BGCOLOR), (ww1, y_2d-hh))
|
||||
surf_main.blit(sled, (ww1-15, y_2d-hh))
|
||||
if myDSP.led_clip_ct > 0: # overflow flag
|
||||
sled = led_clip.get_LED_surface(RED)
|
||||
myDSP.led_clip_ct -= 1
|
||||
else:
|
||||
sled = led_clip.get_LED_surface(None) #off!
|
||||
msg = "Pulse clip"
|
||||
ww, hh = medfont.size(msg)
|
||||
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
|
||||
iq_data_cmplx = dataIn.ReadSamples(chunk_size)
|
||||
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()
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
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.
|
||||
|
||||
yscale = float(h_2d)/(sp_max-sp_min) # yscale is screen units per dB
|
||||
# Set the 2d surface to background/graticule.
|
||||
surf_2d.blit(surf_2d_graticule, (0, 0))
|
||||
|
||||
# Draw the "2d" spectrum graph
|
||||
sp_scaled = ((sp_log - sp_min) * yscale) + 3.
|
||||
ylist = list(sp_scaled)
|
||||
ylist = [ h_2d - x for x in ylist ] # flip the y's
|
||||
lylist = len(ylist)
|
||||
xlist = [ x* w_spectra/lylist for x in xrange(lylist) ]
|
||||
# Draw the spectrum based on our data lists.
|
||||
pg.draw.lines(surf_2d, WHITE, False, zip(xlist,ylist), 1)
|
||||
|
||||
# Place 2d spectrum on main surface
|
||||
surf_main.blit(surf_2d, (x_spectra, y_2d))
|
||||
|
||||
if opt.waterfall:
|
||||
# Calculate the new Waterfall line and blit it to main surface
|
||||
nsum = opt.waterfall_accumulation # 2d spectra per wf line
|
||||
mywf.calculate(sp_log, nsum, surf_wf)
|
||||
surf_main.blit(surf_wf, (x_spectra, y_wf+1))
|
||||
|
||||
if info_phase > 0:
|
||||
# Assemble and show semi-transparent overlay info screen
|
||||
# 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.
|
||||
# Some button labels to show at right of LCD4 window
|
||||
# Add labels for LCD4 buttons.
|
||||
place_buttons = False
|
||||
if opt.lcd4 or (w_main==480):
|
||||
place_buttons = True
|
||||
button_names = [ " LT", " RT ", " UP", " DN", "ENT" ]
|
||||
button_vloc = [ 20, 70, 120, 170, 220 ]
|
||||
button_surfs = []
|
||||
for bb in button_names:
|
||||
button_surfs.append(medfont.render(bb, 1, WHITE, BLACK))
|
||||
|
||||
# Help info will be placed toward top of window.
|
||||
# Info comes in 4 phases (0 - 3), cycle among them with <return>
|
||||
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" ]
|
||||
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")
|
||||
lines.append("RETURN - Cycle to next Help screen")
|
||||
elif info_phase == 2:
|
||||
lines = [ "SPECTRUM ADJUSTMENTS:",
|
||||
"UP - upper screen level +10 dB",
|
||||
"DOWN - upper screen level -10 dB",
|
||||
"RIGHT - lower screen level +10 dB",
|
||||
"LEFT - lower screen level -10 dB",
|
||||
"RETURN - Cycle to next Help screen" ]
|
||||
elif info_phase == 3:
|
||||
lines = [ "WATERFALL PALETTE ADJUSTMENTS:",
|
||||
"UP - upper threshold INCREASE",
|
||||
"DOWN - upper threshold DECREASE",
|
||||
"RIGHT - lower threshold INCREASE",
|
||||
"LEFT - lower threshold DECREASE",
|
||||
"RETURN - Cycle Help screen OFF" ]
|
||||
else:
|
||||
lines = [ "Invalid info phase!"] # we should never arrive here.
|
||||
info_phase = 0
|
||||
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)
|
||||
for ix,x in enumerate(lines):
|
||||
help_matter.blit(medfont.render(x, 1, TCOLOR2), (20,ix*wh[1]+15))
|
||||
|
||||
# "Live" info is placed toward bottom of window...
|
||||
# Width of this surface is a guess. (It should be computed.)
|
||||
live_surface = pg.Surface((430,48), 0)
|
||||
# give live sp_min, sp_max, v_min, v_max
|
||||
msg = "dB scale min= %d, max= %d" % (sp_min, sp_max)
|
||||
live_surface.blit(medfont.render(msg, 1, TCOLOR2), (10,0))
|
||||
if opt.waterfall:
|
||||
# Palette adjustments info
|
||||
msg = "WF palette min= %d, max= %d" % (v_min, v_max)
|
||||
live_surface.blit(medfont.render(msg, 1, TCOLOR2), (200, 0))
|
||||
live_surface.blit(parms_matter, (10,16))
|
||||
if opt.source=='audio':
|
||||
msg = "ADC max I:%05d; Q:%05d" % (stats[0], stats[1])
|
||||
live_surface.blit(medfont.render(msg, 1, TCOLOR2), (10, 32))
|
||||
# Show the live cpu load information from cpu_usage thread.
|
||||
msg = "Load usr=%3.2f; sys=%3.2f; load avg=%.2f" % \
|
||||
(cpu_usage[0], cpu_usage[1], cpu_usage[2])
|
||||
live_surface.blit(medfont.render(msg, 1, TCOLOR2), (200, 32))
|
||||
# Blit newly formatted -- or old -- screen to main surface.
|
||||
if place_buttons: # Do we have rt hand buttons to place?
|
||||
for ix, bb in enumerate(button_surfs):
|
||||
surf_main.blit(bb, (449, button_vloc[ix]))
|
||||
surf_main.blit(help_matter, (20,20))
|
||||
surf_main.blit(live_surface,(20,SCREEN_SIZE[1]-60))
|
||||
|
||||
# Check for pygame events - keyboard, etc.
|
||||
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)
|
||||
# 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:
|
||||
quit_all()
|
||||
elif event.key == pg.K_u: # 'u' or 'U' - chg upper dB
|
||||
if shifted: # 'U' move up
|
||||
if sp_max < 0:
|
||||
sp_max += 10
|
||||
else: # 'u' move dn
|
||||
if sp_max > -130 and sp_max > sp_min + 10:
|
||||
sp_max -= 10
|
||||
mygraticule.set_range(sp_min, sp_max)
|
||||
surf_2d_graticule = mygraticule.make()
|
||||
elif event.key == pg.K_l: # 'l' or 'L' - chg lower dB
|
||||
if shifted: # 'L' move up lower dB
|
||||
if sp_min < sp_max -10:
|
||||
sp_min += 10
|
||||
else: # 'l' move down lower dB
|
||||
if sp_min > -140:
|
||||
sp_min -= 10
|
||||
mygraticule.set_range(sp_min, sp_max)
|
||||
surf_2d_graticule = mygraticule.make()
|
||||
elif event.key == pg.K_b: # 'b' or 'B' - chg upper pal.
|
||||
if shifted:
|
||||
if v_max < -10:
|
||||
v_max += 10
|
||||
else:
|
||||
if v_max > v_min + 20:
|
||||
v_max -= 10
|
||||
mywf.set_range(v_min,v_max)
|
||||
elif event.key == pg.K_d: # 'd' or 'D' - chg lower pal.
|
||||
if shifted:
|
||||
if v_min < v_max - 20:
|
||||
v_min += 10
|
||||
else:
|
||||
if v_min > -130:
|
||||
v_min -= 10
|
||||
mywf.set_range(v_min,v_max)
|
||||
elif event.key == pg.K_r: # 'r' or 'R' = reset levels
|
||||
sp_min, sp_max = sp_min_def, sp_max_def
|
||||
mygraticule.set_range(sp_min, sp_max)
|
||||
surf_2d_graticule = mygraticule.make()
|
||||
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)
|
||||
|
||||
elif event.key == pg.K_RIGHT: # right arrow + freq
|
||||
if opt.source=='rtl':
|
||||
finc = 100e3 if shifted else 10e3
|
||||
dataIn.rtl.center_freq = dataIn.rtl.get_center_freq()+finc
|
||||
else: # audio mode
|
||||
if opt.hamlib:
|
||||
finc = 1.0 if shifted else 0.1
|
||||
rigfreq_request = rigfreq + finc
|
||||
else:
|
||||
print "Rt arrow ignored, no Hamlib"
|
||||
elif event.key == pg.K_LEFT: # left arrow - freq
|
||||
if opt.source=='rtl':
|
||||
finc = -100e3 if shifted else -10e3
|
||||
dataIn.rtl.center_freq = dataIn.rtl.get_center_freq()+finc
|
||||
else: # audio mode
|
||||
if opt.hamlib:
|
||||
finc = -1.0 if shifted else -0.1
|
||||
rigfreq_request = rigfreq + finc
|
||||
else:
|
||||
print "Lt arrow ignored, no Hamlib"
|
||||
elif event.key == pg.K_UP:
|
||||
print "Up"
|
||||
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)
|
||||
|
||||
# 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
|
||||
# Showing 2d spectrum gain/offset adjustments
|
||||
# Note: making graticule is moderately slow.
|
||||
# Do not repeat range changes too quickly!
|
||||
if event.key == pg.K_UP:
|
||||
if sp_max < 0:
|
||||
sp_max += 10
|
||||
mygraticule.set_range(sp_min, sp_max)
|
||||
surf_2d_graticule = mygraticule.make()
|
||||
elif event.key == pg.K_DOWN:
|
||||
if sp_max > -130 and sp_max > sp_min + 10:
|
||||
sp_max -= 10
|
||||
mygraticule.set_range(sp_min, sp_max)
|
||||
surf_2d_graticule = mygraticule.make()
|
||||
elif event.key == pg.K_RIGHT:
|
||||
if sp_min < sp_max -10:
|
||||
sp_min += 10
|
||||
mygraticule.set_range(sp_min, sp_max)
|
||||
surf_2d_graticule = mygraticule.make()
|
||||
elif event.key == pg.K_LEFT:
|
||||
if sp_min > -140:
|
||||
sp_min -= 10
|
||||
mygraticule.set_range(sp_min, sp_max)
|
||||
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.
|
||||
info_counter = 0
|
||||
|
||||
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?)
|
||||
if event.key == pg.K_UP:
|
||||
if v_max < -10:
|
||||
v_max += 10
|
||||
mywf.set_range(v_min,v_max)
|
||||
elif event.key == pg.K_DOWN:
|
||||
if v_max > v_min + 20:
|
||||
v_max -= 10
|
||||
mywf.set_range(v_min,v_max)
|
||||
elif event.key == pg.K_RIGHT:
|
||||
if v_min < v_max - 20:
|
||||
v_min += 10
|
||||
mywf.set_range(v_min,v_max)
|
||||
elif event.key == pg.K_LEFT:
|
||||
if v_min > -130:
|
||||
v_min -= 10
|
||||
mywf.set_range(v_min,v_max)
|
||||
elif event.key == pg.K_RETURN:
|
||||
info_phase = 0 # Turn OFF overlay
|
||||
info_counter = 0
|
||||
|
||||
# Finally, update display for user
|
||||
pg.display.update()
|
||||
|
||||
# End of main loop
|
||||
|
||||
# END OF IQ.PY
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
# start iq on BeagleBone Black with SB1240 sound card (typical)
|
||||
|
||||
nice -20 ./iq/iq.py -i 1 --hamlib -z 256 -b 14 --waterfall
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Program iq_af.py - manage I/Q audio from soundcard using pyaudio
|
||||
# Copyright (C) 2013 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
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Contact the author by e-mail: aa6e@arrl.net
|
||||
#
|
||||
# Part of the iq.py program.
|
||||
#
|
||||
|
||||
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
|
||||
|
||||
cbcount += 1
|
||||
if status == pa.paAbort:
|
||||
led_underrun_ct = 1 # signal LED "underrun"
|
||||
try:
|
||||
cbqueue.put_nowait(in_data) # send to queue for iq main to pick up
|
||||
except Queue.Full:
|
||||
print "ERROR: Internal queue is filled. Reconfigure to use less CPU."
|
||||
print "\n\n (Ignore remaining errors!)"
|
||||
sys.exit()
|
||||
return (None, pa.paContinue) # Return to pyaudio. All OK.
|
||||
# END OF CALLBACK ROUTINE
|
||||
|
||||
class DataInput(object):
|
||||
""" Set up audio input, optionally using callback mode.
|
||||
"""
|
||||
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
|
||||
# set up stereo / 48K IQ input channel. Stream will be started.
|
||||
if self.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'])
|
||||
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'])
|
||||
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?
|
||||
except ValueError as e:
|
||||
print "ERROR self.audio.is_format_supported", e
|
||||
sys.exit()
|
||||
print "Requested audio mode is supported:", support
|
||||
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
|
||||
stream_callback=pa_callback_iqin )
|
||||
|
||||
self.dataqueue = Queue.Queue(opt.max_queue) # needs to be "big enough"
|
||||
cbqueue = self.dataqueue
|
||||
return
|
||||
|
||||
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
|
||||
self.afiqstream.stop_stream()
|
||||
self.afiqstream.close()
|
||||
self.audio.terminate()
|
||||
|
||||
if __name__ == '__main__':
|
||||
print 'debug' # Insert module test code below
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Program iq_dsp.py - Compute spectrum from I/Q data.
|
||||
# Copyright (C) 2013 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
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Contact the author by e-mail: aa6e@arrl.net
|
||||
#
|
||||
# Part of the iq.py program.
|
||||
|
||||
import math, time
|
||||
import numpy as np
|
||||
import numpy.fft as fft
|
||||
|
||||
class DSP(object):
|
||||
def __init__(self, opt):
|
||||
self.opt = opt
|
||||
self.stats = list()
|
||||
# This is dB output for full scale 16bit input = max signal.
|
||||
self.db_adjust = 20. * math.log10(self.opt.size * 2**15)
|
||||
self.rejected_count = 0
|
||||
self.led_clip_ct = 0
|
||||
# Use "Hanning" window function
|
||||
self.w = np.empty(self.opt.size)
|
||||
for i in range(self.opt.size):
|
||||
self.w[i] = 0.5 * (1. - math.cos((2*math.pi*i)/(self.opt.size-1)))
|
||||
return
|
||||
|
||||
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.
|
||||
# We find the "normal" signal level, by computing the median of the
|
||||
# absolute value. We only do this for the first buffer of a chunk,
|
||||
# using the median for the remaining buffers in the chunk.
|
||||
# A "noise pulse" is a signal level greater than some threshold
|
||||
# times the median. When such a pulse is found, we skip the current
|
||||
# buffer. It would be better to blank out just the pulse, but that
|
||||
# would be more costly in CPU time.
|
||||
|
||||
# Find the median abs value of first buffer to use for this chunk.
|
||||
td_median = np.median(np.abs(data[:size]))
|
||||
# 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):
|
||||
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.
|
||||
# EXPERIMENTAL TAPER
|
||||
td_segment *= self.w
|
||||
fd_spectrum = fft.fft(td_segment)
|
||||
# Frequency-domain:
|
||||
# Rotate array to place 0 freq. in center. (It was at left.)
|
||||
fd_spectrum_rot = np.fft.fftshift(fd_spectrum)
|
||||
# Compute the real-valued squared magnitude (ie power) and
|
||||
# accumulate into pwr_acc.
|
||||
# fastest way to sum |z|**2 ??
|
||||
nbuf_taken += 1
|
||||
power_spectrum = power_spectrum + \
|
||||
np.real(fd_spectrum_rot*fd_spectrum_rot.conj())
|
||||
else: # Yes, abort buffer.
|
||||
self.rejected_count += 1
|
||||
self.led_clip_ct = 1 # flash a red light
|
||||
#if DEBUG: print "REJECT! %d" % self.rejected_count
|
||||
if nbuf_taken > 0:
|
||||
power_spectrum = power_spectrum / nbuf_taken # normalize the sum.
|
||||
else:
|
||||
power_spectrum = np.ones(size) # if no good buffers!
|
||||
# Convert to dB. Note log(0) = "-inf" in Numpy. It can happen if ADC
|
||||
# isn't working right. Numpy issues a warning.
|
||||
log_power_spectrum = 10. * np.log10(power_spectrum)
|
||||
return log_power_spectrum - self.db_adjust # max poss. signal = 0 dB
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Program iq_opt.py - Handle program options and command line parameters.
|
||||
# Copyright (C) 2013 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
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Contact the author by e-mail: aa6e@arrl.net
|
||||
#
|
||||
# Part of the iq.py program.
|
||||
|
||||
import optparse
|
||||
|
||||
# This module gets command-line options from the invocation of the main program,
|
||||
# iq.py.
|
||||
|
||||
# Set up command line parser. (Use iq.py --help to see a formatted qlisting.)
|
||||
op = optparse.OptionParser()
|
||||
|
||||
# Boolean options / modes.
|
||||
op.add_option("--FULLSCREEN", action="store_true", dest="fullscreen",
|
||||
help="Switch to full screen display.")
|
||||
op.add_option("--HAMLIB", action="store_true", dest="hamlib",
|
||||
help="use Hamlib to monitor/control rig frequency.")
|
||||
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("--RTL", action="store_true", dest="source_rtl",
|
||||
help="Set source to RTL-SDR")
|
||||
op.add_option("--WATERFALL", action="store_true", dest="waterfall",
|
||||
help="Use waterfall display.")
|
||||
|
||||
# Options with a parameter.
|
||||
op.add_option("--cpu_load_intvl", action="store", type="float", dest="cpu_load_interval",
|
||||
help="Seconds delay between CPU load calculations")
|
||||
op.add_option("--rate", action="store", type="int", dest="sample_rate",
|
||||
help="sample rate (Hz), eg 48000, 96000, or 1024000 or 2048000 (for rtl)")
|
||||
op.add_option("--hamlib_device", action="store", type="string", dest="hamlib_device",
|
||||
help="Hamlib serial port. Default /dev/ttyUSB0.")
|
||||
op.add_option("--hamlib_intvl", action="store", type="float", dest="hamlib_interval",
|
||||
help="Seconds delay between Hamlib operations")
|
||||
op.add_option("--hamlib_rig", action="store", type="int", dest="hamlib_rigtype",
|
||||
help="Hamlib rig type (int). Run 'rigctl --list' for possibilities. Default "
|
||||
"is 229 (Elecraft K3/KX3).")
|
||||
op.add_option("--index", action="store", type="int", dest="index",
|
||||
help="index of audio input card. Use pa.py to examine choices. Index -1 " \
|
||||
"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")
|
||||
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",
|
||||
help="pulse clipping threshold, default 10.")
|
||||
op.add_option("--rtl_freq", action="store", type="float", dest="rtl_frequency",
|
||||
help="Initial RTL operating frequency (float kHz)")
|
||||
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("--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",
|
||||
help="Waterfall color palette (1 or 2)")
|
||||
|
||||
# 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.
|
||||
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
|
||||
)
|
||||
|
||||
opt, args = op.parse_args()
|
||||
|
||||
# This is an "option" that the user can't change.
|
||||
opt.ident = "IQ.PY v. 0.30 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
|
||||
|
||||
# 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)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Program iq_rtl.py - Manage input from RTL_SDR dongle.
|
||||
# Copyright (C) 2013 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
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Contact the author by e-mail: aa6e@arrl.net
|
||||
#
|
||||
# Part of the iq.py program.
|
||||
|
||||
import rtlsdr
|
||||
|
||||
class RTL_In(object):
|
||||
def __init__(self, opt):
|
||||
self.opt = opt
|
||||
self.rtl = rtlsdr.RtlSdr()
|
||||
# Set up rtl-sdr dongle with options from command line.
|
||||
self.rtl.sample_rate = opt.sample_rate
|
||||
self.rtl.center_freq = opt.rtl_frequency
|
||||
self.rtl.set_gain(opt.rtl_gain)
|
||||
return
|
||||
|
||||
def ReadSamples(self,size):
|
||||
return self.rtl.read_samples(size)
|
||||
|
||||
if __name__ == '__main__':
|
||||
print "Debug"
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Program iq_wf.py - Create waterfall spectrum display.
|
||||
# Copyright (C) 2013 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
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Contact the author by e-mail: aa6e@arrl.net
|
||||
#
|
||||
# Part of the iq.py program.
|
||||
|
||||
import pygame as pg
|
||||
import numpy as np
|
||||
import math, sys
|
||||
|
||||
def palette_color(palette, val, vmin0, vmax0):
|
||||
""" translate a data value into a color according to several different
|
||||
methods. (PALETTE variable)
|
||||
input: value of data, minimum value, maximum value for transform
|
||||
return: pygame color tuple
|
||||
"""
|
||||
f = (float(val) - vmin0) / (vmax0 - vmin0) # btw 0 and 1.0
|
||||
f *= 2
|
||||
f = min(1., max(0., f))
|
||||
if palette == 1:
|
||||
g, b = 0, 0
|
||||
if f < 0.333:
|
||||
r = int(f*255*3)
|
||||
elif f < 0.666:
|
||||
r = 200
|
||||
g = int((f-.333)*255*3)
|
||||
else:
|
||||
r = 200
|
||||
g = 200
|
||||
b = int((f-.666)*255*3)
|
||||
elif palette == 2:
|
||||
bright = min (1.0, f + 0.15)
|
||||
tpi = 2 * math.pi
|
||||
r = bright * 128 *(1.0 + math.cos(tpi*f))
|
||||
g = bright * 128 *(1.0 + math.cos(tpi*f + tpi/3))
|
||||
b = bright * 128 *(1.0 + math.cos(tpi*f + 2*tpi/3))
|
||||
else:
|
||||
print "Invalid palette requested!"
|
||||
sys.exit()
|
||||
return ( max(0,min(255,r)), max(0,min(255,g)), max(0,min(255,b)) )
|
||||
|
||||
class Wf(object):
|
||||
""" Make a waterfall '3d' display of spectral power vs frequency & time.
|
||||
init: min, max palette parameter, no. of steps between min & max,
|
||||
size for each freq,time data plot 'pixel' (a box)
|
||||
"""
|
||||
def __init__(self, opt, vmin, vmax, nsteps, pxsz):
|
||||
""" Initialize data and
|
||||
pre-calculate palette & filled rect surfaces, based on vmin, vmax,
|
||||
no. of surfaces = nsteps
|
||||
"""
|
||||
self.opt = opt
|
||||
self.vmin = vmin
|
||||
self.vmin_rst = vmin
|
||||
self.vmax = vmax
|
||||
self.vmax_rst = vmax
|
||||
self.nsteps = nsteps
|
||||
self.pixel_size = pxsz
|
||||
self.firstcalc = True
|
||||
self.initialize_palette()
|
||||
|
||||
def initialize_palette(self):
|
||||
""" Set up surfaces for each possible color value in list self.pixels.
|
||||
"""
|
||||
self.pixels = list()
|
||||
for istep in range(self.nsteps):
|
||||
ps = pg.Surface(self.pixel_size)
|
||||
val = float(istep)*(self.vmax-self.vmin)/self.nsteps + self.vmin
|
||||
color = palette_color(self.opt.waterfall_palette, val, self.vmin, self.vmax)
|
||||
ps.fill( color )
|
||||
self.pixels.append(ps)
|
||||
|
||||
def set_range(self, vmin, vmax):
|
||||
""" define a new data range for palette calculation going forward.
|
||||
input: vmin, vmax
|
||||
"""
|
||||
self.vmin = vmin
|
||||
self.vmax = vmax
|
||||
self.initialize_palette()
|
||||
|
||||
def reset_range(self):
|
||||
""" reset palette data range to original settings.
|
||||
"""
|
||||
self.vmin = self.vmin_rst
|
||||
self.vmax = self.vmax_rst
|
||||
self.initialize_palette()
|
||||
return self.vmin, self.vmax
|
||||
|
||||
def calculate(self, datalist, nsum, surface): # (datalist is np.array)
|
||||
if self.firstcalc: # First time through,
|
||||
self.datasize = len(datalist) # pick up dimension of datalist
|
||||
self.wfacc = np.zeros(self.datasize) # and establish accumulator
|
||||
self.dx = float(surface.get_width()) / self.datasize # x spacing of wf cells
|
||||
# Note: self.dx must be >= 1
|
||||
self.wfcount = 0
|
||||
self.firstcalc = False
|
||||
self.wfcount += 1
|
||||
self.wfacc += datalist # Accumulate data
|
||||
if self.wfcount % nsum != 0: # Don't plot wf data until enough spectra accumulated
|
||||
return
|
||||
else:
|
||||
surface.blit(surface, (0, self.pixel_size[1])) # push old wf down one row
|
||||
for ix in xrange(self.datasize):
|
||||
v = datalist[ix] #self.wfacc[ix] / nsum #datalist[ix] # dB units
|
||||
vi = int( self.nsteps * (v-self.vmin) / (self.vmax-self.vmin) )
|
||||
vi = max(0, min(vi, self.nsteps-1) )
|
||||
px_surf = self.pixels[vi]
|
||||
x = int(ix * self.dx)
|
||||
surface.blit(px_surf, (x, 0))
|
||||
self.wfcount = 0 # Initialize counter
|
||||
self.wfacc.fill(0) # and accumulator
|
|
@ -0,0 +1,9 @@
|
|||
#! /bin/bash
|
||||
# Set LCD4 brightness 0-100 from command line.
|
||||
# Insist on being root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "Warning: can't adjust brightness - we are not root." 2>&1
|
||||
exit 1
|
||||
fi
|
||||
echo $1 > /sys/class/backlight/backlight.11/brightness
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# File: pa.py
|
||||
# This program prints out your system's audio input configuration as seen
|
||||
# by pyaudio (PortAudio).
|
||||
|
||||
import pyaudio as pa
|
||||
|
||||
print """First, you will receive a number of ALSA warnings about unknown PCM cards, etc.
|
||||
This is an annoying but harmless feature of PortAudio."""
|
||||
print
|
||||
print "-------------------------"
|
||||
x = pa.PyAudio()
|
||||
print "-------------------------"
|
||||
print
|
||||
print "API'S FOUND (TYPICALLY ALSA and OSS):"
|
||||
for i in range(x.get_host_api_count()):
|
||||
print "API %d:" % i
|
||||
print x.get_host_api_info_by_index(i)
|
||||
print
|
||||
print "DEFAULT HOST API INFO:", x.get_default_host_api_info()['name']
|
||||
print
|
||||
print "DEVICE COUNT =", x.get_device_count()
|
||||
print
|
||||
print "ALL DEVICE INFO: (For iq.py, choose one of these as 'index'.)"
|
||||
print
|
||||
for i in range(x.get_device_count()):
|
||||
di = x.get_device_info_by_index(i)
|
||||
print "DEVICE: %d; NAME: '%s'" % (i, di['name'])
|
||||
for j in ['defaultSampleRate', 'maxInputChannels', 'maxOutputChannels']:
|
||||
print j, ":", di[j]
|
||||
print
|
||||
print "DEFAULT INPUT DEVICE FULL INFO:"
|
||||
ddi = x.get_default_input_device_info()
|
||||
print ddi
|
||||
print
|
||||
print "DEFAULT INDEX =", ddi['index']
|
||||
|
Ładowanie…
Reference in New Issue