summaryrefslogtreecommitdiff
path: root/gr-wxgui
diff options
context:
space:
mode:
Diffstat (limited to 'gr-wxgui')
-rw-r--r--gr-wxgui/ChangeLog171
-rw-r--r--gr-wxgui/Makefile.am28
-rw-r--r--gr-wxgui/README1
-rw-r--r--gr-wxgui/gr-wxgui.conf7
-rw-r--r--gr-wxgui/src/Makefile.am24
-rw-r--r--gr-wxgui/src/python/Makefile.am40
-rw-r--r--gr-wxgui/src/python/__init__.py1
-rwxr-xr-xgr-wxgui/src/python/fftsink.py488
-rwxr-xr-xgr-wxgui/src/python/form.py391
-rw-r--r--gr-wxgui/src/python/plot.py1744
-rwxr-xr-xgr-wxgui/src/python/powermate.py437
-rwxr-xr-xgr-wxgui/src/python/scopesink.py650
-rwxr-xr-xgr-wxgui/src/python/slider.py49
-rw-r--r--gr-wxgui/src/python/stdgui.py90
-rwxr-xr-xgr-wxgui/src/python/waterfallsink.py469
15 files changed, 4590 insertions, 0 deletions
diff --git a/gr-wxgui/ChangeLog b/gr-wxgui/ChangeLog
new file mode 100644
index 000000000..ed4e5ad51
--- /dev/null
+++ b/gr-wxgui/ChangeLog
@@ -0,0 +1,171 @@
+2006-07-24 Eric Blossom <eb@comsec.com>
+
+ * src/python/powermate.py (powermate._open_device): added additional
+ name for ID_SHUTTLE_XPRESS per Kwan Hong Lee <kwan@media.mit.edu>
+
+2006-06-15 Eric Blossom <eb@comsec.com>
+
+ * src/python/fftsink.py, src/python/waterfallsink.py,
+ src/python/scopesink.py: added set_sample_rate method.
+
+2006-04-02 Eric Blossom <eb@comsec.com>
+
+ * src/python/fftsink.py (default_fft_rate): query prefs for default.
+ * src/python/waterfallsink.py (default_fft_rate): query prefs for default.
+ * src/python/scopesink (default_frame_decim): query prefs for default.
+
+2006-03-29 Eric Blossom <eb@comsec.com>
+
+ * src/python/fftsink.py: updated to use renamed stream_to_vector
+ instead of serial_to_parallel. Updated ref_level and y_per_div in
+ builtin test case.
+
+2006-02-02 Eric Blossom <eb@comsec.com>
+
+ * src/python/scopesink.py: now supports manual as well as
+ autoscaling of the y-axis. Thank to Jon Jacky.
+
+2005-12-08 Eric Blossom <eb@comsec.com>
+
+ * src/python/stdgui.py (stdapp.__init__): added redirect=False arg
+ to wx.App.__init__ for Mac users. Thanks to Jon Jacky.
+
+2005-11-15 Eric Blossom <eb@comsec.com>
+
+ * src/python/fftsink.py, src/python/scopesink.py: refactored to
+ use messages and message queues instead of pipes to communicate
+ with the C++ side. A side benefit is that the C++ side now will
+ not block when sending data to the gui.
+
+2005-10-25 Eric Blossom <eb@comsec.com>
+
+ * src/python/fftsink.py: added peak_hold function and menu item.
+
+2005-10-14 Eric Blossom <eb@comsec.com>
+
+ * src/python/form.py (quantized_slider_field): new field type,
+ very nice for quantized floats such as frequency, gain, etc.
+
+2005-08-28 Eric Blossom <eb@comsec.com>
+
+ * src/python/form.py: new. tools for building forms based GUIs.
+
+2005-08-15 Eric Blossom <eb@comsec.com>
+
+ * src/python/waterfallsink.py: fftshift data so it comes out as
+ expected -- -ve freqs on the left, 0 in the middle, +ve freqs on
+ right. Thanks to James Smith.
+
+2005-08-15 Krzysztof Kamieniecki <krys@kamieniecki.com>
+
+ * src/python/powermate.py: on GNU/Linux get exclusive access to knob.
+
+2005-07-02 Eric Blossom <eb@comsec.com>
+
+ * config/gr_no_undefined.m4, config/gr_x86_64.m4: new, x86_64 support.
+ * config/gr_python.m4: backed out search for libpython, making
+ x86_64 work and breaking Cygwin/MinGW.
+ * configure.ac: mods for x86_64, $(NO_UNDEFINED)
+
+2005-06-19 Eric Blossom <eb@comsec.com>
+
+ * src/python/waterfallsink.py: reworked to use latest FFT sink stuff.
+ * src/python/fftsink.py (fft_sink_f.__init__): added missing call
+ to set_average.
+
+2005-06-11 Eric Blossom <eb@comsec.com>
+
+ * src/python/fftsink.py: normalized FFT by number of points.
+
+2005-06-08 Krzysztof Kamieniecki <krys@kamieniecki.com>
+
+ * src/python/powermate.py: added support for ShuttlePRO v2.
+
+2005-05-15 Eric Blossom <eb@comsec.com>
+
+ * src/python/powermate.py: new. Support the Griffin PowerMate and
+ Countour Shuttle/Jog usb knobs. (Revised version of what I got
+ from Matt.)
+
+2005-05-11 Eric Blossom <eb@comsec.com>
+
+ * src/python/fftsink.py, src/python/scopesink.py: Use
+ gru.os_read_exactly instead of os.read to avoid problems with
+ short reads [thanks to Jon Jacky for troubleshooting].
+ Added throttle block to demo to keep it from sucking down all CPU.
+
+2005-05-09 Stephane Fillod <f8cfe@free.fr>
+
+ * config/gr_sysv_shm.m4: SysV shared memory not mandatory
+ * config/gr_pwin32.m4, config/gr_python.m4, config/lf_cxx.m4:
+ fixes for Cygwin, MinGW
+
+2005-03-16 Eric Blossom <eb@comsec.com>
+
+ * src/python/scopesink.py (graph_window.format_data): enabled legend.
+
+2005-03-13 David Carr <dc@dcarr.org>
+
+ * src/python/waterfallsink.py: New faster, in color
+
+2005-03-04 Eric Blossom <eb@comsec.com>
+
+ * src/python/slider.py: high level interface to wx.Slider
+
+2005-02-25 Eric Blossom <eb@comsec.com>
+
+ Moved everything from src/python/gnuradio/wxgui to src/python and
+ removed the unnecessary hierarchy.
+
+2004-11-15 Matt Ettus <matt@ettus.com>
+
+ * src/python/gnuradio/wxgui/waterfallsink.py: new, from David Carr <dc@dcarr.org>
+
+2004-10-13 Eric Blossom <eb@comsec.com>
+
+ * configure.ac: upped rev to 0.1cvs
+
+2004-10-11 Eric Blossom <eb@comsec.com>
+
+ * configure.ac: bumped rev to 0.1, make release
+ * Makefile.am (EXTRA_DIST): added config.h.in
+
+2004-09-23 Eric Blossom <eb@comsec.com>
+
+ * config/usrp_fusb_tech.m4, config/bnv_have_qt.m4, config/cppunit.m4,
+ config/gr_check_mc4020.m4, config/gr_check_usrp.m4, config/gr_doxygen.m4,
+ config/gr_gprof.m4, config/gr_scripting.m4, config/gr_set_md_cpu.m4,
+ config/pkg.m4, config/usrp_fusb_tech.m4: added additional quoting
+ to first arg of AC_DEFUN to silence automake warning.
+
+2004-09-19 Eric Blossom <eb@comsec.com>
+
+ * src/python/gnuradio/wxgui/stdgui.py: reworked to really subclass
+ wx.App
+
+2004-09-18 Eric Blossom <eb@comsec.com>
+
+ * src/python/gnuradio/wxgui/stdgui.py: new.
+ * src/python/gnuradio/wxgui/fftsink.py: new.
+ * src/python/gnuradio/wxgui/scopesink.py: new. Needs work
+
+#
+# Copyright 2004,2005,2006 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio 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 2, or (at your option)
+# any later version.
+#
+# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+#
diff --git a/gr-wxgui/Makefile.am b/gr-wxgui/Makefile.am
new file mode 100644
index 000000000..2a0b4b4ac
--- /dev/null
+++ b/gr-wxgui/Makefile.am
@@ -0,0 +1,28 @@
+#
+# Copyright 2004,2006 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio 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 2, or (at your option)
+# any later version.
+#
+# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+#
+
+include $(top_srcdir)/Makefile.common
+
+EXTRA_DIST = gr-wxgui.conf
+SUBDIRS = src
+
+etcdir = $(sysconfdir)/gnuradio/conf.d
+etc_DATA = gr-wxgui.conf
diff --git a/gr-wxgui/README b/gr-wxgui/README
new file mode 100644
index 000000000..ebd0d2397
--- /dev/null
+++ b/gr-wxgui/README
@@ -0,0 +1 @@
+This requires wxPython 2.5.2.7 or later. See http://www.wxpython.org
diff --git a/gr-wxgui/gr-wxgui.conf b/gr-wxgui/gr-wxgui.conf
new file mode 100644
index 000000000..f6b128c68
--- /dev/null
+++ b/gr-wxgui/gr-wxgui.conf
@@ -0,0 +1,7 @@
+# This file contains system wide configuration data for GNU Radio.
+# You may override any setting on a per-user basis by editing
+# ~/.gnuradio/config.conf
+
+[wxgui]
+fft_rate = 15 # fftsink and waterfallsink
+frame_decim = 1 # scopesink
diff --git a/gr-wxgui/src/Makefile.am b/gr-wxgui/src/Makefile.am
new file mode 100644
index 000000000..5da68b9dd
--- /dev/null
+++ b/gr-wxgui/src/Makefile.am
@@ -0,0 +1,24 @@
+#
+# Copyright 2004 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio 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 2, or (at your option)
+# any later version.
+#
+# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+#
+
+include $(top_srcdir)/Makefile.common
+
+SUBDIRS = python
diff --git a/gr-wxgui/src/python/Makefile.am b/gr-wxgui/src/python/Makefile.am
new file mode 100644
index 000000000..2d25de05b
--- /dev/null
+++ b/gr-wxgui/src/python/Makefile.am
@@ -0,0 +1,40 @@
+#
+# Copyright 2004,2005 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio 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 2, or (at your option)
+# any later version.
+#
+# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+#
+
+include $(top_srcdir)/Makefile.common
+
+# Install this stuff so that it ends up as the gnuradio.wxgui module
+# This usually ends up at:
+# ${prefix}/lib/python${python_version}/site-packages/gnuradio/wxgui
+
+ourpythondir = $(grpythondir)/wxgui
+ourlibdir = $(grpyexecdir)/wxgui
+
+ourpython_PYTHON = \
+ __init__.py \
+ form.py \
+ fftsink.py \
+ plot.py \
+ powermate.py \
+ scopesink.py \
+ waterfallsink.py \
+ slider.py \
+ stdgui.py
diff --git a/gr-wxgui/src/python/__init__.py b/gr-wxgui/src/python/__init__.py
new file mode 100644
index 000000000..027150db1
--- /dev/null
+++ b/gr-wxgui/src/python/__init__.py
@@ -0,0 +1 @@
+# make this directory a package
diff --git a/gr-wxgui/src/python/fftsink.py b/gr-wxgui/src/python/fftsink.py
new file mode 100755
index 000000000..8796a1b92
--- /dev/null
+++ b/gr-wxgui/src/python/fftsink.py
@@ -0,0 +1,488 @@
+#!/usr/bin/env python
+#
+# Copyright 2003,2004,2005,2006 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio 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 2, or (at your option)
+# any later version.
+#
+# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+#
+
+from gnuradio import gr, gru, window
+from gnuradio.wxgui import stdgui
+import wx
+import gnuradio.wxgui.plot as plot
+import Numeric
+import threading
+import math
+
+default_fftsink_size = (640,240)
+default_fft_rate = gr.prefs().get_long('wxgui', 'fft_rate', 15)
+
+class fft_sink_base(object):
+ def __init__(self, input_is_real=False, baseband_freq=0, y_per_div=10, ref_level=50,
+ sample_rate=1, fft_size=512,
+ fft_rate=default_fft_rate,
+ average=False, avg_alpha=None, title='', peak_hold=False):
+
+ # initialize common attributes
+ self.baseband_freq = baseband_freq
+ self.y_divs = 8
+ self.y_per_div=y_per_div
+ self.ref_level = ref_level
+ self.sample_rate = sample_rate
+ self.fft_size = fft_size
+ self.fft_rate = fft_rate
+ self.average = average
+ if avg_alpha is None:
+ self.avg_alpha = 2.0 / fft_rate
+ else:
+ self.avg_alpha = avg_alpha
+ self.title = title
+ self.peak_hold = peak_hold
+ self.input_is_real = input_is_real
+ self.msgq = gr.msg_queue(2) # queue that holds a maximum of 2 messages
+
+ def set_y_per_div(self, y_per_div):
+ self.y_per_div = y_per_div
+
+ def set_ref_level(self, ref_level):
+ self.ref_level = ref_level
+
+ def set_average(self, average):
+ self.average = average
+ if average:
+ self.avg.set_taps(self.avg_alpha)
+ self.set_peak_hold(False)
+ else:
+ self.avg.set_taps(1.0)
+
+ def set_peak_hold(self, enable):
+ self.peak_hold = enable
+ if enable:
+ self.set_average(False)
+ self.win.set_peak_hold(enable)
+
+ def set_avg_alpha(self, avg_alpha):
+ self.avg_alpha = avg_alpha
+
+ def set_baseband_freq(self, baseband_freq):
+ self.baseband_freq = baseband_freq
+
+ def set_sample_rate(self, sample_rate):
+ self.sample_rate = sample_rate
+ self._set_n()
+
+ def _set_n(self):
+ self.one_in_n.set_n(max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
+
+
+class fft_sink_f(gr.hier_block, fft_sink_base):
+ def __init__(self, fg, parent, baseband_freq=0,
+ y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
+ fft_rate=default_fft_rate, average=False, avg_alpha=None,
+ title='', size=default_fftsink_size, peak_hold=False):
+
+ fft_sink_base.__init__(self, input_is_real=True, baseband_freq=baseband_freq,
+ y_per_div=y_per_div, ref_level=ref_level,
+ sample_rate=sample_rate, fft_size=fft_size,
+ fft_rate=fft_rate,
+ average=average, avg_alpha=avg_alpha, title=title,
+ peak_hold=peak_hold)
+
+ s2p = gr.stream_to_vector(gr.sizeof_float, self.fft_size)
+ self.one_in_n = gr.keep_one_in_n(gr.sizeof_float * self.fft_size,
+ max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
+
+ mywindow = window.blackmanharris(self.fft_size)
+ fft = gr.fft_vfc(self.fft_size, True, mywindow)
+ power = 0
+ for tap in mywindow:
+ power += tap*tap
+
+ c2mag = gr.complex_to_mag(self.fft_size)
+ self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
+
+ # FIXME We need to add 3dB to all bins but the DC bin
+ log = gr.nlog10_ff(20, self.fft_size,
+ -20*math.log10(self.fft_size)-10*math.log10(power/self.fft_size))
+ sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
+
+ fg.connect (s2p, self.one_in_n, fft, c2mag, self.avg, log, sink)
+ gr.hier_block.__init__(self, fg, s2p, sink)
+
+ self.win = fft_window(self, parent, size=size)
+ self.set_average(self.average)
+
+
+class fft_sink_c(gr.hier_block, fft_sink_base):
+ def __init__(self, fg, parent, baseband_freq=0,
+ y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
+ fft_rate=default_fft_rate, average=False, avg_alpha=None,
+ title='', size=default_fftsink_size, peak_hold=False):
+
+ fft_sink_base.__init__(self, input_is_real=False, baseband_freq=baseband_freq,
+ y_per_div=y_per_div, ref_level=ref_level,
+ sample_rate=sample_rate, fft_size=fft_size,
+ fft_rate=fft_rate,
+ average=average, avg_alpha=avg_alpha, title=title,
+ peak_hold=peak_hold)
+
+ s2p = gr.stream_to_vector(gr.sizeof_gr_complex, self.fft_size)
+ self.one_in_n = gr.keep_one_in_n(gr.sizeof_gr_complex * self.fft_size,
+ max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
+ mywindow = window.blackmanharris(self.fft_size)
+ power = 0
+ for tap in mywindow:
+ power += tap*tap
+
+ fft = gr.fft_vcc(self.fft_size, True, mywindow)
+ c2mag = gr.complex_to_mag(fft_size)
+ self.avg = gr.single_pole_iir_filter_ff(1.0, fft_size)
+ log = gr.nlog10_ff(20, self.fft_size,
+ -20*math.log10(self.fft_size)-10*math.log10(power/self.fft_size))
+ sink = gr.message_sink(gr.sizeof_float * fft_size, self.msgq, True)
+
+ fg.connect(s2p, self.one_in_n, fft, c2mag, self.avg, log, sink)
+ gr.hier_block.__init__(self, fg, s2p, sink)
+
+ self.win = fft_window(self, parent, size=size)
+ self.set_average(self.average)
+
+
+# ------------------------------------------------------------------------
+
+myDATA_EVENT = wx.NewEventType()
+EVT_DATA_EVENT = wx.PyEventBinder (myDATA_EVENT, 0)
+
+
+class DataEvent(wx.PyEvent):
+ def __init__(self, data):
+ wx.PyEvent.__init__(self)
+ self.SetEventType (myDATA_EVENT)
+ self.data = data
+
+ def Clone (self):
+ self.__class__ (self.GetId())
+
+
+class input_watcher (threading.Thread):
+ def __init__ (self, msgq, fft_size, event_receiver, **kwds):
+ threading.Thread.__init__ (self, **kwds)
+ self.setDaemon (1)
+ self.msgq = msgq
+ self.fft_size = fft_size
+ self.event_receiver = event_receiver
+ self.keep_running = True
+ self.start ()
+
+ def run (self):
+ while (self.keep_running):
+ msg = self.msgq.delete_head() # blocking read of message queue
+ itemsize = int(msg.arg1())
+ nitems = int(msg.arg2())
+
+ s = msg.to_string() # get the body of the msg as a string
+
+ # There may be more than one FFT frame in the message.
+ # If so, we take only the last one
+ if nitems > 1:
+ start = itemsize * (nitems - 1)
+ s = s[start:start+itemsize]
+
+ complex_data = Numeric.fromstring (s, Numeric.Float32)
+ de = DataEvent (complex_data)
+ wx.PostEvent (self.event_receiver, de)
+ del de
+
+
+class fft_window (plot.PlotCanvas):
+ def __init__ (self, fftsink, parent, id = -1,
+ pos = wx.DefaultPosition, size = wx.DefaultSize,
+ style = wx.DEFAULT_FRAME_STYLE, name = ""):
+ plot.PlotCanvas.__init__ (self, parent, id, pos, size, style, name)
+
+ self.y_range = None
+ self.fftsink = fftsink
+ self.peak_hold = False
+ self.peak_vals = None
+
+ self.SetEnableGrid (True)
+ # self.SetEnableZoom (True)
+ # self.SetBackgroundColour ('black')
+
+ self.build_popup_menu()
+
+ EVT_DATA_EVENT (self, self.set_data)
+ wx.EVT_CLOSE (self, self.on_close_window)
+ self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
+
+ self.input_watcher = input_watcher(fftsink.msgq, fftsink.fft_size, self)
+
+
+ def on_close_window (self, event):
+ print "fft_window:on_close_window"
+ self.keep_running = False
+
+
+ def set_data (self, evt):
+ dB = evt.data
+ L = len (dB)
+
+ if self.peak_hold:
+ if self.peak_vals is None:
+ self.peak_vals = dB
+ else:
+ self.peak_vals = Numeric.maximum(dB, self.peak_vals)
+ dB = self.peak_vals
+
+ x = max(abs(self.fftsink.sample_rate), abs(self.fftsink.baseband_freq))
+ if x >= 1e9:
+ sf = 1e-9
+ units = "GHz"
+ elif x >= 1e6:
+ sf = 1e-6
+ units = "MHz"
+ else:
+ sf = 1e-3
+ units = "kHz"
+
+ if self.fftsink.input_is_real: # only plot 1/2 the points
+ x_vals = ((Numeric.arrayrange (L/2)
+ * (self.fftsink.sample_rate * sf / L))
+ + self.fftsink.baseband_freq * sf)
+ points = Numeric.zeros((len(x_vals), 2), Numeric.Float64)
+ points[:,0] = x_vals
+ points[:,1] = dB[0:L/2]
+ else:
+ # the "negative freqs" are in the second half of the array
+ x_vals = ((Numeric.arrayrange (-L/2, L/2)
+ * (self.fftsink.sample_rate * sf / L))
+ + self.fftsink.baseband_freq * sf)
+ points = Numeric.zeros((len(x_vals), 2), Numeric.Float64)
+ points[:,0] = x_vals
+ points[:,1] = Numeric.concatenate ((dB[L/2:], dB[0:L/2]))
+
+
+ lines = plot.PolyLine (points, colour='BLUE')
+
+ graphics = plot.PlotGraphics ([lines],
+ title=self.fftsink.title,
+ xLabel = units, yLabel = "dB")
+
+ self.Draw (graphics, xAxis=None, yAxis=self.y_range)
+ self.update_y_range ()
+
+ def set_peak_hold(self, enable):
+ self.peak_hold = enable
+ self.peak_vals = None
+
+ def update_y_range (self):
+ ymax = self.fftsink.ref_level
+ ymin = self.fftsink.ref_level - self.fftsink.y_per_div * self.fftsink.y_divs
+ self.y_range = self._axisInterval ('min', ymin, ymax)
+
+ def on_average(self, evt):
+ # print "on_average"
+ self.fftsink.set_average(evt.IsChecked())
+
+ def on_peak_hold(self, evt):
+ # print "on_peak_hold"
+ self.fftsink.set_peak_hold(evt.IsChecked())
+
+ def on_incr_ref_level(self, evt):
+ # print "on_incr_ref_level"
+ self.fftsink.set_ref_level(self.fftsink.ref_level
+ + self.fftsink.y_per_div)
+
+ def on_decr_ref_level(self, evt):
+ # print "on_decr_ref_level"
+ self.fftsink.set_ref_level(self.fftsink.ref_level
+ - self.fftsink.y_per_div)
+
+ def on_incr_y_per_div(self, evt):
+ # print "on_incr_y_per_div"
+ self.fftsink.set_y_per_div(next_up(self.fftsink.y_per_div, (1,2,5,10,20)))
+
+ def on_decr_y_per_div(self, evt):
+ # print "on_decr_y_per_div"
+ self.fftsink.set_y_per_div(next_down(self.fftsink.y_per_div, (1,2,5,10,20)))
+
+ def on_y_per_div(self, evt):
+ # print "on_y_per_div"
+ Id = evt.GetId()
+ if Id == self.id_y_per_div_1:
+ self.fftsink.set_y_per_div(1)
+ elif Id == self.id_y_per_div_2:
+ self.fftsink.set_y_per_div(2)
+ elif Id == self.id_y_per_div_5:
+ self.fftsink.set_y_per_div(5)
+ elif Id == self.id_y_per_div_10:
+ self.fftsink.set_y_per_div(10)
+ elif Id == self.id_y_per_div_20:
+ self.fftsink.set_y_per_div(20)
+
+
+ def on_right_click(self, event):
+ menu = self.popup_menu
+ for id, pred in self.checkmarks.items():
+ item = menu.FindItemById(id)
+ item.Check(pred())
+ self.PopupMenu(menu, event.GetPosition())
+
+
+ def build_popup_menu(self):
+ self.id_incr_ref_level = wx.NewId()
+ self.id_decr_ref_level = wx.NewId()
+ self.id_incr_y_per_div = wx.NewId()
+ self.id_decr_y_per_div = wx.NewId()
+ self.id_y_per_div_1 = wx.NewId()
+ self.id_y_per_div_2 = wx.NewId()
+ self.id_y_per_div_5 = wx.NewId()
+ self.id_y_per_div_10 = wx.NewId()
+ self.id_y_per_div_20 = wx.NewId()
+ self.id_average = wx.NewId()
+ self.id_peak_hold = wx.NewId()
+
+ self.Bind(wx.EVT_MENU, self.on_average, id=self.id_average)
+ self.Bind(wx.EVT_MENU, self.on_peak_hold, id=self.id_peak_hold)
+ self.Bind(wx.EVT_MENU, self.on_incr_ref_level, id=self.id_incr_ref_level)
+ self.Bind(wx.EVT_MENU, self.on_decr_ref_level, id=self.id_decr_ref_level)
+ self.Bind(wx.EVT_MENU, self.on_incr_y_per_div, id=self.id_incr_y_per_div)
+ self.Bind(wx.EVT_MENU, self.on_decr_y_per_div, id=self.id_decr_y_per_div)
+ self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_1)
+ self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_2)
+ self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_5)
+ self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_10)
+ self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_20)
+
+
+ # make a menu
+ menu = wx.Menu()
+ self.popup_menu = menu
+ menu.AppendCheckItem(self.id_average, "Average")
+ menu.AppendCheckItem(self.id_peak_hold, "Peak Hold")
+ menu.Append(self.id_incr_ref_level, "Incr Ref Level")
+ menu.Append(self.id_decr_ref_level, "Decr Ref Level")
+ # menu.Append(self.id_incr_y_per_div, "Incr dB/div")
+ # menu.Append(self.id_decr_y_per_div, "Decr dB/div")
+ menu.AppendSeparator()
+ # we'd use RadioItems for these, but they're not supported on Mac
+ menu.AppendCheckItem(self.id_y_per_div_1, "1 dB/div")
+ menu.AppendCheckItem(self.id_y_per_div_2, "2 dB/div")
+ menu.AppendCheckItem(self.id_y_per_div_5, "5 dB/div")
+ menu.AppendCheckItem(self.id_y_per_div_10, "10 dB/div")
+ menu.AppendCheckItem(self.id_y_per_div_20, "20 dB/div")
+
+ self.checkmarks = {
+ self.id_average : lambda : self.fftsink.average,
+ self.id_peak_hold : lambda : self.fftsink.peak_hold,
+ self.id_y_per_div_1 : lambda : self.fftsink.y_per_div == 1,
+ self.id_y_per_div_2 : lambda : self.fftsink.y_per_div == 2,
+ self.id_y_per_div_5 : lambda : self.fftsink.y_per_div == 5,
+ self.id_y_per_div_10 : lambda : self.fftsink.y_per_div == 10,
+ self.id_y_per_div_20 : lambda : self.fftsink.y_per_div == 20,
+ }
+
+
+def next_up(v, seq):
+ """
+ Return the first item in seq that is > v.
+ """
+ for s in seq:
+ if s > v:
+ return s
+ return v
+
+def next_down(v, seq):
+ """
+ Return the last item in seq that is < v.
+ """
+ rseq = list(seq[:])
+ rseq.reverse()
+
+ for s in rseq:
+ if s < v:
+ return s
+ return v
+
+
+# ----------------------------------------------------------------
+# Deprecated interfaces
+# ----------------------------------------------------------------
+
+# returns (block, win).
+# block requires a single input stream of float
+# win is a subclass of wxWindow
+
+def make_fft_sink_f(fg, parent, title, fft_size, input_rate, ymin = 0, ymax=50):
+
+ block = fft_sink_f(fg, parent, title=title, fft_size=fft_size, sample_rate=input_rate,
+ y_per_div=(ymax - ymin)/8, ref_level=ymax)
+ return (block, block.win)
+
+# returns (block, win).
+# block requires a single input stream of gr_complex
+# win is a subclass of wxWindow
+
+def make_fft_sink_c(fg, parent, title, fft_size, input_rate, ymin=0, ymax=50):
+ block = fft_sink_c(fg, parent, title=title, fft_size=fft_size, sample_rate=input_rate,
+ y_per_div=(ymax - ymin)/8, ref_level=ymax)
+ return (block, block.win)
+
+
+# ----------------------------------------------------------------
+# Standalone test app
+# ----------------------------------------------------------------
+
+class test_app_flow_graph (stdgui.gui_flow_graph):
+ def __init__(self, frame, panel, vbox, argv):
+ stdgui.gui_flow_graph.__init__ (self, frame, panel, vbox, argv)
+
+ fft_size = 256
+
+ # build our flow graph
+ input_rate = 20.48e3
+
+ # Generate a complex sinusoid
+ #src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
+ src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1)
+
+ # We add these throttle blocks so that this demo doesn't
+ # suck down all the CPU available. Normally you wouldn't use these.
+ thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
+
+ sink1 = fft_sink_c (self, panel, title="Complex Data", fft_size=fft_size,
+ sample_rate=input_rate, baseband_freq=100e3,
+ ref_level=0, y_per_div=20)
+ vbox.Add (sink1.win, 1, wx.EXPAND)
+ self.connect (src1, thr1, sink1)
+
+ #src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
+ src2 = gr.sig_source_f (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1)
+ thr2 = gr.throttle(gr.sizeof_float, input_rate)
+ sink2 = fft_sink_f (self, panel, title="Real Data", fft_size=fft_size*2,
+ sample_rate=input_rate, baseband_freq=100e3,
+ ref_level=0, y_per_div=20)
+ vbox.Add (sink2.win, 1, wx.EXPAND)
+ self.connect (src2, thr2, sink2)
+
+def main ():
+ app = stdgui.stdapp (test_app_flow_graph,
+ "FFT Sink Test App")
+ app.MainLoop ()
+
+if __name__ == '__main__':
+ main ()
diff --git a/gr-wxgui/src/python/form.py b/gr-wxgui/src/python/form.py
new file mode 100755
index 000000000..bb41817ca
--- /dev/null
+++ b/gr-wxgui/src/python/form.py
@@ -0,0 +1,391 @@
+#!/usr/bin/env python
+#
+# Copyright 2005 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio 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 2, or (at your option)
+# any later version.
+#
+# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+#
+
+import wx
+from gnuradio import eng_notation
+
+# ----------------------------------------------------------------
+# Wrappers for certain widgets
+# ----------------------------------------------------------------
+
+def button_with_callback(parent, label, callback):
+ new_id = wx.NewId()
+ btn = wx.Button(parent, new_id, label)
+ wx.EVT_BUTTON(parent, new_id, lambda evt: callback())
+ return btn
+
+
+# ----------------------------------------------------------------
+# Format converters
+# ----------------------------------------------------------------
+
+class abstract_converter(object):
+ def value_to_prim(self, v):
+ """
+ Convert from user specified value to value acceptable to underlying primitive.
+ The underlying primitive usually expects strings.
+ """
+ raise NotImplementedError
+ def prim_to_value(self, s):
+ """
+ Convert from underlying primitive value to user specified value.
+ The underlying primitive usually expects strings.
+ """
+ raise NotImplementedError
+ def help(self):
+ return "Any string is acceptable"
+
+class identity_converter(abstract_converter):
+ def value_to_prim(self,v):
+ return v
+ def prim_to_value(self, s):
+ return s
+
+class int_converter(abstract_converter):
+ def value_to_prim(self, v):
+ return str(v)
+ def prim_to_value(self, s):
+ return int(s, 0)
+ def help(self):
+ return "Enter an integer. Leading 0x indicates hex"
+
+class float_converter(abstract_converter):
+ def value_to_prim(self, v):
+ return eng_notation.num_to_str(v)
+ def prim_to_value(self, s):
+ return eng_notation.str_to_num(s)
+ def help(self):
+ return "Enter a float with optional scale suffix. E.g., 100.1M"
+
+
+# ----------------------------------------------------------------
+# Various types of data entry fields
+# ----------------------------------------------------------------
+
+class field(object):
+ """
+ A field in a form.
+ """
+ def __init__(self, converter, value):
+ self.converter = converter
+ if value is not None:
+ self.set_value(value)
+
+ def set_value(self, v):
+ self._set_prim_value(self.converter.value_to_prim(v))
+
+ def get_value(self):
+ return self.converter.prim_to_value(self._get_prim_value())
+
+ def get_value_with_check(self):
+ """
+ Returns (value, error_msg), where error_msg is not None if there was problem
+ """
+ try:
+ return (self.get_value(), None)
+ except:
+ return (None, self._error_msg())
+
+ def _set_prim_value(self, v):
+ raise NotImplementedError
+
+ def _get_prim_value(self):
+ raise NotImplementedError
+
+ def _pair_with_label(self, widget, parent=None, sizer=None, label=None, weight=1):
+ self.label = label
+ if label is None:
+ sizer.Add (widget, weight, wx.EXPAND)
+ return widget
+ elif 0:
+ hbox = wx.BoxSizer(wx.HORIZONTAL)
+ label_widget = wx.StaticText(parent, -1, label + ': ')
+ hbox.Add(label_widget, 0, wx.EXPAND)
+ hbox.Add(widget, 1, wx.EXPAND)
+ sizer.Add(hbox, weight, wx.EXPAND)
+ return widget
+ else:
+ label_widget = wx.StaticText(parent, -1, label + ': ')
+ sizer.Add(label_widget, 0, wx.EXPAND)
+ sizer.Add(widget, weight, wx.EXPAND)
+ return widget
+
+ def _error_msg(self):
+ prefix = ''
+ if self.label:
+ prefix = self.label + ': '
+ return "%s%s is invalid. %s" % (prefix, self._get_prim_value(),
+ self.converter.help())
+
+# static (display-only) text fields
+
+class static_text_field(field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ converter=identity_converter(), weight=0):
+ self.f = self._pair_with_label(wx.StaticText(parent, -1, ""),
+ parent=parent, sizer=sizer, label=label, weight=weight)
+ field.__init__(self, converter, value)
+
+ def _get_prim_value(self):
+ return self.f.GetLabel()
+
+ def _set_prim_value(self, v):
+ self.f.SetLabel(v)
+
+
+class static_int_field(static_text_field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None, weight=0):
+ static_text_field.__init__(self, parent, sizer, label, value, int_converter(), weight)
+
+class static_float_field(static_text_field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None, weight=0):
+ static_text_field.__init__(self, parent, sizer, label, value, float_converter(), weight)
+
+
+# editable text fields
+
+class text_field(field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ converter=identity_converter(), callback=None, weight=1):
+ style = 0
+ if callback:
+ style = wx.TE_PROCESS_ENTER
+
+ new_id = wx.NewId()
+ w = wx.TextCtrl(parent, new_id, "", style=style)
+ self.f = self._pair_with_label(w, parent=parent, sizer=sizer, label=label, weight=weight)
+ if callback:
+ wx.EVT_TEXT_ENTER(w, new_id, lambda evt: callback())
+ field.__init__(self, converter, value)
+
+ def _get_prim_value(self):
+ return self.f.GetValue()
+
+ def _set_prim_value(self, v):
+ self.f.SetValue(v)
+
+
+class int_field(text_field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ callback=None, weight=1):
+ text_field.__init__(self, parent, sizer, label, value, int_converter(), callback, weight)
+
+class float_field(text_field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ callback=None, weight=1):
+ text_field.__init__(self, parent, sizer, label, value, float_converter(), callback, weight)
+
+# other fields
+
+class slider_field(field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ converter=identity_converter(), callback=None, min=0, max=100, weight=1):
+ new_id = wx.NewId()
+ w = wx.Slider(parent, new_id, (max+min)/2, min, max,
+ size=wx.Size(250, -1), style=wx.SL_HORIZONTAL | wx.SL_LABELS)
+ self.f = self._pair_with_label(w, parent=parent, sizer=sizer, label=label, weight=weight)
+ if callback:
+ wx.EVT_COMMAND_SCROLL(w, new_id, lambda evt: callback(evt.GetInt()))
+ field.__init__(self, converter, value)
+
+ def _get_prim_value(self):
+ return self.f.GetValue()
+
+ def _set_prim_value(self, v):
+ self.f.SetValue(int(v))
+
+class quantized_slider_field(field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ converter=identity_converter(), callback=None, range=None, weight=1):
+ if not isinstance(range, (tuple, list)) or len(range) != 3:
+ raise ValueError, range
+
+ self.min = range[0]
+ self.max = range[1]
+ self.step_size = float(range[2])
+ nsteps = int((self.max-self.min)/self.step_size)
+
+ new_id = wx.NewId()
+ w = wx.Slider(parent, new_id, 0, 0, nsteps,
+ size=wx.Size(250, -1), style=wx.SL_HORIZONTAL)
+ self.f = self._pair_with_label(w, parent=parent, sizer=sizer, label=label, weight=weight)
+ if callback:
+ wx.EVT_COMMAND_SCROLL(w, new_id,
+ lambda evt: callback(self._map_out(evt.GetInt())))
+ field.__init__(self, converter, value)
+
+ def _get_prim_value(self):
+ return self._map_out(self.f.GetValue())
+
+ def _set_prim_value(self, v):
+ self.f.SetValue(self._map_in(v))
+
+ def _map_in(self, x):
+ return int((x-self.min) / self.step_size)
+
+ def _map_out(self, x):
+ return x * self.step_size + self.min
+
+class checkbox_field(field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ converter=identity_converter(), callback=None, weight=1):
+ new_id = wx.NewId()
+ w = wx.CheckBox(parent, new_id, label, style=wx.CHK_2STATE)
+ self.f = self._pair_with_label(w, parent=parent, sizer=sizer, label=None, weight=weight)
+ if callback:
+ wx.EVT_CHECKBOX(w, new_id, lambda evt: callback(evt.GetInt()))
+ field.__init__(self, converter, value)
+
+ def _get_prim_value(self):
+ return self.f.GetValue()
+
+ def _set_prim_value(self, v):
+ self.f.SetValue(int(v))
+
+
+class radiobox_field(field):
+ def __init__(self, parent=None, sizer=None, label="", value=None,
+ converter=identity_converter(), callback=None, weight=1,
+ choices=None, major_dimension=1, specify_rows=False):
+ new_id = wx.NewId()
+
+ if specify_rows:
+ style=wx.RA_SPECIFY_ROWS | wx.RA_HORIZONTAL
+ else:
+ style=wx.RA_SPECIFY_COLS | wx.RA_HORIZONTAL
+
+ w = wx.RadioBox(parent, new_id, label, style=style, majorDimension=major_dimension,
+ choices=choices)
+ self.f = self._pair_with_label(w, parent=parent, sizer=sizer, label=label, weight=weight)
+ if callback:
+ wx.EVT_RADIOBOX(w, new_id, lambda evt: callback(evt.GetString()))
+ field.__init__(self, converter, value)
+
+ def _get_prim_value(self):
+ return self.f.GetStringSelection()
+
+ def _set_prim_value(self, v):
+ self.f.SetStringSelection(str(v))
+
+# ----------------------------------------------------------------
+# the form class
+# ----------------------------------------------------------------
+
+class form(dict):
+ def __init__(self):
+ dict.__init__(self)
+
+ def check_input_for_errors(self):
+ """
+ Returns list of error messages if there's trouble,
+ else empty list.
+ """
+ vals = [f.get_value_with_check() for f in self.values()]
+ return [t[1] for t in vals if t[1] is not None]
+
+ def get_key_vals(self):
+ d = {}
+ for (key, f) in self.items():
+ d[key] = f.get_value()
+ return d
+
+
+ def _nop(*args): pass
+
+ def check_input_and_call(self, callback, status_handler=_nop):
+ """
+ Return a function that checks the form for errors, and then if it's OK,
+ invokes the user specified callback, passing it the form key/value dictionary.
+ status_handler is called with a string indicating results.
+ """
+ def doit_callback(*ignore):
+ errors = self.check_input_for_errors()
+ if errors:
+ status_handler(errors[0])
+ #print '\n'.join(tuple(errors))
+ else:
+ kv = self.get_key_vals()
+ if callback(kv):
+ status_handler("OK")
+ else:
+ status_handler("Failed")
+
+ return doit_callback
+
+
+
+# ----------------------------------------------------------------
+# Stand-alone example code
+# ----------------------------------------------------------------
+
+import sys
+from gnuradio.wxgui import stdgui
+
+class demo_app_flow_graph (stdgui.gui_flow_graph):
+ def __init__(self, frame, panel, vbox, argv):
+ stdgui.gui_flow_graph.__init__ (self, frame, panel, vbox, argv)
+
+ self.frame = frame
+ self.panel = panel
+
+ def _print_kv(kv):
+ print "kv =", kv
+ return True
+
+ self.form = form()
+
+ self.form['static1'] = \
+ static_text_field(parent=panel, sizer=vbox,
+ label="Static Text",
+ value="The Static Value")
+
+ self.form['text1'] = \
+ text_field(parent=panel, sizer=vbox,
+ label="TextCtrl",
+ value="The Editable Value")
+
+ self.form['int1'] = \
+ int_field(parent=panel, sizer=vbox,
+ label="Int Field",
+ value=1234)
+
+ self.form['float1'] = \
+ float_field(parent=panel, sizer=vbox,
+ label="Float Field",
+ value=3.14159)
+
+ self.doit = button_with_callback(
+ panel, "Do It!",
+ self.form.check_input_and_call(_print_kv, self._set_status_msg))
+
+ vbox.Add(self.doit, 0, wx.CENTER)
+
+ def _set_status_msg(self, msg):
+ self.frame.GetStatusBar().SetStatusText(msg, 0)
+
+
+def main ():
+ app = stdgui.stdapp (demo_app_flow_graph, "wxgui form demo", nstatus=1)
+ app.MainLoop ()
+
+if __name__ == '__main__':
+ main ()
diff --git a/gr-wxgui/src/python/plot.py b/gr-wxgui/src/python/plot.py
new file mode 100644
index 000000000..d902d417c
--- /dev/null
+++ b/gr-wxgui/src/python/plot.py
@@ -0,0 +1,1744 @@
+#-----------------------------------------------------------------------------
+# Name: wx.lib.plot.py
+# Purpose: Line, Bar and Scatter Graphs
+#
+# Author: Gordon Williams
+#
+# Created: 2003/11/03
+# RCS-ID: $Id$
+# Copyright: (c) 2002
+# Licence: Use as you wish.
+#-----------------------------------------------------------------------------
+# 12/15/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o 2.5 compatability update.
+# o Renamed to plot.py in the wx.lib directory.
+# o Reworked test frame to work with wx demo framework. This saves a bit
+# of tedious cut and paste, and the test app is excellent.
+#
+# 12/18/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o wxScrolledMessageDialog -> ScrolledMessageDialog
+#
+# Oct 6, 2004 Gordon Williams (g_will@cyberus.ca)
+# - Added bar graph demo
+# - Modified line end shape from round to square.
+# - Removed FloatDCWrapper for conversion to ints and ints in arguments
+#
+# Oct 15, 2004 Gordon Williams (g_will@cyberus.ca)
+# - Imported modules given leading underscore to name.
+# - Added Cursor Line Tracking and User Point Labels.
+# - Demo for Cursor Line Tracking and Point Labels.
+# - Size of plot preview frame adjusted to show page better.
+# - Added helper functions PositionUserToScreen and PositionScreenToUser in PlotCanvas.
+# - Added functions GetClosestPoints (all curves) and GetClosestPoint (only closest curve)
+# can be in either user coords or screen coords.
+#
+#
+
+"""
+This is a simple light weight plotting module that can be used with
+Boa or easily integrated into your own wxPython application. The
+emphasis is on small size and fast plotting for large data sets. It
+has a reasonable number of features to do line and scatter graphs
+easily as well as simple bar graphs. It is not as sophisticated or
+as powerful as SciPy Plt or Chaco. Both of these are great packages
+but consume huge amounts of computer resources for simple plots.
+They can be found at http://scipy.com
+
+This file contains two parts; first the re-usable library stuff, then,
+after a "if __name__=='__main__'" test, a simple frame and a few default
+plots for examples and testing.
+
+Based on wxPlotCanvas
+Written by K.Hinsen, R. Srinivasan;
+Ported to wxPython Harm van der Heijden, feb 1999
+
+Major Additions Gordon Williams Feb. 2003 (g_will@cyberus.ca)
+ -More style options
+ -Zooming using mouse 'rubber band'
+ -Scroll left, right
+ -Grid(graticule)
+ -Printing, preview, and page set up (margins)
+ -Axis and title labels
+ -Cursor xy axis values
+ -Doc strings and lots of comments
+ -Optimizations for large number of points
+ -Legends
+
+Did a lot of work here to speed markers up. Only a factor of 4
+improvement though. Lines are much faster than markers, especially
+filled markers. Stay away from circles and triangles unless you
+only have a few thousand points.
+
+Times for 25,000 points
+Line - 0.078 sec
+Markers
+Square - 0.22 sec
+dot - 0.10
+circle - 0.87
+cross,plus - 0.28
+triangle, triangle_down - 0.90
+
+Thanks to Chris Barker for getting this version working on Linux.
+
+Zooming controls with mouse (when enabled):
+ Left mouse drag - Zoom box.
+ Left mouse double click - reset zoom.
+ Right mouse click - zoom out centred on click location.
+"""
+
+import string as _string
+import time as _time
+import wx
+
+# Needs Numeric or numarray
+try:
+ import Numeric as _Numeric
+except:
+ try:
+ import numarray as _Numeric #if numarray is used it is renamed Numeric
+ except:
+ msg= """
+ This module requires the Numeric or numarray module,
+ which could not be imported. It probably is not installed
+ (it's not part of the standard Python distribution). See the
+ Python site (http://www.python.org) for information on
+ downloading source or binaries."""
+ raise ImportError, "Numeric or numarray not found. \n" + msg
+
+
+
+#
+# Plotting classes...
+#
+class PolyPoints:
+ """Base Class for lines and markers
+ - All methods are private.
+ """
+
+ def __init__(self, points, attr):
+ self.points = _Numeric.array(points)
+ self.currentScale= (1,1)
+ self.currentShift= (0,0)
+ self.scaled = self.points
+ self.attributes = {}
+ self.attributes.update(self._attributes)
+ for name, value in attr.items():
+ if name not in self._attributes.keys():
+ raise KeyError, "Style attribute incorrect. Should be one of %s" % self._attributes.keys()
+ self.attributes[name] = value
+
+ def boundingBox(self):
+ if len(self.points) == 0:
+ # no curves to draw
+ # defaults to (-1,-1) and (1,1) but axis can be set in Draw
+ minXY= _Numeric.array([-1,-1])
+ maxXY= _Numeric.array([ 1, 1])
+ else:
+ minXY= _Numeric.minimum.reduce(self.points)
+ maxXY= _Numeric.maximum.reduce(self.points)
+ return minXY, maxXY
+
+ def scaleAndShift(self, scale=(1,1), shift=(0,0)):
+ if len(self.points) == 0:
+ # no curves to draw
+ return
+ if (scale is not self.currentScale) or (shift is not self.currentShift):
+ # update point scaling
+ self.scaled = scale*self.points+shift
+ self.currentScale= scale
+ self.currentShift= shift
+ # else unchanged use the current scaling
+
+ def getLegend(self):
+ return self.attributes['legend']
+
+ def getClosestPoint(self, pntXY, pointScaled= True):
+ """Returns the index of closest point on the curve, pointXY, scaledXY, distance
+ x, y in user coords
+ if pointScaled == True based on screen coords
+ if pointScaled == False based on user coords
+ """
+ if pointScaled == True:
+ #Using screen coords
+ p = self.scaled
+ pxy = self.currentScale * _Numeric.array(pntXY)+ self.currentShift
+ else:
+ #Using user coords
+ p = self.points
+ pxy = _Numeric.array(pntXY)
+ #determine distance for each point
+ d= _Numeric.sqrt(_Numeric.add.reduce((p-pxy)**2,1)) #sqrt(dx^2+dy^2)
+ pntIndex = _Numeric.argmin(d)
+ dist = d[pntIndex]
+ return [pntIndex, self.points[pntIndex], self.scaled[pntIndex], dist]
+
+
+class PolyLine(PolyPoints):
+ """Class to define line type and style
+ - All methods except __init__ are private.
+ """
+
+ _attributes = {'colour': 'black',
+ 'width': 1,
+ 'style': wx.SOLID,
+ 'legend': ''}
+
+ def __init__(self, points, **attr):
+ """Creates PolyLine object
+ points - sequence (array, tuple or list) of (x,y) points making up line
+ **attr - key word attributes
+ Defaults:
+ 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
+ 'width'= 1, - Pen width
+ 'style'= wx.SOLID, - wx.Pen style
+ 'legend'= '' - Line Legend to display
+ """
+ PolyPoints.__init__(self, points, attr)
+
+ def draw(self, dc, printerScale, coord= None):
+ colour = self.attributes['colour']
+ width = self.attributes['width'] * printerScale
+ style= self.attributes['style']
+ pen = wx.Pen(wx.NamedColour(colour), width, style)
+ pen.SetCap(wx.CAP_BUTT)
+ dc.SetPen(pen)
+ if coord == None:
+ dc.DrawLines(self.scaled)
+ else:
+ dc.DrawLines(coord) # draw legend line
+
+ def getSymExtent(self, printerScale):
+ """Width and Height of Marker"""
+ h= self.attributes['width'] * printerScale
+ w= 5 * h
+ return (w,h)
+
+
+class PolyMarker(PolyPoints):
+ """Class to define marker type and style
+ - All methods except __init__ are private.
+ """
+
+ _attributes = {'colour': 'black',
+ 'width': 1,
+ 'size': 2,
+ 'fillcolour': None,
+ 'fillstyle': wx.SOLID,
+ 'marker': 'circle',
+ 'legend': ''}
+
+ def __init__(self, points, **attr):
+ """Creates PolyMarker object
+ points - sequence (array, tuple or list) of (x,y) points
+ **attr - key word attributes
+ Defaults:
+ 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
+ 'width'= 1, - Pen width
+ 'size'= 2, - Marker size
+ 'fillcolour'= same as colour, - wx.Brush Colour any wx.NamedColour
+ 'fillstyle'= wx.SOLID, - wx.Brush fill style (use wx.TRANSPARENT for no fill)
+ 'marker'= 'circle' - Marker shape
+ 'legend'= '' - Marker Legend to display
+
+ Marker Shapes:
+ - 'circle'
+ - 'dot'
+ - 'square'
+ - 'triangle'
+ - 'triangle_down'
+ - 'cross'
+ - 'plus'
+ """
+
+ PolyPoints.__init__(self, points, attr)
+
+ def draw(self, dc, printerScale, coord= None):
+ colour = self.attributes['colour']
+ width = self.attributes['width'] * printerScale
+ size = self.attributes['size'] * printerScale
+ fillcolour = self.attributes['fillcolour']
+ fillstyle = self.attributes['fillstyle']
+ marker = self.attributes['marker']
+
+ dc.SetPen(wx.Pen(wx.NamedColour(colour), width))
+ if fillcolour:
+ dc.SetBrush(wx.Brush(wx.NamedColour(fillcolour),fillstyle))
+ else:
+ dc.SetBrush(wx.Brush(wx.NamedColour(colour), fillstyle))
+ if coord == None:
+ self._drawmarkers(dc, self.scaled, marker, size)
+ else:
+ self._drawmarkers(dc, coord, marker, size) # draw legend marker
+
+ def getSymExtent(self, printerScale):
+ """Width and Height of Marker"""
+ s= 5*self.attributes['size'] * printerScale
+ return (s,s)
+
+ def _drawmarkers(self, dc, coords, marker,size=1):
+ f = eval('self._' +marker)
+ f(dc, coords, size)
+
+ def _circle(self, dc, coords, size=1):
+ fact= 2.5*size
+ wh= 5.0*size
+ rect= _Numeric.zeros((len(coords),4),_Numeric.Float)+[0.0,0.0,wh,wh]
+ rect[:,0:2]= coords-[fact,fact]
+ dc.DrawEllipseList(rect.astype(_Numeric.Int32))
+
+ def _dot(self, dc, coords, size=1):
+ dc.DrawPointList(coords)
+
+ def _square(self, dc, coords, size=1):
+ fact= 2.5*size
+ wh= 5.0*size
+ rect= _Numeric.zeros((len(coords),4),_Numeric.Float)+[0.0,0.0,wh,wh]
+ rect[:,0:2]= coords-[fact,fact]
+ dc.DrawRectangleList(rect.astype(_Numeric.Int32))
+
+ def _triangle(self, dc, coords, size=1):
+ shape= [(-2.5*size,1.44*size), (2.5*size,1.44*size), (0.0,-2.88*size)]
+ poly= _Numeric.repeat(coords,3)
+ poly.shape= (len(coords),3,2)
+ poly += shape
+ dc.DrawPolygonList(poly.astype(_Numeric.Int32))
+
+ def _triangle_down(self, dc, coords, size=1):
+ shape= [(-2.5*size,-1.44*size), (2.5*size,-1.44*size), (0.0,2.88*size)]
+ poly= _Numeric.repeat(coords,3)
+ poly.shape= (len(coords),3,2)
+ poly += shape
+ dc.DrawPolygonList(poly.astype(_Numeric.Int32))
+
+ def _cross(self, dc, coords, size=1):
+ fact= 2.5*size
+ for f in [[-fact,-fact,fact,fact],[-fact,fact,fact,-fact]]:
+ lines= _Numeric.concatenate((coords,coords),axis=1)+f
+ dc.DrawLineList(lines.astype(_Numeric.Int32))
+
+ def _plus(self, dc, coords, size=1):
+ fact= 2.5*size
+ for f in [[-fact,0,fact,0],[0,-fact,0,fact]]:
+ lines= _Numeric.concatenate((coords,coords),axis=1)+f
+ dc.DrawLineList(lines.astype(_Numeric.Int32))
+
+class PlotGraphics:
+ """Container to hold PolyXXX objects and graph labels
+ - All methods except __init__ are private.
+ """
+
+ def __init__(self, objects, title='', xLabel='', yLabel= ''):
+ """Creates PlotGraphics object
+ objects - list of PolyXXX objects to make graph
+ title - title shown at top of graph
+ xLabel - label shown on x-axis
+ yLabel - label shown on y-axis
+ """
+ if type(objects) not in [list,tuple]:
+ raise TypeError, "objects argument should be list or tuple"
+ self.objects = objects
+ self.title= title
+ self.xLabel= xLabel
+ self.yLabel= yLabel
+
+ def boundingBox(self):
+ p1, p2 = self.objects[0].boundingBox()
+ for o in self.objects[1:]:
+ p1o, p2o = o.boundingBox()
+ p1 = _Numeric.minimum(p1, p1o)
+ p2 = _Numeric.maximum(p2, p2o)
+ return p1, p2
+
+ def scaleAndShift(self, scale=(1,1), shift=(0,0)):
+ for o in self.objects:
+ o.scaleAndShift(scale, shift)
+
+ def setPrinterScale(self, scale):
+ """Thickens up lines and markers only for printing"""
+ self.printerScale= scale
+
+ def setXLabel(self, xLabel= ''):
+ """Set the X axis label on the graph"""
+ self.xLabel= xLabel
+
+ def setYLabel(self, yLabel= ''):
+ """Set the Y axis label on the graph"""
+ self.yLabel= yLabel
+
+ def setTitle(self, title= ''):
+ """Set the title at the top of graph"""
+ self.title= title
+
+ def getXLabel(self):
+ """Get x axis label string"""
+ return self.xLabel
+
+ def getYLabel(self):
+ """Get y axis label string"""
+ return self.yLabel
+
+ def getTitle(self, title= ''):
+ """Get the title at the top of graph"""
+ return self.title
+
+ def draw(self, dc):
+ for o in self.objects:
+ #t=_time.clock() # profile info
+ o.draw(dc, self.printerScale)
+ #dt= _time.clock()-t
+ #print o, "time=", dt
+
+ def getSymExtent(self, printerScale):
+ """Get max width and height of lines and markers symbols for legend"""
+ symExt = self.objects[0].getSymExtent(printerScale)
+ for o in self.objects[1:]:
+ oSymExt = o.getSymExtent(printerScale)
+ symExt = _Numeric.maximum(symExt, oSymExt)
+ return symExt
+
+ def getLegendNames(self):
+ """Returns list of legend names"""
+ lst = [None]*len(self)
+ for i in range(len(self)):
+ lst[i]= self.objects[i].getLegend()
+ return lst
+
+ def __len__(self):
+ return len(self.objects)
+
+ def __getitem__(self, item):
+ return self.objects[item]
+
+
+#-------------------------------------------------------------------------------
+# Main window that you will want to import into your application.
+
+class PlotCanvas(wx.Window):
+ """Subclass of a wx.Window to allow simple general plotting
+ of data with zoom, labels, and automatic axis scaling."""
+
+ def __init__(self, parent, id = -1, pos=wx.DefaultPosition,
+ size=wx.DefaultSize, style= wx.DEFAULT_FRAME_STYLE, name= ""):
+ """Constucts a window, which can be a child of a frame, dialog or
+ any other non-control window"""
+
+ wx.Window.__init__(self, parent, id, pos, size, style, name)
+ self.border = (1,1)
+
+ self.SetBackgroundColour("white")
+
+ # Create some mouse events for zooming
+ self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
+ self.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp)
+ self.Bind(wx.EVT_MOTION, self.OnMotion)
+ self.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseDoubleClick)
+ self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDown)
+
+ # set curser as cross-hairs
+ self.SetCursor(wx.CROSS_CURSOR)
+
+ # Things for printing
+ self.print_data = wx.PrintData()
+ self.print_data.SetPaperId(wx.PAPER_LETTER)
+ self.print_data.SetOrientation(wx.LANDSCAPE)
+ self.pageSetupData= wx.PageSetupDialogData()
+ self.pageSetupData.SetMarginBottomRight((25,25))
+ self.pageSetupData.SetMarginTopLeft((25,25))
+ self.pageSetupData.SetPrintData(self.print_data)
+ self.printerScale = 1
+ self.parent= parent
+
+ # Zooming variables
+ self._zoomInFactor = 0.5
+ self._zoomOutFactor = 2
+ self._zoomCorner1= _Numeric.array([0.0, 0.0]) # left mouse down corner
+ self._zoomCorner2= _Numeric.array([0.0, 0.0]) # left mouse up corner
+ self._zoomEnabled= False
+ self._hasDragged= False
+
+ # Drawing Variables
+ self.last_draw = None
+ self._pointScale= 1
+ self._pointShift= 0
+ self._xSpec= 'auto'
+ self._ySpec= 'auto'
+ self._gridEnabled= False
+ self._legendEnabled= False
+ self._xUseScopeTicks= False
+
+ # Fonts
+ self._fontCache = {}
+ self._fontSizeAxis= 10
+ self._fontSizeTitle= 15
+ self._fontSizeLegend= 7
+
+ # pointLabels
+ self._pointLabelEnabled= False
+ self.last_PointLabel= None
+ self._pointLabelFunc= None
+ self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
+
+ self.Bind(wx.EVT_PAINT, self.OnPaint)
+ self.Bind(wx.EVT_SIZE, self.OnSize)
+ # OnSize called to make sure the buffer is initialized.
+ # This might result in OnSize getting called twice on some
+ # platforms at initialization, but little harm done.
+ self.OnSize(None) # sets the initial size based on client size
+ # UNCONDITIONAL, needed to create self._Buffer
+
+ # SaveFile
+ def SaveFile(self, fileName= ''):
+ """Saves the file to the type specified in the extension. If no file
+ name is specified a dialog box is provided. Returns True if sucessful,
+ otherwise False.
+
+ .bmp Save a Windows bitmap file.
+ .xbm Save an X bitmap file.
+ .xpm Save an XPM bitmap file.
+ .png Save a Portable Network Graphics file.
+ .jpg Save a Joint Photographic Experts Group file.
+ """
+ if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
+ dlg1 = wx.FileDialog(
+ self,
+ "Choose a file with extension bmp, gif, xbm, xpm, png, or jpg", ".", "",
+ "BMP files (*.bmp)|*.bmp|XBM files (*.xbm)|*.xbm|XPM file (*.xpm)|*.xpm|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg",
+ wx.SAVE|wx.OVERWRITE_PROMPT
+ )
+ try:
+ while 1:
+ if dlg1.ShowModal() == wx.ID_OK:
+ fileName = dlg1.GetPath()
+ # Check for proper exension
+ if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
+ dlg2 = wx.MessageDialog(self, 'File name extension\n'
+ 'must be one of\n'
+ 'bmp, xbm, xpm, png, or jpg',
+ 'File Name Error', wx.OK | wx.ICON_ERROR)
+ try:
+ dlg2.ShowModal()
+ finally:
+ dlg2.Destroy()
+ else:
+ break # now save file
+ else: # exit without saving
+ return False
+ finally:
+ dlg1.Destroy()
+
+ # File name has required extension
+ fType = _string.lower(fileName[-3:])
+ if fType == "bmp":
+ tp= wx.BITMAP_TYPE_BMP # Save a Windows bitmap file.
+ elif fType == "xbm":
+ tp= wx.BITMAP_TYPE_XBM # Save an X bitmap file.
+ elif fType == "xpm":
+ tp= wx.BITMAP_TYPE_XPM # Save an XPM bitmap file.
+ elif fType == "jpg":
+ tp= wx.BITMAP_TYPE_JPEG # Save a JPG file.
+ else:
+ tp= wx.BITMAP_TYPE_PNG # Save a PNG file.
+ # Save Bitmap
+ res= self._Buffer.SaveFile(fileName, tp)
+ return res
+
+ def PageSetup(self):
+ """Brings up the page setup dialog"""
+ data = self.pageSetupData
+ data.SetPrintData(self.print_data)
+ dlg = wx.PageSetupDialog(self.parent, data)
+ try:
+ if dlg.ShowModal() == wx.ID_OK:
+ data = dlg.GetPageSetupData() # returns wx.PageSetupDialogData
+ # updates page parameters from dialog
+ self.pageSetupData.SetMarginBottomRight(data.GetMarginBottomRight())
+ self.pageSetupData.SetMarginTopLeft(data.GetMarginTopLeft())
+ self.pageSetupData.SetPrintData(data.GetPrintData())
+ self.print_data=data.GetPrintData() # updates print_data
+ finally:
+ dlg.Destroy()
+
+ def Printout(self, paper=None):
+ """Print current plot."""
+ if paper != None:
+ self.print_data.SetPaperId(paper)
+ pdd = wx.PrintDialogData()
+ pdd.SetPrintData(self.print_data)
+ printer = wx.Printer(pdd)
+ out = PlotPrintout(self)
+ print_ok = printer.Print(self.parent, out)
+ if print_ok:
+ self.print_data = printer.GetPrintDialogData().GetPrintData()
+ out.Destroy()
+
+ def PrintPreview(self):
+ """Print-preview current plot."""
+ printout = PlotPrintout(self)
+ printout2 = PlotPrintout(self)
+ self.preview = wx.PrintPreview(printout, printout2, self.print_data)
+ if not self.preview.Ok():
+ wx.MessageDialog(self, "Print Preview failed.\n" \
+ "Check that default printer is configured\n", \
+ "Print error", wx.OK|wx.CENTRE).ShowModal()
+ self.preview.SetZoom(40)
+ # search up tree to find frame instance
+ frameInst= self
+ while not isinstance(frameInst, wx.Frame):
+ frameInst= frameInst.GetParent()
+ frame = wx.PreviewFrame(self.preview, frameInst, "Preview")
+ frame.Initialize()
+ frame.SetPosition(self.GetPosition())
+ frame.SetSize((600,550))
+ frame.Centre(wx.BOTH)
+ frame.Show(True)
+
+ def SetFontSizeAxis(self, point= 10):
+ """Set the tick and axis label font size (default is 10 point)"""
+ self._fontSizeAxis= point
+
+ def GetFontSizeAxis(self):
+ """Get current tick and axis label font size in points"""
+ return self._fontSizeAxis
+
+ def SetFontSizeTitle(self, point= 15):
+ """Set Title font size (default is 15 point)"""
+ self._fontSizeTitle= point
+
+ def GetFontSizeTitle(self):
+ """Get current Title font size in points"""
+ return self._fontSizeTitle
+
+ def SetFontSizeLegend(self, point= 7):
+ """Set Legend font size (default is 7 point)"""
+ self._fontSizeLegend= point
+
+ def GetFontSizeLegend(self):
+ """Get current Legend font size in points"""
+ return self._fontSizeLegend
+
+ def SetEnableZoom(self, value):
+ """Set True to enable zooming."""
+ if value not in [True,False]:
+ raise TypeError, "Value should be True or False"
+ self._zoomEnabled= value
+
+ def GetEnableZoom(self):
+ """True if zooming enabled."""
+ return self._zoomEnabled
+
+ def SetEnableGrid(self, value):
+ """Set True to enable grid."""
+ if value not in [True,False]:
+ raise TypeError, "Value should be True or False"
+ self._gridEnabled= value
+ self.Redraw()
+
+ def GetEnableGrid(self):
+ """True if grid enabled."""
+ return self._gridEnabled
+
+ def SetEnableLegend(self, value):
+ """Set True to enable legend."""
+ if value not in [True,False]:
+ raise TypeError, "Value should be True or False"
+ self._legendEnabled= value
+ self.Redraw()
+
+ def GetEnableLegend(self):
+ """True if Legend enabled."""
+ return self._legendEnabled
+
+ def SetEnablePointLabel(self, value):
+ """Set True to enable pointLabel."""
+ if value not in [True,False]:
+ raise TypeError, "Value should be True or False"
+ self._pointLabelEnabled= value
+ self.Redraw() #will erase existing pointLabel if present
+ self.last_PointLabel = None
+
+ def GetEnablePointLabel(self):
+ """True if pointLabel enabled."""
+ return self._pointLabelEnabled
+
+ def SetPointLabelFunc(self, func):
+ """Sets the function with custom code for pointLabel drawing
+ ******** more info needed ***************
+ """
+ self._pointLabelFunc= func
+
+ def GetPointLabelFunc(self):
+ """Returns pointLabel Drawing Function"""
+ return self._pointLabelFunc
+
+ def Reset(self):
+ """Unzoom the plot."""
+ self.last_PointLabel = None #reset pointLabel
+ if self.last_draw is not None:
+ self.Draw(self.last_draw[0])
+
+ def ScrollRight(self, units):
+ """Move view right number of axis units."""
+ self.last_PointLabel = None #reset pointLabel
+ if self.last_draw is not None:
+ graphics, xAxis, yAxis= self.last_draw
+ xAxis= (xAxis[0]+units, xAxis[1]+units)
+ self.Draw(graphics,xAxis,yAxis)
+
+ def ScrollUp(self, units):
+ """Move view up number of axis units."""
+ self.last_PointLabel = None #reset pointLabel
+ if self.last_draw is not None:
+ graphics, xAxis, yAxis= self.last_draw
+ yAxis= (yAxis[0]+units, yAxis[1]+units)
+ self.Draw(graphics,xAxis,yAxis)
+
+ def GetXY(self,event):
+ """Takes a mouse event and returns the XY user axis values."""
+ x,y= self.PositionScreenToUser(event.GetPosition())
+ return x,y
+
+ def PositionUserToScreen(self, pntXY):
+ """Converts User position to Screen Coordinates"""
+ userPos= _Numeric.array(pntXY)
+ x,y= userPos * self._pointScale + self._pointShift
+ return x,y
+
+ def PositionScreenToUser(self, pntXY):
+ """Converts Screen position to User Coordinates"""
+ screenPos= _Numeric.array(pntXY)
+ x,y= (screenPos-self._pointShift)/self._pointScale
+ return x,y
+
+ def SetXSpec(self, type= 'auto'):
+ """xSpec- defines x axis type. Can be 'none', 'min' or 'auto'
+ where:
+ 'none' - shows no axis or tick mark values
+ 'min' - shows min bounding box values
+ 'auto' - rounds axis range to sensible values
+ """
+ self._xSpec= type
+
+ def SetYSpec(self, type= 'auto'):
+ """ySpec- defines x axis type. Can be 'none', 'min' or 'auto'
+ where:
+ 'none' - shows no axis or tick mark values
+ 'min' - shows min bounding box values
+ 'auto' - rounds axis range to sensible values
+ """
+ self._ySpec= type
+
+ def GetXSpec(self):
+ """Returns current XSpec for axis"""
+ return self._xSpec
+
+ def GetYSpec(self):
+ """Returns current YSpec for axis"""
+ return self._ySpec
+
+ def GetXMaxRange(self):
+ """Returns (minX, maxX) x-axis range for displayed graph"""
+ graphics= self.last_draw[0]
+ p1, p2 = graphics.boundingBox() # min, max points of graphics
+ xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
+ return xAxis
+
+ def GetYMaxRange(self):
+ """Returns (minY, maxY) y-axis range for displayed graph"""
+ graphics= self.last_draw[0]
+ p1, p2 = graphics.boundingBox() # min, max points of graphics
+ yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
+ return yAxis
+
+ def GetXCurrentRange(self):
+ """Returns (minX, maxX) x-axis for currently displayed portion of graph"""
+ return self.last_draw[1]
+
+ def GetYCurrentRange(self):
+ """Returns (minY, maxY) y-axis for currently displayed portion of graph"""
+ return self.last_draw[2]
+
+ def SetXUseScopeTicks(self, v=False):
+ """Always 10 divisions, no labels"""
+ self._xUseScopeTicks = v
+
+ def GetXUseScopeTicks(self):
+ return self._xUseScopeTicks
+
+ def Draw(self, graphics, xAxis = None, yAxis = None, dc = None):
+ """Draw objects in graphics with specified x and y axis.
+ graphics- instance of PlotGraphics with list of PolyXXX objects
+ xAxis - tuple with (min, max) axis range to view
+ yAxis - same as xAxis
+ dc - drawing context - doesn't have to be specified.
+ If it's not, the offscreen buffer is used
+ """
+ # check Axis is either tuple or none
+ if type(xAxis) not in [type(None),tuple]:
+ raise TypeError, "xAxis should be None or (minX,maxX)"
+ if type(yAxis) not in [type(None),tuple]:
+ raise TypeError, "yAxis should be None or (minY,maxY)"
+
+ # check case for axis = (a,b) where a==b caused by improper zooms
+ if xAxis != None:
+ if xAxis[0] == xAxis[1]:
+ return
+ if yAxis != None:
+ if yAxis[0] == yAxis[1]:
+ return
+
+ if dc == None:
+ # sets new dc and clears it
+ dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
+ dc.Clear()
+
+ dc.BeginDrawing()
+ # dc.Clear()
+
+ # set font size for every thing but title and legend
+ dc.SetFont(self._getFont(self._fontSizeAxis))
+
+ # sizes axis to axis type, create lower left and upper right corners of plot
+ if xAxis == None or yAxis == None:
+ # One or both axis not specified in Draw
+ p1, p2 = graphics.boundingBox() # min, max points of graphics
+ if xAxis == None:
+ xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
+ if yAxis == None:
+ yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
+ # Adjust bounding box for axis spec
+ p1[0],p1[1] = xAxis[0], yAxis[0] # lower left corner user scale (xmin,ymin)
+ p2[0],p2[1] = xAxis[1], yAxis[1] # upper right corner user scale (xmax,ymax)
+ else:
+ # Both axis specified in Draw
+ p1= _Numeric.array([xAxis[0], yAxis[0]]) # lower left corner user scale (xmin,ymin)
+ p2= _Numeric.array([xAxis[1], yAxis[1]]) # upper right corner user scale (xmax,ymax)
+
+ self.last_draw = (graphics, xAxis, yAxis) # saves most recient values
+
+ # Get ticks and textExtents for axis if required
+ if self._xSpec is not 'none':
+ if self._xUseScopeTicks:
+ xticks = self._scope_ticks(xAxis[0], xAxis[1])
+ else:
+ xticks = self._ticks(xAxis[0], xAxis[1])
+ xTextExtent = dc.GetTextExtent(xticks[-1][1])# w h of x axis text last number on axis
+ else:
+ xticks = None
+ xTextExtent= (0,0) # No text for ticks
+ if self._ySpec is not 'none':
+ yticks = self._ticks(yAxis[0], yAxis[1])
+ yTextExtentBottom= dc.GetTextExtent(yticks[0][1])
+ yTextExtentTop = dc.GetTextExtent(yticks[-1][1])
+ yTextExtent= (max(yTextExtentBottom[0],yTextExtentTop[0]),
+ max(yTextExtentBottom[1],yTextExtentTop[1]))
+ else:
+ yticks = None
+ yTextExtent= (0,0) # No text for ticks
+
+ # TextExtents for Title and Axis Labels
+ titleWH, xLabelWH, yLabelWH= self._titleLablesWH(dc, graphics)
+
+ # TextExtents for Legend
+ legendBoxWH, legendSymExt, legendTextExt = self._legendWH(dc, graphics)
+
+ # room around graph area
+ rhsW= max(xTextExtent[0], legendBoxWH[0]) # use larger of number width or legend width
+ lhsW= yTextExtent[0]+ yLabelWH[1]
+ bottomH= max(xTextExtent[1], yTextExtent[1]/2.)+ xLabelWH[1]
+ topH= yTextExtent[1]/2. + titleWH[1]
+ textSize_scale= _Numeric.array([rhsW+lhsW,bottomH+topH]) # make plot area smaller by text size
+ textSize_shift= _Numeric.array([lhsW, bottomH]) # shift plot area by this amount
+
+ # drawing title and labels text
+ dc.SetFont(self._getFont(self._fontSizeTitle))
+ titlePos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- titleWH[0]/2.,
+ self.plotbox_origin[1]- self.plotbox_size[1])
+ dc.DrawText(graphics.getTitle(),titlePos[0],titlePos[1])
+ dc.SetFont(self._getFont(self._fontSizeAxis))
+ xLabelPos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- xLabelWH[0]/2.,
+ self.plotbox_origin[1]- xLabelWH[1])
+ dc.DrawText(graphics.getXLabel(),xLabelPos[0],xLabelPos[1])
+ yLabelPos= (self.plotbox_origin[0],
+ self.plotbox_origin[1]- bottomH- (self.plotbox_size[1]-bottomH-topH)/2.+ yLabelWH[0]/2.)
+ if graphics.getYLabel(): # bug fix for Linux
+ dc.DrawRotatedText(graphics.getYLabel(),yLabelPos[0],yLabelPos[1],90)
+
+ # drawing legend makers and text
+ if self._legendEnabled:
+ self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt)
+
+ # allow for scaling and shifting plotted points
+ scale = (self.plotbox_size-textSize_scale) / (p2-p1)* _Numeric.array((1,-1))
+ shift = -p1*scale + self.plotbox_origin + textSize_shift * _Numeric.array((1,-1))
+ self._pointScale= scale # make available for mouse events
+ self._pointShift= shift
+ self._drawAxes(dc, p1, p2, scale, shift, xticks, yticks)
+
+ graphics.scaleAndShift(scale, shift)
+ graphics.setPrinterScale(self.printerScale) # thicken up lines and markers if printing
+
+ # set clipping area so drawing does not occur outside axis box
+ ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2)
+ dc.SetClippingRegion(ptx,pty,rectWidth,rectHeight)
+ # Draw the lines and markers
+ #start = _time.clock()
+ graphics.draw(dc)
+ # print "entire graphics drawing took: %f second"%(_time.clock() - start)
+ # remove the clipping region
+ dc.DestroyClippingRegion()
+ dc.EndDrawing()
+
+ def Redraw(self, dc= None):
+ """Redraw the existing plot."""
+ if self.last_draw is not None:
+ graphics, xAxis, yAxis= self.last_draw
+ self.Draw(graphics,xAxis,yAxis,dc)
+
+ def Clear(self):
+ """Erase the window."""
+ self.last_PointLabel = None #reset pointLabel
+ dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
+ dc.Clear()
+ self.last_draw = None
+
+ def Zoom(self, Center, Ratio):
+ """ Zoom on the plot
+ Centers on the X,Y coords given in Center
+ Zooms by the Ratio = (Xratio, Yratio) given
+ """
+ self.last_PointLabel = None #reset maker
+ x,y = Center
+ if self.last_draw != None:
+ (graphics, xAxis, yAxis) = self.last_draw
+ w = (xAxis[1] - xAxis[0]) * Ratio[0]
+ h = (yAxis[1] - yAxis[0]) * Ratio[1]
+ xAxis = ( x - w/2, x + w/2 )
+ yAxis = ( y - h/2, y + h/2 )
+ self.Draw(graphics, xAxis, yAxis)
+
+ def GetClosestPoints(self, pntXY, pointScaled= True):
+ """Returns list with
+ [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
+ list for each curve.
+ Returns [] if no curves are being plotted.
+
+ x, y in user coords
+ if pointScaled == True based on screen coords
+ if pointScaled == False based on user coords
+ """
+ if self.last_draw == None:
+ #no graph available
+ return []
+ graphics, xAxis, yAxis= self.last_draw
+ l = []
+ for curveNum,obj in enumerate(graphics):
+ #check there are points in the curve
+ if len(obj.points) == 0:
+ continue #go to next obj
+ #[curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
+ cn = [curveNum]+ [obj.getLegend()]+ obj.getClosestPoint( pntXY, pointScaled)
+ l.append(cn)
+ return l
+
+ def GetClosetPoint(self, pntXY, pointScaled= True):
+ """Returns list with
+ [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
+ list for only the closest curve.
+ Returns [] if no curves are being plotted.
+
+ x, y in user coords
+ if pointScaled == True based on screen coords
+ if pointScaled == False based on user coords
+ """
+ #closest points on screen based on screen scaling (pointScaled= True)
+ #list [curveNumber, index, pointXY, scaledXY, distance] for each curve
+ closestPts= self.GetClosestPoints(pntXY, pointScaled)
+ if closestPts == []:
+ return [] #no graph present
+ #find one with least distance
+ dists = [c[-1] for c in closestPts]
+ mdist = min(dists) #Min dist
+ i = dists.index(mdist) #index for min dist
+ return closestPts[i] #this is the closest point on closest curve
+
+ def UpdatePointLabel(self, mDataDict):
+ """Updates the pointLabel point on screen with data contained in
+ mDataDict.
+
+ mDataDict will be passed to your function set by
+ SetPointLabelFunc. It can contain anything you
+ want to display on the screen at the scaledXY point
+ you specify.
+
+ This function can be called from parent window with onClick,
+ onMotion events etc.
+ """
+ if self.last_PointLabel != None:
+ #compare pointXY
+ if mDataDict["pointXY"] != self.last_PointLabel["pointXY"]:
+ #closest changed
+ self._drawPointLabel(self.last_PointLabel) #erase old
+ self._drawPointLabel(mDataDict) #plot new
+ else:
+ #just plot new with no erase
+ self._drawPointLabel(mDataDict) #plot new
+ #save for next erase
+ self.last_PointLabel = mDataDict
+
+ # event handlers **********************************
+ def OnMotion(self, event):
+ if self._zoomEnabled and event.LeftIsDown():
+ if self._hasDragged:
+ self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
+ else:
+ self._hasDragged= True
+ self._zoomCorner2[0], self._zoomCorner2[1] = self.GetXY(event)
+ self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # add new
+
+ def OnMouseLeftDown(self,event):
+ self._zoomCorner1[0], self._zoomCorner1[1]= self.GetXY(event)
+
+ def OnMouseLeftUp(self, event):
+ if self._zoomEnabled:
+ if self._hasDragged == True:
+ self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
+ self._zoomCorner2[0], self._zoomCorner2[1]= self.GetXY(event)
+ self._hasDragged = False # reset flag
+ minX, minY= _Numeric.minimum( self._zoomCorner1, self._zoomCorner2)
+ maxX, maxY= _Numeric.maximum( self._zoomCorner1, self._zoomCorner2)
+ self.last_PointLabel = None #reset pointLabel
+ if self.last_draw != None:
+ self.Draw(self.last_draw[0], xAxis = (minX,maxX), yAxis = (minY,maxY), dc = None)
+ #else: # A box has not been drawn, zoom in on a point
+ ## this interfered with the double click, so I've disables it.
+ # X,Y = self.GetXY(event)
+ # self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) )
+
+ def OnMouseDoubleClick(self,event):
+ if self._zoomEnabled:
+ self.Reset()
+
+ def OnMouseRightDown(self,event):
+ if self._zoomEnabled:
+ X,Y = self.GetXY(event)
+ self.Zoom( (X,Y), (self._zoomOutFactor, self._zoomOutFactor) )
+
+ def OnPaint(self, event):
+ # All that is needed here is to draw the buffer to screen
+ if self.last_PointLabel != None:
+ self._drawPointLabel(self.last_PointLabel) #erase old
+ self.last_PointLabel = None
+ dc = wx.BufferedPaintDC(self, self._Buffer)
+
+ def OnSize(self,event):
+ # The Buffer init is done here, to make sure the buffer is always
+ # the same size as the Window
+ Size = self.GetClientSize()
+
+ # Make new offscreen bitmap: this bitmap will always have the
+ # current drawing in it, so it can be used to save the image to
+ # a file, or whatever.
+ self._Buffer = wx.EmptyBitmap(Size[0],Size[1])
+ self._setSize()
+
+ self.last_PointLabel = None #reset pointLabel
+
+ if self.last_draw is None:
+ self.Clear()
+ else:
+ graphics, xSpec, ySpec = self.last_draw
+ self.Draw(graphics,xSpec,ySpec)
+
+ def OnLeave(self, event):
+ """Used to erase pointLabel when mouse outside window"""
+ if self.last_PointLabel != None:
+ self._drawPointLabel(self.last_PointLabel) #erase old
+ self.last_PointLabel = None
+
+
+ # Private Methods **************************************************
+ def _setSize(self, width=None, height=None):
+ """DC width and height."""
+ if width == None:
+ (self.width,self.height) = self.GetClientSize()
+ else:
+ self.width, self.height= width,height
+ self.plotbox_size = 0.97*_Numeric.array([self.width, self.height])
+ xo = 0.5*(self.width-self.plotbox_size[0])
+ yo = self.height-0.5*(self.height-self.plotbox_size[1])
+ self.plotbox_origin = _Numeric.array([xo, yo])
+
+ def _setPrinterScale(self, scale):
+ """Used to thicken lines and increase marker size for print out."""
+ # line thickness on printer is very thin at 600 dot/in. Markers small
+ self.printerScale= scale
+
+ def _printDraw(self, printDC):
+ """Used for printing."""
+ if self.last_draw != None:
+ graphics, xSpec, ySpec= self.last_draw
+ self.Draw(graphics,xSpec,ySpec,printDC)
+
+ def _drawPointLabel(self, mDataDict):
+ """Draws and erases pointLabels"""
+ width = self._Buffer.GetWidth()
+ height = self._Buffer.GetHeight()
+ tmp_Buffer = wx.EmptyBitmap(width,height)
+ dcs = wx.MemoryDC()
+ dcs.SelectObject(tmp_Buffer)
+ dcs.Clear()
+ dcs.BeginDrawing()
+ self._pointLabelFunc(dcs,mDataDict) #custom user pointLabel function
+ dcs.EndDrawing()
+
+ dc = wx.ClientDC( self )
+ #this will erase if called twice
+ dc.Blit(0, 0, width, height, dcs, 0, 0, wx.EQUIV) #(NOT src) XOR dst
+
+
+ def _drawLegend(self,dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt):
+ """Draws legend symbols and text"""
+ # top right hand corner of graph box is ref corner
+ trhc= self.plotbox_origin+ (self.plotbox_size-[rhsW,topH])*[1,-1]
+ legendLHS= .091* legendBoxWH[0] # border space between legend sym and graph box
+ lineHeight= max(legendSymExt[1], legendTextExt[1]) * 1.1 #1.1 used as space between lines
+ dc.SetFont(self._getFont(self._fontSizeLegend))
+ for i in range(len(graphics)):
+ o = graphics[i]
+ s= i*lineHeight
+ if isinstance(o,PolyMarker):
+ # draw marker with legend
+ pnt= (trhc[0]+legendLHS+legendSymExt[0]/2., trhc[1]+s+lineHeight/2.)
+ o.draw(dc, self.printerScale, coord= _Numeric.array([pnt]))
+ elif isinstance(o,PolyLine):
+ # draw line with legend
+ pnt1= (trhc[0]+legendLHS, trhc[1]+s+lineHeight/2.)
+ pnt2= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.)
+ o.draw(dc, self.printerScale, coord= _Numeric.array([pnt1,pnt2]))
+ else:
+ raise TypeError, "object is neither PolyMarker or PolyLine instance"
+ # draw legend txt
+ pnt= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.-legendTextExt[1]/2)
+ dc.DrawText(o.getLegend(),pnt[0],pnt[1])
+ dc.SetFont(self._getFont(self._fontSizeAxis)) # reset
+
+ def _titleLablesWH(self, dc, graphics):
+ """Draws Title and labels and returns width and height for each"""
+ # TextExtents for Title and Axis Labels
+ dc.SetFont(self._getFont(self._fontSizeTitle))
+ title= graphics.getTitle()
+ titleWH= dc.GetTextExtent(title)
+ dc.SetFont(self._getFont(self._fontSizeAxis))
+ xLabel, yLabel= graphics.getXLabel(),graphics.getYLabel()
+ xLabelWH= dc.GetTextExtent(xLabel)
+ yLabelWH= dc.GetTextExtent(yLabel)
+ return titleWH, xLabelWH, yLabelWH
+
+ def _legendWH(self, dc, graphics):
+ """Returns the size in screen units for legend box"""
+ if self._legendEnabled != True:
+ legendBoxWH= symExt= txtExt= (0,0)
+ else:
+ # find max symbol size
+ symExt= graphics.getSymExtent(self.printerScale)
+ # find max legend text extent
+ dc.SetFont(self._getFont(self._fontSizeLegend))
+ txtList= graphics.getLegendNames()
+ txtExt= dc.GetTextExtent(txtList[0])
+ for txt in graphics.getLegendNames()[1:]:
+ txtExt= _Numeric.maximum(txtExt,dc.GetTextExtent(txt))
+ maxW= symExt[0]+txtExt[0]
+ maxH= max(symExt[1],txtExt[1])
+ # padding .1 for lhs of legend box and space between lines
+ maxW= maxW* 1.1
+ maxH= maxH* 1.1 * len(txtList)
+ dc.SetFont(self._getFont(self._fontSizeAxis))
+ legendBoxWH= (maxW,maxH)
+ return (legendBoxWH, symExt, txtExt)
+
+ def _drawRubberBand(self, corner1, corner2):
+ """Draws/erases rect box from corner1 to corner2"""
+ ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(corner1, corner2)
+ # draw rectangle
+ dc = wx.ClientDC( self )
+ dc.BeginDrawing()
+ dc.SetPen(wx.Pen(wx.BLACK))
+ dc.SetBrush(wx.Brush( wx.WHITE, wx.TRANSPARENT ) )
+ dc.SetLogicalFunction(wx.INVERT)
+ dc.DrawRectangle( ptx,pty, rectWidth,rectHeight)
+ dc.SetLogicalFunction(wx.COPY)
+ dc.EndDrawing()
+
+ def _getFont(self,size):
+ """Take font size, adjusts if printing and returns wx.Font"""
+ s = size*self.printerScale
+ of = self.GetFont()
+ # Linux speed up to get font from cache rather than X font server
+ key = (int(s), of.GetFamily (), of.GetStyle (), of.GetWeight ())
+ font = self._fontCache.get (key, None)
+ if font:
+ return font # yeah! cache hit
+ else:
+ font = wx.Font(int(s), of.GetFamily(), of.GetStyle(), of.GetWeight())
+ self._fontCache[key] = font
+ return font
+
+
+ def _point2ClientCoord(self, corner1, corner2):
+ """Converts user point coords to client screen int coords x,y,width,height"""
+ c1= _Numeric.array(corner1)
+ c2= _Numeric.array(corner2)
+ # convert to screen coords
+ pt1= c1*self._pointScale+self._pointShift
+ pt2= c2*self._pointScale+self._pointShift
+ # make height and width positive
+ pul= _Numeric.minimum(pt1,pt2) # Upper left corner
+ plr= _Numeric.maximum(pt1,pt2) # Lower right corner
+ rectWidth, rectHeight= plr-pul
+ ptx,pty= pul
+ return ptx, pty, rectWidth, rectHeight
+
+ def _axisInterval(self, spec, lower, upper):
+ """Returns sensible axis range for given spec"""
+ if spec == 'none' or spec == 'min':
+ if lower == upper:
+ return lower-0.5, upper+0.5
+ else:
+ return lower, upper
+ elif spec == 'auto':
+ range = upper-lower
+ # if range == 0.:
+ if abs(range) < 1e-36:
+ return lower-0.5, upper+0.5
+ log = _Numeric.log10(range)
+ power = _Numeric.floor(log)
+ fraction = log-power
+ if fraction <= 0.05:
+ power = power-1
+ grid = 10.**power
+ lower = lower - lower % grid
+ mod = upper % grid
+ if mod != 0:
+ upper = upper - mod + grid
+ return lower, upper
+ elif type(spec) == type(()):
+ lower, upper = spec
+ if lower <= upper:
+ return lower, upper
+ else:
+ return upper, lower
+ else:
+ raise ValueError, str(spec) + ': illegal axis specification'
+
+ def _drawAxes(self, dc, p1, p2, scale, shift, xticks, yticks):
+
+ penWidth= self.printerScale # increases thickness for printing only
+ dc.SetPen(wx.Pen(wx.NamedColour('BLACK'), penWidth))
+
+ # set length of tick marks--long ones make grid
+ if self._gridEnabled:
+ x,y,width,height= self._point2ClientCoord(p1,p2)
+ yTickLength= width/2.0 +1
+ xTickLength= height/2.0 +1
+ else:
+ yTickLength= 3 * self.printerScale # lengthens lines for printing
+ xTickLength= 3 * self.printerScale
+
+ if self._xSpec is not 'none':
+ lower, upper = p1[0],p2[0]
+ text = 1
+ for y, d in [(p1[1], -xTickLength), (p2[1], xTickLength)]: # miny, maxy and tick lengths
+ a1 = scale*_Numeric.array([lower, y])+shift
+ a2 = scale*_Numeric.array([upper, y])+shift
+ dc.DrawLine(a1[0],a1[1],a2[0],a2[1]) # draws upper and lower axis line
+ for x, label in xticks:
+ pt = scale*_Numeric.array([x, y])+shift
+ dc.DrawLine(pt[0],pt[1],pt[0],pt[1] + d) # draws tick mark d units
+ if text:
+ dc.DrawText(label,pt[0],pt[1])
+ text = 0 # axis values not drawn on top side
+
+ if self._ySpec is not 'none':
+ lower, upper = p1[1],p2[1]
+ text = 1
+ h = dc.GetCharHeight()
+ for x, d in [(p1[0], -yTickLength), (p2[0], yTickLength)]:
+ a1 = scale*_Numeric.array([x, lower])+shift
+ a2 = scale*_Numeric.array([x, upper])+shift
+ dc.DrawLine(a1[0],a1[1],a2[0],a2[1])
+ for y, label in yticks:
+ pt = scale*_Numeric.array([x, y])+shift
+ dc.DrawLine(pt[0],pt[1],pt[0]-d,pt[1])
+ if text:
+ dc.DrawText(label,pt[0]-dc.GetTextExtent(label)[0],
+ pt[1]-0.5*h)
+ text = 0 # axis values not drawn on right side
+
+ def _ticks(self, lower, upper):
+ ideal = (upper-lower)/7.
+ log = _Numeric.log10(ideal)
+ power = _Numeric.floor(log)
+ fraction = log-power
+ factor = 1.
+ error = fraction
+ for f, lf in self._multiples:
+ e = _Numeric.fabs(fraction-lf)
+ if e < error:
+ error = e
+ factor = f
+ grid = factor * 10.**power
+ if power > 4 or power < -4:
+ format = '%+7.1e'
+ elif power >= 0:
+ digits = max(1, int(power))
+ format = '%' + `digits`+'.0f'
+ else:
+ digits = -int(power)
+ format = '%'+`digits+2`+'.'+`digits`+'f'
+ ticks = []
+ t = -grid*_Numeric.floor(-lower/grid)
+ while t <= upper:
+ ticks.append( (t, format % (t,)) )
+ t = t + grid
+ return ticks
+
+ def _scope_ticks (self, lower, upper):
+ '''Always 10 divisions, no labels'''
+ grid = (upper - lower) / 10.0
+ ticks = []
+ t = lower
+ while t <= upper:
+ ticks.append( (t, ""))
+ t = t + grid
+ return ticks
+
+ _multiples = [(2., _Numeric.log10(2.)), (5., _Numeric.log10(5.))]
+
+
+#-------------------------------------------------------------------------------
+# Used to layout the printer page
+
+class PlotPrintout(wx.Printout):
+ """Controls how the plot is made in printing and previewing"""
+ # Do not change method names in this class,
+ # we have to override wx.Printout methods here!
+ def __init__(self, graph):
+ """graph is instance of plotCanvas to be printed or previewed"""
+ wx.Printout.__init__(self)
+ self.graph = graph
+
+ def HasPage(self, page):
+ if page == 1:
+ return True
+ else:
+ return False
+
+ def GetPageInfo(self):
+ return (1, 1, 1, 1) # disable page numbers
+
+ def OnPrintPage(self, page):
+ dc = self.GetDC() # allows using floats for certain functions
+## print "PPI Printer",self.GetPPIPrinter()
+## print "PPI Screen", self.GetPPIScreen()
+## print "DC GetSize", dc.GetSize()
+## print "GetPageSizePixels", self.GetPageSizePixels()
+ # Note PPIScreen does not give the correct number
+ # Calulate everything for printer and then scale for preview
+ PPIPrinter= self.GetPPIPrinter() # printer dots/inch (w,h)
+ #PPIScreen= self.GetPPIScreen() # screen dots/inch (w,h)
+ dcSize= dc.GetSize() # DC size
+ pageSize= self.GetPageSizePixels() # page size in terms of pixcels
+ clientDcSize= self.graph.GetClientSize()
+
+ # find what the margins are (mm)
+ margLeftSize,margTopSize= self.graph.pageSetupData.GetMarginTopLeft()
+ margRightSize, margBottomSize= self.graph.pageSetupData.GetMarginBottomRight()
+
+ # calculate offset and scale for dc
+ pixLeft= margLeftSize*PPIPrinter[0]/25.4 # mm*(dots/in)/(mm/in)
+ pixRight= margRightSize*PPIPrinter[0]/25.4
+ pixTop= margTopSize*PPIPrinter[1]/25.4
+ pixBottom= margBottomSize*PPIPrinter[1]/25.4
+
+ plotAreaW= pageSize[0]-(pixLeft+pixRight)
+ plotAreaH= pageSize[1]-(pixTop+pixBottom)
+
+ # ratio offset and scale to screen size if preview
+ if self.IsPreview():
+ ratioW= float(dcSize[0])/pageSize[0]
+ ratioH= float(dcSize[1])/pageSize[1]
+ pixLeft *= ratioW
+ pixTop *= ratioH
+ plotAreaW *= ratioW
+ plotAreaH *= ratioH
+
+ # rescale plot to page or preview plot area
+ self.graph._setSize(plotAreaW,plotAreaH)
+
+ # Set offset and scale
+ dc.SetDeviceOrigin(pixLeft,pixTop)
+
+ # Thicken up pens and increase marker size for printing
+ ratioW= float(plotAreaW)/clientDcSize[0]
+ ratioH= float(plotAreaH)/clientDcSize[1]
+ aveScale= (ratioW+ratioH)/2
+ self.graph._setPrinterScale(aveScale) # tickens up pens for printing
+
+ self.graph._printDraw(dc)
+ # rescale back to original
+ self.graph._setSize()
+ self.graph._setPrinterScale(1)
+ self.graph.Redraw() #to get point label scale and shift correct
+
+ return True
+
+
+
+
+#---------------------------------------------------------------------------
+# if running standalone...
+#
+# ...a sample implementation using the above
+#
+
+def _draw1Objects():
+ # 100 points sin function, plotted as green circles
+ data1 = 2.*_Numeric.pi*_Numeric.arange(200)/200.
+ data1.shape = (100, 2)
+ data1[:,1] = _Numeric.sin(data1[:,0])
+ markers1 = PolyMarker(data1, legend='Green Markers', colour='green', marker='circle',size=1)
+
+ # 50 points cos function, plotted as red line
+ data1 = 2.*_Numeric.pi*_Numeric.arange(100)/100.
+ data1.shape = (50,2)
+ data1[:,1] = _Numeric.cos(data1[:,0])
+ lines = PolyLine(data1, legend= 'Red Line', colour='red')
+
+ # A few more points...
+ pi = _Numeric.pi
+ markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
+ (3.*pi/4., -1)], legend='Cross Legend', colour='blue',
+ marker='cross')
+
+ return PlotGraphics([markers1, lines, markers2],"Graph Title", "X Axis", "Y Axis")
+
+def _draw2Objects():
+ # 100 points sin function, plotted as green dots
+ data1 = 2.*_Numeric.pi*_Numeric.arange(200)/200.
+ data1.shape = (100, 2)
+ data1[:,1] = _Numeric.sin(data1[:,0])
+ line1 = PolyLine(data1, legend='Green Line', colour='green', width=6, style=wx.DOT)
+
+ # 50 points cos function, plotted as red dot-dash
+ data1 = 2.*_Numeric.pi*_Numeric.arange(100)/100.
+ data1.shape = (50,2)
+ data1[:,1] = _Numeric.cos(data1[:,0])
+ line2 = PolyLine(data1, legend='Red Line', colour='red', width=3, style= wx.DOT_DASH)
+
+ # A few more points...
+ pi = _Numeric.pi
+ markers1 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
+ (3.*pi/4., -1)], legend='Cross Hatch Square', colour='blue', width= 3, size= 6,
+ fillcolour= 'red', fillstyle= wx.CROSSDIAG_HATCH,
+ marker='square')
+
+ return PlotGraphics([markers1, line1, line2], "Big Markers with Different Line Styles")
+
+def _draw3Objects():
+ markerList= ['circle', 'dot', 'square', 'triangle', 'triangle_down',
+ 'cross', 'plus', 'circle']
+ m=[]
+ for i in range(len(markerList)):
+ m.append(PolyMarker([(2*i+.5,i+.5)], legend=markerList[i], colour='blue',
+ marker=markerList[i]))
+ return PlotGraphics(m, "Selection of Markers", "Minimal Axis", "No Axis")
+
+def _draw4Objects():
+ # 25,000 point line
+ data1 = _Numeric.arange(5e5,1e6,10)
+ data1.shape = (25000, 2)
+ line1 = PolyLine(data1, legend='Wide Line', colour='green', width=5)
+
+ # A few more points...
+ markers2 = PolyMarker(data1, legend='Square', colour='blue',
+ marker='square')
+ return PlotGraphics([line1, markers2], "25,000 Points", "Value X", "")
+
+def _draw5Objects():
+ # Empty graph with axis defined but no points/lines
+ points=[]
+ line1 = PolyLine(points, legend='Wide Line', colour='green', width=5)
+ return PlotGraphics([line1], "Empty Plot With Just Axes", "Value X", "Value Y")
+
+def _draw6Objects():
+ # Bar graph
+ points1=[(1,0), (1,10)]
+ line1 = PolyLine(points1, colour='green', legend='Feb.', width=10)
+ points1g=[(2,0), (2,4)]
+ line1g = PolyLine(points1g, colour='red', legend='Mar.', width=10)
+ points1b=[(3,0), (3,6)]
+ line1b = PolyLine(points1b, colour='blue', legend='Apr.', width=10)
+
+ points2=[(4,0), (4,12)]
+ line2 = PolyLine(points2, colour='Yellow', legend='May', width=10)
+ points2g=[(5,0), (5,8)]
+ line2g = PolyLine(points2g, colour='orange', legend='June', width=10)
+ points2b=[(6,0), (6,4)]
+ line2b = PolyLine(points2b, colour='brown', legend='July', width=10)
+
+ return PlotGraphics([line1, line1g, line1b, line2, line2g, line2b],
+ "Bar Graph - (Turn on Grid, Legend)", "Months", "Number of Students")
+
+
+class TestFrame(wx.Frame):
+ def __init__(self, parent, id, title):
+ wx.Frame.__init__(self, parent, id, title,
+ wx.DefaultPosition, (600, 400))
+
+ # Now Create the menu bar and items
+ self.mainmenu = wx.MenuBar()
+
+ menu = wx.Menu()
+ menu.Append(200, 'Page Setup...', 'Setup the printer page')
+ self.Bind(wx.EVT_MENU, self.OnFilePageSetup, id=200)
+
+ menu.Append(201, 'Print Preview...', 'Show the current plot on page')
+ self.Bind(wx.EVT_MENU, self.OnFilePrintPreview, id=201)
+
+ menu.Append(202, 'Print...', 'Print the current plot')
+ self.Bind(wx.EVT_MENU, self.OnFilePrint, id=202)
+
+ menu.Append(203, 'Save Plot...', 'Save current plot')
+ self.Bind(wx.EVT_MENU, self.OnSaveFile, id=203)
+
+ menu.Append(205, 'E&xit', 'Enough of this already!')
+ self.Bind(wx.EVT_MENU, self.OnFileExit, id=205)
+ self.mainmenu.Append(menu, '&File')
+
+ menu = wx.Menu()
+ menu.Append(206, 'Draw1', 'Draw plots1')
+ self.Bind(wx.EVT_MENU,self.OnPlotDraw1, id=206)
+ menu.Append(207, 'Draw2', 'Draw plots2')
+ self.Bind(wx.EVT_MENU,self.OnPlotDraw2, id=207)
+ menu.Append(208, 'Draw3', 'Draw plots3')
+ self.Bind(wx.EVT_MENU,self.OnPlotDraw3, id=208)
+ menu.Append(209, 'Draw4', 'Draw plots4')
+ self.Bind(wx.EVT_MENU,self.OnPlotDraw4, id=209)
+ menu.Append(210, 'Draw5', 'Draw plots5')
+ self.Bind(wx.EVT_MENU,self.OnPlotDraw5, id=210)
+ menu.Append(260, 'Draw6', 'Draw plots6')
+ self.Bind(wx.EVT_MENU,self.OnPlotDraw6, id=260)
+
+
+ menu.Append(211, '&Redraw', 'Redraw plots')
+ self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211)
+ menu.Append(212, '&Clear', 'Clear canvas')
+ self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212)
+ menu.Append(213, '&Scale', 'Scale canvas')
+ self.Bind(wx.EVT_MENU,self.OnPlotScale, id=213)
+ menu.Append(214, 'Enable &Zoom', 'Enable Mouse Zoom', kind=wx.ITEM_CHECK)
+ self.Bind(wx.EVT_MENU,self.OnEnableZoom, id=214)
+ menu.Append(215, 'Enable &Grid', 'Turn on Grid', kind=wx.ITEM_CHECK)
+ self.Bind(wx.EVT_MENU,self.OnEnableGrid, id=215)
+ menu.Append(220, 'Enable &Legend', 'Turn on Legend', kind=wx.ITEM_CHECK)
+ self.Bind(wx.EVT_MENU,self.OnEnableLegend, id=220)
+ menu.Append(222, 'Enable &Point Label', 'Show Closest Point', kind=wx.ITEM_CHECK)
+ self.Bind(wx.EVT_MENU,self.OnEnablePointLabel, id=222)
+
+ menu.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit')
+ self.Bind(wx.EVT_MENU,self.OnScrUp, id=225)
+ menu.Append(230, 'Scroll Rt 2', 'Move View Right 2 Units')
+ self.Bind(wx.EVT_MENU,self.OnScrRt, id=230)
+ menu.Append(235, '&Plot Reset', 'Reset to original plot')
+ self.Bind(wx.EVT_MENU,self.OnReset, id=235)
+
+ self.mainmenu.Append(menu, '&Plot')
+
+ menu = wx.Menu()
+ menu.Append(300, '&About', 'About this thing...')
+ self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=300)
+ self.mainmenu.Append(menu, '&Help')
+
+ self.SetMenuBar(self.mainmenu)
+
+ # A status bar to tell people what's happening
+ self.CreateStatusBar(1)
+
+ self.client = PlotCanvas(self)
+ #define the function for drawing pointLabels
+ self.client.SetPointLabelFunc(self.DrawPointLabel)
+ # Create mouse event for showing cursor coords in status bar
+ self.client.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
+ # Show closest point when enabled
+ self.client.Bind(wx.EVT_MOTION, self.OnMotion)
+
+ self.Show(True)
+
+ def DrawPointLabel(self, dc, mDataDict):
+ """This is the fuction that defines how the pointLabels are plotted
+ dc - DC that will be passed
+ mDataDict - Dictionary of data that you want to use for the pointLabel
+
+ As an example I have decided I want a box at the curve point
+ with some text information about the curve plotted below.
+ Any wxDC method can be used.
+ """
+ # ----------
+ dc.SetPen(wx.Pen(wx.BLACK))
+ dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) )
+
+ sx, sy = mDataDict["scaledXY"] #scaled x,y of closest point
+ dc.DrawRectangle( sx-5,sy-5, 10, 10) #10by10 square centered on point
+ px,py = mDataDict["pointXY"]
+ cNum = mDataDict["curveNum"]
+ pntIn = mDataDict["pIndex"]
+ legend = mDataDict["legend"]
+ #make a string to display
+ s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" %(cNum, legend, px, py, pntIn)
+ dc.DrawText(s, sx , sy+1)
+ # -----------
+
+ def OnMouseLeftDown(self,event):
+ s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client.GetXY(event)
+ self.SetStatusText(s)
+ event.Skip() #allows plotCanvas OnMouseLeftDown to be called
+
+ def OnMotion(self, event):
+ #show closest point (when enbled)
+ if self.client.GetEnablePointLabel() == True:
+ #make up dict with info for the pointLabel
+ #I've decided to mark the closest point on the closest curve
+ dlst= self.client.GetClosetPoint( self.client.GetXY(event), pointScaled= True)
+ if dlst != []: #returns [] if none
+ curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst
+ #make up dictionary to pass to my user function (see DrawPointLabel)
+ mDataDict= {"curveNum":curveNum, "legend":legend, "pIndex":pIndex,\
+ "pointXY":pointXY, "scaledXY":scaledXY}
+ #pass dict to update the pointLabel
+ self.client.UpdatePointLabel(mDataDict)
+ event.Skip() #go to next handler
+
+ def OnFilePageSetup(self, event):
+ self.client.PageSetup()
+
+ def OnFilePrintPreview(self, event):
+ self.client.PrintPreview()
+
+ def OnFilePrint(self, event):
+ self.client.Printout()
+
+ def OnSaveFile(self, event):
+ self.client.SaveFile()
+
+ def OnFileExit(self, event):
+ self.Close()
+
+ def OnPlotDraw1(self, event):
+ self.resetDefaults()
+ self.client.Draw(_draw1Objects())
+
+ def OnPlotDraw2(self, event):
+ self.resetDefaults()
+ self.client.Draw(_draw2Objects())
+
+ def OnPlotDraw3(self, event):
+ self.resetDefaults()
+ self.client.SetFont(wx.Font(10,wx.SCRIPT,wx.NORMAL,wx.NORMAL))
+ self.client.SetFontSizeAxis(20)
+ self.client.SetFontSizeLegend(12)
+ self.client.SetXSpec('min')
+ self.client.SetYSpec('none')
+ self.client.Draw(_draw3Objects())
+
+ def OnPlotDraw4(self, event):
+ self.resetDefaults()
+ drawObj= _draw4Objects()
+ self.client.Draw(drawObj)
+## # profile
+## start = _time.clock()
+## for x in range(10):
+## self.client.Draw(drawObj)
+## print "10 plots of Draw4 took: %f sec."%(_time.clock() - start)
+## # profile end
+
+ def OnPlotDraw5(self, event):
+ # Empty plot with just axes
+ self.resetDefaults()
+ drawObj= _draw5Objects()
+ # make the axis X= (0,5), Y=(0,10)
+ # (default with None is X= (-1,1), Y= (-1,1))
+ self.client.Draw(drawObj, xAxis= (0,5), yAxis= (0,10))
+
+ def OnPlotDraw6(self, event):
+ #Bar Graph Example
+ self.resetDefaults()
+ #self.client.SetEnableLegend(True) #turn on Legend
+ #self.client.SetEnableGrid(True) #turn on Grid
+ self.client.SetXSpec('none') #turns off x-axis scale
+ self.client.SetYSpec('auto')
+ self.client.Draw(_draw6Objects(), xAxis= (0,7))
+
+ def OnPlotRedraw(self,event):
+ self.client.Redraw()
+
+ def OnPlotClear(self,event):
+ self.client.Clear()
+
+ def OnPlotScale(self, event):
+ if self.client.last_draw != None:
+ graphics, xAxis, yAxis= self.client.last_draw
+ self.client.Draw(graphics,(1,3.05),(0,1))
+
+ def OnEnableZoom(self, event):
+ self.client.SetEnableZoom(event.IsChecked())
+
+ def OnEnableGrid(self, event):
+ self.client.SetEnableGrid(event.IsChecked())
+
+ def OnEnableLegend(self, event):
+ self.client.SetEnableLegend(event.IsChecked())
+
+ def OnEnablePointLabel(self, event):
+ self.client.SetEnablePointLabel(event.IsChecked())
+
+ def OnScrUp(self, event):
+ self.client.ScrollUp(1)
+
+ def OnScrRt(self,event):
+ self.client.ScrollRight(2)
+
+ def OnReset(self,event):
+ self.client.Reset()
+
+ def OnHelpAbout(self, event):
+ from wx.lib.dialogs import ScrolledMessageDialog
+ about = ScrolledMessageDialog(self, __doc__, "About...")
+ about.ShowModal()
+
+ def resetDefaults(self):
+ """Just to reset the fonts back to the PlotCanvas defaults"""
+ self.client.SetFont(wx.Font(10,wx.SWISS,wx.NORMAL,wx.NORMAL))
+ self.client.SetFontSizeAxis(10)
+ self.client.SetFontSizeLegend(7)
+ self.client.SetXSpec('auto')
+ self.client.SetYSpec('auto')
+
+
+def __test():
+
+ class MyApp(wx.App):
+ def OnInit(self):
+ wx.InitAllImageHandlers()
+ frame = TestFrame(None, -1, "PlotCanvas")
+ #frame.Show(True)
+ self.SetTopWindow(frame)
+ return True
+
+
+ app = MyApp(0)
+ app.MainLoop()
+
+if __name__ == '__main__':
+ __test()
diff --git a/gr-wxgui/src/python/powermate.py b/gr-wxgui/src/python/powermate.py
new file mode 100755
index 000000000..36be408c4
--- /dev/null
+++ b/gr-wxgui/src/python/powermate.py
@@ -0,0 +1,437 @@
+#!/usr/bin/env python
+#
+# Copyright 2005 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio 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 2, or (at your option)
+# any later version.
+#
+# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+#
+
+"""
+Handler for Griffin PowerMate, Contour ShuttlePro & ShuttleXpress USB knobs
+
+This is Linux and wxPython specific.
+"""
+import select
+import os
+import fcntl
+import struct
+import exceptions
+import threading
+import sys
+import wx
+from gnuradio import gru
+
+# First a little bit of background:
+#
+# The Griffin PowerMate has
+# * a single knob which rotates
+# * a single button (pressing the knob)
+#
+# The Contour ShuttleXpress (aka SpaceShuttle) has
+# * "Jog Wheel" -- the knob (rotary encoder) on the inside
+# * "Shuttle Ring" -- the spring loaded rubber covered ring
+# * 5 buttons
+#
+# The Contour ShuttlePro has
+# * "Jog Wheel" -- the knob (rotary encoder) on the inside
+# * "Shuttle Ring" -- the spring loaded rubber covered ring
+# * 13 buttons
+#
+# The Contour ShuttlePro V2 has
+# *"Jog Wheel" -- the knob (rotary encoder) on the inside
+# * "Shuttle Ring" -- the spring loaded rubber covered ring
+# * 15 buttons
+
+# We remap all the buttons on the devices so that they start at zero.
+
+# For the ShuttleXpress the buttons are 0 to 4 (left to right)
+
+# For the ShuttlePro, we number the buttons immediately above
+# the ring 0 to 4 (left to right) so that they match our numbering
+# on the ShuttleXpress. The top row is 5, 6, 7, 8. The first row below
+# the ring is 9, 10, and the bottom row is 11, 12.
+
+# For the ShuttlePro V2, buttons 13 & 14 are to the
+# left and right of the wheel respectively.
+
+# We generate 3 kinds of events:
+#
+# button press/release (button_number, press/release)
+# knob rotation (relative_clicks) # typically -1, +1
+# shuttle position (absolute_position) # -7,-6,...,0,...,6,7
+
+# ----------------------------------------------------------------
+# Our ID's for the devices:
+# Not to be confused with anything related to magic hardware numbers.
+
+ID_POWERMATE = 'powermate'
+ID_SHUTTLE_XPRESS = 'shuttle xpress'
+ID_SHUTTLE_PRO = 'shuttle pro'
+ID_SHUTTLE_PRO_V2 = 'shuttle pro v2'
+
+# ------------------------------------------------------------------------
+# format of messages that we read from /dev/input/event*
+# See /usr/include/linux/input.h for more info
+#
+#struct input_event {
+# struct timeval time; = {long seconds, long microseconds}
+# unsigned short type;
+# unsigned short code;
+# unsigned int value;
+#};
+
+input_event_struct = "@llHHi"
+input_event_size = struct.calcsize(input_event_struct)
+
+# ------------------------------------------------------------------------
+# input_event types
+# ------------------------------------------------------------------------
+
+IET_SYN = 0x00 # aka RESET
+IET_KEY = 0x01 # key or button press/release
+IET_REL = 0x02 # relative movement (knob rotation)
+IET_ABS = 0x03 # absolute position (graphics pad, etc)
+IET_MSC = 0x04
+IET_LED = 0x11
+IET_SND = 0x12
+IET_REP = 0x14
+IET_FF = 0x15
+IET_PWR = 0x16
+IET_FF_STATUS = 0x17
+IET_MAX = 0x1f
+
+# ------------------------------------------------------------------------
+# input_event codes (there are a zillion of them, we only define a few)
+# ------------------------------------------------------------------------
+
+# these are valid for IET_KEY
+
+IEC_BTN_0 = 0x100
+IEC_BTN_1 = 0x101
+IEC_BTN_2 = 0x102
+IEC_BTN_3 = 0x103
+IEC_BTN_4 = 0x104
+IEC_BTN_5 = 0x105
+IEC_BTN_6 = 0x106
+IEC_BTN_7 = 0x107
+IEC_BTN_8 = 0x108
+IEC_BTN_9 = 0x109
+IEC_BTN_10 = 0x10a
+IEC_BTN_11 = 0x10b
+IEC_BTN_12 = 0x10c
+IEC_BTN_13 = 0x10d
+IEC_BTN_14 = 0x10e
+IEC_BTN_15 = 0x10f
+
+# these are valid for IET_REL (Relative axes)
+
+IEC_REL_X = 0x00
+IEC_REL_Y = 0x01
+IEC_REL_Z = 0x02
+IEC_REL_HWHEEL = 0x06
+IEC_REL_DIAL = 0x07 # rotating the knob
+IEC_REL_WHEEL = 0x08 # moving the shuttle ring
+IEC_REL_MISC = 0x09
+IEC_REL_MAX = 0x0f
+
+# ------------------------------------------------------------------------
+
+class powermate(threading.Thread):
+ """
+ Interface to Griffin PowerMate and Contour Shuttles
+ """
+ def __init__(self, event_receiver=None, filename=None, **kwargs):
+ self.event_receiver = event_receiver
+ self.handle = -1
+ if filename:
+ if not self._open_device(filename):
+ raise exceptions.RuntimeError, 'Unable to find powermate'
+ else:
+ ok = False
+ for d in range(0, 16):
+ if self._open_device("/dev/input/event%d" % d):
+ ok = True
+ break
+ if not ok:
+ raise exceptions.RuntimeError, 'Unable to find powermate'
+
+ threading.Thread.__init__(self, **kwargs)
+ self.setDaemon (1)
+ self.keep_running = True
+ self.start ()
+
+ def __del__(self):
+ self.keep_running = False
+ if self.handle >= 0:
+ os.close(self.handle)
+ self.handle = -1
+
+ def _open_device(self, filename):
+ try:
+ self.handle = os.open(filename, os.O_RDWR)
+ if self.handle < 0:
+ return False
+
+ # read event device name
+ name = fcntl.ioctl(self.handle, gru.hexint(0x80ff4506), chr(0) * 256)
+ name = name.replace(chr(0), '')
+
+ # do we see anything we recognize?
+ if name == 'Griffin PowerMate' or name == 'Griffin SoundKnob':
+ self.id = ID_POWERMATE
+ self.mapper = _powermate_remapper()
+ elif name == 'CAVS SpaceShuttle A/V' or name == 'Contour Design ShuttleXpress':
+ self.id = ID_SHUTTLE_XPRESS
+ self.mapper = _contour_remapper()
+ elif name == 'Contour Design ShuttlePRO':
+ self.id = ID_SHUTTLE_PRO
+ self.mapper = _contour_remapper()
+ elif name == 'Contour Design ShuttlePRO v2':
+ self.id = ID_SHUTTLE_PRO_V2
+ self.mapper = _contour_remapper()
+ else:
+ os.close(self.handle)
+ self.handle = -1
+ return False
+
+ # get exclusive control of the device, using ioctl EVIOCGRAB
+ # there may be an issue with this on non x86 platforms and if
+ # the _IOW,_IOC,... macros in <asm/ioctl.h> are changed
+ fcntl.ioctl(self.handle,gru.hexint(0x40044590), 1)
+ return True
+ except exceptions.OSError:
+ return False
+
+
+ def set_event_receiver(self, obj):
+ self.event_receiver = obj
+
+
+ def set_led_state(self, static_brightness, pulse_speed=0,
+ pulse_table=0, pulse_on_sleep=0, pulse_on_wake=0):
+ """
+ What do these magic values mean...
+ """
+ if self.id != ID_POWERMATE:
+ return False
+
+ static_brightness &= 0xff;
+ if pulse_speed < 0:
+ pulse_speed = 0
+ if pulse_speed > 510:
+ pulse_speed = 510
+ if pulse_table < 0:
+ pulse_table = 0
+ if pulse_table > 2:
+ pulse_table = 2
+ pulse_on_sleep = not not pulse_on_sleep # not not = convert to 0/1
+ pulse_on_wake = not not pulse_on_wake
+ magic = (static_brightness
+ | (pulse_speed << 8)
+ | (pulse_table << 17)
+ | (pulse_on_sleep << 19)
+ | (pulse_on_wake << 20))
+ data = struct.pack(input_event_struct, 0, 0, 0x04, 0x01, magic)
+ os.write(self.handle, data)
+ return True
+
+ def run (self):
+ while (self.keep_running):
+ s = os.read (self.handle, input_event_size)
+ if not s:
+ self.keep_running = False
+ break
+
+ raw_input_event = struct.unpack(input_event_struct,s)
+ sec, usec, type, code, val = self.mapper(raw_input_event)
+
+ if self.event_receiver is None:
+ continue
+
+ if type == IET_SYN: # ignore
+ pass
+ elif type == IET_MSC: # ignore (seems to be PowerMate reporting led brightness)
+ pass
+ elif type == IET_REL and code == IEC_REL_DIAL:
+ #print "Dial: %d" % (val,)
+ wx.PostEvent(self.event_receiver, PMRotateEvent(val))
+ elif type == IET_REL and code == IEC_REL_WHEEL:
+ #print "Shuttle: %d" % (val,)
+ wx.PostEvent(self.event_receiver, PMShuttleEvent(val))
+ elif type == IET_KEY:
+ #print "Key: Btn%d %d" % (code - IEC_BTN_0, val)
+ wx.PostEvent(self.event_receiver,
+ PMButtonEvent(code - IEC_BTN_0, val))
+ else:
+ print "powermate: unrecognized event: type = 0x%x code = 0x%x val = %d" % (type, code, val)
+
+
+class _powermate_remapper(object):
+ def __init__(self):
+ pass
+ def __call__(self, event):
+ """
+ Notice how nice and simple this is...
+ """
+ return event
+
+class _contour_remapper(object):
+ def __init__(self):
+ self.prev = None
+ def __call__(self, event):
+ """
+ ...and how screwed up this is
+ """
+ sec, usec, type, code, val = event
+ if type == IET_REL and code == IEC_REL_WHEEL:
+ # === Shuttle ring ===
+ # First off, this really ought to be IET_ABS, not IET_REL!
+ # They never generate a zero value so you can't
+ # tell when the shuttle ring is back in the center.
+ # We kludge around this by calling both -1 and 1 zero.
+ if val == -1 or val == 1:
+ return (sec, usec, type, code, 0)
+ return event
+
+ if type == IET_REL and code == IEC_REL_DIAL:
+ # === Jog knob (rotary encoder) ===
+ # Dim wits got it wrong again! This one should return a
+ # a relative value, e.g., -1, +1. Instead they return
+ # a total that runs modulo 256 (almost!). For some
+ # reason they count like this 253, 254, 255, 1, 2, 3
+
+ if self.prev is None: # first time call
+ self.prev = val
+ return (sec, usec, IET_SYN, 0, 0) # will be ignored above
+
+ diff = val - self.prev
+ if diff == 0: # sometimes it just sends stuff...
+ return (sec, usec, IET_SYN, 0, 0) # will be ignored above
+
+ if abs(diff) > 100: # crossed into the twilight zone
+ if self.prev > val: # we've wrapped going forward
+ self.prev = val
+ return (sec, usec, type, code, +1)
+ else: # we've wrapped going backward
+ self.prev = val
+ return (sec, usec, type, code, -1)
+
+ self.prev = val
+ return (sec, usec, type, code, diff)
+
+ if type == IET_KEY:
+ # remap keys so that all 3 gadgets have buttons 0 to 4 in common
+ return (sec, usec, type,
+ (IEC_BTN_5, IEC_BTN_6, IEC_BTN_7, IEC_BTN_8,
+ IEC_BTN_0, IEC_BTN_1, IEC_BTN_2, IEC_BTN_3, IEC_BTN_4,
+ IEC_BTN_9, IEC_BTN_10,
+ IEC_BTN_11, IEC_BTN_12,
+ IEC_BTN_13, IEC_BTN_14)[code - IEC_BTN_0], val)
+
+ return event
+
+# ------------------------------------------------------------------------
+# new wxPython event classes
+# ------------------------------------------------------------------------
+
+grEVT_POWERMATE_BUTTON = wx.NewEventType()
+grEVT_POWERMATE_ROTATE = wx.NewEventType()
+grEVT_POWERMATE_SHUTTLE = wx.NewEventType()
+
+EVT_POWERMATE_BUTTON = wx.PyEventBinder(grEVT_POWERMATE_BUTTON, 0)
+EVT_POWERMATE_ROTATE = wx.PyEventBinder(grEVT_POWERMATE_ROTATE, 0)
+EVT_POWERMATE_SHUTTLE = wx.PyEventBinder(grEVT_POWERMATE_SHUTTLE, 0)
+
+class PMButtonEvent(wx.PyEvent):
+ def __init__(self, button, value):
+ wx.PyEvent.__init__(self)
+ self.SetEventType(grEVT_POWERMATE_BUTTON)
+ self.button = button
+ self.value = value
+
+ def Clone (self):
+ self.__class__(self.GetId())
+
+
+class PMRotateEvent(wx.PyEvent):
+ def __init__(self, delta):
+ wx.PyEvent.__init__(self)
+ self.SetEventType (grEVT_POWERMATE_ROTATE)
+ self.delta = delta
+
+ def Clone (self):
+ self.__class__(self.GetId())
+
+
+class PMShuttleEvent(wx.PyEvent):
+ def __init__(self, position):
+ wx.PyEvent.__init__(self)
+ self.SetEventType (grEVT_POWERMATE_SHUTTLE)
+ self.position = position
+
+ def Clone (self):
+ self.__class__(self.GetId())
+
+# ------------------------------------------------------------------------
+# Example usage
+# ------------------------------------------------------------------------
+
+if __name__ == '__main__':
+ class Frame(wx.Frame):
+ def __init__(self,parent=None,id=-1,title='Title',
+ pos=wx.DefaultPosition, size=(400,200)):
+ wx.Frame.__init__(self,parent,id,title,pos,size)
+ EVT_POWERMATE_BUTTON(self, self.on_button)
+ EVT_POWERMATE_ROTATE(self, self.on_rotate)
+ EVT_POWERMATE_SHUTTLE(self, self.on_shuttle)
+ self.brightness = 128
+ self.pulse_speed = 0
+
+ try:
+ self.pm = powermate(self)
+ except:
+ sys.stderr.write("Unable to find PowerMate or Contour Shuttle\n")
+ sys.exit(1)
+
+ self.pm.set_led_state(self.brightness, self.pulse_speed)
+
+
+ def on_button(self, evt):
+ print "Button %d %s" % (evt.button,
+ ("Released", "Pressed")[evt.value])
+
+ def on_rotate(self, evt):
+ print "Rotated %d" % (evt.delta,)
+ if 0:
+ new = max(0, min(255, self.brightness + evt.delta))
+ if new != self.brightness:
+ self.brightness = new
+ self.pm.set_led_state(self.brightness, self.pulse_speed)
+
+ def on_shuttle(self, evt):
+ print "Shuttle %d" % (evt.position,)
+
+ class App(wx.App):
+ def OnInit(self):
+ title='PowerMate Demo'
+ self.frame = Frame(parent=None,id=-1,title=title)
+ self.frame.Show()
+ self.SetTopWindow(self.frame)
+ return True
+
+ app = App()
+ app.MainLoop ()
diff --git a/gr-wxgui/src/python/scopesink.py b/gr-wxgui/src/python/scopesink.py
new file mode 100755
index 000000000..f231b3b79
--- /dev/null
+++ b/gr-wxgui/src/python/scopesink.py
@@ -0,0 +1,650 @@
+#!/usr/bin/env python
+#
+# Copyright 2003,2004,2006 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio 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 2, or (at your option)
+# any later version.
+#
+# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+#
+
+from gnuradio import gr, gru, eng_notation
+from gnuradio.wxgui import stdgui
+import wx
+import gnuradio.wxgui.plot as plot
+import Numeric
+import threading
+import struct
+
+default_scopesink_size = (640, 240)
+default_v_scale = 1000
+default_frame_decim = gr.prefs().get_long('wxgui', 'frame_decim', 1)
+
+class scope_sink_f(gr.hier_block):
+ def __init__(self, fg, parent, title='', sample_rate=1,
+ size=default_scopesink_size, frame_decim=default_frame_decim,
+ v_scale=default_v_scale, t_scale=None):
+ msgq = gr.msg_queue(2) # message queue that holds at most 2 messages
+ self.guts = gr.oscope_sink_f(sample_rate, msgq)
+ gr.hier_block.__init__(self, fg, self.guts, self.guts)
+ self.win = scope_window(win_info (msgq, sample_rate, frame_decim,
+ v_scale, t_scale, self.guts, title), parent)
+
+ def set_sample_rate(self, sample_rate):
+ self.guts.set_sample_rate(sample_rate)
+ self.win.info.set_sample_rate(sample_rate)
+
+class scope_sink_c(gr.hier_block):
+ def __init__(self, fg, parent, title='', sample_rate=1,
+ size=default_scopesink_size, frame_decim=default_frame_decim,
+ v_scale=default_v_scale, t_scale=None):
+ msgq = gr.msg_queue(2) # message queue that holds at most 2 messages
+ c2f = gr.complex_to_float()
+ self.guts = gr.oscope_sink_f(sample_rate, msgq)
+ fg.connect((c2f, 0), (self.guts, 0))
+ fg.connect((c2f, 1), (self.guts, 1))
+ gr.hier_block.__init__(self, fg, c2f, self.guts)
+ self.win = scope_window(win_info(msgq, sample_rate, frame_decim,
+ v_scale, t_scale, self.guts, title), parent)
+
+ def set_sample_rate(self, sample_rate):
+ self.guts.set_sample_rate(sample_rate)
+ self.win.info.set_sample_rate(sample_rate)
+
+# ========================================================================
+# This is the deprecated interface, retained for compatibility...
+#
+# returns (block, win).
+# block requires a N input stream of float
+# win is a subclass of wxWindow
+
+def make_scope_sink_f (fg, parent, label, input_rate):
+ block = scope_sink_f(fg, parent, title=label, sample_rate=input_rate)
+ return (block, block.win)
+
+# ========================================================================
+
+
+time_base_list = [ # time / division
+ 1.0e-7, # 100ns / div
+ 2.5e-7,
+ 5.0e-7,
+ 1.0e-6, # 1us / div
+ 2.5e-6,
+ 5.0e-6,
+ 1.0e-5, # 10us / div
+ 2.5e-5,
+ 5.0e-5,
+ 1.0e-4, # 100us / div
+ 2.5e-4,
+ 5.0e-4,
+ 1.0e-3, # 1ms / div
+ 2.5e-3,
+ 5.0e-3,
+ 1.0e-2, # 10ms / div
+ 2.5e-2,
+ 5.0e-2
+ ]
+
+v_scale_list = [ # counts / div, LARGER gains are SMALLER /div, appear EARLIER
+ 2.0e-3, # 2m / div, don't call it V/div it's actually counts/div
+ 5.0e-3,
+ 1.0e-2,
+ 2.0e-2,
+ 5.0e-2,
+ 1.0e-1,
+ 2.0e-1,
+ 5.0e-1,
+ 1.0e+0,
+ 2.0e+0,
+ 5.0e+0,
+ 1.0e+1,
+ 2.0e+1,
+ 5.0e+1,
+ 1.0e+2,
+ 2.0e+2,
+ 5.0e+2,
+ 1.0e+3,
+ 2.0e+3,
+ 5.0e+3,
+ 1.0e+4 # 10000 /div, USRP full scale is -/+ 32767
+ ]
+
+
+wxDATA_EVENT = wx.NewEventType()
+
+def EVT_DATA_EVENT(win, func):
+ win.Connect(-1, -1, wxDATA_EVENT, func)
+
+class DataEvent(wx.PyEvent):
+ def __init__(self, data):
+ wx.PyEvent.__init__(self)
+ self.SetEventType (wxDATA_EVENT)
+ self.data = data
+
+ def Clone (self):
+ self.__class__ (self.GetId())
+
+
+class win_info (object):
+ __slots__ = ['msgq', 'sample_rate', 'frame_decim', 'v_scale',
+ 'scopesink', 'title',
+ 'time_scale_cursor', 'v_scale_cursor', 'marker', 'xy',
+ 'autorange', 'running']
+
+ def __init__ (self, msgq, sample_rate, frame_decim, v_scale, t_scale,
+ scopesink, title = "Oscilloscope"):
+ self.msgq = msgq
+ self.sample_rate = sample_rate
+ self.frame_decim = frame_decim
+ self.scopesink = scopesink
+ self.title = title;
+
+ self.time_scale_cursor = gru.seq_with_cursor(time_base_list, initial_value = t_scale)
+ self.v_scale_cursor = gru.seq_with_cursor(v_scale_list, initial_value = v_scale)
+
+ self.marker = 'line'
+ self.xy = False
+ if v_scale == None: # 0 and None are both False, but 0 != None
+ self.autorange = True
+ else:
+ self.autorange = False # 0 is a valid v_scale
+ self.running = True
+
+ def get_time_per_div (self):
+ return self.time_scale_cursor.current ()
+
+ def get_volts_per_div (self):
+ return self.v_scale_cursor.current ()
+
+ def set_sample_rate(self, sample_rate):
+ self.sample_rate = sample_rate
+
+ def get_sample_rate (self):
+ return self.sample_rate
+
+ def get_decimation_rate (self):
+ return 1.0
+
+ def set_marker (self, s):
+ self.marker = s
+
+ def get_marker (self):
+ return self.marker
+
+
+class input_watcher (threading.Thread):
+ def __init__ (self, msgq, event_receiver, frame_decim, **kwds):
+ threading.Thread.__init__ (self, **kwds)
+ self.setDaemon (1)
+ self.msgq = msgq
+ self.event_receiver = event_receiver
+ self.frame_decim = frame_decim
+ self.iscan = 0
+ self.keep_running = True
+ self.start ()
+
+ def run (self):
+ # print "input_watcher: pid = ", os.getpid ()
+ while (self.keep_running):
+ msg = self.msgq.delete_head() # blocking read of message queue
+ if self.iscan == 0: # only display at frame_decim
+ self.iscan = self.frame_decim
+
+ nchan = int(msg.arg1()) # number of channels of data in msg
+ nsamples = int(msg.arg2()) # number of samples in each channel
+
+ s = msg.to_string() # get the body of the msg as a string
+
+ bytes_per_chan = nsamples * gr.sizeof_float
+
+ records = []
+ for ch in range (nchan):
+
+ start = ch * bytes_per_chan
+ chan_data = s[start:start+bytes_per_chan]
+ rec = Numeric.fromstring (chan_data, Numeric.Float32)
+ records.append (rec)
+
+ # print "nrecords = %d, reclen = %d" % (len (records),nsamples)
+
+ de = DataEvent (records)
+ wx.PostEvent (self.event_receiver, de)
+ records = []
+ del de
+
+ # end if iscan == 0
+ self.iscan -= 1
+
+
+class scope_window (wx.Panel):
+
+ def __init__ (self, info, parent, id = -1,
+ pos = wx.DefaultPosition, size = wx.DefaultSize, name = ""):
+ wx.Panel.__init__ (self, parent, -1)
+ self.info = info
+
+ vbox = wx.BoxSizer (wx.VERTICAL)
+
+ self.graph = graph_window (info, self, -1)
+
+ vbox.Add (self.graph, 1, wx.EXPAND)
+ vbox.Add (self.make_control_box(), 0, wx.EXPAND)
+ vbox.Add (self.make_control2_box(), 0, wx.EXPAND)
+
+ self.sizer = vbox
+ self.SetSizer (self.sizer)
+ self.SetAutoLayout (True)
+ self.sizer.Fit (self)
+ self.set_autorange(self.info.autorange)
+
+
+ # second row of control buttons etc. appears BELOW control_box
+ def make_control2_box (self):
+ ctrlbox = wx.BoxSizer (wx.HORIZONTAL)
+
+ self.inc_v_button = wx.Button (self, 1101, " < ", style=wx.BU_EXACTFIT)
+ self.inc_v_button.SetToolTipString ("Increase vertical range")
+ wx.EVT_BUTTON (self, 1101, self.incr_v_scale) # ID matches button ID above
+
+ self.dec_v_button = wx.Button (self, 1100, " > ", style=wx.BU_EXACTFIT)
+ self.dec_v_button.SetToolTipString ("Decrease vertical range")
+ wx.EVT_BUTTON (self, 1100, self.decr_v_scale)
+
+ self.v_scale_label = wx.StaticText (self, 1002, "None") # vertical /div
+ self.update_v_scale_label ()
+
+ self.autorange_checkbox = wx.CheckBox (self, 1102, "Autorange")
+ self.autorange_checkbox.SetToolTipString ("Select autorange on/off")
+ wx.EVT_CHECKBOX(self, 1102, self.autorange_checkbox_event)
+
+ ctrlbox.Add ((5,0) ,0) # left margin space
+ ctrlbox.Add (self.inc_v_button, 0, wx.EXPAND)
+ ctrlbox.Add (self.dec_v_button, 0, wx.EXPAND)
+ ctrlbox.Add (self.v_scale_label, 0, wx.ALIGN_CENTER)
+ ctrlbox.Add ((20,0) ,0) # spacer
+ ctrlbox.Add (self.autorange_checkbox, 0, wx.ALIGN_CENTER)
+
+ return ctrlbox
+
+ def make_control_box (self):
+ ctrlbox = wx.BoxSizer (wx.HORIZONTAL)
+
+ tb_left = wx.Button (self, 1001, " < ", style=wx.BU_EXACTFIT)
+ tb_left.SetToolTipString ("Increase time base")
+ wx.EVT_BUTTON (self, 1001, self.incr_timebase)
+
+
+ tb_right = wx.Button (self, 1000, " > ", style=wx.BU_EXACTFIT)
+ tb_right.SetToolTipString ("Decrease time base")
+ wx.EVT_BUTTON (self, 1000, self.decr_timebase)
+
+ self.time_base_label = wx.StaticText (self, 1002, "")
+ self.update_timebase_label ()
+
+ ctrlbox.Add ((5,0) ,0)
+ # ctrlbox.Add (wx.StaticText (self, -1, "Horiz Scale: "), 0, wx.ALIGN_CENTER)
+ ctrlbox.Add (tb_left, 0, wx.EXPAND)
+ ctrlbox.Add (tb_right, 0, wx.EXPAND)
+ ctrlbox.Add (self.time_base_label, 0, wx.ALIGN_CENTER)
+
+ ctrlbox.Add ((10,0) ,1) # stretchy space
+
+ ctrlbox.Add (wx.StaticText (self, -1, "Trig: "), 0, wx.ALIGN_CENTER)
+ self.trig_chan_choice = wx.Choice (self, 1004,
+ choices = ['Ch1', 'Ch2', 'Ch3', 'Ch4'])
+ self.trig_chan_choice.SetToolTipString ("Select channel for trigger")
+ wx.EVT_CHOICE (self, 1004, self.trig_chan_choice_event)
+ ctrlbox.Add (self.trig_chan_choice, 0, wx.ALIGN_CENTER)
+
+ self.trig_mode_choice = wx.Choice (self, 1005,
+ choices = ['Pos', 'Neg', 'Auto'])
+ self.trig_mode_choice.SetToolTipString ("Select trigger slope or Auto (untriggered roll)")
+ wx.EVT_CHOICE (self, 1005, self.trig_mode_choice_event)
+ ctrlbox.Add (self.trig_mode_choice, 0, wx.ALIGN_CENTER)
+
+ trig_level50 = wx.Button (self, 1006, "50%")
+ trig_level50.SetToolTipString ("Set trigger level to 50%")
+ wx.EVT_BUTTON (self, 1006, self.set_trig_level50)
+ ctrlbox.Add (trig_level50, 0, wx.EXPAND)
+
+ run_stop = wx.Button (self, 1007, "Run/Stop")
+ run_stop.SetToolTipString ("Toggle Run/Stop mode")
+ wx.EVT_BUTTON (self, 1007, self.run_stop)
+ ctrlbox.Add (run_stop, 0, wx.EXPAND)
+
+ ctrlbox.Add ((10, 0) ,1) # stretchy space
+
+ ctrlbox.Add (wx.StaticText (self, -1, "Fmt: "), 0, wx.ALIGN_CENTER)
+ self.marker_choice = wx.Choice (self, 1002, choices = self._marker_choices)
+ self.marker_choice.SetToolTipString ("Select plotting with lines, pluses or dots")
+ wx.EVT_CHOICE (self, 1002, self.marker_choice_event)
+ ctrlbox.Add (self.marker_choice, 0, wx.ALIGN_CENTER)
+
+ self.xy_choice = wx.Choice (self, 1003, choices = ['X:t', 'X:Y'])
+ self.xy_choice.SetToolTipString ("Select X vs time or X vs Y display")
+ wx.EVT_CHOICE (self, 1003, self.xy_choice_event)
+ ctrlbox.Add (self.xy_choice, 0, wx.ALIGN_CENTER)
+
+ return ctrlbox
+
+ _marker_choices = ['line', 'plus', 'dot']
+
+ def update_timebase_label (self):
+ time_per_div = self.info.get_time_per_div ()
+ s = ' ' + eng_notation.num_to_str (time_per_div) + 's/div'
+ self.time_base_label.SetLabel (s)
+
+ def decr_timebase (self, evt):
+ self.info.time_scale_cursor.prev ()
+ self.update_timebase_label ()
+
+ def incr_timebase (self, evt):
+ self.info.time_scale_cursor.next ()
+ self.update_timebase_label ()
+
+ def update_v_scale_label (self):
+ volts_per_div = self.info.get_volts_per_div ()
+ s = ' ' + eng_notation.num_to_str (volts_per_div) + '/div' # Not V/div
+ self.v_scale_label.SetLabel (s)
+
+ def decr_v_scale (self, evt):
+ self.info.v_scale_cursor.prev ()
+ self.update_v_scale_label ()
+
+ def incr_v_scale (self, evt):
+ self.info.v_scale_cursor.next ()
+ self.update_v_scale_label ()
+
+ def marker_choice_event (self, evt):
+ s = evt.GetString ()
+ self.set_marker (s)
+
+ def set_autorange(self, on):
+ if on:
+ self.v_scale_label.SetLabel(" (auto)")
+ self.info.autorange = True
+ self.autorange_checkbox.SetValue(True)
+ self.inc_v_button.Enable(False)
+ self.dec_v_button.Enable(False)
+ else:
+ if self.graph.y_range:
+ (l,u) = self.graph.y_range # found by autorange
+ self.info.v_scale_cursor.set_index_by_value((u-l)/8.0)
+ self.update_v_scale_label()
+ self.info.autorange = False
+ self.autorange_checkbox.SetValue(False)
+ self.inc_v_button.Enable(True)
+ self.dec_v_button.Enable(True)
+
+ def autorange_checkbox_event(self, evt):
+ if evt.Checked():
+ self.set_autorange(True)
+ else:
+ self.set_autorange(False)
+
+ def set_marker (self, s):
+ self.info.set_marker (s) # set info for drawing routines
+ i = self.marker_choice.FindString (s)
+ assert i >= 0, "Hmmm, set_marker problem"
+ self.marker_choice.SetSelection (i)
+
+ def set_format_line (self):
+ self.set_marker ('line')
+
+ def set_format_dot (self):
+ self.set_marker ('dot')
+
+ def set_format_plus (self):
+ self.set_marker ('plus')
+
+ def xy_choice_event (self, evt):
+ s = evt.GetString ()
+ self.info.xy = s == 'X:Y'
+
+ def trig_chan_choice_event (self, evt):
+ s = evt.GetString ()
+ ch = int (s[-1]) - 1
+ self.info.scopesink.set_trigger_channel (ch)
+
+ def trig_mode_choice_event (self, evt):
+ sink = self.info.scopesink
+ s = evt.GetString ()
+ if s == 'Pos':
+ sink.set_trigger_mode (gr.gr_TRIG_POS_SLOPE)
+ elif s == 'Neg':
+ sink.set_trigger_mode (gr.gr_TRIG_NEG_SLOPE)
+ elif s == 'Auto':
+ sink.set_trigger_mode (gr.gr_TRIG_AUTO)
+ else:
+ assert 0, "Bad trig_mode_choice string"
+
+ def set_trig_level50 (self, evt):
+ self.info.scopesink.set_trigger_level_auto ()
+
+ def run_stop (self, evt):
+ self.info.running = not self.info.running
+
+
+class graph_window (plot.PlotCanvas):
+
+ channel_colors = ['BLUE', 'RED',
+ 'CYAN', 'MAGENTA', 'GREEN', 'YELLOW']
+
+ def __init__ (self, info, parent, id = -1,
+ pos = wx.DefaultPosition, size = (640, 240),
+ style = wx.DEFAULT_FRAME_STYLE, name = ""):
+ plot.PlotCanvas.__init__ (self, parent, id, pos, size, style, name)
+
+ self.SetXUseScopeTicks (True)
+ self.SetEnableGrid (True)
+ self.SetEnableZoom (True)
+ self.SetEnableLegend(True)
+ # self.SetBackgroundColour ('black')
+
+ self.info = info;
+ self.y_range = None
+ self.x_range = None
+ self.avg_y_min = None
+ self.avg_y_max = None
+ self.avg_x_min = None
+ self.avg_x_max = None
+
+ EVT_DATA_EVENT (self, self.format_data)
+
+ self.input_watcher = input_watcher (info.msgq, self, info.frame_decim)
+
+ def channel_color (self, ch):
+ return self.channel_colors[ch % len(self.channel_colors)]
+
+ def format_data (self, evt):
+ if not self.info.running:
+ return
+
+ if self.info.xy:
+ self.format_xy_data (evt)
+ return
+
+ info = self.info
+ records = evt.data
+ nchannels = len (records)
+ npoints = len (records[0])
+
+ objects = []
+
+ Ts = 1.0 / (info.get_sample_rate () / info.get_decimation_rate ())
+ x_vals = Ts * Numeric.arrayrange (-npoints/2, npoints/2)
+
+ # preliminary clipping based on time axis here, instead of in graphics code
+ time_per_window = self.info.get_time_per_div () * 10
+ n = int (time_per_window / Ts + 0.5)
+ n = n & ~0x1 # make even
+ n = max (2, min (n, npoints))
+
+ self.SetXUseScopeTicks (True) # use 10 divisions, no labels
+
+ for ch in range(nchannels):
+ r = records[ch]
+
+ # plot middle n points of record
+
+ lb = npoints/2 - n/2
+ ub = npoints/2 + n/2
+ # points = zip (x_vals[lb:ub], r[lb:ub])
+ points = Numeric.zeros ((ub-lb, 2), Numeric.Float64)
+ points[:,0] = x_vals[lb:ub]
+ points[:,1] = r[lb:ub]
+
+ m = info.get_marker ()
+ if m == 'line':
+ objects.append (plot.PolyLine (points,
+ colour=self.channel_color (ch),
+ legend=('Ch%d' % (ch+1,))))
+ else:
+ objects.append (plot.PolyMarker (points,
+ marker=m,
+ colour=self.channel_color (ch),
+ legend=('Ch%d' % (ch+1,))))
+
+ graphics = plot.PlotGraphics (objects,
+ title=self.info.title,
+ xLabel = '', yLabel = '')
+
+ time_per_div = info.get_time_per_div ()
+ x_range = (-5.0 * time_per_div, 5.0 * time_per_div) # ranges are tuples!
+ volts_per_div = info.get_volts_per_div ()
+ if not self.info.autorange:
+ self.y_range = (-4.0 * volts_per_div, 4.0 * volts_per_div)
+ self.Draw (graphics, xAxis=x_range, yAxis=self.y_range)
+ self.update_y_range () # autorange to self.y_range
+
+
+ def format_xy_data (self, evt):
+ info = self.info
+ records = evt.data
+ nchannels = len (records)
+ npoints = len (records[0])
+
+ if nchannels < 2:
+ return
+
+ objects = []
+ # points = zip (records[0], records[1])
+ points = Numeric.zeros ((len(records[0]), 2), Numeric.Float32)
+ points[:,0] = records[0]
+ points[:,1] = records[1]
+
+ self.SetXUseScopeTicks (False)
+
+ m = info.get_marker ()
+ if m == 'line':
+ objects.append (plot.PolyLine (points,
+ colour=self.channel_color (0)))
+ else:
+ objects.append (plot.PolyMarker (points,
+ marker=m,
+ colour=self.channel_color (0)))
+
+ graphics = plot.PlotGraphics (objects,
+ title=self.info.title,
+ xLabel = 'I', yLabel = 'Q')
+
+ self.Draw (graphics, xAxis=self.x_range, yAxis=self.y_range)
+ self.update_y_range ()
+ self.update_x_range ()
+
+
+ def update_y_range (self):
+ alpha = 1.0/25
+ graphics = self.last_draw[0]
+ p1, p2 = graphics.boundingBox () # min, max points of graphics
+
+ if self.avg_y_min: # prevent vertical scale from jumping abruptly --?
+ self.avg_y_min = p1[1] * alpha + self.avg_y_min * (1 - alpha)
+ self.avg_y_max = p2[1] * alpha + self.avg_y_max * (1 - alpha)
+ else: # initial guess
+ self.avg_y_min = p1[1] # -500.0 workaround, sometimes p1 is ~ 10^35
+ self.avg_y_max = p2[1] # 500.0
+
+ self.y_range = self._axisInterval ('auto', self.avg_y_min, self.avg_y_max)
+ # print "p1 %s p2 %s y_min %s y_max %s y_range %s" \
+ # % (p1, p2, self.avg_y_min, self.avg_y_max, self.y_range)
+
+
+ def update_x_range (self):
+ alpha = 1.0/25
+ graphics = self.last_draw[0]
+ p1, p2 = graphics.boundingBox () # min, max points of graphics
+
+ if self.avg_x_min:
+ self.avg_x_min = p1[0] * alpha + self.avg_x_min * (1 - alpha)
+ self.avg_x_max = p2[0] * alpha + self.avg_x_max * (1 - alpha)
+ else:
+ self.avg_x_min = p1[0]
+ self.avg_x_max = p2[0]
+
+ self.x_range = self._axisInterval ('auto', self.avg_x_min, self.avg_x_max)
+
+
+# ----------------------------------------------------------------
+# Stand-alone test application
+# ----------------------------------------------------------------
+
+class test_app_flow_graph (stdgui.gui_flow_graph):
+ def __init__(self, frame, panel, vbox, argv):
+ stdgui.gui_flow_graph.__init__ (self, frame, panel, vbox, argv)
+
+ if len(argv) > 1:
+ frame_decim = int(argv[1])
+ else:
+ frame_decim = 1
+
+ if len(argv) > 2:
+ v_scale = float(argv[2]) # start up at this v_scale value
+ else:
+ v_scale = None # start up in autorange mode, default
+
+ if len(argv) > 3:
+ t_scale = float(argv[3]) # start up at this t_scale value
+ else:
+ t_scale = None # old behavior
+
+ print "frame decim %s v_scale %s t_scale %s" % (frame_decim,v_scale,t_scale)
+
+ input_rate = 1e6
+
+ # Generate a complex sinusoid
+ src0 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 25.1e3, 1e3)
+
+ # We add this throttle block so that this demo doesn't suck down
+ # all the CPU available. You normally wouldn't use it...
+ throttle = gr.throttle(gr.sizeof_gr_complex, input_rate)
+
+ scope = scope_sink_c (self, panel,"Secret Data",sample_rate=input_rate,
+ frame_decim=frame_decim,
+ v_scale=v_scale, t_scale=t_scale)
+ vbox.Add (scope.win, 1, wx.EXPAND)
+
+ # wire the blocks together
+ self.connect (src0, throttle, scope)
+
+def main ():
+ app = stdgui.stdapp (test_app_flow_graph, "O'Scope Test App")
+ app.MainLoop ()
+
+if __name__ == '__main__':
+ main ()
+
+# ----------------------------------------------------------------
diff --git a/gr-wxgui/src/python/slider.py b/gr-wxgui/src/python/slider.py
new file mode 100755
index 000000000..e8cdcfcac
--- /dev/null
+++ b/gr-wxgui/src/python/slider.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+
+import wx
+
+def slider(parent, min, max, callback):
+ """
+ Return a wx.Slider object.
+
+ @param min: minimum slider value
+ @type min: float
+ @param max: maximum slider value
+ @type max: float
+ @param callback: function of one arg invoked when slider moves.
+ @rtype: wx.Slider
+ """
+ new_id = wx.NewId()
+ s = wx.Slider(parent, new_id, (max+min)/2, min, max, wx.DefaultPosition,
+ wx.Size(250,-1), wx.SL_HORIZONTAL | wx.SL_LABELS)
+ wx.EVT_COMMAND_SCROLL(parent, new_id,
+ lambda evt : callback(evt.GetInt()))
+ return s
+
+
+# ----------------------------------------------------------------
+# Demo app
+# ----------------------------------------------------------------
+if __name__ == '__main__':
+
+ from gnuradio.wxgui import stdgui
+
+ class demo_graph(stdgui.gui_flow_graph):
+
+ def __init__(self, frame, panel, vbox, argv):
+ stdgui.gui_flow_graph.__init__ (self, frame, panel, vbox, argv)
+
+ vbox.Add(slider(panel, 23, 47, self.my_callback1), 1, wx.ALIGN_CENTER)
+ vbox.Add(slider(panel, -100, 100, self.my_callback2), 1, wx.ALIGN_CENTER)
+
+ def my_callback1(self, val):
+ print "cb1 = ", val
+
+ def my_callback2(self, val):
+ print "cb2 = ", val
+
+ def main ():
+ app = stdgui.stdapp (demo_graph, "Slider Demo")
+ app.MainLoop ()
+
+ main ()
diff --git a/gr-wxgui/src/python/stdgui.py b/gr-wxgui/src/python/stdgui.py
new file mode 100644
index 000000000..76cc1773b
--- /dev/null
+++ b/gr-wxgui/src/python/stdgui.py
@@ -0,0 +1,90 @@
+#
+# Copyright 2004 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio 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 2, or (at your option)
+# any later version.
+#
+# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+#
+
+'''A simple wx gui for GNU Radio applications'''
+
+import wx
+import sys
+from gnuradio import gr
+
+
+class stdapp (wx.App):
+ def __init__ (self, flow_graph_maker, title="GNU Radio", nstatus=2):
+ self.flow_graph_maker = flow_graph_maker
+ self.title = title
+ self._nstatus = nstatus
+ # All our initialization must come before calling wx.App.__init__.
+ # OnInit is called from somewhere in the guts of __init__.
+ wx.App.__init__ (self, redirect=False)
+
+ def OnInit (self):
+ frame = stdframe (self.flow_graph_maker, self.title, self._nstatus)
+ frame.Show (True)
+ self.SetTopWindow (frame)
+ return True
+
+
+class stdframe (wx.Frame):
+ def __init__ (self, flow_graph_maker, title="GNU Radio", nstatus=2):
+ # print "stdframe.__init__"
+ wx.Frame.__init__(self, None, -1, title)
+
+ self.CreateStatusBar (nstatus)
+ mainmenu = wx.MenuBar ()
+
+ menu = wx.Menu ()
+ item = menu.Append (200, 'E&xit', 'Exit')
+ self.Bind (wx.EVT_MENU, self.OnCloseWindow, item)
+ mainmenu.Append (menu, "&File")
+ self.SetMenuBar (mainmenu)
+
+ self.Bind (wx.EVT_CLOSE, self.OnCloseWindow)
+ self.panel = stdpanel (self, self, flow_graph_maker)
+ vbox = wx.BoxSizer(wx.VERTICAL)
+ vbox.Add(self.panel, 1, wx.EXPAND)
+ self.SetSizer(vbox)
+ self.SetAutoLayout(True)
+ vbox.Fit(self)
+
+ def OnCloseWindow (self, event):
+ self.flow_graph().stop()
+ self.Destroy ()
+
+ def flow_graph (self):
+ return self.panel.fg
+
+class stdpanel (wx.Panel):
+ def __init__ (self, parent, frame, flow_graph_maker):
+ # print "stdpanel.__init__"
+ wx.Panel.__init__ (self, parent, -1)
+ self.frame = frame
+
+ vbox = wx.BoxSizer (wx.VERTICAL)
+ self.fg = flow_graph_maker (frame, self, vbox, sys.argv)
+ self.SetSizer (vbox)
+ self.SetAutoLayout (True)
+ vbox.Fit (self)
+
+ self.fg.start ()
+
+class gui_flow_graph (gr.flow_graph):
+ def __init__ (self, *ignore):
+ gr.flow_graph.__init__ (self)
diff --git a/gr-wxgui/src/python/waterfallsink.py b/gr-wxgui/src/python/waterfallsink.py
new file mode 100755
index 000000000..f5a6d243f
--- /dev/null
+++ b/gr-wxgui/src/python/waterfallsink.py
@@ -0,0 +1,469 @@
+#!/usr/bin/env python
+#
+# Copyright 2003,2004,2005 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio 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 2, or (at your option)
+# any later version.
+#
+# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+#
+
+from gnuradio import gr, gru, window
+from gnuradio.wxgui import stdgui
+import wx
+import gnuradio.wxgui.plot as plot
+import Numeric
+import os
+import threading
+import math
+
+default_fftsink_size = (640,240)
+default_fft_rate = gr.prefs().get_long('wxgui', 'fft_rate', 15)
+
+class waterfall_sink_base(object):
+ def __init__(self, input_is_real=False, baseband_freq=0,
+ sample_rate=1, fft_size=512,
+ fft_rate=default_fft_rate,
+ average=False, avg_alpha=None, title=''):
+
+ # initialize common attributes
+ self.baseband_freq = baseband_freq
+ self.sample_rate = sample_rate
+ self.fft_size = fft_size
+ self.fft_rate = fft_rate
+ self.average = average
+ if avg_alpha is None:
+ self.avg_alpha = 2.0 / fft_rate
+ else:
+ self.avg_alpha = avg_alpha
+ self.title = title
+ self.input_is_real = input_is_real
+ (self.r_fd, self.w_fd) = os.pipe()
+
+ def set_average(self, average):
+ self.average = average
+ if average:
+ self.avg.set_taps(self.avg_alpha)
+ else:
+ self.avg.set_taps(1.0)
+
+ def set_avg_alpha(self, avg_alpha):
+ self.avg_alpha = avg_alpha
+
+ def set_baseband_freq(self, baseband_freq):
+ self.baseband_freq = baseband_freq
+
+ def set_sample_rate(self, sample_rate):
+ self.sample_rate = sample_rate
+ self._set_n()
+
+ def _set_n(self):
+ self.one_in_n.set_n(max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
+
+class waterfall_sink_f(gr.hier_block, waterfall_sink_base):
+ def __init__(self, fg, parent, baseband_freq=0,
+ y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
+ fft_rate=default_fft_rate, average=False, avg_alpha=None,
+ title='', size=default_fftsink_size):
+
+ waterfall_sink_base.__init__(self, input_is_real=True, baseband_freq=baseband_freq,
+ sample_rate=sample_rate, fft_size=fft_size,
+ fft_rate=fft_rate,
+ average=average, avg_alpha=avg_alpha, title=title)
+
+ s2p = gr.serial_to_parallel(gr.sizeof_float, self.fft_size)
+ self.one_in_n = gr.keep_one_in_n(gr.sizeof_float * self.fft_size,
+ max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
+ mywindow = window.blackmanharris(self.fft_size)
+ fft = gr.fft_vfc(self.fft_size, True, mywindow)
+ c2mag = gr.complex_to_mag(self.fft_size)
+ self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
+ log = gr.nlog10_ff(20, self.fft_size, -20*math.log10(self.fft_size))
+ sink = gr.file_descriptor_sink(gr.sizeof_float * self.fft_size, self.w_fd)
+
+ fg.connect (s2p, self.one_in_n, fft, c2mag, self.avg, log, sink)
+ gr.hier_block.__init__(self, fg, s2p, sink)
+
+ self.win = waterfall_window(self, parent, size=size)
+ self.set_average(self.average)
+
+
+class waterfall_sink_c(gr.hier_block, waterfall_sink_base):
+ def __init__(self, fg, parent, baseband_freq=0,
+ y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
+ fft_rate=default_fft_rate, average=False, avg_alpha=None,
+ title='', size=default_fftsink_size):
+
+ waterfall_sink_base.__init__(self, input_is_real=False, baseband_freq=baseband_freq,
+ sample_rate=sample_rate, fft_size=fft_size,
+ fft_rate=fft_rate,
+ average=average, avg_alpha=avg_alpha, title=title)
+
+ s2p = gr.serial_to_parallel(gr.sizeof_gr_complex, self.fft_size)
+ self.one_in_n = gr.keep_one_in_n(gr.sizeof_gr_complex * self.fft_size,
+ max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
+
+ mywindow = window.blackmanharris(self.fft_size)
+ fft = gr.fft_vcc(self.fft_size, True, mywindow)
+ c2mag = gr.complex_to_mag(self.fft_size)
+ self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
+ log = gr.nlog10_ff(20, self.fft_size, -20*math.log10(self.fft_size))
+ sink = gr.file_descriptor_sink(gr.sizeof_float * self.fft_size, self.w_fd)
+
+ fg.connect(s2p, self.one_in_n, fft, c2mag, self.avg, log, sink)
+ gr.hier_block.__init__(self, fg, s2p, sink)
+
+ self.win = waterfall_window(self, parent, size=size)
+ self.set_average(self.average)
+
+
+# ------------------------------------------------------------------------
+
+myDATA_EVENT = wx.NewEventType()
+EVT_DATA_EVENT = wx.PyEventBinder (myDATA_EVENT, 0)
+
+
+class DataEvent(wx.PyEvent):
+ def __init__(self, data):
+ wx.PyEvent.__init__(self)
+ self.SetEventType (myDATA_EVENT)
+ self.data = data
+
+ def Clone (self):
+ self.__class__ (self.GetId())
+
+
+class input_watcher (threading.Thread):
+ def __init__ (self, file_descriptor, fft_size, event_receiver, **kwds):
+ threading.Thread.__init__ (self, **kwds)
+ self.setDaemon (1)
+ self.file_descriptor = file_descriptor
+ self.fft_size = fft_size
+ self.event_receiver = event_receiver
+ self.keep_running = True
+ self.start ()
+
+ def run (self):
+ while (self.keep_running):
+ s = gru.os_read_exactly (self.file_descriptor,
+ gr.sizeof_float * self.fft_size)
+ if not s:
+ self.keep_running = False
+ break
+
+ complex_data = Numeric.fromstring (s, Numeric.Float32)
+ de = DataEvent (complex_data)
+ wx.PostEvent (self.event_receiver, de)
+ del de
+
+
+class waterfall_window (wx.Panel):
+ def __init__ (self, fftsink, parent, id = -1,
+ pos = wx.DefaultPosition, size = wx.DefaultSize,
+ style = wx.DEFAULT_FRAME_STYLE, name = ""):
+ wx.Panel.__init__(self, parent, id, pos, size, style, name)
+
+ self.fftsink = fftsink
+ self.bm = wx.EmptyBitmap(self.fftsink.fft_size, 300, -1)
+
+ self.scale_factor = 5.0 # FIXME should autoscale, or set this
+
+ dc1 = wx.MemoryDC()
+ dc1.SelectObject(self.bm)
+ dc1.Clear()
+
+ self.pens = self.make_pens()
+
+ wx.EVT_PAINT( self, self.OnPaint )
+ wx.EVT_CLOSE (self, self.on_close_window)
+ EVT_DATA_EVENT (self, self.set_data)
+
+ self.build_popup_menu()
+
+ wx.EVT_CLOSE (self, self.on_close_window)
+ self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
+
+ self.input_watcher = input_watcher(fftsink.r_fd, fftsink.fft_size, self)
+
+
+ def on_close_window (self, event):
+ print "waterfall_window: on_close_window"
+ self.keep_running = False
+
+ def const_list(self,const,len):
+ return [const] * len
+
+ def make_colormap(self):
+ r = []
+ r.extend(self.const_list(0,96))
+ r.extend(range(0,255,4))
+ r.extend(self.const_list(255,64))
+ r.extend(range(255,128,-4))
+
+ g = []
+ g.extend(self.const_list(0,32))
+ g.extend(range(0,255,4))
+ g.extend(self.const_list(255,64))
+ g.extend(range(255,0,-4))
+ g.extend(self.const_list(0,32))
+
+ b = range(128,255,4)
+ b.extend(self.const_list(255,64))
+ b.extend(range(255,0,-4))
+ b.extend(self.const_list(0,96))
+ return (r,g,b)
+
+ def make_pens(self):
+ (r,g,b) = self.make_colormap()
+ pens = []
+ for i in range(0,256):
+ colour = wx.Colour(r[i], g[i], b[i])
+ pens.append( wx.Pen(colour, 2, wx.SOLID))
+ return pens
+
+ def OnPaint(self, event):
+ dc = wx.PaintDC(self)
+ self.DoDrawing(dc)
+
+ def DoDrawing(self, dc=None):
+ if dc is None:
+ dc = wx.ClientDC(self)
+ dc.DrawBitmap(self.bm, 0, 0, False )
+
+
+ def const_list(self,const,len):
+ a = [const]
+ for i in range(1,len):
+ a.append(const)
+ return a
+
+ def make_colormap(self):
+ r = []
+ r.extend(self.const_list(0,96))
+ r.extend(range(0,255,4))
+ r.extend(self.const_list(255,64))
+ r.extend(range(255,128,-4))
+
+ g = []
+ g.extend(self.const_list(0,32))
+ g.extend(range(0,255,4))
+ g.extend(self.const_list(255,64))
+ g.extend(range(255,0,-4))
+ g.extend(self.const_list(0,32))
+
+ b = range(128,255,4)
+ b.extend(self.const_list(255,64))
+ b.extend(range(255,0,-4))
+ b.extend(self.const_list(0,96))
+ return (r,g,b)
+
+ def set_data (self, evt):
+ dB = evt.data
+ L = len (dB)
+
+ dc1 = wx.MemoryDC()
+ dc1.SelectObject(self.bm)
+ dc1.Blit(0,1,self.fftsink.fft_size,300,dc1,0,0,wx.COPY,False,-1,-1)
+
+ x = max(abs(self.fftsink.sample_rate), abs(self.fftsink.baseband_freq))
+ if x >= 1e9:
+ sf = 1e-9
+ units = "GHz"
+ elif x >= 1e6:
+ sf = 1e-6
+ units = "MHz"
+ else:
+ sf = 1e-3
+ units = "kHz"
+
+
+ if self.fftsink.input_is_real: # only plot 1/2 the points
+ d_max = L/2
+ p_width = 2
+ else:
+ d_max = L/2
+ p_width = 1
+
+ scale_factor = self.scale_factor
+ if self.fftsink.input_is_real: # real fft
+ for x_pos in range(0, d_max):
+ value = int(dB[x_pos] * scale_factor)
+ value = min(255, max(0, value))
+ dc1.SetPen(self.pens[value])
+ dc1.DrawRectangle(x_pos*p_width, 0, p_width, 1)
+ else: # complex fft
+ for x_pos in range(0, d_max): # positive freqs
+ value = int(dB[x_pos] * scale_factor)
+ value = min(255, max(0, value))
+ dc1.SetPen(self.pens[value])
+ dc1.DrawRectangle(x_pos*p_width + d_max, 0, p_width, 1)
+ for x_pos in range(0 , d_max): # negative freqs
+ value = int(dB[x_pos+d_max] * scale_factor)
+ value = min(255, max(0, value))
+ dc1.SetPen(self.pens[value])
+ dc1.DrawRectangle(x_pos*p_width, 0, p_width, 1)
+
+ self.DoDrawing (None)
+
+ def on_average(self, evt):
+ # print "on_average"
+ self.fftsink.set_average(evt.IsChecked())
+
+ def on_right_click(self, event):
+ menu = self.popup_menu
+ for id, pred in self.checkmarks.items():
+ item = menu.FindItemById(id)
+ item.Check(pred())
+ self.PopupMenu(menu, event.GetPosition())
+
+
+ def build_popup_menu(self):
+ self.id_incr_ref_level = wx.NewId()
+ self.id_decr_ref_level = wx.NewId()
+ self.id_incr_y_per_div = wx.NewId()
+ self.id_decr_y_per_div = wx.NewId()
+ self.id_y_per_div_1 = wx.NewId()
+ self.id_y_per_div_2 = wx.NewId()
+ self.id_y_per_div_5 = wx.NewId()
+ self.id_y_per_div_10 = wx.NewId()
+ self.id_y_per_div_20 = wx.NewId()
+ self.id_average = wx.NewId()
+
+ self.Bind(wx.EVT_MENU, self.on_average, id=self.id_average)
+ #self.Bind(wx.EVT_MENU, self.on_incr_ref_level, id=self.id_incr_ref_level)
+ #self.Bind(wx.EVT_MENU, self.on_decr_ref_level, id=self.id_decr_ref_level)
+ #self.Bind(wx.EVT_MENU, self.on_incr_y_per_div, id=self.id_incr_y_per_div)
+ #self.Bind(wx.EVT_MENU, self.on_decr_y_per_div, id=self.id_decr_y_per_div)
+ #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_1)
+ #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_2)
+ #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_5)
+ #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_10)
+ #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_20)
+
+
+ # make a menu
+ menu = wx.Menu()
+ self.popup_menu = menu
+ menu.AppendCheckItem(self.id_average, "Average")
+ # menu.Append(self.id_incr_ref_level, "Incr Ref Level")
+ # menu.Append(self.id_decr_ref_level, "Decr Ref Level")
+ # menu.Append(self.id_incr_y_per_div, "Incr dB/div")
+ # menu.Append(self.id_decr_y_per_div, "Decr dB/div")
+ # menu.AppendSeparator()
+ # we'd use RadioItems for these, but they're not supported on Mac
+ #menu.AppendCheckItem(self.id_y_per_div_1, "1 dB/div")
+ #menu.AppendCheckItem(self.id_y_per_div_2, "2 dB/div")
+ #menu.AppendCheckItem(self.id_y_per_div_5, "5 dB/div")
+ #menu.AppendCheckItem(self.id_y_per_div_10, "10 dB/div")
+ #menu.AppendCheckItem(self.id_y_per_div_20, "20 dB/div")
+
+ self.checkmarks = {
+ self.id_average : lambda : self.fftsink.average
+ #self.id_y_per_div_1 : lambda : self.fftsink.y_per_div == 1,
+ #self.id_y_per_div_2 : lambda : self.fftsink.y_per_div == 2,
+ #self.id_y_per_div_5 : lambda : self.fftsink.y_per_div == 5,
+ #self.id_y_per_div_10 : lambda : self.fftsink.y_per_div == 10,
+ #self.id_y_per_div_20 : lambda : self.fftsink.y_per_div == 20,
+ }
+
+
+def next_up(v, seq):
+ """
+ Return the first item in seq that is > v.
+ """
+ for s in seq:
+ if s > v:
+ return s
+ return v
+
+def next_down(v, seq):
+ """
+ Return the last item in seq that is < v.
+ """
+ rseq = list(seq[:])
+ rseq.reverse()
+
+ for s in rseq:
+ if s < v:
+ return s
+ return v
+
+
+# ----------------------------------------------------------------
+# Deprecated interfaces
+# ----------------------------------------------------------------
+
+# returns (block, win).
+# block requires a single input stream of float
+# win is a subclass of wxWindow
+
+def make_waterfall_sink_f(fg, parent, title, fft_size, input_rate):
+
+ block = waterfall_sink_f(fg, parent, title=title, fft_size=fft_size,
+ sample_rate=input_rate)
+ return (block, block.win)
+
+# returns (block, win).
+# block requires a single input stream of gr_complex
+# win is a subclass of wxWindow
+
+def make_waterfall_sink_c(fg, parent, title, fft_size, input_rate):
+ block = waterfall_sink_c(fg, parent, title=title, fft_size=fft_size,
+ sample_rate=input_rate)
+ return (block, block.win)
+
+
+# ----------------------------------------------------------------
+# Standalone test app
+# ----------------------------------------------------------------
+
+class test_app_flow_graph (stdgui.gui_flow_graph):
+ def __init__(self, frame, panel, vbox, argv):
+ stdgui.gui_flow_graph.__init__ (self, frame, panel, vbox, argv)
+
+ fft_size = 512
+
+ # build our flow graph
+ input_rate = 20.000e3
+
+ # Generate a complex sinusoid
+ src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
+ #src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
+
+ # We add these throttle blocks so that this demo doesn't
+ # suck down all the CPU available. Normally you wouldn't use these.
+ thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
+
+ sink1 = waterfall_sink_c (self, panel, title="Complex Data", fft_size=fft_size,
+ sample_rate=input_rate, baseband_freq=100e3)
+ vbox.Add (sink1.win, 1, wx.EXPAND)
+ self.connect (src1, thr1, sink1)
+
+ # generate a real sinusoid
+ src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
+ #src2 = gr.sig_source_f (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
+ thr2 = gr.throttle(gr.sizeof_float, input_rate)
+ sink2 = waterfall_sink_f (self, panel, title="Real Data", fft_size=fft_size,
+ sample_rate=input_rate, baseband_freq=100e3)
+ vbox.Add (sink2.win, 1, wx.EXPAND)
+ self.connect (src2, thr2, sink2)
+
+def main ():
+ app = stdgui.stdapp (test_app_flow_graph,
+ "Waterfall Sink Test App")
+ app.MainLoop ()
+
+if __name__ == '__main__':
+ main ()