See Centos 5 / Fedora 9 notes, below - 11/2009.
PSKmeter.py is a display and analysis program for the PSKmeter station accessory. It is written in Python under Linux, but it should be portable to Windows or MacOS systems. It uses the Tkinter package for window management and display. If you wish to use PSKmeter.py, you will have to obtain the pyserial package for serial port I/O.
Below is the code for pskmeter025.py. This is new and improved Version 0.25. This version is more tolerant of disconnecting and reconnecting the serial port or the power supply.
[Note added 1/29/2012: Current versions of Linux typically use PulseAudio, which will require some changes in the code.]
#!/usr/bin/python
#
# PSKmeter.py communications & analysis for the PSKmeter
# See http://www.ssiserver.com/info/pskmeter/
# Copyright (c) 2004 Martin S. Ewing AA6E
# Permission granted to copy and adapt for amateur, non-commercial use.
# Developed with Python v. 2.2.3 on Fedora Linux FC1
# Requires pyserial package (pyserial.sourceforge.net)
# Thanks to WA5ZNU for pskmeter.c, which helped get this software going.
#
import string, serial, time, sys, math, os
# Want to use ossaudiodev, but not available until Python 2.3
from Tkinter import *
#
# This is the serial port for communication with the PSKmeter
#DEVICE="/dev/ttyS0"
DEVICE="/dev/ham.pskmtr"
# Set the audio card mixer to work with
#MIXER="/dev/mixer"
MIXER="/dev/mixer1"
#
IDENT="PSKmeter.py 0.25 www.aa6e.net/aa6e 4/2004"
IDENT_SHORT="PSKmeter.py 0.25 AA6E 2004"
#
#
MS = 934 # msec update interval
val = 64 * [ 0.0 ]
rects = 64 * [ 0 ] # rectangle items for data display
satmark = 0 # saturation marker
tcanvas = 0 # canvas for text
txtitm = 0 # ADC text message
adcwin = 0 # Window for ADC text
txtitem = 0
textRun = True # Run the text dump (or freeze)
sndMixer = 0 # Mixer mixer object
pcm_level = 0 # PCM output level
pcm_level_last = -1 # last level commanded by us
statusitem = 0 # status message
arcitem = 0 # Busy bee indicator widget
arcval = 0.0 # current value
trunitm = 0 # run/stop for text dump
#
# This way of interacting with the audio level settings is moderately
# expensive (launching a new process), but it's the easiest route
# until we have Python 2.3 and ossaudiodevice support.
#
def getLevel() : # Get aumix PCM setting (Linux specific!)
command = "/usr/bin/aumix -d "+MIXER+" -wq"
pp = os.popen(command,"r")
response = pp.read() # "pcm xxx, yyy,"
pp.close()
k = response.find(",")
value = int(response[4:k])
value = max(0, min(100, value) )
return value
#
def setLevel(v) : # Set sound level via aumix (Linux specific!)
global pcm_level_last
v = max(0, min(100, v) )
command = "/usr/bin/aumix -d "+MIXER+" -w"+str(v)+"\n"
pp = os.popen(command, "r")
pp.close()
return None
#
def openMixer() : # "Open" the mixer. Not much to do until ossaudio.
global pcm_level
pcm_level = getLevel()
print "Starting with output audio level = ", pcm_level
return None
#
def updateMixer() :
global out_scale, pcm_level, pcm_level_last
pcm_level = out_scale.get()
if pcm_level <> pcm_level_last : # Has our control value changed?
setLevel(pcm_level) # If so, take it and be done
pcm_level_last = pcm_level
return None
pcm_level_os = getLevel() # Allow user to set some other way
if pcm_level <> pcm_level_os : # Has the OS value changed somehow?
out_scale.set(pcm_level_os) # Set our control to his value.
pcm_level_last = pcm_level_os
return None
#
def calibrate(uncaldata):
SAT_THRESHOLD = 180 # When are we losing at high signal?
ABS_MAX_LEVEL = 188 # When we stopped measuring!
# ADC to RF volts conversion ... 189 values 0..188 ADC possibilities
# Note: this curve applies to a particular unit. Your mileage may vary.
# 3/7/04 mse
CAL = [ \
0, 7, 8, 10, 11, 13, 14, 16, 18, 19, 21, 23, 24, 26, \
27, 29, 30, 32, 33, 35, 37, 38, 40, 41, 43, 44, 46, 47, \
49, 50, 52, 53, 55, 56, 58, 59, 61, 63, 64, 66, 67, 69, \
71, 72, 73, 75, 76, 78, 79, 81, 82, 83, 85, 86, 88, 89, \
91, 92, 94, 95, 97, 98, 100, 101, 103, 104, 106, 107, 109, 110, \
112, 113, 115, 116, 118, 119, 120, 121, 123, 124, 125, 127, 128, 129, \
130, 132, 133, 134, 136, 137, 138, 140, 141, 143, 144, 146, 147, 149, \
150, 152, 153, 155, 156, 158, 160, 162, 164, 166, 168, 170, 172, 175, \
177, 179, 181, 183, 185, 187, 190, 191, 193, 194, 196, 197, 199, 200, \
202, 203, 205, 206, 208, 210, 212, 214, 216, 218, 220, 222, 224, 226, \
228, 230, 232, 235, 238, 241, 243, 246, 249, 252, 255, 257, 260, 262, \
265, 267, 270, 272, 275, 278, 281, 285, 288, 291, 295, 298, 301, 304, \
307, 310, 315, 320, 325, 330, 335, 340, 345, 348, 352, 356, 360, 363, \
366, 370, 372, 375, 377, 380, 390 ]
#
caldata = 64 * [ 0 ]
for i in range(64) :
v = ord( uncaldata[i] )
v = min(v, ABS_MAX_LEVEL) # total saturation?
caldata[i] = CAL[v]
return caldata, v >= SAT_THRESHOLD # Calibrated data, saturation flg
#
def analyze(data):
global txtitem, canvas
MAX_AMP = 380.0 # for our unit only?
p = 64 * [ 0.0 ]
vmax = max(data)
amp = "%0.2f"%(vmax/MAX_AMP)
if vmax > 0 :
for i in range(64) : val[i] = float(data[i]) / vmax
for frq in range(16) :
u = 0; v = 0
for i in range(64) :
u += val[i] * math.cos( frq * (2*math.pi/64) * i )
v += val[i] * math.sin( frq * (2*math.pi/64) * i )
p[frq] = u**2 + v**2
tpwr = 0
for frq in range(1,16): tpwr += p[frq] # Ignore D.C.
hpwr = tpwr - p[1]
if tpwr > 0 :
imd = 10 * math.log10(hpwr/tpwr)
else : imd = -99
if txtitem == 0 :
txtitem = canvas.create_text(100,155, \
anchor=CENTER, \
text="IMD = %3.1f dB, Amp = "%(imd)+amp )
else :
canvas.itemconfigure(txtitem, \
text="IMD = %3.1f dB, Amp = "%(imd)+amp )
return None
#
# Check what kind of mode we are in
# 0 - zero power (Tx off)
# 1 - power, but no zeroes (CW signal, most likely)
# 2 - one zero (sending data)
# 3 - two zeroes (data or idling)
# Returns d_mode (above) and list of two zero positions
#
def modeCheck(d): # d is calibrated integer list
Z_THRESH = 0.2 # defines relative value for zero location
s = 64 * [ False ] # threshold array
list = [0,0]
vmax = max(d)
if vmax < 10: return 0, [0.0, 0.0] # mode 0 (no power)
# How many zeroes can we find?
nz = 0
for i in range(64):
s[i] = ( float(d[i]) / vmax ) < Z_THRESH
for i in range(64): # Treat as circular buffer
if (not s[i-1]) and s[i]: nz += 1
if nz == 0: return 1, [0.0, 0.0] # mode 1 (power, no zeroes)
# find first zero
x = 0; nx = 0; j = 0
if s[0] : j -= 8 # we have a zero at phase 0, offset
for i in range(64):
if s[j] :
nx += 1; x += j
elif s[j-1] and (nx>0) : break # have finished the zero
j += 1
xz1 = float(x) / nx
xz1 = xz1 % 64 # in case the center is < 0
if nz == 1: return 2, [xz1, 0.0] # mode 2 (one zero)
# find second zero
x = 0; nx = 0; j = (int( xz1 )+8) % 64
for i in range(64):
if s[j] :
nx += 1; x += j
elif s[j-1] and (nx>0): break
j = (j+1) % 64
xz2 = float(x) / nx
xz2 = xz2 % 64
if xz1 > xz2 : # ensure xz1 < xz2
t = xz1; xz1 = xz2; xz2 = t
return 3, [xz1, xz2] # mode 3 (two zeroes)
#
# Finish off update functions
#
def finish_update():
global canvas
canvas.update_idletasks()
canvas.pack()
canvas.after(MS, update) # Schedule ourselves again
updateMixer() # Check if level setting has changed.
return None
#
# Dim an existing graph
#
def doDimGraph():
global canvas, rects
for i in range(64):
if rects[i]<>0 :
canvas.itemconfigure(rects[i],fill='#8080FF')
return None
# Draw bar graph for data
#
def doGraph(v,flag):
global canvas, rects, satmark
vmax = max(0.01, max(v) )
tpi = 2.0*math.pi
x0 = 5.0; y0 = 90.0; dx = 3.0
pd = float(dx * 64)
for i in range(64) :
x = x0 + i*dx
dy = v[i]*50/vmax
if rects[i] == 0 : # first time through?
rects[i] = canvas.create_rectangle(x,y0-dy,x+(dx-1),y0+dy, \
fill='blue', outline='')
canvas.create_line(x,y0-50*math.sin(tpi*float(x-x0)/pd), \
x+dx,y0-50*math.sin(tpi*float(x-x0+dx)/pd),fill='red',width=2)
canvas.create_line(x,y0+50*math.sin(tpi*float(x-x0)/pd), \
x+dx,y0+50*math.sin(tpi*float(x-x0+dx)/pd),fill='red',width=2)
else :
canvas.coords(rects[i],x,y0-dy,x+(dx-1),y0+dy)
canvas.itemconfigure(rects[i],fill='blue')
# Draw saturation indicator (or not)
if satmark == 0 :
smctrx = 14; smctry = 20; smrad = 5
satmark = canvas.create_oval(smctrx-smrad,smctry-smrad, \
smctrx+smrad,smctry+smrad, \
outline='black', fill='')
canvas.create_text(smctrx+2,smctry+15,text="Over", \
font=('Verdana','8'),fill="black")
if flag:
canvas.itemconfigure(satmark, fill='red')
else :
canvas.itemconfigure(satmark, fill='')
return None
#
# Start the periodic (1 second) update cycle.
# This routine called by the main graphic "canvas" after specification
#
def update():
global satmark, txtitm, tcanvas, adcwin, out_scale
global statusitem, canvas, arcitem, arcval
vrot = 64 * [ 0.0 ]
zeroes = [0.0, 0.0]
# Bump the busy bee indicator
arcval -= 90.0
canvas.itemconfigure(arcitem, start=arcval, extent=180.0)
# Interrogate the PSKmeter
ser.write("s")
uncaldata = ser.read(64)
if len(uncaldata) <> 64 :
# We may have lost power or been disconnected.
# In this case, signal user, but do nothing more.
canvas.itemconfigure(statusitem, text="Disconnected", \
fill='#B08080')
finish_update()
return None
if (tcanvas <> 0) : # Dump raw data
if textRun :
uncaldmp = ""
for i in range(0,64,2) :
uncaldmp = uncaldmp + \
"%02d %03d %03d\n"%(i,ord(uncaldata[i]),ord(uncaldata[i+1]))
tcanvas.itemconfigure(txtitm,text=uncaldmp)
tcanvas.pack()
data, isSat = calibrate (uncaldata)
if statusitem == 0 :
statusitem = canvas.create_text(100,25, \
anchor=CENTER, \
text="" )
# Four possibilities: no data, data w no zero, one zero, or two zeroes
d_mode, zeroes = modeCheck(data)
if d_mode == 0:
canvas.itemconfigure(statusitem, text="Low Signal", \
fill='red')
canvas.itemconfigure(txtitem, text="- - -")
doGraph(64 * [0.0], isSat)
finish_update()
return None
else :
canvas.itemconfigure(statusitem, text="")
if d_mode == 1: # No zeroes -- CW?
canvas.itemconfigure(statusitem, text="Holding", \
fill='black')
doDimGraph()
finish_update()
return None
if d_mode == 2: # One zero -- data/non-idle
canvas.itemconfigure(statusitem, text="Holding", \
fill='black')
doDimGraph()
finish_update()
return None
# mode 3 - that's normal idle mode.
# De-rectify signal
for i in range(zeroes[0],zeroes[1]): data[i] = -data[i]
for i in range(64) : # rotate data
vrot[i] = data[ (i+32+int(zeroes[0]))%64 ]
data = vrot
canvas.itemconfigure(statusitem, text="Running", fill='#008000')
# Make bargraph
doGraph(data, isSat)
analyze(data) # Do the math analysis.
finish_update()
return None
#
# Launch a child text window for diagnostic ADC dumps
#
def doText() :
global txtitm, adcwin, root, tcanvas, trunning, trunitm
if adcwin == 0:
textRun = True
adcwin = Toplevel(root)
adcwin.title('Diagnostic')
tcanvas = Canvas(adcwin, width=90, height=480)
trunitm = tcanvas.create_text(45,470,text="running", \
fill='blue',anchor=CENTER,font=('Verdana','8'))
txtitm = tcanvas.create_text(10,10,anchor=NW,text="")
tcanvas.pack()
mfreeze= Button(adcwin, font=('Verdana',8), width=5, \
text="Run/Stop", command=freezeText).pack(side=LEFT)
mquitb = Button(adcwin, font=('Verdana',8), width=5, \
text="Dismiss", command=closeText).pack(side=RIGHT)
adcwin.protocol("WM_DELETE_WINDOW", closeText)
return None
#
# Freeze or unfreeze the text updates
#
def freezeText() :
global textRun, tcanvas, trunitm
# It would be nice to change the button label, but Tkinter
# doesn't make it simple.
textRun = not textRun
if textRun: info = "running"
else : info = "stopped"
tcanvas.itemconfigure(trunitm,text=info)
return None
#
# Closer - shut down the text window
#
def closeText() :
global adcwin, tcanvas, txtitm, trunitm
tcanvas.destroy()
adcwin.destroy()
adcwin = 0
tcanvas = 0
txtitm = 0
trunitm = 0
return None
#
# Main entry point.
#
print IDENT # Print our own ID
root = Tk()
root.title('PSK31 monitor')
canvas = Canvas(root, width=200, height=170)
ser = serial.Serial(DEVICE,baudrate=19200,rtscts=0,timeout=0.2)
ser.flushInput()
ser.flushOutput()
# Get, check, and print the PSKmeter's ID
ser.write("v")
ver = ser.read(255)
if len(ver) == 0 :
print "No responsei from PSKmeter. Check power & cabling."
# sys.exit()
# Note: version = 2 lines, terminating in cr/lf. Allow timeout.
else : print ver[:-2]
canvas.create_text(100,6,text=IDENT_SHORT,fill="#808080", \
font=('Verdana','8'),anchor=CENTER)
arcx=185; arcy=20; arcrad=5
canvas.create_oval(arcx-arcrad,arcy-arcrad,arcx+arcrad,arcy+arcrad)
arcitem = canvas.create_arc(arcx-arcrad,arcy-arcrad,\
arcx+arcrad,arcy+arcrad,fill='green')
canvas.after(1, update) # schedule immediate update
canvas.pack()
openMixer() # sets initial pcm_level
out_scale = Scale(root, label="Soundcard PCM Output", orient=HORIZONTAL, \
troughcolor="#C0C0E0",fg="Blue", bg="#B0B0B0", length=180, to=100, \
tickinterval=20)
out_scale.set(pcm_level)
out_scale.pack()
adcb = Button(root, text="View Raw", command=doText).pack(side=LEFT)
quitb = Button(root, text="Quit", command=root.quit).pack(side=RIGHT)
root.mainloop() # Loop forever, responding to events
ser.flushInput() # until the "quit" comes along.
ser.close()
PSKmeter.py is copyrighted software. You may copy or adapt it freely for non-commercial applications. It may not be republished without permission.
Release Notes
FeaturesKnown issues and areas for development
Your comments and suggestions are welcome -- to aa6e ...at... arrl.net.
Updated: 3/14/2004, 11/18/2009; Wikified 7/6/2010
(David Ranch, Nov., 2009)
Download the ported pskmeter.py app and the currently stable versionof
pyserial module at http://aa6e.net/software/psk/
As root,
- download and install the pyserial module (for version 2.4):
python setup.py install
- install Tkinter
yum install tkinter
- install aumix (no Yum package available)
download from http://freshmeat.net/projects/aumix/
#This is a nice mixer that gives you NUMBERS for levels instead of
just the usual arbitrary ticks:
tar xivf aumix-2.8.tar.bz2
cd aumix-2.8
./configure
make
sudo make install
ln -s /usr/local/bin/aumix /usr/bin/aumix
Note: I re-used the USB to serial interface I had connected to my Yaesu
FT-950's CAT interface. To make sure that fldigi wasn't going to
conflict, I disabled the HAMLIB interface.
Configuring pskmeter.py:
- the pskmeter.py program defaults to looking for it's controlling
serial interface at /dev/ham.pskmtr. To make it work for my setup, I
did the following as root:
ln -s /dev/ttyUSB0 /dev/ham.pskmtr
- Make sure you run "chmod 666 /dev/ttyUSB0" so that the pskmeter.py
program can access the serial port
- The pskmeter.py program defaults to an alternative mixer. Edit the
pskmeter.py program file and change the MIXER setting to:
MIXER="/dev/mixer"
- Follow the PSKmeter instructions in testing the PSKmeter's serial
port but if you run minicom at 19200-8N1 and turn it on, you should see
a plain english initialization string
Now, fire up fldigi (or whatever app your using to do PSK31):
- find a clear, open frequency
- set your rig to low power
- tune up to a low SWR
- now insert the pskmeter between the rig and the antenna tuner to protect
it from reflected power
- Start up the meter with:
python pskmeter025.py
- In Fldigi, there is the TUNE button in the upper right but that
transmits SINGLE tone. Not what you want. You want to run a PSK Tx by
typing in control-t
Note on changing your soundcard's volume settings:
- setting the PCM output too low only looses resolution but
doesn't distort the signal. Setting it too high creates distortion and
the tops of the pskmeter lobes start to drop in. That's bad.
> On Mon, Nov 16, 2009 at 12:47 AM, David Ranch wrote:
>>
>> Anyway, I appreciate your work on the port. I'm working on this with Centos 5.4 (I can send you the instructions if you'd like to post it) and it's responding but it's not graphing anything within the double lobes. If I view the RAW mode, the digits from 00 to 62 do increase though they are all the same number as I increase power. Running pskmeter on windows does graph things though all bars are the same height which seems to agree with why pskmeter.py shows though without any green bars.
>>
>> Thoughts?
>>
>> --Davi
>>
>
11/18/09
Hello Martin,
It turns out that the test signal I was sending was CW and not PSK.
Upon reflection, that makes perfect sense of why all the values would be the same!
To me, adding a FAQ entries to the original PSKmeter (and #3 on your
page) would be helpful:
1. If there isn't any signal waveform but under the text view, all
fields are equal and increase as you add more Tx power, you're probably
sending a CW test signal and NOT a PSK signal
2. PSKmeter for linux won't display any signal graphics until the
expected PSK waveform is detected. For example, PSKmeter for Linux
won't display a flat CW waveform
Btw, here are my notes on how to get PSKmeter for LInux installed and
working. Discovering and fixing some of the dependances might be
overwhelming for newbies. They are easy to do and the more we can do to get/keep HAMs on Linux, the better!