From 26dceecc80390f10cedb94bd9e4fd655827d7f17 Mon Sep 17 00:00:00 2001
From: Johnathan Corgan <johnathan@corganlabs.com>
Date: Tue, 26 Mar 2013 20:18:53 -0700
Subject: runtime: migrate remaining gnuradio-core contents into
 gnuradio-runtime

---
 gnuradio-runtime/python/CMakeLists.txt             |  23 +
 gnuradio-runtime/python/build_utils.py             | 226 ++++++
 gnuradio-runtime/python/build_utils_codes.py       |  52 ++
 gnuradio-runtime/python/gnuradio/CMakeLists.txt    |  37 +
 gnuradio-runtime/python/gnuradio/__init__.py       |  12 +
 .../python/gnuradio/ctrlport/CMakeLists.txt        |  97 +++
 .../python/gnuradio/ctrlport/GrDataPlotter.py      | 428 +++++++++++
 .../python/gnuradio/ctrlport/IceRadioClient.py     | 102 +++
 .../python/gnuradio/ctrlport/__init__.py           |  30 +
 .../python/gnuradio/ctrlport/gr-ctrlport-curses    | 268 +++++++
 .../python/gnuradio/ctrlport/gr-ctrlport-monitor   | 721 +++++++++++++++++++
 .../python/gnuradio/ctrlport/gr-perf-monitor       | 591 +++++++++++++++
 .../python/gnuradio/ctrlport/gr-perf-monitorx      | 727 +++++++++++++++++++
 gnuradio-runtime/python/gnuradio/ctrlport/icon.png | Bin 0 -> 1532 bytes
 .../python/gnuradio/ctrlport/monitor.py            |  59 ++
 gnuradio-runtime/python/gnuradio/gr/CMakeLists.txt |  45 ++
 gnuradio-runtime/python/gnuradio/gr/__init__.py    |  39 +
 gnuradio-runtime/python/gnuradio/gr/exceptions.py  |  27 +
 gnuradio-runtime/python/gnuradio/gr/gateway.py     | 243 +++++++
 .../python/gnuradio/gr/gr_threading.py             |  35 +
 .../python/gnuradio/gr/gr_threading_23.py          | 724 +++++++++++++++++++
 .../python/gnuradio/gr/gr_threading_24.py          | 793 +++++++++++++++++++++
 gnuradio-runtime/python/gnuradio/gr/hier_block2.py | 132 ++++
 gnuradio-runtime/python/gnuradio/gr/prefs.py       | 127 ++++
 gnuradio-runtime/python/gnuradio/gr/pubsub.py      | 153 ++++
 gnuradio-runtime/python/gnuradio/gr/qa_feval.py    | 110 +++
 .../python/gnuradio/gr/qa_kludged_imports.py       |  39 +
 .../python/gnuradio/gr/qa_tag_utils.py             |  55 ++
 gnuradio-runtime/python/gnuradio/gr/tag_utils.py   |  57 ++
 gnuradio-runtime/python/gnuradio/gr/top_block.py   | 170 +++++
 gnuradio-runtime/python/gnuradio/gr_unittest.py    | 170 +++++
 gnuradio-runtime/python/gnuradio/gr_xmlrunner.py   | 387 ++++++++++
 .../python/gnuradio/gru/CMakeLists.txt             |  36 +
 gnuradio-runtime/python/gnuradio/gru/__init__.py   |  13 +
 gnuradio-runtime/python/gnuradio/gru/daemon.py     | 102 +++
 gnuradio-runtime/python/gnuradio/gru/freqz.py      | 344 +++++++++
 .../python/gnuradio/gru/gnuplot_freqz.py           | 102 +++
 gnuradio-runtime/python/gnuradio/gru/hexint.py     |  44 ++
 gnuradio-runtime/python/gnuradio/gru/listmisc.py   |  29 +
 gnuradio-runtime/python/gnuradio/gru/mathmisc.py   |  33 +
 .../python/gnuradio/gru/msgq_runner.py             |  82 +++
 .../python/gnuradio/gru/os_read_exactly.py         |  36 +
 .../python/gnuradio/gru/seq_with_cursor.py         |  77 ++
 .../python/gnuradio/gru/socket_stuff.py            |  62 ++
 44 files changed, 7639 insertions(+)
 create mode 100644 gnuradio-runtime/python/CMakeLists.txt
 create mode 100644 gnuradio-runtime/python/build_utils.py
 create mode 100644 gnuradio-runtime/python/build_utils_codes.py
 create mode 100644 gnuradio-runtime/python/gnuradio/CMakeLists.txt
 create mode 100644 gnuradio-runtime/python/gnuradio/__init__.py
 create mode 100644 gnuradio-runtime/python/gnuradio/ctrlport/CMakeLists.txt
 create mode 100644 gnuradio-runtime/python/gnuradio/ctrlport/GrDataPlotter.py
 create mode 100644 gnuradio-runtime/python/gnuradio/ctrlport/IceRadioClient.py
 create mode 100644 gnuradio-runtime/python/gnuradio/ctrlport/__init__.py
 create mode 100755 gnuradio-runtime/python/gnuradio/ctrlport/gr-ctrlport-curses
 create mode 100755 gnuradio-runtime/python/gnuradio/ctrlport/gr-ctrlport-monitor
 create mode 100755 gnuradio-runtime/python/gnuradio/ctrlport/gr-perf-monitor
 create mode 100755 gnuradio-runtime/python/gnuradio/ctrlport/gr-perf-monitorx
 create mode 100644 gnuradio-runtime/python/gnuradio/ctrlport/icon.png
 create mode 100644 gnuradio-runtime/python/gnuradio/ctrlport/monitor.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gr/CMakeLists.txt
 create mode 100644 gnuradio-runtime/python/gnuradio/gr/__init__.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gr/exceptions.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gr/gateway.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gr/gr_threading.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gr/gr_threading_23.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gr/gr_threading_24.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gr/hier_block2.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gr/prefs.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gr/pubsub.py
 create mode 100755 gnuradio-runtime/python/gnuradio/gr/qa_feval.py
 create mode 100755 gnuradio-runtime/python/gnuradio/gr/qa_kludged_imports.py
 create mode 100755 gnuradio-runtime/python/gnuradio/gr/qa_tag_utils.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gr/tag_utils.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gr/top_block.py
 create mode 100755 gnuradio-runtime/python/gnuradio/gr_unittest.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gr_xmlrunner.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gru/CMakeLists.txt
 create mode 100644 gnuradio-runtime/python/gnuradio/gru/__init__.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gru/daemon.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gru/freqz.py
 create mode 100755 gnuradio-runtime/python/gnuradio/gru/gnuplot_freqz.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gru/hexint.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gru/listmisc.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gru/mathmisc.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gru/msgq_runner.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gru/os_read_exactly.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gru/seq_with_cursor.py
 create mode 100644 gnuradio-runtime/python/gnuradio/gru/socket_stuff.py

(limited to 'gnuradio-runtime/python')

diff --git a/gnuradio-runtime/python/CMakeLists.txt b/gnuradio-runtime/python/CMakeLists.txt
new file mode 100644
index 0000000000..74adec3f11
--- /dev/null
+++ b/gnuradio-runtime/python/CMakeLists.txt
@@ -0,0 +1,23 @@
+# Copyright 2012 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+
+########################################################################
+include(GrPython)
+
+add_subdirectory(gnuradio)
diff --git a/gnuradio-runtime/python/build_utils.py b/gnuradio-runtime/python/build_utils.py
new file mode 100644
index 0000000000..cf58a97637
--- /dev/null
+++ b/gnuradio-runtime/python/build_utils.py
@@ -0,0 +1,226 @@
+#
+# Copyright 2004,2009,2012 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+"""Misc utilities used at build time
+"""
+
+import re, os, os.path
+from build_utils_codes import *
+
+
+# set srcdir to the directory that contains Makefile.am
+try:
+    srcdir = os.environ['srcdir']
+except KeyError, e:
+    srcdir = "."
+srcdir = srcdir + '/'
+
+# set do_makefile to either true or false dependeing on the environment
+try:
+    if os.environ['do_makefile'] == '0':
+        do_makefile = False
+    else:
+        do_makefile = True
+except KeyError, e:
+    do_makefile = False
+
+# set do_sources to either true or false dependeing on the environment
+try:
+    if os.environ['do_sources'] == '0':
+        do_sources = False
+    else:
+        do_sources = True
+except KeyError, e:
+    do_sources = True
+
+name_dict = {}
+
+def log_output_name (name):
+    (base, ext) = os.path.splitext (name)
+    ext = ext[1:]                       # drop the leading '.'
+
+    entry = name_dict.setdefault (ext, [])
+    entry.append (name)
+
+def open_and_log_name (name, dir):
+    global do_sources
+    if do_sources:
+        f = open (name, dir)
+    else:
+        f = None
+    log_output_name (name)
+    return f
+
+def expand_template (d, template_filename, extra = ""):
+    '''Given a dictionary D and a TEMPLATE_FILENAME, expand template into output file
+    '''
+    global do_sources
+    output_extension = extract_extension (template_filename)
+    template = open_src (template_filename, 'r')
+    output_name = d['NAME'] + extra + '.' + output_extension
+    log_output_name (output_name)
+    if do_sources:
+        output = open (output_name, 'w')
+        do_substitution (d, template, output)
+        output.close ()
+    template.close ()
+
+def output_glue (dirname):
+    output_makefile_fragment ()
+    output_ifile_include (dirname)
+
+def output_makefile_fragment ():
+    global do_makefile
+    if not do_makefile:
+        return
+# overwrite the source, which must be writable; this should have been
+# checked for beforehand in the top-level Makefile.gen.gen .
+    f = open (os.path.join (os.environ.get('gendir', os.environ.get('srcdir', '.')), 'Makefile.gen'), 'w')
+    f.write ('#\n# This file is machine generated.  All edits will be overwritten\n#\n')
+    output_subfrag (f, 'h')
+    output_subfrag (f, 'i')
+    output_subfrag (f, 'cc')
+    f.close ()
+
+def output_ifile_include (dirname):
+    global do_sources
+    if do_sources:
+        f = open ('%s_generated.i' % (dirname,), 'w')
+        f.write ('//\n// This file is machine generated.  All edits will be overwritten\n//\n')
+        files = name_dict.setdefault ('i', [])
+        files.sort ()
+        f.write ('%{\n')
+        for file in files:
+            f.write ('#include <%s>\n' % (file[0:-1] + 'h',))
+        f.write ('%}\n\n')
+        for file in files:
+            f.write ('%%include <%s>\n' % (file,))
+
+def output_subfrag (f, ext):
+    files = name_dict.setdefault (ext, [])
+    files.sort ()
+    f.write ("GENERATED_%s =" % (ext.upper ()))
+    for file in files:
+        f.write (" \\\n\t%s" % (file,))
+    f.write ("\n\n")
+
+def extract_extension (template_name):
+    # template name is something like: GrFIRfilterXXX.h.t
+    # we return everything between the penultimate . and .t
+    mo = re.search (r'\.([a-z]+)\.t$', template_name)
+    if not mo:
+        raise ValueError, "Incorrectly formed template_name '%s'" % (template_name,)
+    return mo.group (1)
+
+def open_src (name, mode):
+    global srcdir
+    return open (os.path.join (srcdir, name), mode)
+
+def do_substitution (d, in_file, out_file):
+    def repl (match_obj):
+        key = match_obj.group (1)
+        # print key
+        return d[key]
+
+    inp = in_file.read ()
+    out = re.sub (r"@([a-zA-Z0-9_]+)@", repl, inp)
+    out_file.write (out)
+
+
+
+copyright = '''/* -*- c++ -*- */
+/*
+ * Copyright 2003,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 3, 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., 51 Franklin Street,
+ * Boston, MA 02110-1301, USA.
+ */
+'''
+
+def is_complex (code3):
+    if i_code (code3) == 'c' or o_code (code3) == 'c':
+        return '1'
+    else:
+        return '0'
+
+
+def standard_dict (name, code3, package='gr'):
+    d = {}
+    d['NAME'] = name
+    d['NAME_IMPL'] = name+'_impl'
+    d['GUARD_NAME'] = 'INCLUDED_%s_%s_H' % (package.upper(), name.upper())
+    d['GUARD_NAME_IMPL'] = 'INCLUDED_%s_%s_IMPL_H' % (package.upper(), name.upper())
+    d['BASE_NAME'] = re.sub ('^' + package + '_', '', name)
+    d['SPTR_NAME'] = '%s_sptr' % name
+    d['WARNING'] = 'WARNING: this file is machine generated. Edits will be overwritten'
+    d['COPYRIGHT'] = copyright
+    d['TYPE'] = i_type (code3)
+    d['I_TYPE'] = i_type (code3)
+    d['O_TYPE'] = o_type (code3)
+    d['TAP_TYPE'] = tap_type (code3)
+    d['IS_COMPLEX'] = is_complex (code3)
+    return d
+
+
+def standard_dict2 (name, code3, package):
+    d = {}
+    d['NAME'] = name
+    d['BASE_NAME'] = name
+    d['GUARD_NAME'] = 'INCLUDED_%s_%s_H' % (package.upper(), name.upper())
+    d['WARNING'] = 'WARNING: this file is machine generated. Edits will be overwritten'
+    d['COPYRIGHT'] = copyright
+    d['TYPE'] = i_type (code3)
+    d['I_TYPE'] = i_type (code3)
+    d['O_TYPE'] = o_type (code3)
+    d['TAP_TYPE'] = tap_type (code3)
+    d['IS_COMPLEX'] = is_complex (code3)
+    return d
+
+def standard_impl_dict2 (name, code3, package):
+    d = {}
+    d['NAME'] = name
+    d['IMPL_NAME'] = name
+    d['BASE_NAME'] = name.rstrip("impl").rstrip("_")
+    d['GUARD_NAME'] = 'INCLUDED_%s_%s_H' % (package.upper(), name.upper())
+    d['WARNING'] = 'WARNING: this file is machine generated. Edits will be overwritten'
+    d['COPYRIGHT'] = copyright
+    d['FIR_TYPE'] = "fir_filter_" + code3
+    d['CFIR_TYPE'] = "fir_filter_" + code3[0:2] + 'c'
+    d['TYPE'] = i_type (code3)
+    d['I_TYPE'] = i_type (code3)
+    d['O_TYPE'] = o_type (code3)
+    d['TAP_TYPE'] = tap_type (code3)
+    d['IS_COMPLEX'] = is_complex (code3)
+    return d
diff --git a/gnuradio-runtime/python/build_utils_codes.py b/gnuradio-runtime/python/build_utils_codes.py
new file mode 100644
index 0000000000..9ea96baae4
--- /dev/null
+++ b/gnuradio-runtime/python/build_utils_codes.py
@@ -0,0 +1,52 @@
+#
+# 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+def i_code (code3):
+    return code3[0]
+
+def o_code (code3):
+    if len (code3) >= 2:
+        return code3[1]
+    else:
+        return code3[0]
+
+def tap_code (code3):
+    if len (code3) >= 3:
+        return code3[2]
+    else:
+        return code3[0]
+
+def i_type (code3):
+    return char_to_type[i_code (code3)]
+
+def o_type (code3):
+    return char_to_type[o_code (code3)]
+
+def tap_type (code3):
+    return char_to_type[tap_code (code3)]
+
+
+char_to_type = {}
+char_to_type['s'] = 'short'
+char_to_type['i'] = 'int'
+char_to_type['f'] = 'float'
+char_to_type['c'] = 'gr_complex'
+char_to_type['b'] = 'unsigned char'
diff --git a/gnuradio-runtime/python/gnuradio/CMakeLists.txt b/gnuradio-runtime/python/gnuradio/CMakeLists.txt
new file mode 100644
index 0000000000..bf5ec331d3
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/CMakeLists.txt
@@ -0,0 +1,37 @@
+# Copyright 2012 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+
+########################################################################
+include(GrPython)
+
+add_subdirectory(gr)
+add_subdirectory(gru)
+add_subdirectory(ctrlport)
+
+if(ENABLE_GR_CTRLPORT)
+#add_subdirectory(ctrlport)
+endif(ENABLE_GR_CTRLPORT)
+
+GR_PYTHON_INSTALL(FILES
+    __init__.py
+    gr_unittest.py
+    gr_xmlrunner.py
+    DESTINATION ${GR_PYTHON_DIR}/gnuradio
+    COMPONENT "runtime_python"
+)
diff --git a/gnuradio-runtime/python/gnuradio/__init__.py b/gnuradio-runtime/python/gnuradio/__init__.py
new file mode 100644
index 0000000000..d55dac79db
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/__init__.py
@@ -0,0 +1,12 @@
+"""
+GNU Radio is a free & open-source software development toolkit that provides signal processing blocks to implement software radios. It can be used with readily-available low-cost external RF hardware to create software-defined radios, or without hardware in a simulation-like environment. It is widely used in hobbyist, academic and commercial environments to support both wireless communications research and real-world radio systems.
+
+GNU Radio applications are primarily written using the Python programming language, while the supplied performance-critical signal-processing path is implemented in C++ using processor floating-point extensions, where available. Thus, the developer is able to implement real-time, high-throughput radio systems in a simple-to-use, rapid-application-development environment.
+
+While not primarily a simulation tool, GNU Radio does support development of signal processing algorithms using pre-recorded or generated data, avoiding the need for actual RF hardware.
+
+GNU Radio is licensed under the GNU General Public License (GPL) version 3. All of the code is copyright of the Free Software Foundation.
+"""
+
+# This file makes gnuradio a package
+# The docstring will be associated with the top level of the package.
diff --git a/gnuradio-runtime/python/gnuradio/ctrlport/CMakeLists.txt b/gnuradio-runtime/python/gnuradio/ctrlport/CMakeLists.txt
new file mode 100644
index 0000000000..c68694785f
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/ctrlport/CMakeLists.txt
@@ -0,0 +1,97 @@
+# Copyright 2012 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+
+########################################################################
+include(GrPython)
+
+EXECUTE_PROCESS(
+  COMMAND ${ICE_SLICE2PY} -I${CMAKE_SOURCE_DIR}/gnuradio-runtime/lib
+          --output-dir=${CMAKE_BINARY_DIR}/gnuradio-runtime/python
+          ${CMAKE_SOURCE_DIR}/gnuradio-runtime/lib/gnuradio.ice
+)
+
+EXECUTE_PROCESS(
+  COMMAND ${ICE_SLICE2PY} -I${CMAKE_SOURCE_DIR}/gnuradio-runtime/lib
+          --output-dir=${CMAKE_BINARY_DIR}/gnuradio-runtime/python
+          ${CMAKE_SOURCE_DIR}/gnuradio-runtime/lib/frontend.ice
+)
+
+GR_PYTHON_INSTALL(
+    FILES
+    ${CMAKE_CURRENT_SOURCE_DIR}/__init__.py
+    ${CMAKE_CURRENT_SOURCE_DIR}/IceRadioClient.py
+    DESTINATION ${GR_PYTHON_DIR}/gnuradio/ctrlport/
+    COMPONENT "core_python"
+)
+
+# We don't want to install these in the root Python directory, but we
+# aren't given a choice based on the way slice2py generates the
+# information.
+GR_PYTHON_INSTALL(
+    FILES
+    ${CMAKE_BINARY_DIR}/gnuradio-runtime/python/gnuradio_ice.py
+    ${CMAKE_BINARY_DIR}/gnuradio-runtime/python/frontend_ice.py
+    DESTINATION ${GR_PYTHON_DIR}
+    COMPONENT "core_python"
+)
+
+GR_PYTHON_INSTALL(
+    FILES
+    ${CMAKE_CURRENT_BINARY_DIR}/GNURadio/__init__.py
+    DESTINATION ${GR_PYTHON_DIR}/gnuradio/ctrlport/GNURadio
+    COMPONENT "core_python"
+)
+
+GR_PYTHON_INSTALL(
+    FILES
+    ${CMAKE_CURRENT_BINARY_DIR}/GNURadio/Booter/__init__.py
+    DESTINATION ${GR_PYTHON_DIR}/gnuradio/ctrlport/GNURadio/Booter
+    COMPONENT "core_python"
+)
+
+GR_PYTHON_INSTALL(
+    FILES
+    ${CMAKE_CURRENT_BINARY_DIR}/GNURadio/Frontend/__init__.py
+    DESTINATION ${GR_PYTHON_DIR}/gnuradio/ctrlport/GNURadio/Frontend
+    COMPONENT "core_python"
+)
+
+install(
+    FILES
+    ${CMAKE_CURRENT_SOURCE_DIR}/icon.png
+    DESTINATION ${GR_PYTHON_DIR}/gnuradio/ctrlport
+    COMPONENT "core_python"
+)
+
+GR_PYTHON_INSTALL(
+    FILES
+    ${CMAKE_CURRENT_SOURCE_DIR}/GrDataPlotter.py
+    ${CMAKE_CURRENT_SOURCE_DIR}/monitor.py
+    DESTINATION ${GR_PYTHON_DIR}/gnuradio/ctrlport/
+    COMPONENT "core_python"
+)
+
+GR_PYTHON_INSTALL(
+    FILES
+    ${CMAKE_CURRENT_SOURCE_DIR}/gr-ctrlport-monitor
+    ${CMAKE_CURRENT_SOURCE_DIR}/gr-ctrlport-curses
+    DESTINATION ${GR_RUNTIME_DIR}
+    PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE 
+    COMPONENT "core_python"
+)
diff --git a/gnuradio-runtime/python/gnuradio/ctrlport/GrDataPlotter.py b/gnuradio-runtime/python/gnuradio/ctrlport/GrDataPlotter.py
new file mode 100644
index 0000000000..8597ca6497
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/ctrlport/GrDataPlotter.py
@@ -0,0 +1,428 @@
+#!/usr/bin/env python
+#
+# Copyright 2012,2013 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+from gnuradio import gr
+from gnuradio import blocks
+from gnuradio import filter
+import sys, time
+
+try:
+    from gnuradio import qtgui
+    from PyQt4 import QtGui, QtCore
+    import sip
+except ImportError:
+    print "Error: Program requires PyQt4 and gr-qtgui."
+    sys.exit(1)
+
+class GrDataPlotParent(gr.top_block, QtGui.QWidget):
+    # Setup signals
+    plotupdated = QtCore.pyqtSignal(QtGui.QWidget)
+
+    def __init__(self, name, rate, pmin=None, pmax=None):
+        gr.top_block.__init__(self)
+        QtGui.QWidget.__init__(self, None)
+
+        self._name = name
+        self._npts = 500
+        self._rate = rate
+        self.knobnames = [name,]
+
+        self.layout = QtGui.QVBoxLayout()
+        self.setLayout(self.layout)
+
+        self.setAcceptDrops(True)
+
+    def _setup(self, nconnections):
+        self.stop()
+        self.wait()
+
+        if(self.layout.count() > 0):
+            # Remove and disconnect. Making sure no references to snk
+            # remain so that the plot gets deleted.
+            self.layout.removeWidget(self.py_window)
+            self.disconnect(self.thr, (self.snk, 0))
+            self.disconnect(self.src[0], self.thr)
+            for n in xrange(1, self._ncons):
+                self.disconnect(self.src[n], (self.snk,n))
+
+        self._ncons = nconnections
+        self._data_len = self._ncons*[0,]
+
+        self.thr = blocks.throttle(self._datasize, self._rate)
+        self.snk = self.get_qtsink()
+
+        self.connect(self.thr, (self.snk, 0))
+
+        self._last_data = []
+        self.src = []
+        for n in xrange(self._ncons):
+            self.set_line_label(n, self.knobnames[n])
+
+            self._last_data.append(int(self._npts)*[0,])
+            self.src.append(self.get_vecsource())
+
+            if(n == 0):
+                self.connect(self.src[n], self.thr)
+            else:
+                self.connect(self.src[n], (self.snk,n))
+
+        self.py_window = sip.wrapinstance(self.snk.pyqwidget(), QtGui.QWidget)
+
+        self.layout.addWidget(self.py_window)
+
+    def __del__(self):
+        pass
+
+    def close(self):
+        self.snk.close()
+
+    def qwidget(self):
+        return self.py_window
+
+    def name(self):
+        return self._name
+
+    def semilogy(self, en=True):
+        self.snk.enable_semilogy(en)
+
+    def dragEnterEvent(self, e):
+        e.acceptProposedAction()
+
+    def dropEvent(self, e):
+        if(e.mimeData().hasFormat("text/plain")):
+            data = str(e.mimeData().text())
+
+            #"PlotData:{0}:{1}".format(tag, iscomplex)
+            datalst = data.split(":::")
+            tag = datalst[0]
+            name = datalst[1]
+            cpx = datalst[2] != "0"
+
+            if(tag == "PlotData" and cpx == self._iscomplex):
+                self.knobnames.append(name)
+
+                # create a new qwidget plot with the new data stream.
+                self._setup(len(self.knobnames))
+
+                # emit that this plot has been updated with a new qwidget.
+                self.plotupdated.emit(self)
+
+                e.acceptProposedAction()
+
+    def data_to_complex(self, data):
+        if(self._iscomplex):
+            data_r = data[0::2]
+            data_i = data[1::2]
+            data = [complex(r,i) for r,i in zip(data_r, data_i)]
+        return data
+
+    def update(self, data):
+        # Ask GUI if there has been a change in nsamps
+        npts = self.get_npts()
+        if(self._npts != npts):
+
+            # Adjust buffers to accomodate new settings
+            for n in xrange(self._ncons):
+                if(npts < self._npts):
+                    if(self._data_len[n] < npts):
+                        self._last_data[n] = self._last_data[n][0:npts]
+                    else:
+                        self._last_data[n] = self._last_data[n][self._data_len[n]-npts:self._data_len[n]]
+                        self._data_len[n] = npts
+                else:
+                    self._last_data[n] += (npts - self._npts)*[0,]
+            self._npts = npts
+            self.snk.reset()
+
+        if(self._stripchart):
+            # Update the plot data depending on type
+            for n in xrange(self._ncons):
+                if(type(data[n]) == list):
+                    data[n] = self.data_to_complex(data[n])
+                    if(len(data[n]) > self._npts):
+                        self.src[n].set_data(data[n])
+                        self._last_data[n] = data[n][-self._npts:]
+                    else:
+                        newdata = self._last_data[n][-(self._npts-len(data)):]
+                        newdata += data[n]
+                        self.src[n].set_data(newdata)
+                        self._last_data[n] = newdata
+
+                else: # single value update
+                    if(self._iscomplex):
+                        data[n] = complex(data[n][0], data[n][1])
+                    if(self._data_len[n] < self._npts):
+                        self._last_data[n][self._data_len[n]] = data[n]
+                        self._data_len[n] += 1
+                    else:
+                        self._last_data[n] = self._last_data[n][1:]
+                        self._last_data[n].append(data[n])
+                    self.src[n].set_data(self._last_data[n])
+        else:
+            for n in xrange(self._ncons):
+                if(type(data[n]) != list):
+                    data[n] = [data[n],]
+                data[n] = self.data_to_complex(data[n])
+                self.src[n].set_data(data[n])
+            
+
+
+class GrDataPlotterC(GrDataPlotParent):
+    def __init__(self, name, rate, pmin=None, pmax=None, stripchart=False):
+        GrDataPlotParent.__init__(self, name, rate, pmin, pmax)
+
+        self._stripchart = stripchart
+        self._datasize = gr.sizeof_gr_complex
+        self._iscomplex = True
+
+        self._setup(1)
+
+    def stem(self, en=True):
+        self.snk.enable_stem_plot(en)
+
+    def get_qtsink(self):
+        snk = qtgui.time_sink_c(self._npts, 1.0,
+                                self._name, self._ncons)
+        snk.enable_autoscale(True)
+        return snk
+
+    def get_vecsource(self):
+        return blocks.vector_source_c([])
+
+    def get_npts(self):
+        self._npts = self.snk.nsamps()
+        return self._npts
+
+    def set_line_label(self, n, name):
+        self.snk.set_line_label(2*n+0, "Re{" + self.knobnames[n] + "}")
+        self.snk.set_line_label(2*n+1, "Im{" + self.knobnames[n] + "}")
+
+
+class GrDataPlotterF(GrDataPlotParent):
+    def __init__(self, name, rate, pmin=None, pmax=None, stripchart=False):
+        GrDataPlotParent.__init__(self, name, rate, pmin, pmax)
+
+        self._stripchart = stripchart
+        self._datasize = gr.sizeof_float
+        self._iscomplex = False
+
+        self._setup(1)
+
+    def stem(self, en=True):
+        self.snk.enable_stem_plot(en)
+
+    def get_qtsink(self):
+        snk = qtgui.time_sink_f(self._npts, 1.0,
+                                self._name, self._ncons)
+        snk.enable_autoscale(True)
+        return snk
+    
+    def get_vecsource(self):
+        return blocks.vector_source_f([])
+
+    def get_npts(self):
+        self._npts = self.snk.nsamps()
+        return self._npts
+
+    def set_line_label(self, n, name):
+        self.snk.set_line_label(n, self.knobnames[n])
+            
+
+class GrDataPlotterConst(GrDataPlotParent):
+    def __init__(self, name, rate, pmin=None, pmax=None):
+        GrDataPlotParent.__init__(self, name, rate, pmin, pmax)
+
+        self._datasize = gr.sizeof_gr_complex
+        self._stripchart = False
+        self._iscomplex = True
+
+        self._setup(1)
+
+    def get_qtsink(self):
+        snk = qtgui.const_sink_c(self._npts,
+                                 self._name,
+                                 self._ncons)
+        snk.enable_autoscale(True)
+        return snk
+
+    def get_vecsource(self):
+        return blocks.vector_source_c([])
+
+    def get_npts(self):
+        self._npts = self.snk.nsamps()
+        return self._npts
+
+    def scatter(self, en=True):
+        if(en):
+            self.snk.set_line_style(0, 0)
+        else:
+            self.snk.set_line_style(0, 1)
+
+    def set_line_label(self, n, name):
+        self.snk.set_line_label(n, self.knobnames[n])
+
+
+class GrDataPlotterPsdC(GrDataPlotParent):
+    def __init__(self, name, rate, pmin=None, pmax=None):
+        GrDataPlotParent.__init__(self, name, rate, pmin, pmax)
+
+        self._datasize = gr.sizeof_gr_complex
+        self._stripchart = True
+        self._iscomplex = True
+
+        self._npts = 2048
+        self._wintype = filter.firdes.WIN_BLACKMAN_hARRIS
+        self._fc = 0
+
+        self._setup(1)
+
+    def get_qtsink(self):
+        snk = qtgui.freq_sink_c(self._npts, self._wintype,
+                                self._fc, 1.0,
+                                self._name,
+                                self._ncons)
+        snk.enable_autoscale(True)
+        return snk
+
+    def get_vecsource(self):
+        return blocks.vector_source_c([])
+
+    def get_npts(self):
+        self._npts = self.snk.fft_size()
+        return self._npts
+
+    def set_line_label(self, n, name):
+        self.snk.set_line_label(n, self.knobnames[n])
+
+
+class GrDataPlotterPsdF(GrDataPlotParent):
+    def __init__(self, name, rate, pmin=None, pmax=None):
+        GrDataPlotParent.__init__(self, name, rate, pmin, pmax)
+
+        self._datasize = gr.sizeof_float
+        self._stripchart = True
+        self._iscomplex = False
+
+        self._npts = 2048
+        self._wintype = filter.firdes.WIN_BLACKMAN_hARRIS
+        self._fc = 0
+
+        self._setup(1)
+
+    def get_qtsink(self):
+        snk = qtgui.freq_sink_f(self._npts, self._wintype,
+                                self._fc, 1.0,
+                                self._name,
+                                self._ncons)
+        snk.enable_autoscale(True)
+        return snk
+
+    def get_vecsource(self):
+        return blocks.vector_source_f([])
+
+    def get_npts(self):
+        self._npts = self.snk.fft_size()
+        return self._npts
+
+    def set_line_label(self, n, name):
+        self.snk.set_line_label(n, self.knobnames[n])
+
+
+class GrTimeRasterF(GrDataPlotParent):
+    def __init__(self, name, rate, pmin=None, pmax=None):
+        GrDataPlotParent.__init__(self, name, rate, pmin, pmax)
+
+        self._npts = 10
+        self._rows = 40
+
+        self._datasize = gr.sizeof_float
+        self._stripchart = False
+        self._iscomplex = False
+
+        self._setup(1)
+
+    def get_qtsink(self):
+        snk = qtgui.time_raster_sink_f(1.0, self._npts, self._rows,
+                                       [], [], self._name,
+                                       self._ncons)
+        return snk
+
+    def get_vecsource(self):
+        return blocks.vector_source_f([])
+
+    def get_npts(self):
+        self._npts = self.snk.num_cols()
+        return self._npts
+
+    def set_line_label(self, n, name):
+        self.snk.set_line_label(n, self.knobnames[n])
+
+class GrTimeRasterB(GrDataPlotParent):
+    def __init__(self, name, rate, pmin=None, pmax=None):
+        GrDataPlotParent.__init__(self, name, rate, pmin, pmax)
+
+        self._npts = 10
+        self._rows = 40
+
+        self._datasize = gr.sizeof_char
+        self._stripchart = False
+        self._iscomplex = False
+
+        self._setup(1)
+
+    def get_qtsink(self):
+        snk = qtgui.time_raster_sink_b(1.0, self._npts, self._rows,
+                                       [], [], self._name,
+                                       self._ncons)
+        return snk
+
+    def get_vecsource(self):
+        return blocks.vector_source_b([])
+
+    def get_npts(self):
+        self._npts = self.snk.num_cols()
+        return self._npts
+
+    def set_line_label(self, n, name):
+        self.snk.set_line_label(n, self.knobnames[n])
+
+
+class GrDataPlotterValueTable:
+    def __init__(self, uid, parent, x, y, xsize, ysize,
+                 headers=['Statistic Key ( Source Block :: Stat Name )  ',
+                          'Curent Value', 'Units', 'Description']):
+	# must encapsulate, cuz Qt's bases are not classes
+        self.uid = uid
+        self.treeWidget = QtGui.QTreeWidget(parent)
+        self.treeWidget.setColumnCount(len(headers))
+        self.treeWidget.setGeometry(x,y,xsize,ysize)
+        self.treeWidget.setHeaderLabels(headers)
+        self.treeWidget.resizeColumnToContents(0)
+
+    def updateItems(self, knobs, knobprops):
+        items = [];
+        self.treeWidget.clear()
+        for k, v in knobs.iteritems():
+            items.append(QtGui.QTreeWidgetItem([str(k), str(v.value),
+                                                knobprops[k].units,
+                                                knobprops[k].description]))
+        self.treeWidget.insertTopLevelItems(0, items)
diff --git a/gnuradio-runtime/python/gnuradio/ctrlport/IceRadioClient.py b/gnuradio-runtime/python/gnuradio/ctrlport/IceRadioClient.py
new file mode 100644
index 0000000000..0964b5a4ba
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/ctrlport/IceRadioClient.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+#
+# Copyright 2012 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+import Ice, Glacier2
+from PyQt4 import QtGui, QtCore
+import sys, time, Ice
+from gnuradio import gr
+from gnuradio.ctrlport import GNURadio
+
+class IceRadioClient(Ice.Application):
+    def __init__(self, parentClass):
+        self.parentClass = parentClass
+
+    def getRadio(self, host, port):
+        radiostr = "gnuradio -t:tcp -h " + host + " -p " + port + " -t 3000"
+        base = self.communicator().stringToProxy(radiostr).ice_twoway()
+        radio = GNURadio.ControlPortPrx.checkedCast(base)
+
+        if not radio:
+            sys.stderr.write("{0} : invalid proxy.\n".format(args[0]))
+            return None
+
+        return radio
+
+    def run(self,args):
+        if len(args) < 2:
+                print "useage: [glacierinstance glacierhost glacierport] host port"
+                return
+        if len(args) == 8:
+                self.useglacier = True
+		guser = args[1]
+                gpass = args[2]
+                ginst = args[3]
+                ghost = args[4]
+                gport = args[5]
+                host = args[6]
+                port = args[7]
+        else:
+                self.useglacier = False
+                host = args[1]
+                port = args[2]
+
+        if self.useglacier:
+	  gstring = ginst + "/router -t:tcp -h " + ghost + " -p " + gport
+	  print "GLACIER: {0}".format(gstring)
+	  
+	  setrouter = Glacier2.RouterPrx.checkedCast(self.communicator().stringToProxy(gstring))
+	  self.communicator().setDefaultRouter(setrouter)
+          defaultRouter = self.communicator().getDefaultRouter()
+	  #defaultRouter = self.communicator().stringToProxy(gstring)
+          if not defaultRouter:
+              print self.appName() + ": no default router set"
+              return 1
+	  else:
+              print str(defaultRouter)
+          router = Glacier2.RouterPrx.checkedCast(defaultRouter)
+          if not router:
+              print self.appName() + ": configured router is not a Glacier2 router"
+              return 1
+
+          while True:
+            print "This demo accepts any user-id / password combination."
+	    if not guser == '' and not gpass == '':
+		id = guser
+                pw = gpass
+	    else:
+            	id = raw_input("user id: ")
+                pw = raw_input("password: ")
+
+            try:
+                router.createSession(id, pw)
+                break
+            except Glacier2.PermissionDeniedException, ex:
+                print "permission denied:\n" + ex.reason
+
+        radio = self.getRadio(host, port)
+        if(radio is None):
+            return 1
+
+        app = QtGui.QApplication(sys.argv)
+        ex = self.parentClass(radio, port, self)
+        ex.show();
+        app.exec_()
diff --git a/gnuradio-runtime/python/gnuradio/ctrlport/__init__.py b/gnuradio-runtime/python/gnuradio/ctrlport/__init__.py
new file mode 100644
index 0000000000..031c3b424e
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/ctrlport/__init__.py
@@ -0,0 +1,30 @@
+#
+# Copyright 2012 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 3, 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 this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+# The presence of this file turns this directory into a Python package
+
+import Ice, IcePy
+
+# import swig generated symbols into the ctrlport namespace
+#from ctrlport_swig import *
+from monitor import *
+
+# import any pure python here
+#import GNURadio
diff --git a/gnuradio-runtime/python/gnuradio/ctrlport/gr-ctrlport-curses b/gnuradio-runtime/python/gnuradio/ctrlport/gr-ctrlport-curses
new file mode 100755
index 0000000000..1bee3b1a1e
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/ctrlport/gr-ctrlport-curses
@@ -0,0 +1,268 @@
+#!/usr/bin/env python
+#
+# Copyright 2012 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+import threading
+import curses
+import os, sys, time
+from optparse import OptionParser
+
+import Ice
+from gnuradio.ctrlport import GNURadio
+
+ENTER = chr(10)
+UP_ARROW = chr(65)
+DOWN_ARROW = chr(66)
+
+class modem_monitor(threading.Thread):
+    def __init__(self, cb_live, cb_exit, interface):
+        threading.Thread.__init__(self)
+        self.cb_live = cb_live
+        self.cb_exit = cb_exit
+
+        self.running = True
+
+    def __del__(self):
+        rx.close()
+
+    def run(self):                
+        while self.running:
+            time.sleep(0.5)
+
+    def shutdown(self):
+        self.running = False
+        self.rx.close()
+
+    def cb(self,contents):
+        (op, sep, payload) = contents.partition(":")
+        if(op == "live"):
+            print "live"
+            self.cb_live(payload)
+        elif(op == "exit"):
+            self.cb_exit(payload)
+        else:
+            print "unknown op arrived! garbage on multicast adx?"
+
+class modem_window(threading.Thread):
+    def __init__(self, locator):
+        threading.Thread.__init__(self)
+        self.locator = locator
+
+        # curses init
+        self.win = curses.newwin(30,100,4,4)
+
+        # Ice/GRCP init
+        self.comm = Ice.initialize()
+        proxy = self.comm.stringToProxy(locator)
+        self.radio = GNURadio.ControlPortPrx.checkedCast(proxy)
+        self.updateKnobs()
+
+        # GUI init
+        self.running = True
+        self.ssel = 0
+        self.start()
+        #self.updateGUI()
+        
+        # input loop
+        while(self.running):
+            self.getInput()
+
+        # wait for update thread exit
+        self.join()
+
+    def updateKnobs(self):
+        self.knobs = self.radio.get([])
+
+    def getInput(self):
+        a = self.win.getch()
+        if(a <= 256):
+            a = chr(a)
+            if(a == 'q'):
+                self.running = False
+            elif(a == UP_ARROW):
+                self.ssel = max(self.ssel-1, 0)
+                self.updateGUI()
+            elif(a == DOWN_ARROW):
+                self.ssel = max(min(self.ssel+1, len(self.knobs.keys())-1),0)
+                self.updateGUI()
+        self.updateGUI()
+
+    def updateGUI(self):
+        self.win.clear()
+        self.win.border(0)
+        self.win.addstr(1, 2, "Modem Statistics :: %s"%(self.locator))
+        self.win.addstr(2, 2, "---------------------------------------------------")
+
+        maxnb = 0
+        maxk = 0
+        for k in self.knobs.keys():
+            (nb,k) = k.split("::", 2)
+            maxnb = max(maxnb,len(nb))
+            maxk = max(maxk,len(k))
+
+        offset = 3
+        keys = self.knobs.keys()
+        keys.sort()
+        for k in keys:
+            (nb,sk) = k.split("::", 2)
+            v = self.knobs[k].value
+            sv = str(v)
+            if(len(sv) > 20):
+                sv = sv[0:20]
+            props = 0
+            if(self.ssel == offset-3):
+                props = props | curses.A_REVERSE
+            self.win.addstr(offset, 2, "%s %s %s" % \
+                (nb.rjust(maxnb," "), sk.ljust(maxk," "), sv),props)
+            offset = offset + 1
+        self.win.refresh()
+
+    def run(self):
+        while(self.running):
+            self.updateKnobs()
+            self.updateGUI()
+            time.sleep(1)
+
+class monitor_gui:
+    def __init__(self, interfaces, options):
+
+        locator = None
+
+        # Extract options into a locator
+        if(options.host and options.port):
+            locator = "{0} -t:{1} -h {2} -p {3}".format(
+                options.app, options.protocol,
+                options.host, options.port)
+
+        # Set up GUI
+        self.locators = {}
+
+        self.mode = 0 # modem index screen (modal keyboard input)
+        self.lsel = 0 # selected locator
+        self.scr = curses.initscr()
+        self.updateGUI()
+
+        # Kick off initial monitors
+        self.monitors = []
+        for i in interfaces:
+            self.monitors.append( modem_monitor(self.addModem, self.delModem, i) )
+            self.monitors[-1].start()
+
+        if not ((locator == None) or (locator == "")):
+            self.addModem(locator)
+
+        # wait for user input
+        while(True):
+            self.getInput()
+
+    def addModem(self, locator):
+        if(not self.locators.has_key(locator)):
+            self.locators[locator] = {}
+        self.locators[locator]["update_time"] = time.time()
+        self.locators[locator]["status"] = "live"
+
+        self.updateGUI();
+
+    def delModem(self, locator):
+        #if(self.locators.has_key(locator)):
+        if(not self.locators.has_key(locator)):
+            self.locators[locator] = {}
+        self.locators[locator]["update_time"] = time.time()
+        self.locators[locator]["status"] = "exit"
+
+        self.updateGUI()
+
+    def updateGUI(self):
+        if(self.mode == 0): #redraw locators
+            self.scr.clear()
+            self.scr.border(0)
+            self.scr.addstr(1, 2, " GRCP-Curses Modem Monitor :: (A)dd (R)efresh, (Q)uit, ...")
+            for i in range(len(self.locators.keys())):
+                locator = self.locators.keys()[i]
+                lhash = self.locators[locator]
+                #self.scr.addstr(3+i, 5, locator + str(lhash))
+                props = 0
+                if(self.lsel == i):
+                    props = props | curses.A_REVERSE
+                self.scr.addstr(3+i, 5, locator + str(lhash), props)
+            self.scr.refresh()
+
+    def connectGUI(self):
+        self.scr.clear()
+        self.scr.addstr(1, 1, "Connect to radio:")
+        locator = self.scr.getstr(200)
+        self.addModem(locator)
+        self.updateGUI()
+
+    def getInput(self):
+        a = self.scr.getch()
+        self.scr.addstr(20, 2, "got key (%d)    " % (int(a)))
+        if(a <= 256):
+            a = chr(a)
+            if(a =='r'):
+                self.updateGUI()
+            elif(a == 'q'):
+                self.shutdown()
+            elif(a == 'a'):
+                self.connectGUI()
+            elif(a == UP_ARROW):
+                self.lsel = max(self.lsel-1, 0)
+                self.updateGUI()
+            elif(a == DOWN_ARROW):
+                self.lsel = max(min(self.lsel+1, len(self.locators.keys())-1),0)
+                self.updateGUI()
+            elif(a == ENTER):
+                try:
+                    locator = self.locators.keys()[self.lsel]
+                    self.mode = 1
+                    mwin = modem_window(locator)
+                    self.mode = 0
+                    # pop up a new modem display ...
+                    self.updateGUI()
+                except:
+                    pass
+        
+    def shutdown(self):
+        curses.endwin()
+        os._exit(0)
+
+if __name__ == "__main__":
+    parser = OptionParser()
+    parser.add_option("-H", "--host", type="string",
+                      help="Hostname of ControlPort server.")
+    parser.add_option("-p", "--port", type="int",
+                      help="Port number of host's ControlPort endpoint.")
+    parser.add_option("-i", "--interfaces", type="string",
+                      action="append", default=["lo"],
+                      help="Interfaces to use. [default=%default]")
+    parser.add_option("-P", "--protocol", type="string", default="tcp",
+                      help="Type of protocol to use (usually tcp). [default=%default]")
+    parser.add_option("-a", "--app", type="string", default="gnuradio",
+                      help="Name of application [default=%default]")
+    (options, args) = parser.parse_args()
+
+    if((options.host == None) ^ (options.port == None)):
+        print "Please set both a hostname and a port number.\n"
+        parser.print_help()
+        sys.exit(1)
+
+    mg = monitor_gui(options.interfaces, options)
+
diff --git a/gnuradio-runtime/python/gnuradio/ctrlport/gr-ctrlport-monitor b/gnuradio-runtime/python/gnuradio/ctrlport/gr-ctrlport-monitor
new file mode 100755
index 0000000000..e71cd92ab7
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/ctrlport/gr-ctrlport-monitor
@@ -0,0 +1,721 @@
+#!/usr/bin/env python
+#
+# Copyright 2012,2013 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+from gnuradio import gr, ctrlport
+
+from PyQt4 import QtCore,Qt
+import PyQt4.QtGui as QtGui
+import os, sys, time
+
+import Ice
+from gnuradio.ctrlport.IceRadioClient import *
+from gnuradio.ctrlport.GrDataPlotter import *
+from gnuradio.ctrlport import GNURadio
+
+class RateDialog(QtGui.QDialog):
+    def __init__(self, delay, parent=None):
+        super(RateDialog, self).__init__(parent)
+        self.gridLayout = QtGui.QGridLayout(self)
+        self.setWindowTitle("Update Delay (ms)");
+        self.delay = QtGui.QLineEdit(self);
+        self.delay.setText(str(delay));
+        self.buttonBox = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel)
+        self.gridLayout.addWidget(self.delay);
+        self.gridLayout.addWidget(self.buttonBox);
+        self.buttonBox.accepted.connect(self.accept)
+        self.buttonBox.rejected.connect(self.reject)
+    def accept(self):
+        self.done(1);
+    def reject(self):
+        self.done(0);
+
+class MAINWindow(QtGui.QMainWindow):
+    def minimumSizeHint(self):
+        return Qtgui.QSize(800,600)
+
+    def __init__(self, radio, port, interface):
+        
+        super(MAINWindow, self).__init__()
+        self.updateRate = 1000;
+        self.conns = []
+        self.plots = []
+        self.knobprops = []
+        self.interface = interface
+
+        self.mdiArea = QtGui.QMdiArea()
+        self.mdiArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+        self.mdiArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+        self.setCentralWidget(self.mdiArea)
+
+        self.mdiArea.subWindowActivated.connect(self.updateMenus)
+        self.windowMapper = QtCore.QSignalMapper(self)
+        self.windowMapper.mapped[QtGui.QWidget].connect(self.setActiveSubWindow)
+
+        self.createActions()
+        self.createMenus()
+        self.createToolBars()
+        self.createStatusBar()
+        self.updateMenus()
+
+        self.setWindowTitle("GNU Radio Control Port Monitor")
+        self.setUnifiedTitleAndToolBarOnMac(True)
+
+        self.newCon(radio, port)
+        icon = QtGui.QIcon(ctrlport.__path__[0] + "/icon.png" )
+        self.setWindowIcon(icon)
+
+        # Locally turn off ControlPort export from GR. This prevents
+        # our GR-based plotters from launching their own ControlPort
+        # instance (and possibly causing a port collision if one has
+        # been specified).
+        os.environ['GR_CONF_CONTROLPORT_ON'] = 'False'
+
+    def setUpdateRate(self,nur):
+        self.updateRate = int(nur);
+        for c in self.conns:
+            c.updateRate = self.updateRate;
+            c.timer.setInterval(self.updateRate);
+
+    def newCon(self, radio=None, port=None):
+        child = MForm(radio, port, len(self.conns), self.updateRate, self)
+        if(child.radio is not None):
+            child.setWindowTitle(str(child.radio))
+            self.mdiArea.addSubWindow(child)
+            child.showMaximized()
+        self.conns.append(child)
+        self.plots.append([])
+
+    def propertiesMenu(self, key, radio, uid):
+        r = str(radio).split(" ")
+        title = "{0}:{1}".format(r[3], r[5])
+
+        props = radio.properties([key])
+        pmin = props[key].min.value
+        pmax = props[key].max.value
+        if pmin == []:
+            pmin = None
+        else:
+            pmin = 1.1*pmin
+        if pmax == []:
+            pmax = None
+        else:
+            pmax = 1.1*pmax
+
+        # Use display option mask of item to set up available plot
+        # types and default options.
+        disp = self.knobprops[uid][key].display
+        cplx = disp & gr.DISPOPTCPLX | disp & gr.DISPXY
+        strip = disp & gr.DISPOPTSTRIP
+        stem = disp & gr.DISPOPTSTEM
+        log = disp & gr.DISPOPTLOG
+        scatter = disp & gr.DISPOPTSCATTER
+
+        def newUpdaterProxy():
+            self.newUpdater(key, radio)
+
+        def newPlotterFProxy():
+            self.newPlotF(key, uid, title, pmin, pmax,
+                          log, strip, stem)
+
+        def newPlotterCProxy():
+            self.newPlotC(key, uid, title, pmin, pmax,
+                          log, strip, stem)
+
+        def newPlotterConstProxy():
+            self.newPlotConst(key, uid, title, pmin, pmax, scatter)
+
+        def newPlotterPsdFProxy():
+            self.newPlotPsdF(key, uid, title)
+
+        def newPlotterPsdCProxy():
+            self.newPlotPsdC(key, uid, title)
+
+        def newPlotterRasterFProxy():
+            self.newPlotRasterF(key, uid, title, pmin, pmax)
+
+        def newPlotterRasterBProxy():
+            self.newPlotRasterB(key, uid, title, pmin, pmax)
+
+        menu = QtGui.QMenu(self)
+        menu.setTitle("Item Actions")
+        menu.setTearOffEnabled(False)
+
+        # object properties
+        menu.addAction("Properties", newUpdaterProxy)
+
+        # displays available
+        if(cplx == 0):
+            menu.addAction("Plot Time", newPlotterFProxy)
+            menu.addAction("Plot PSD", newPlotterPsdFProxy)
+            menu.addAction("Plot Raster (real)", newPlotterRasterFProxy)
+            #menu.addAction("Plot Raster (bits)", newPlotterRasterBProxy)
+        else:
+            menu.addAction("Plot Time", newPlotterCProxy)
+            menu.addAction("Plot PSD", newPlotterPsdCProxy)
+            menu.addAction("Plot Constellation", newPlotterConstProxy)
+
+        menu.popup(QtGui.QCursor.pos())
+
+    def newUpdater(self, key, radio):
+        updater = UpdaterWindow(key, radio, None)
+        updater.setWindowTitle("Updater: " + key)
+        updater.setModal(False)
+        updater.exec_()
+
+    def newSub(self, e):
+        tag = str(e.text(0))
+        tree = e.treeWidget().parent()
+        uid = tree.uid
+        knobprop = self.knobprops[uid][tag]
+
+        r = str(tree.radio).split(" ")
+        title = "{0}:{1}".format(r[3], r[5])
+        pmin = knobprop.min.value
+        pmax = knobprop.max.value
+        if pmin == []:
+            pmin = None
+        else:
+            pmin = 1.1*pmin
+        if pmax == []:
+            pmax = None
+        else:
+            pmax = 1.1*pmax
+
+        disp = knobprop.display
+        if(disp & gr.DISPTIME):
+            strip = disp & gr.DISPOPTSTRIP
+            stem = disp & gr.DISPOPTSTEM
+            log = disp & gr.DISPOPTLOG
+            if(disp & gr.DISPOPTCPLX == 0):
+                self.newPlotF(tag, uid, title, pmin, pmax,
+                              log, strip, stem)
+            else:
+                self.newPlotC(tag, uid, title, pmin, pmax,
+                              log, strip, stem)
+            
+        elif(disp & gr.DISPXY):
+            scatter = disp & gr.DISPOPTSCATTER
+            self.newPlotConst(tag, uid, title, pmin, pmax, scatter)
+            
+        elif(disp & gr.DISPPSD):
+            if(disp & gr.DISPOPTCPLX == 0):
+                self.newPlotPsdF(tag, uid, title)
+            else:
+                self.newPlotPsdC(tag, uid, title)
+
+    def startDrag(self, e):
+        drag = QtGui.QDrag(self)
+        mime_data = QtCore.QMimeData()
+
+        tag = str(e.text(0))
+        tree = e.treeWidget().parent()
+        knobprop = self.knobprops[tree.uid][tag]
+        disp = knobprop.display
+        iscomplex = (disp & gr.DISPOPTCPLX) or (disp & gr.DISPXY)
+        
+        if(disp != gr.DISPNULL):
+            data = "PlotData:::{0}:::{1}".format(tag, iscomplex)
+        else:
+            data = "OtherData:::{0}:::{1}".format(tag, iscomplex)
+
+        mime_data.setText(data)
+        drag.setMimeData(mime_data)
+
+        drop = drag.start()
+
+    def createPlot(self, plot, uid, title):
+        plot.start()
+        self.plots[uid].append(plot)
+
+        self.mdiArea.addSubWindow(plot)
+        plot.setWindowTitle("{0}: {1}".format(title, plot.name()))
+        self.connect(plot.qwidget(),
+                     QtCore.SIGNAL('destroyed(QObject*)'),
+                     self.destroyPlot)
+
+        # when the plot is updated via drag-and-drop, we need to be
+        # notified of the new qwidget that's created so we can
+        # properly destroy it.
+        plot.plotupdated.connect(self.plotUpdated)
+
+        plot.show()
+
+    def plotUpdated(self, q):
+        # the plot has been updated with a new qwidget; make sure this
+        # gets dies to the destroyPlot function.
+        for i, plots in enumerate(self.plots):
+            for p in plots:
+                if(p == q):
+                    #plots.remove(p)
+                    #plots.append(q)
+                    self.connect(q.qwidget(),
+                                 QtCore.SIGNAL('destroyed(QObject*)'),
+                                 self.destroyPlot)
+                    break
+
+    def destroyPlot(self, obj):
+        for plots in self.plots:
+            for p in plots:
+                if p.qwidget() == obj:
+                    plots.remove(p)
+                    break
+
+    def newPlotConst(self, tag, uid, title="", pmin=None, pmax=None,
+                     scatter=False):
+        plot = GrDataPlotterConst(tag, 32e6, pmin, pmax)
+        plot.scatter(scatter)
+        self.createPlot(plot, uid, title)
+    
+    def newPlotF(self, tag, uid, title="", pmin=None, pmax=None, 
+                 logy=False, stripchart=False, stem=False):
+        plot = GrDataPlotterF(tag, 32e6, pmin, pmax, stripchart)
+        plot.semilogy(logy)
+        plot.stem(stem)
+        self.createPlot(plot, uid, title)
+
+    def newPlotC(self, tag, uid, title="", pmin=None, pmax=None,
+                 logy=False, stripchart=False, stem=False):
+        plot = GrDataPlotterC(tag, 32e6, pmin, pmax, stripchart)
+        plot.semilogy(logy)
+        plot.stem(stem)
+        self.createPlot(plot, uid, title)
+
+    def newPlotPsdF(self, tag, uid, title="", pmin=None, pmax=None):
+        plot = GrDataPlotterPsdF(tag, 32e6, pmin, pmax)
+        self.createPlot(plot, uid, title)
+
+    def newPlotPsdC(self, tag, uid, title="", pmin=None, pmax=None):
+        plot = GrDataPlotterPsdC(tag, 32e6, pmin, pmax)
+        self.createPlot(plot, uid, title)
+
+    def newPlotRasterF(self, tag, uid, title="", pmin=None, pmax=None):
+        plot = GrTimeRasterF(tag, 32e6, pmin, pmax)
+        self.createPlot(plot, uid, title)
+
+    def newPlotRasterB(self, tag, uid, title="", pmin=None, pmax=None):
+        plot = GrTimeRasterB(tag, 32e6, pmin, pmax)
+        self.createPlot(plot, uid, title)
+
+    def update(self, knobs, uid):
+        #sys.stderr.write("KNOB KEYS: {0}\n".format(knobs.keys()))
+        for plot in self.plots[uid]:
+            data = []
+            for n in plot.knobnames:
+                data.append(knobs[n].value)
+            plot.update(data)
+            plot.stop()
+            plot.wait()
+            plot.start()
+
+    def setActiveSubWindow(self, window):
+        if window:
+            self.mdiArea.setActiveSubWindow(window)
+
+
+    def createActions(self):
+        self.newConAct = QtGui.QAction("&New Connection",
+                self, shortcut=QtGui.QKeySequence.New,
+                statusTip="Create a new file", triggered=self.newCon)
+        #self.newAct = QtGui.QAction(QtGui.QIcon(':/images/new.png'), "&New Plot",
+        self.newPlotAct = QtGui.QAction("&New Plot",
+                self,
+                statusTip="Create a new file", triggered=self.newPlotF)
+
+        self.exitAct = QtGui.QAction("E&xit", self, shortcut="Ctrl+Q",
+                statusTip="Exit the application",
+                triggered=QtGui.qApp.closeAllWindows)
+
+        self.closeAct = QtGui.QAction("Cl&ose", self, shortcut="Ctrl+F4",
+                statusTip="Close the active window",
+                triggered=self.mdiArea.closeActiveSubWindow)
+
+        self.closeAllAct = QtGui.QAction("Close &All", self,
+                statusTip="Close all the windows",
+                triggered=self.mdiArea.closeAllSubWindows)
+
+        self.urAct = QtGui.QAction("Update Rate", self, shortcut="F5",
+                statusTip="Change Update Rate",
+                triggered=self.updateRateShow)
+
+        qks = QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_T);
+        self.tileAct = QtGui.QAction("&Tile", self,
+                statusTip="Tile the windows",
+                triggered=self.mdiArea.tileSubWindows,
+                shortcut=qks)
+
+        qks = QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_C);
+        self.cascadeAct = QtGui.QAction("&Cascade", self,
+                statusTip="Cascade the windows", shortcut=qks,
+                triggered=self.mdiArea.cascadeSubWindows)
+
+        self.nextAct = QtGui.QAction("Ne&xt", self,
+                shortcut=QtGui.QKeySequence.NextChild,
+                statusTip="Move the focus to the next window",
+                triggered=self.mdiArea.activateNextSubWindow)
+
+        self.previousAct = QtGui.QAction("Pre&vious", self,
+                shortcut=QtGui.QKeySequence.PreviousChild,
+                statusTip="Move the focus to the previous window",
+                triggered=self.mdiArea.activatePreviousSubWindow)
+
+        self.separatorAct = QtGui.QAction(self)
+        self.separatorAct.setSeparator(True)
+
+        self.aboutAct = QtGui.QAction("&About", self,
+                statusTip="Show the application's About box",
+                triggered=self.about)
+
+        self.aboutQtAct = QtGui.QAction("About &Qt", self,
+                statusTip="Show the Qt library's About box",
+                triggered=QtGui.qApp.aboutQt)
+
+    def createMenus(self):
+        self.fileMenu = self.menuBar().addMenu("&File")
+        self.fileMenu.addAction(self.newConAct)
+        self.fileMenu.addAction(self.newPlotAct)
+        self.fileMenu.addAction(self.urAct)
+        self.fileMenu.addSeparator()
+        self.fileMenu.addAction(self.exitAct)
+
+        self.windowMenu = self.menuBar().addMenu("&Window")
+        self.updateWindowMenu()
+        self.windowMenu.aboutToShow.connect(self.updateWindowMenu)
+
+        self.menuBar().addSeparator()
+
+        self.helpMenu = self.menuBar().addMenu("&Help")
+        self.helpMenu.addAction(self.aboutAct)
+        self.helpMenu.addAction(self.aboutQtAct)
+
+    def updateRateShow(self):
+        askrate = RateDialog(self.updateRate, self);
+        if askrate.exec_():
+            ur = float(str(askrate.delay.text()));
+            self.setUpdateRate(ur);
+            return;
+        else:
+            return;
+
+    def createToolBars(self):
+        self.fileToolBar = self.addToolBar("File")
+        self.fileToolBar.addAction(self.newConAct)
+        self.fileToolBar.addAction(self.newPlotAct)
+        self.fileToolBar.addAction(self.urAct)
+
+        self.fileToolBar = self.addToolBar("Window")
+        self.fileToolBar.addAction(self.tileAct)
+        self.fileToolBar.addAction(self.cascadeAct)
+
+    def createStatusBar(self):
+        self.statusBar().showMessage("Ready")
+
+
+    def activeMdiChild(self):
+        activeSubWindow = self.mdiArea.activeSubWindow()
+        if activeSubWindow:
+            return activeSubWindow.widget()
+        return None
+
+    def updateMenus(self):
+        hasMdiChild = (self.activeMdiChild() is not None)
+        self.closeAct.setEnabled(hasMdiChild)
+        self.closeAllAct.setEnabled(hasMdiChild)
+        self.tileAct.setEnabled(hasMdiChild)
+        self.cascadeAct.setEnabled(hasMdiChild)
+        self.nextAct.setEnabled(hasMdiChild)
+        self.previousAct.setEnabled(hasMdiChild)
+        self.separatorAct.setVisible(hasMdiChild)
+
+    def updateWindowMenu(self):
+        self.windowMenu.clear()
+        self.windowMenu.addAction(self.closeAct)
+        self.windowMenu.addAction(self.closeAllAct)
+        self.windowMenu.addSeparator()
+        self.windowMenu.addAction(self.tileAct)
+        self.windowMenu.addAction(self.cascadeAct)
+        self.windowMenu.addSeparator()
+        self.windowMenu.addAction(self.nextAct)
+        self.windowMenu.addAction(self.previousAct)
+        self.windowMenu.addAction(self.separatorAct)
+
+    def about(self):
+        about_info = \
+'''Copyright 2012 Free Software Foundation, Inc.\n
+This program is part of GNU Radio.\n
+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 3, or (at your option) any later version.\n
+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.\n
+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., 51 Franklin Street, Boston, MA 02110-1301, USA.'''
+
+        QtGui.QMessageBox.about(None, "gr-ctrlport-monitor", about_info)
+
+
+class ConInfoDialog(QtGui.QDialog):
+    def __init__(self, parent=None):
+        super(ConInfoDialog, self).__init__(parent)
+
+        self.gridLayout = QtGui.QGridLayout(self)
+        
+
+        self.host = QtGui.QLineEdit(self);
+        self.port = QtGui.QLineEdit(self);
+        self.host.setText("localhost");
+        self.port.setText("43243");
+
+        self.buttonBox = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel)
+
+        self.gridLayout.addWidget(self.host);
+        self.gridLayout.addWidget(self.port);
+        self.gridLayout.addWidget(self.buttonBox);
+
+        self.buttonBox.accepted.connect(self.accept)
+        self.buttonBox.rejected.connect(self.reject)
+
+
+    def accept(self):
+        self.done(1);
+
+    def reject(self):
+        self.done(0);
+
+
+class UpdaterWindow(QtGui.QDialog):
+    def __init__(self, key, radio, parent):
+        QtGui.QDialog.__init__(self, parent)
+
+        self.key = key;
+        self.radio = radio
+
+        self.resize(300,200)
+        self.layout = QtGui.QVBoxLayout()
+
+        self.props = radio.properties([key])[key]
+        info = str(self.props)
+
+        self.infoLabel = QtGui.QLabel(info)
+        self.layout.addWidget(self.infoLabel)
+
+        # Test here to make sure that a 'set' function
+        try:
+            a = radio.set(radio.get([key]))
+            has_set = True
+        except Ice.UnknownException:
+            has_set = False
+
+        if(has_set is False):
+            self.cancelButton = QtGui.QPushButton("Ok")
+            self.cancelButton.connect(self.cancelButton, QtCore.SIGNAL('clicked()'), self.reject)
+
+            self.buttonlayout = QtGui.QHBoxLayout()
+            self.buttonlayout.addWidget(self.cancelButton)
+            self.layout.addLayout(self.buttonlayout)
+ 
+        else: # we have a set function
+            self.textInput = QtGui.QLineEdit()
+            self.layout.addWidget(self.textInput)
+
+            self.applyButton = QtGui.QPushButton("Apply")
+            self.setButton = QtGui.QPushButton("OK")
+            self.cancelButton = QtGui.QPushButton("Cancel")
+
+            rv = radio.get([key])
+            self.textInput.setText(str(rv[key].value))
+            self.sv = rv[key]
+
+            self.applyButton.connect(self.applyButton, QtCore.SIGNAL('clicked()'), self._apply)
+            self.setButton.connect(self.setButton, QtCore.SIGNAL('clicked()'), self._set)
+            self.cancelButton.connect(self.cancelButton, QtCore.SIGNAL('clicked()'), self.reject)
+ 
+            self.is_num = ((type(self.sv.value)==float) or (type(self.sv.value)==int))
+            if(self.is_num):
+                self.sliderlayout = QtGui.QHBoxLayout()
+
+                self.slider = QtGui.QSlider(QtCore.Qt.Horizontal)
+
+                self.sliderlayout.addWidget(QtGui.QLabel(str(self.props.min.value)))
+                self.sliderlayout.addWidget(self.slider)
+                self.sliderlayout.addWidget(QtGui.QLabel(str(self.props.max.value)))
+
+                self.steps = 10000
+                self.valspan = self.props.max.value - self.props.min.value
+            
+                self.slider.setRange(0, 10000)
+                self._set_slider_value(self.sv.value)
+
+                self.connect(self.slider, QtCore.SIGNAL("sliderReleased()"), self._slide)
+
+                self.layout.addLayout(self.sliderlayout)
+
+                self.buttonlayout = QtGui.QHBoxLayout()
+                self.buttonlayout.addWidget(self.applyButton)
+                self.buttonlayout.addWidget(self.setButton)
+                self.buttonlayout.addWidget(self.cancelButton)
+                self.layout.addLayout(self.buttonlayout)
+
+        # set layout and go...
+        self.setLayout(self.layout)
+            
+    def _set_slider_value(self, val):
+        self.slider.setValue(self.steps*(val-self.props.min.value)/self.valspan)
+             
+    def _slide(self):
+        val = (self.slider.value()*self.valspan + self.props.min.value)/float(self.steps)
+        self.textInput.setText(str(val))
+
+    def _apply(self):
+        if(type(self.sv.value) == str):
+            val = str(self.textInput.text())
+        elif(type(self.sv.value) == int):
+            val = int(round(float(self.textInput.text())))
+        elif(type(self.sv.value) == float):
+            val = float(self.textInput.text())
+        else:
+            sys.stderr.write("set type not supported! ({0})\n".format(type(self.sv.value)))
+            sys.exit(-1)
+
+        self.sv.value = val
+        km = {}
+        km[self.key] = self.sv
+        self.radio.set(km)
+        self._set_slider_value(self.sv.value)
+
+    def _set(self):
+        self._apply()
+        self.done(0)
+
+
+class MForm(QtGui.QWidget):
+    def update(self):
+        try:
+            st = time.time();
+            knobs = self.radio.get([]);
+            ft = time.time();
+            latency = ft-st;
+            self.parent.statusBar().showMessage("Current GNU Radio Control Port Query Latency: %f ms"%(latency*1000))
+            
+        except Exception, e:
+            sys.stderr.write("ctrlport-monitor: radio.get threw exception ({0}).\n".format(e))
+            if(type(self.parent) is MAINWindow):
+                # Find window of connection
+                remove = []
+                for p in self.parent.mdiArea.subWindowList():
+                    if self.parent.conns[self.uid] == p.widget():
+                        remove.append(p)
+
+                # Find any subplot windows of connection
+                for p in self.parent.mdiArea.subWindowList():
+                    for plot in self.parent.plots[self.uid]:
+                        if plot.qwidget() == p.widget():
+                            remove.append(p)
+                
+                # Clean up local references to these
+                self.parent.conns.remove(self.parent.conns[self.uid])
+                self.parent.plots.remove(self.parent.plots[self.uid])
+
+                # Remove subwindows for connection and plots
+                for r in remove:
+                    self.parent.mdiArea.removeSubWindow(r)
+
+                # Clean up self
+                self.close()
+            else:
+                sys.exit(1)
+            return
+
+        tableitems = knobs.keys()
+            
+        #UPDATE TABLE:
+        self.table.updateItems(knobs, self.knobprops)
+
+        #UPDATE PLOTS
+        self.parent.update(knobs, self.uid)
+
+
+    def __init__(self, radio=None, port=None, uid=0, updateRate=2000, parent=None):
+
+        super(MForm, self).__init__()
+
+        if(radio == None or port == None):
+            askinfo = ConInfoDialog(self);
+            if askinfo.exec_():
+                host = str(askinfo.host.text());
+                port = str(askinfo.port.text());
+                radio = parent.interface.getRadio(host, port)
+            else:
+                self.radio = None
+                return
+        
+        self.uid = uid
+        self.parent = parent
+        self.horizontalLayout = QtGui.QVBoxLayout(self)
+        self.gridLayout = QtGui.QGridLayout()
+
+        self.radio = radio
+        self.knobprops = self.radio.properties([])
+        self.parent.knobprops.append(self.knobprops)
+        self.resize(775,500)
+        self.timer = QtCore.QTimer()
+        self.constupdatediv = 0
+        self.tableupdatediv = 0
+        plotsize=250
+            
+        # make table
+        self.table = GrDataPlotterValueTable(uid, self, 0, 0, 400, 200)
+        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
+        self.table.treeWidget.setSizePolicy(sizePolicy)
+        self.table.treeWidget.setEditTriggers(QtGui.QAbstractItemView.EditKeyPressed)
+        self.table.treeWidget.setSortingEnabled(True)
+        self.table.treeWidget.setDragEnabled(True)
+
+        # add things to layouts
+        self.horizontalLayout.addWidget(self.table.treeWidget)
+
+        # set up timer   
+        self.connect(self.timer, QtCore.SIGNAL('timeout()'), self.update)
+        self.updateRate = updateRate;
+        self.timer.start(self.updateRate)
+
+        # set up context menu .. 
+        self.table.treeWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+        self.table.treeWidget.customContextMenuRequested.connect(self.openMenu)
+
+        # Set up double-click to launch default plotter
+        self.connect(self.table.treeWidget,
+                     QtCore.SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'),
+                     self.parent.newSub);
+
+        # Allow drag/drop event from table item to plotter
+        self.connect(self.table.treeWidget,
+                     QtCore.SIGNAL('itemPressed(QTreeWidgetItem*, int)'),
+                     self.parent.startDrag)
+        
+    def openMenu(self, pos):
+        index = self.table.treeWidget.selectedIndexes()
+        item = self.table.treeWidget.itemFromIndex(index[0])
+        itemname = str(item.text(0))
+        self.parent.propertiesMenu(itemname, self.radio, self.uid)
+        
+
+class MyClient(IceRadioClient):
+    def __init__(self):
+        IceRadioClient.__init__(self, MAINWindow)
+
+sys.exit(MyClient().main(sys.argv))
diff --git a/gnuradio-runtime/python/gnuradio/ctrlport/gr-perf-monitor b/gnuradio-runtime/python/gnuradio/ctrlport/gr-perf-monitor
new file mode 100755
index 0000000000..f2c01691a1
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/ctrlport/gr-perf-monitor
@@ -0,0 +1,591 @@
+#!/usr/bin/env python
+#
+# Copyright 2012-2013 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+from gnuradio import gr, ctrlport
+
+from PyQt4 import QtCore,Qt,Qwt5
+import PyQt4.QtGui as QtGui
+import sys, time, re, pprint
+import itertools
+import scipy
+
+import Ice
+from gnuradio.ctrlport.IceRadioClient import *
+from gnuradio.ctrlport.GrDataPlotter import *
+from gnuradio.ctrlport import GNURadio
+
+class MAINWindow(QtGui.QMainWindow):
+    def minimumSizeHint(self):
+        return QtGui.QSize(800,600)
+
+    def __init__(self, radio, port, interface):
+        
+        super(MAINWindow, self).__init__()
+        self.conns = []
+        self.plots = []
+        self.knobprops = []
+        self.interface = interface
+
+        self.mdiArea = QtGui.QMdiArea()
+        self.mdiArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+        self.mdiArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+        self.setCentralWidget(self.mdiArea)
+
+        self.mdiArea.subWindowActivated.connect(self.updateMenus)
+        self.windowMapper = QtCore.QSignalMapper(self)
+        self.windowMapper.mapped[QtGui.QWidget].connect(self.setActiveSubWindow)
+
+        self.createActions()
+        self.createMenus()
+        self.createToolBars()
+        self.createStatusBar()
+        self.updateMenus()
+
+        self.setWindowTitle("GNU Radio Performance Monitor")
+        self.setUnifiedTitleAndToolBarOnMac(True)
+
+        self.newCon(radio, port)
+        icon = QtGui.QIcon(ctrlport.__path__[0] + "/icon.png" )
+        self.setWindowIcon(icon)
+
+    def newCon(self, radio=None, port=None):
+        child = MForm(radio, port, len(self.conns), self)
+        if(child.radio is not None):
+            child.setWindowTitle(str(child.radio))
+            horizbar = QtGui.QScrollArea()
+            horizbar.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+            horizbar.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+            horizbar.setWidget(child)
+            self.mdiArea.addSubWindow(horizbar)
+            self.mdiArea.currentSubWindow().showMaximized()
+
+        self.conns.append(child)
+        self.plots.append([])
+
+    def newUpdater(self, key, radio):
+        updater = UpdaterWindow(key, radio, None)
+        updater.setWindowTitle("Updater: " + key)
+        updater.setModal(False)
+        updater.exec_()
+
+    def update(self, knobs, uid):
+        #sys.stderr.write("KNOB KEYS: {0}\n".format(knobs.keys()))
+        for plot in self.plots[uid]:
+            data = knobs[plot.name()].value
+            plot.update(data)
+            plot.stop()
+            plot.wait()
+            plot.start()
+
+    def setActiveSubWindow(self, window):
+        if window:
+            self.mdiArea.setActiveSubWindow(window)
+
+
+    def createActions(self):
+        self.newConAct = QtGui.QAction("&New Connection",
+                self, shortcut=QtGui.QKeySequence.New,
+                statusTip="Create a new file", triggered=self.newCon)
+
+        self.exitAct = QtGui.QAction("E&xit", self, shortcut="Ctrl+Q",
+                statusTip="Exit the application",
+                triggered=QtGui.qApp.closeAllWindows)
+
+        self.closeAct = QtGui.QAction("Cl&ose", self, shortcut="Ctrl+F4",
+                statusTip="Close the active window",
+                triggered=self.mdiArea.closeActiveSubWindow)
+
+        self.closeAllAct = QtGui.QAction("Close &All", self,
+                statusTip="Close all the windows",
+                triggered=self.mdiArea.closeAllSubWindows)
+
+
+        qks = QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_T);
+        self.tileAct = QtGui.QAction("&Tile", self,
+                statusTip="Tile the windows",
+                triggered=self.mdiArea.tileSubWindows,
+                shortcut=qks)
+
+        qks = QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_C);
+        self.cascadeAct = QtGui.QAction("&Cascade", self,
+                statusTip="Cascade the windows", shortcut=qks,
+                triggered=self.mdiArea.cascadeSubWindows)
+
+        self.nextAct = QtGui.QAction("Ne&xt", self,
+                shortcut=QtGui.QKeySequence.NextChild,
+                statusTip="Move the focus to the next window",
+                triggered=self.mdiArea.activateNextSubWindow)
+
+        self.previousAct = QtGui.QAction("Pre&vious", self,
+                shortcut=QtGui.QKeySequence.PreviousChild,
+                statusTip="Move the focus to the previous window",
+                triggered=self.mdiArea.activatePreviousSubWindow)
+
+        self.separatorAct = QtGui.QAction(self)
+        self.separatorAct.setSeparator(True)
+
+        self.aboutAct = QtGui.QAction("&About", self,
+                statusTip="Show the application's About box",
+                triggered=self.about)
+
+        self.aboutQtAct = QtGui.QAction("About &Qt", self,
+                statusTip="Show the Qt library's About box",
+                triggered=QtGui.qApp.aboutQt)
+
+    def createMenus(self):
+        self.fileMenu = self.menuBar().addMenu("&File")
+        self.fileMenu.addAction(self.newConAct)
+        self.fileMenu.addSeparator()
+        self.fileMenu.addAction(self.exitAct)
+
+        self.windowMenu = self.menuBar().addMenu("&Window")
+        self.updateWindowMenu()
+        self.windowMenu.aboutToShow.connect(self.updateWindowMenu)
+
+        self.menuBar().addSeparator()
+
+        self.helpMenu = self.menuBar().addMenu("&Help")
+        self.helpMenu.addAction(self.aboutAct)
+        self.helpMenu.addAction(self.aboutQtAct)
+
+    def createToolBars(self):
+        self.fileToolBar = self.addToolBar("File")
+        self.fileToolBar.addAction(self.newConAct)
+
+        self.fileToolBar = self.addToolBar("Window")
+        self.fileToolBar.addAction(self.tileAct)
+        self.fileToolBar.addAction(self.cascadeAct)
+
+    def createStatusBar(self):
+        self.statusBar().showMessage("Ready")
+
+
+    def activeMdiChild(self):
+        activeSubWindow = self.mdiArea.activeSubWindow()
+        if activeSubWindow:
+            return activeSubWindow.widget()
+        return None
+
+    def updateMenus(self):
+        hasMdiChild = (self.activeMdiChild() is not None)
+        self.closeAct.setEnabled(hasMdiChild)
+        self.closeAllAct.setEnabled(hasMdiChild)
+        self.tileAct.setEnabled(hasMdiChild)
+        self.cascadeAct.setEnabled(hasMdiChild)
+        self.nextAct.setEnabled(hasMdiChild)
+        self.previousAct.setEnabled(hasMdiChild)
+        self.separatorAct.setVisible(hasMdiChild)
+
+    def updateWindowMenu(self):
+        self.windowMenu.clear()
+        self.windowMenu.addAction(self.closeAct)
+        self.windowMenu.addAction(self.closeAllAct)
+        self.windowMenu.addSeparator()
+        self.windowMenu.addAction(self.tileAct)
+        self.windowMenu.addAction(self.cascadeAct)
+        self.windowMenu.addSeparator()
+        self.windowMenu.addAction(self.nextAct)
+        self.windowMenu.addAction(self.previousAct)
+        self.windowMenu.addAction(self.separatorAct)
+
+    def about(self):
+        about_info = \
+'''Copyright 2012 Free Software Foundation, Inc.\n
+This program is part of GNU Radio.\n
+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 3, or (at your option) any later version.\n
+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.\n
+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., 51 Franklin Street, Boston, MA 02110-1301, USA.'''
+
+        QtGui.QMessageBox.about(None, "gr-ctrlport-monitor", about_info)
+
+
+class ConInfoDialog(QtGui.QDialog):
+    def __init__(self, parent=None):
+        super(ConInfoDialog, self).__init__(parent)
+
+        self.gridLayout = QtGui.QGridLayout(self)
+        
+
+        self.host = QtGui.QLineEdit(self);
+        self.port = QtGui.QLineEdit(self);
+        self.host.setText("localhost");
+        self.port.setText("43243");
+
+        self.buttonBox = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok |
+                                                QtGui.QDialogButtonBox.Cancel)
+
+        self.gridLayout.addWidget(self.host);
+        self.gridLayout.addWidget(self.port);
+        self.gridLayout.addWidget(self.buttonBox);
+
+        self.buttonBox.accepted.connect(self.accept)
+        self.buttonBox.rejected.connect(self.reject)
+
+
+    def accept(self):
+        self.done(1);
+
+    def reject(self):
+        self.done(0);
+
+
+class UpdaterWindow(QtGui.QDialog):
+    def __init__(self, key, radio, parent):
+        QtGui.QDialog.__init__(self, parent)
+
+        self.key = key;
+        self.radio = radio
+
+        self.resize(300,200)
+        self.layout = QtGui.QVBoxLayout()
+
+        self.props = radio.properties([key])[key]
+        info = str(self.props)
+
+        self.infoLabel = QtGui.QLabel(info)
+        self.layout.addWidget(self.infoLabel)
+
+        self.cancelButton = QtGui.QPushButton("Ok")
+        self.cancelButton.connect(self.cancelButton, QtCore.SIGNAL('clicked()'), self.reject)
+
+        self.buttonlayout = QtGui.QHBoxLayout()
+        self.buttonlayout.addWidget(self.cancelButton)
+        self.layout.addLayout(self.buttonlayout)
+ 
+        # set layout and go...
+        self.setLayout(self.layout)
+            
+    def _set_slider_value(self, val):
+        self.slider.setValue(self.steps*(val-self.props.min.value)/self.valspan)
+             
+    def _slide(self):
+        val = (self.slider.value()*self.valspan + self.props.min.value)/float(self.steps)
+        self.textInput.setText(str(val))
+
+    def _apply(self):
+        if(type(self.sv.value) == str):
+            val = str(self.textInput.text())
+        elif(type(self.sv.value) == int):
+            val = int(round(float(self.textInput.text())))
+        elif(type(self.sv.value) == float):
+            val = float(self.textInput.text())
+        else:
+            sys.stderr.write("set type not supported! ({0})\n".format(type(self.sv.value)))
+            sys.exit(-1)
+
+        self.sv.value = val
+        km = {}
+        km[self.key] = self.sv
+        self.radio.set(km)
+        self._set_slider_value(self.sv.value)
+
+    def _set(self):
+        self._apply()
+        self.done(0)
+
+
+def build_edge_graph(sources, sinks, edges):
+    '''
+    Starting from the sources, walks through all of the edges to find
+    the next connected block. The output is stored in 'allblocks'
+    where each row starts with a source and follows one path down
+    until it terminates in either a sink or as an input to a block
+    that is part of another chain.
+    '''
+    def find_edge(src, sinks, edges, row, col):
+        #print "\n\nAll blocks: "
+        #printer.pprint(allblocks)
+        #print "\nLooking for: ", src
+
+        src0 = src.split(":")[0]
+        if(src0 in sinks):
+            if(len(allblocks) <= row):
+                allblocks.append(col*[""])
+            allblocks[row].append(src)
+            return row+1
+
+        for edge in edges:
+            if(re.match(src0, edge)):
+                s = edge.split("->")[0]
+                b = edge.split("->")[1]
+                if(len(allblocks) <= row):
+                    allblocks.append(col*[""])
+                allblocks[row].append(s)
+                #print "Source: {0}     Sink: {1}".format(s, b)
+                row = find_edge(b, sinks, edges, row, col+1)
+        return row
+
+    # Recursively get all edges as a matrix of source->sink
+    n = 0
+    allblocks = []
+    for src in sources:
+        n = find_edge(src, sinks, edges, n, 0)
+
+    # Sort by longest list
+    allblocks = sorted(allblocks, key=len)
+    allblocks.reverse()
+
+    # Make all rows same length by padding '' in front of sort rows
+    maxrowlen = len(allblocks[0])
+    for i,a in enumerate(allblocks):
+        rowlen = len(a)
+        allblocks[i] = (maxrowlen-rowlen)*[''] + a
+
+    # Dedup rows
+    allblocks = sorted(allblocks)
+    allblocks = list(k for k,_ in itertools.groupby(allblocks))
+    allblocks.reverse()
+
+    return allblocks
+
+
+class MForm(QtGui.QWidget):
+    def update(self):
+        try:
+            st = time.time()
+            knobs = self.radio.get([b[0] for b in self.block_dict])
+
+            ft = time.time()
+            latency = ft-st
+            self.parent.statusBar().showMessage("Current GNU Radio Control Port Query Latency: %f ms"%\
+                                                    (latency*1000))
+            
+        except Exception, e:
+            sys.stderr.write("ctrlport-monitor: radio.get threw exception ({0}).\n".format(e))
+            if(type(self.parent) is MAINWindow):
+                # Find window of connection
+                remove = []
+                for p in self.parent.mdiArea.subWindowList():
+                    if self.parent.conns[self.uid] == p.widget():
+                        remove.append(p)
+
+                # Find any subplot windows of connection
+                for p in self.parent.mdiArea.subWindowList():
+                    for plot in self.parent.plots[self.uid]:
+                        if plot.qwidget() == p.widget():
+                            remove.append(p)
+                
+                # Clean up local references to these
+                self.parent.conns.remove(self.parent.conns[self.uid])
+                self.parent.plots.remove(self.parent.plots[self.uid])
+
+                # Remove subwindows for connection and plots
+                for r in remove:
+                    self.parent.mdiArea.removeSubWindow(r)
+
+                # Clean up self
+                self.close()
+            else:
+                sys.exit(1)
+            return
+            
+        #UPDATE TABLE:
+        self.updateItems(knobs)
+
+        #UPDATE PLOTS
+        self.parent.update(knobs, self.uid)
+    
+    def updateItems(self, knobs):
+        for b in self.block_dict:
+            if(knobs[b[0]].ice_id.im_class == GNURadio.KnobVecF):
+                b[1].setText("{0:.4f}".format(knobs[b[0]].value[b[2]]))
+            else:
+                b[1].setText("{0:.4f}".format(knobs[b[0]].value))
+
+    def __init__(self, radio=None, port=None, uid=0, parent=None):
+
+        super(MForm, self).__init__()
+
+        if(radio == None or port == None):
+            askinfo = ConInfoDialog(self);
+            if askinfo.exec_():
+                host = str(askinfo.host.text());
+                port = str(askinfo.port.text());
+                radio = parent.interface.getRadio(host, port)
+            else:
+                self.radio = None
+                return
+        
+        self.uid = uid
+        self.parent = parent
+        self.layout = QtGui.QGridLayout(self)
+        self.layout.setSizeConstraint(QtGui.QLayout.SetFixedSize)
+
+        self.radio = radio
+        self.knobprops = self.radio.properties([])
+        self.parent.knobprops.append(self.knobprops)
+        self.resize(775,500)
+        self.timer = QtCore.QTimer()
+        self.constupdatediv = 0
+        self.tableupdatediv = 0
+        plotsize=250
+
+
+        # Set up the graph of blocks
+        input_name = lambda x: x+"::avg input % full"
+        output_name = lambda x: x+"::avg output % full"
+        wtime_name = lambda x: x+"::avg work time"
+        nout_name = lambda x: x+"::avg noutput_items"
+        nprod_name = lambda x: x+"::avg nproduced"
+
+        tmplist = []
+        knobs = self.radio.get([])
+        edgelist = None
+        for k in knobs:
+            propname = k.split("::")
+            blockname = propname[0]
+            keyname = propname[1]
+            if(keyname == "edge list"):
+                edgelist = knobs[k].value
+            elif(blockname not in tmplist):
+                # only take gr_blocks (no hier_block2)
+                if(knobs.has_key(input_name(blockname))):
+                    tmplist.append(blockname)
+
+        if not edgelist:
+            sys.stderr.write("Could not find list of edges from flowgraph. " + \
+                                 "Make sure the option 'edges_list' is enabled " + \
+                                 "in the ControlPort configuration.\n\n")
+            sys.exit(1)
+
+        edges = edgelist.split("\n")[0:-1]
+        producers = []
+        consumers = []
+        for e in edges:
+            _e = e.split("->")
+            producers.append(_e[0])
+            consumers.append(_e[1])
+
+        # Get producers and consumers as sets while ignoring the
+        # ports.
+        prods = set(map(lambda x: x.split(":")[0], producers))
+        cons  = set(map(lambda x: x.split(":")[0], consumers))
+
+        # Split out all blocks, sources, and sinks based on how they
+        # appear as consumers and/or producers.
+        blocks = prods.intersection(cons)
+        sources = prods.difference(blocks)
+        sinks = cons.difference(blocks)
+
+        nblocks = len(prods) + len(cons)
+
+        allblocks = build_edge_graph(sources, sinks, edges)
+        nrows = len(allblocks)
+        ncols = len(allblocks[0])
+
+        col_width = 120
+
+        self.block_dict = []
+
+        for row, blockrow in enumerate(allblocks):
+            for col, block in enumerate(blockrow):
+                if(block == ''):
+                    continue
+                
+                bgroup = QtGui.QGroupBox(block)
+                playout = QtGui.QFormLayout()
+                bgroup.setLayout(playout)
+                self.layout.addWidget(bgroup, row, 2*col)
+
+                blockname = block.split(":")[0]
+
+                name = wtime_name(blockname)
+                wtime = knobs[name].value
+                newtime = QtGui.QLineEdit()
+                newtime.setMinimumWidth(col_width)
+                newtime.setText("{0:.4f}".format(wtime))
+                self.block_dict.append((name, newtime))
+
+                name = nout_name(blockname)
+                nout = knobs[name].value
+                newnout = QtGui.QLineEdit()
+                newnout.setText("{0:.4f}".format(nout))
+                newnout.setMinimumWidth(col_width)
+                self.block_dict.append((name, newnout))
+
+                name = nprod_name(blockname)
+                nprod = knobs[name].value
+                newnprod = QtGui.QLineEdit()
+                newnprod.setMinimumWidth(col_width)
+                newnprod.setText("{0:.4f}".format(nprod))
+                self.block_dict.append((name, newnprod))
+
+                playout.addRow("Work time", newtime)
+                playout.addRow("noutput_items", newnout)
+                playout.addRow("nproduced", newnprod)
+
+                if blockname in blocks or blockname in sources:
+                    # Add a buffer between blocks
+                    buffgroup = QtGui.QGroupBox("Buffer")
+                    bufflayout = QtGui.QFormLayout()
+                    buffgroup.setLayout(bufflayout)
+                    self.layout.addWidget(buffgroup, row, 2*col+1)
+                
+                    i = int(block.split(":")[1])
+                    name = output_name(blockname)
+                    obuff = knobs[name].value
+                    for i,o in enumerate(obuff):
+                        newobuff = QtGui.QLineEdit()
+                        newobuff.setMinimumWidth(col_width)
+                        newobuff.setText("{0:.4f}".format(o))
+                        self.block_dict.append((name, newobuff, i))
+                        bufflayout.addRow("Out Buffer {0}".format(i),
+                                          newobuff)
+                
+                if blockname in blocks or blockname in sinks:
+                    item = self.layout.itemAtPosition(row, 2*col-1)
+                    if(item):
+                        buffgroup = item.widget()
+                        bufflayout = buffgroup.layout()
+                    else:
+                        buffgroup = QtGui.QGroupBox("Buffer")
+                        bufflayout = QtGui.QFormLayout()
+                        buffgroup.setLayout(bufflayout)
+                        self.layout.addWidget(buffgroup, row, 2*col-1)
+                
+                    i = int(block.split(":")[1])
+                    name = input_name(blockname)
+                    ibuff = knobs[name].value[i]
+                    newibuff = QtGui.QLineEdit()
+                    newibuff.setMinimumWidth(col_width)
+                    newibuff.setText("{0:.4f}".format(ibuff))
+                    self.block_dict.append((name, newibuff, i))
+                    bufflayout.addRow("In Buffer {0}".format(i),
+                                      newibuff)
+
+        # set up timer
+        self.timer = QtCore.QTimer()
+        self.connect(self.timer, QtCore.SIGNAL('timeout()'), self.update)
+        self.timer.start(1000)
+
+    def openMenu(self, pos):
+        index = self.table.treeWidget.selectedIndexes()
+        item = self.table.treeWidget.itemFromIndex(index[0])
+        itemname = str(item.text(0))
+        self.parent.propertiesMenu(itemname, self.radio, self.uid)
+        
+
+class MyClient(IceRadioClient):
+    def __init__(self):
+        IceRadioClient.__init__(self, MAINWindow)
+
+sys.exit(MyClient().main(sys.argv))
diff --git a/gnuradio-runtime/python/gnuradio/ctrlport/gr-perf-monitorx b/gnuradio-runtime/python/gnuradio/ctrlport/gr-perf-monitorx
new file mode 100755
index 0000000000..a65b0406e4
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/ctrlport/gr-perf-monitorx
@@ -0,0 +1,727 @@
+#!/usr/bin/env python
+#
+# Copyright 2012-2013 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+import random,math,operator
+import networkx as nx;
+import matplotlib.pyplot as plt
+
+from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
+from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar
+from matplotlib.figure import Figure
+
+from gnuradio import gr, ctrlport
+
+from PyQt4 import QtCore,Qt,Qwt5
+import PyQt4.QtGui as QtGui
+import sys, time, re, pprint
+import itertools
+import scipy
+from scipy import spatial
+
+import Ice
+from gnuradio.ctrlport.IceRadioClient import *
+from gnuradio.ctrlport.GrDataPlotter import *
+from gnuradio.ctrlport import GNURadio
+
+class MAINWindow(QtGui.QMainWindow):
+    def minimumSizeHint(self):
+        return QtGui.QSize(800,600)
+
+    def __init__(self, radio, port, interface):
+        
+        super(MAINWindow, self).__init__()
+        self.conns = []
+        self.plots = []
+        self.knobprops = []
+        self.interface = interface
+
+        self.mdiArea = QtGui.QMdiArea()
+        self.mdiArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+        self.mdiArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+        self.setCentralWidget(self.mdiArea)
+
+        self.mdiArea.subWindowActivated.connect(self.updateMenus)
+        self.windowMapper = QtCore.QSignalMapper(self)
+        self.windowMapper.mapped[QtGui.QWidget].connect(self.setActiveSubWindow)
+
+        self.createActions()
+        self.createMenus()
+        self.createToolBars()
+        self.createStatusBar()
+        self.updateMenus()
+
+        self.setWindowTitle("GNU Radio Performance Monitor")
+        self.setUnifiedTitleAndToolBarOnMac(True)
+
+        self.newCon(radio, port)
+        icon = QtGui.QIcon(ctrlport.__path__[0] + "/icon.png" )
+        self.setWindowIcon(icon)
+
+
+    def newSubWindow(self, window, title):
+        child = window;
+        child.setWindowTitle(title)
+        self.mdiArea.addSubWindow(child)
+        self.conns.append(child)
+        child.show();
+        self.mdiArea.currentSubWindow().showMaximized()
+
+
+    def newCon(self, radio=None, port=None):
+        child = MForm(radio, port, len(self.conns), self)
+        if(child.radio is not None):
+            child.setWindowTitle(str(child.radio))
+#            horizbar = QtGui.QScrollArea()
+#            horizbar.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+#            horizbar.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+#            horizbar.setWidget(child)
+#            self.mdiArea.addSubWindow(horizbar)
+            self.mdiArea.addSubWindow(child)
+            self.mdiArea.currentSubWindow().showMaximized()
+
+        self.conns.append(child)
+        self.plots.append([])
+
+    def update(self, knobs, uid):
+        #sys.stderr.write("KNOB KEYS: {0}\n".format(knobs.keys()))
+        for plot in self.plots[uid]:
+            data = knobs[plot.name()].value
+            plot.update(data)
+            plot.stop()
+            plot.wait()
+            plot.start()
+
+    def setActiveSubWindow(self, window):
+        if window:
+            self.mdiArea.setActiveSubWindow(window)
+
+
+    def createActions(self):
+        self.newConAct = QtGui.QAction("&New Connection",
+                self, shortcut=QtGui.QKeySequence.New,
+                statusTip="Create a new file", triggered=self.newCon)
+
+        self.exitAct = QtGui.QAction("E&xit", self, shortcut="Ctrl+Q",
+                statusTip="Exit the application",
+                triggered=QtGui.qApp.closeAllWindows)
+
+        self.closeAct = QtGui.QAction("Cl&ose", self, shortcut="Ctrl+F4",
+                statusTip="Close the active window",
+                triggered=self.mdiArea.closeActiveSubWindow)
+
+        self.closeAllAct = QtGui.QAction("Close &All", self,
+                statusTip="Close all the windows",
+                triggered=self.mdiArea.closeAllSubWindows)
+
+
+        qks = QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_T);
+        self.tileAct = QtGui.QAction("&Tile", self,
+                statusTip="Tile the windows",
+                triggered=self.mdiArea.tileSubWindows,
+                shortcut=qks)
+
+        qks = QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_C);
+        self.cascadeAct = QtGui.QAction("&Cascade", self,
+                statusTip="Cascade the windows", shortcut=qks,
+                triggered=self.mdiArea.cascadeSubWindows)
+
+        self.nextAct = QtGui.QAction("Ne&xt", self,
+                shortcut=QtGui.QKeySequence.NextChild,
+                statusTip="Move the focus to the next window",
+                triggered=self.mdiArea.activateNextSubWindow)
+
+        self.previousAct = QtGui.QAction("Pre&vious", self,
+                shortcut=QtGui.QKeySequence.PreviousChild,
+                statusTip="Move the focus to the previous window",
+                triggered=self.mdiArea.activatePreviousSubWindow)
+
+        self.separatorAct = QtGui.QAction(self)
+        self.separatorAct.setSeparator(True)
+
+        self.aboutAct = QtGui.QAction("&About", self,
+                statusTip="Show the application's About box",
+                triggered=self.about)
+
+        self.aboutQtAct = QtGui.QAction("About &Qt", self,
+                statusTip="Show the Qt library's About box",
+                triggered=QtGui.qApp.aboutQt)
+
+    def createMenus(self):
+        self.fileMenu = self.menuBar().addMenu("&File")
+        self.fileMenu.addAction(self.newConAct)
+        self.fileMenu.addSeparator()
+        self.fileMenu.addAction(self.exitAct)
+
+        self.windowMenu = self.menuBar().addMenu("&Window")
+        self.updateWindowMenu()
+        self.windowMenu.aboutToShow.connect(self.updateWindowMenu)
+
+        self.menuBar().addSeparator()
+
+        self.helpMenu = self.menuBar().addMenu("&Help")
+        self.helpMenu.addAction(self.aboutAct)
+        self.helpMenu.addAction(self.aboutQtAct)
+
+    def createToolBars(self):
+        self.fileToolBar = self.addToolBar("File")
+        self.fileToolBar.addAction(self.newConAct)
+
+        self.fileToolBar = self.addToolBar("Window")
+        self.fileToolBar.addAction(self.tileAct)
+        self.fileToolBar.addAction(self.cascadeAct)
+
+    def createStatusBar(self):
+        self.statusBar().showMessage("Ready")
+
+
+    def activeMdiChild(self):
+        activeSubWindow = self.mdiArea.activeSubWindow()
+        if activeSubWindow:
+            return activeSubWindow.widget()
+        return None
+
+    def updateMenus(self):
+        hasMdiChild = (self.activeMdiChild() is not None)
+        self.closeAct.setEnabled(hasMdiChild)
+        self.closeAllAct.setEnabled(hasMdiChild)
+        self.tileAct.setEnabled(hasMdiChild)
+        self.cascadeAct.setEnabled(hasMdiChild)
+        self.nextAct.setEnabled(hasMdiChild)
+        self.previousAct.setEnabled(hasMdiChild)
+        self.separatorAct.setVisible(hasMdiChild)
+
+    def updateWindowMenu(self):
+        self.windowMenu.clear()
+        self.windowMenu.addAction(self.closeAct)
+        self.windowMenu.addAction(self.closeAllAct)
+        self.windowMenu.addSeparator()
+        self.windowMenu.addAction(self.tileAct)
+        self.windowMenu.addAction(self.cascadeAct)
+        self.windowMenu.addSeparator()
+        self.windowMenu.addAction(self.nextAct)
+        self.windowMenu.addAction(self.previousAct)
+        self.windowMenu.addAction(self.separatorAct)
+
+    def about(self):
+        about_info = \
+'''Copyright 2012 Free Software Foundation, Inc.\n
+This program is part of GNU Radio.\n
+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 3, or (at your option) any later version.\n
+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.\n
+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., 51 Franklin Street, Boston, MA 02110-1301, USA.'''
+
+        QtGui.QMessageBox.about(None, "gr-ctrlport-monitor", about_info)
+
+
+class ConInfoDialog(QtGui.QDialog):
+    def __init__(self, parent=None):
+        super(ConInfoDialog, self).__init__(parent)
+
+        self.gridLayout = QtGui.QGridLayout(self)
+        
+
+        self.host = QtGui.QLineEdit(self);
+        self.port = QtGui.QLineEdit(self);
+        self.host.setText("localhost");
+        self.port.setText("43243");
+
+        self.buttonBox = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok |
+                                                QtGui.QDialogButtonBox.Cancel)
+
+        self.gridLayout.addWidget(self.host);
+        self.gridLayout.addWidget(self.port);
+        self.gridLayout.addWidget(self.buttonBox);
+
+        self.buttonBox.accepted.connect(self.accept)
+        self.buttonBox.rejected.connect(self.reject)
+
+
+    def accept(self):
+        self.done(1);
+
+    def reject(self):
+        self.done(0);
+
+
+class DataTable(QtGui.QWidget):
+    def update(self):
+        print "update"
+
+    def __init__(self, radio, G):
+        QtGui.QWidget.__init__( self)
+        
+        self.layout = QtGui.QVBoxLayout(self);
+        self.hlayout = QtGui.QHBoxLayout();
+        self.layout.addLayout(self.hlayout);
+
+        self.G = G;
+        self.radio = radio;
+
+        self._keymap = None
+
+        # Create a combobox to set the type of statistic we want.
+        self._statistic = "Instantaneous"
+        self._statistics_table = {"Instantaneous": "",
+                                  "Average": "avg ",
+                                  "Variance": "var "}
+        self.stattype = QtGui.QComboBox()
+        self.stattype.addItem("Instantaneous")
+        self.stattype.addItem("Average")
+        self.stattype.addItem("Variance")
+        self.stattype.setMaximumWidth(200)
+        self.hlayout.addWidget(self.stattype);
+        self.stattype.currentIndexChanged.connect(self.stat_changed)
+
+        # Create a checkbox to toggle sorting of graphs
+        self._sort = False
+        self.checksort = QtGui.QCheckBox("Sort")
+        self.checksort.setCheckState(self._sort)
+        self.hlayout.addWidget(self.checksort);
+        self.checksort.stateChanged.connect(self.checksort_changed)
+
+        # set up table
+        self.perfTable = Qt.QTableWidget();
+        self.perfTable.setColumnCount(2)
+        self.perfTable.verticalHeader().hide();
+        self.perfTable.setHorizontalHeaderLabels( ["Block Name", "Percent Runtime"] );
+        self.perfTable.horizontalHeader().setStretchLastSection(True);
+        self.perfTable.setSortingEnabled(True)
+        nodes = self.G.nodes(data=True)
+
+        # set up plot
+        self.f = plt.figure(figsize=(10,8), dpi=90)
+        self.sp = self.f.add_subplot(111);
+        self.sp.autoscale_view(True,True,True);
+        self.sp.set_autoscale_on(True)
+        self.canvas = FigureCanvas(self.f)
+
+        # set up tabs
+        self.tabber = QtGui.QTabWidget();
+        self.layout.addWidget(self.tabber);
+        self.tabber.addTab(self.perfTable,"Table View");
+        self.tabber.addTab(self.canvas, "Graph View");
+
+        # set up timer
+        self.timer = QtCore.QTimer()
+        self.connect(self.timer, QtCore.SIGNAL('timeout()'), self.update)
+        self.timer.start(500)
+
+        for i in range(0,len(nodes)):
+            self.perfTable.setItem( 
+                i,0,
+                Qt.QTableWidgetItem(nodes[i][0]))
+
+    def table_update(self,data):
+        for k in data.keys():
+            weight = data[k]
+            existing = self.perfTable.findItems(str(k),QtCore.Qt.MatchFixedString)
+            if(len(existing) == 0):
+                i = self.perfTable.rowCount();
+                self.perfTable.setRowCount( i+1)
+                self.perfTable.setItem( i,0, Qt.QTableWidgetItem(str(k)))
+                self.perfTable.setItem( i,1, Qt.QTableWidgetItem(str(weight)))
+            else:
+                self.perfTable.setItem( self.perfTable.row(existing[0]),1, Qt.QTableWidgetItem(str(weight)))
+
+    def stat_changed(self, index):
+        self._statistic = str(self.stattype.currentText())
+
+    def checksort_changed(self, state):
+        self._sort = state > 0
+
+class DataTableBuffers(DataTable):
+    def __init__(self, radio, G):
+        DataTable.__init__(self,radio,G)
+        self.perfTable.setHorizontalHeaderLabels( ["Block Name", "Percent Buffer Full"] );
+
+    def update(self):
+        nodes = self.G.nodes();
+
+        # get buffer fullness for all blocks
+        kl = map(lambda x: "%s::%soutput %% full" % \
+                     (x, self._statistics_table[self._statistic]),
+                 nodes);
+        buf_knobs = self.radio.get(kl)
+
+        # strip values out of ctrlport response
+        buffer_fullness = dict(zip(
+                    map(lambda x: x.split("::")[0], buf_knobs.keys()),
+                    map(lambda x: x.value, buf_knobs.values())))
+
+        blockport_fullness = {}
+        for blk in buffer_fullness:
+            for port in range(0,len(buffer_fullness[blk])):
+                blockport_fullness["%s:%d"%(blk,port)] =  buffer_fullness[blk][port];
+        
+        self.table_update(blockport_fullness);
+
+        if(self._sort):
+            sorted_fullness = sorted(blockport_fullness.iteritems(), key=operator.itemgetter(1))
+            self._keymap = map(operator.itemgetter(0), sorted_fullness)
+        else:
+            if self._keymap:
+                sorted_fullness = len(self._keymap)*['',]
+                for b in blockport_fullness:
+                    sorted_fullness[self._keymap.index(b)] = (b, blockport_fullness[b])
+            else:
+                sorted_fullness = blockport_fullness.items()
+
+        self.sp.clear();
+        plt.figure(self.f.number)
+        plt.subplot(111);
+        self.sp.bar(range(0,len(sorted_fullness)), map(lambda x: x[1], sorted_fullness),
+                    alpha=0.5)
+        self.sp.set_ylabel("% Buffers Full");
+        self.sp.set_xticks( map(lambda x: x+0.5, range(0,len(sorted_fullness))))
+        self.sp.set_xticklabels( map(lambda x: "  " + x, map(lambda x: x[0], sorted_fullness)),
+                                 rotation="vertical", verticalalignment="bottom" )
+        self.canvas.draw();
+        self.canvas.show();
+
+class DataTableRuntimes(DataTable):
+    def __init__(self, radio, G):
+        DataTable.__init__(self,radio,G)
+        #self.perfTable.setRowCount(len( self.G.nodes() ))
+
+    def update(self):
+        nodes = self.G.nodes();
+
+        # get work time for all blocks
+        kl = map(lambda x: "%s::%swork time" % \
+                     (x, self._statistics_table[self._statistic]),
+                 nodes);
+        wrk_knobs = self.radio.get(kl)
+
+        # strip values out of ctrlport response
+        total_work = sum(map(lambda x: x.value, wrk_knobs.values()))
+        work_times = dict(zip(
+                    map(lambda x: x.split("::")[0], wrk_knobs.keys()),
+                    map(lambda x: x.value/total_work, wrk_knobs.values())))
+
+        # update table view
+        self.table_update(work_times)
+
+        if(self._sort):
+            sorted_work = sorted(work_times.iteritems(), key=operator.itemgetter(1))
+            self._keymap = map(operator.itemgetter(0), sorted_work)
+        else:
+            if self._keymap:
+                sorted_work = len(self._keymap)*['',]
+                for b in work_times:
+                    sorted_work[self._keymap.index(b)] = (b, work_times[b])
+            else:
+                sorted_work = work_times.items()
+            
+        self.sp.clear();  
+        plt.figure(self.f.number)
+        plt.subplot(111);
+        self.sp.bar(range(0,len(sorted_work)), map(lambda x: x[1], sorted_work),
+                    alpha=0.5)
+        self.sp.set_ylabel("% Runtime");
+        self.sp.set_xticks( map(lambda x: x+0.5, range(0,len(sorted_work))))
+        self.sp.set_xticklabels( map(lambda x: "  " + x[0], sorted_work),
+                                 rotation="vertical", verticalalignment="bottom" )
+    
+        self.canvas.draw();
+        self.canvas.show();
+
+class MForm(QtGui.QWidget):
+    def update(self):
+        try:
+
+            nodes = self.G.nodes();
+    
+            # get current buffer depths of all output buffers
+            kl = map(lambda x: "%s::%soutput %% full" % \
+                         (x, self._statistics_table[self._statistic]),
+                     nodes);
+
+            st = time.time()
+            buf_knobs = self.radio.get(kl)
+            td1 = time.time() - st;
+
+            # strip values out of ctrlport response
+            buf_vals = dict(zip(
+                        map(lambda x: x.split("::")[0], buf_knobs.keys()),
+                        map(lambda x: x.value, buf_knobs.values())))
+
+            # get work time for all blocks
+            kl = map(lambda x: "%s::%swork time" % \
+                         (x, self._statistics_table[self._statistic]),
+                     nodes);
+            st = time.time()
+            wrk_knobs = self.radio.get(kl)
+            td2 = time.time() - st;
+
+            # strip values out of ctrlport response
+            total_work = sum(map(lambda x: x.value, wrk_knobs.values()))
+            work_times = dict(zip(
+                        map(lambda x: x.split("::")[0], wrk_knobs.keys()),
+                        map(lambda x: x.value/total_work, wrk_knobs.values())))
+ 
+            for n in nodes:
+                # ne is the list of edges away from this node!
+                ne = self.G.edges([n],True);
+                for e in ne: # iterate over edges from this block
+                    # get the right output buffer/port weight for each edge
+                    sourceport = e[2]["sourceport"];
+                    newweight = buf_vals[n][sourceport]
+                    e[2]["weight"] = newweight;
+
+            # set updated weights
+            self.node_weights = map(lambda x: 20+2000*work_times[x], nodes);
+            self.edge_weights = map(lambda x: 100.0*x[2]["weight"], self.G.edges(data=True));
+
+            # draw graph updates
+            self.updateGraph();
+
+            latency = td1 + td2;
+            self.parent.statusBar().showMessage("Current GNU Radio Control Port Query Latency: %f ms"%\
+                                                    (latency*1000))
+            
+        except Exception, e:
+            sys.stderr.write("ctrlport-monitor: radio.get threw exception ({0}).\n".format(e))
+            if(type(self.parent) is MAINWindow):
+                # Find window of connection
+                remove = []
+                for p in self.parent.mdiArea.subWindowList():
+                    if self.parent.conns[self.uid] == p.widget():
+                        remove.append(p)
+
+                # Remove subwindows for connection and plots
+                for r in remove:
+                    self.parent.mdiArea.removeSubWindow(r)
+
+                # Clean up self
+                self.close()
+            else:
+                sys.exit(1)
+            return
+            
+    def rtt(self):
+        self.parent.newSubWindow(  DataTableRuntimes(self.radio, self.G),  "Runtime Table" );
+
+    def bpt(self):
+        self.parent.newSubWindow(  DataTableBuffers(self.radio, self.G),  "Buffers Table" );
+
+    def stat_changed(self, index):
+        self._statistic = str(self.stattype.currentText())
+
+    def __init__(self, radio=None, port=None, uid=0, parent=None):
+
+        super(MForm, self).__init__()
+
+        if(radio == None or port == None):
+            askinfo = ConInfoDialog(self);
+            if askinfo.exec_():
+                host = str(askinfo.host.text());
+                port = str(askinfo.port.text());
+                radio = parent.interface.getRadio(host, port)
+            else:
+                self.radio = None
+                return
+        
+
+        self.uid = uid
+        self.parent = parent
+
+        self.layoutTop = QtGui.QVBoxLayout(self)
+        self.ctlBox = QtGui.QHBoxLayout();
+        self.layout = QtGui.QHBoxLayout()
+
+        self.layoutTop.addLayout(self.ctlBox);
+        self.layoutTop.addLayout(self.layout);
+        
+        self.rttAct = QtGui.QAction("Runtime Table",
+                self, statusTip="Runtime Table", triggered=self.rtt)
+        self.rttBut = Qt.QToolButton()
+        self.rttBut.setDefaultAction(self.rttAct);
+        self.ctlBox.addWidget(self.rttBut);
+
+        self.bptAct = QtGui.QAction("Buffer Table",
+                self, statusTip="Buffer Table", triggered=self.bpt)
+        self.bptBut = Qt.QToolButton()
+        self.bptBut.setDefaultAction(self.bptAct);
+        self.ctlBox.addWidget(self.bptBut);
+
+        self._statistic = "Instantaneous"
+        self._statistics_table = {"Instantaneous": "",
+                                  "Average": "avg ",
+                                  "Variance": "var "}
+        self.stattype = QtGui.QComboBox()
+        self.stattype.addItem("Instantaneous")
+        self.stattype.addItem("Average")
+        self.stattype.addItem("Variance")
+        self.stattype.setMaximumWidth(200)
+        self.ctlBox.addWidget(self.stattype);
+        self.stattype.currentIndexChanged.connect(self.stat_changed)
+
+#        self.setLayout(self.layout);
+
+        self.radio = radio
+        self.knobprops = self.radio.properties([])
+        self.parent.knobprops.append(self.knobprops)
+
+        self.timer = QtCore.QTimer()
+        self.constupdatediv = 0
+        self.tableupdatediv = 0
+        plotsize=250
+
+
+        # Set up the graph of blocks
+        input_name = lambda x: x+"::avg input % full"
+        output_name = lambda x: x+"::avg output % full"
+        wtime_name = lambda x: x+"::avg work time"
+        nout_name = lambda x: x+"::avg noutput_items"
+        nprod_name = lambda x: x+"::avg nproduced"
+
+        tmplist = []
+        knobs = self.radio.get([])
+        edgelist = None
+        for k in knobs:
+            propname = k.split("::")
+            blockname = propname[0]
+            keyname = propname[1]
+            if(keyname == "edge list"):
+                edgelist = knobs[k].value
+            elif(blockname not in tmplist):
+                # only take gr_blocks (no hier_block2)
+                if(knobs.has_key(input_name(blockname))):
+                    tmplist.append(blockname)
+
+        if not edgelist:
+            sys.stderr.write("Could not find list of edges from flowgraph. " + \
+                                 "Make sure the option 'edges_list' is enabled " + \
+                                 "in the ControlPort configuration.\n\n")
+            sys.exit(1)
+
+        edges = edgelist.split("\n")[0:-1]
+        edgepairs = [];
+        for e in edges:
+            _e = e.split("->")
+            edgepairs.append( (_e[0].split(":")[0], _e[1].split(":")[0],
+                               {"sourceport":int(_e[0].split(":")[1])}) );
+
+        self.G = nx.MultiDiGraph();
+        self.G.add_edges_from(edgepairs);
+
+        n_edges = self.G.edges(data=True);
+        for e in n_edges:
+            e[2]["weight"] = 5+random.random()*10;
+
+        self.G.clear();
+        self.G.add_edges_from(n_edges);
+
+
+        self.f = plt.figure(figsize=(10,8), dpi=90)
+        self.sp = self.f.add_subplot(111);
+        self.sp.autoscale_view(True,True,True);
+        self.sp.set_autoscale_on(True)
+
+        self.canvas = FigureCanvas(self.f)
+        self.layout.addWidget(self.canvas);
+
+        self.pos = nx.graphviz_layout(self.G);
+        #self.pos = nx.pygraphviz_layout(self.G);
+        #self.pos = nx.spectral_layout(self.G);
+        #self.pos = nx.circular_layout(self.G);
+        #self.pos = nx.shell_layout(self.G);
+        #self.pos = nx.spring_layout(self.G);
+
+        # generate weights and plot
+        self.update();
+
+        # set up timer
+        self.timer = QtCore.QTimer()
+        self.connect(self.timer, QtCore.SIGNAL('timeout()'), self.update)
+        self.timer.start(1000)
+
+        # Set up mouse callback functions to move blocks around.
+        self._grabbed = False
+        self._current_block = ''
+        self.f.canvas.mpl_connect('button_press_event',
+                                  self.button_press)
+        self.f.canvas.mpl_connect('motion_notify_event',
+                                  self.mouse_move)
+        self.f.canvas.mpl_connect('button_release_event',
+                                  self.button_release)
+
+    def button_press(self, event):
+        x, y = event.xdata, event.ydata
+        thrsh = 100
+
+        if(x is not None and y is not None):
+            nearby = map(lambda z: spatial.distance.euclidean((x,y), z), self.pos.values())
+            i = nearby.index(min(nearby))
+            if(abs(self.pos.values()[i][0] - x) < thrsh and
+               abs(self.pos.values()[i][1]-y) < thrsh):
+                self._current_block = self.pos.keys()[i]
+                #print "MOVING BLOCK: ", self._current_block
+                #print "CUR POS: ", self.pos.values()[i]
+                self._grabbed = True
+
+    def mouse_move(self, event):
+        if self._grabbed:
+            x, y = event.xdata, event.ydata
+            if(x is not None and y is not None):
+                #print "NEW POS: ", (x,y)
+                self.pos[self._current_block] = (x,y)
+                self.updateGraph();
+
+    def button_release(self, event):
+        self._grabbed = False
+
+
+    def openMenu(self, pos):
+        index = self.table.treeWidget.selectedIndexes()
+        item = self.table.treeWidget.itemFromIndex(index[0])
+        itemname = str(item.text(0))
+        self.parent.propertiesMenu(itemname, self.radio, self.uid)
+ 
+    def updateGraph(self):
+
+        self.canvas.updateGeometry()
+        self.sp.clear();  
+        plt.figure(self.f.number)
+        plt.subplot(111);
+        nx.draw(self.G, self.pos, 
+                edge_color=self.edge_weights,
+                node_color='#A0CBE2',
+                width=map(lambda x: 3+math.log(x), self.edge_weights),
+                node_shape="s",
+                node_size=self.node_weights,
+                #edge_cmap=plt.cm.Blues,
+                edge_cmap=plt.cm.Reds,
+                ax=self.sp,
+                arrows=False
+        )
+        nx.draw_networkx_labels(self.G, self.pos,
+                                font_size=12)
+
+        self.canvas.draw();
+        self.canvas.show();
+
+class MyClient(IceRadioClient):
+    def __init__(self):
+        IceRadioClient.__init__(self, MAINWindow)
+
+sys.exit(MyClient().main(sys.argv))
diff --git a/gnuradio-runtime/python/gnuradio/ctrlport/icon.png b/gnuradio-runtime/python/gnuradio/ctrlport/icon.png
new file mode 100644
index 0000000000..4beb204428
Binary files /dev/null and b/gnuradio-runtime/python/gnuradio/ctrlport/icon.png differ
diff --git a/gnuradio-runtime/python/gnuradio/ctrlport/monitor.py b/gnuradio-runtime/python/gnuradio/ctrlport/monitor.py
new file mode 100644
index 0000000000..53a571a698
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/ctrlport/monitor.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+#
+# Copyright 2012 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+import sys, subprocess, re, signal, time, atexit, os
+from gnuradio import gr
+
+class monitor:
+    def __init__(self):
+        print "ControlPort Monitor running."
+        self.started = False
+        atexit.register(self.shutdown)
+
+    def __del__(self):
+        if(self.started):
+            self.stop()
+
+    def start(self):
+        print "monitor::endpoints() = %s" % (gr.rpcmanager_get().endpoints())
+        try:
+            self.proc = subprocess.Popen(map(lambda a: ["gr-ctrlport-monitor",
+                                                        re.search("\d+\.\d+\.\d+\.\d+",a).group(0),
+                                                        re.search("-p (\d+)",a).group(1)],
+                                             gr.rpcmanager_get().endpoints())[0])
+            self.started = True
+        except:
+            self.proc = None
+            print "failed to to start ControlPort Monitor on specified port"
+
+    def stop(self):
+        if(self.proc):
+            if(self.proc.returncode == None):
+                print "\tcalling stop on shutdown"
+                self.proc.terminate()
+        else:
+            print "\tno proc to shut down, exiting"
+
+    def shutdown(self):
+        print "ctrlport.monitor received shutdown signal"
+        if(self.started):
+            self.stop()
diff --git a/gnuradio-runtime/python/gnuradio/gr/CMakeLists.txt b/gnuradio-runtime/python/gnuradio/gr/CMakeLists.txt
new file mode 100644
index 0000000000..343577deb8
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/CMakeLists.txt
@@ -0,0 +1,45 @@
+# Copyright 2012 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+
+########################################################################
+include(GrPython)
+
+GR_PYTHON_INSTALL(FILES
+    __init__.py
+    gr_threading.py
+    gr_threading_23.py
+    gr_threading_24.py
+    hier_block2.py
+    tag_utils.py
+    top_block.py
+    DESTINATION ${GR_PYTHON_DIR}/gnuradio/gr
+    COMPONENT "runtime_python"
+)
+
+########################################################################
+# Handle the unit tests
+########################################################################
+if(ENABLE_TESTING)
+include(GrTest)
+file(GLOB py_qa_test_files "qa_*.py")
+foreach(py_qa_test_file ${py_qa_test_files})
+    get_filename_component(py_qa_test_name ${py_qa_test_file} NAME_WE)
+    GR_ADD_TEST(${py_qa_test_name} ${PYTHON_EXECUTABLE} ${PYTHON_DASH_B} ${py_qa_test_file})
+endforeach(py_qa_test_file)
+endif(ENABLE_TESTING)
diff --git a/gnuradio-runtime/python/gnuradio/gr/__init__.py b/gnuradio-runtime/python/gnuradio/gr/__init__.py
new file mode 100644
index 0000000000..0a6c859bdd
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/__init__.py
@@ -0,0 +1,39 @@
+#
+# Copyright 2003-2012 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+# The presence of this file turns this directory into a Python package
+
+"""
+Core contents.
+"""
+
+# This is the main GNU Radio python module.
+# We pull the swig output and the other modules into the gnuradio.gr namespace
+
+from runtime_swig import *
+from exceptions import *
+from top_block import *
+from hier_block2 import *
+from tag_utils import *
+#from gateway import basic_block, sync_block, decim_block, interp_block
+
+# Force the preference database to be initialized
+#from prefs import prefs
diff --git a/gnuradio-runtime/python/gnuradio/gr/exceptions.py b/gnuradio-runtime/python/gnuradio/gr/exceptions.py
new file mode 100644
index 0000000000..dba04750bc
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/exceptions.py
@@ -0,0 +1,27 @@
+#
+# 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+
+class NotDAG (Exception):
+    """Not a directed acyclic graph"""
+    pass
+
+class CantHappen (Exception):
+    """Can't happen"""
+    pass
diff --git a/gnuradio-runtime/python/gnuradio/gr/gateway.py b/gnuradio-runtime/python/gnuradio/gr/gateway.py
new file mode 100644
index 0000000000..b595959494
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/gateway.py
@@ -0,0 +1,243 @@
+#
+# Copyright 2011-2012 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+import runtime_swig as gr
+from runtime_swig import io_signature, io_signaturev
+from runtime_swig import gr_block_gw_message_type
+from runtime_swig import block_gateway
+import numpy
+
+########################################################################
+# Magic to turn pointers into numpy arrays
+# http://docs.scipy.org/doc/numpy/reference/arrays.interface.html
+########################################################################
+def pointer_to_ndarray(addr, dtype, nitems):
+    class array_like:
+        __array_interface__ = {
+            'data' : (int(addr), False),
+            'typestr' : dtype.base.str,
+            'descr' : dtype.base.descr,
+            'shape' : (nitems,) + dtype.shape,
+            'strides' : None,
+            'version' : 3
+        }
+    return numpy.asarray(array_like()).view(dtype.base)
+
+########################################################################
+# Handler that does callbacks from C++
+########################################################################
+class gateway_handler(gr.feval_ll):
+
+    #dont put a constructor, it wont work
+
+    def init(self, callback):
+        self._callback = callback
+
+    def eval(self, arg):
+        try: self._callback()
+        except Exception as ex:
+            print("handler caught exception: %s"%ex)
+            import traceback; traceback.print_exc()
+            raise ex
+        return 0
+
+########################################################################
+# Handler that does callbacks from C++
+########################################################################
+class msg_handler(gr.feval_p):
+
+    #dont put a constructor, it wont work
+
+    def init(self, callback):
+        self._callback = callback
+
+    def eval(self, arg):
+        try: self._callback(arg)
+        except Exception as ex:
+            print("handler caught exception: %s"%ex)
+            import traceback; traceback.print_exc()
+            raise ex
+        return 0
+
+########################################################################
+# The guts that make this into a gr block
+########################################################################
+class gateway_block(object):
+
+    def __init__(self, name, in_sig, out_sig, work_type, factor):
+
+        #ensure that the sigs are iterable dtypes
+        def sig_to_dtype_sig(sig):
+            if sig is None: sig = ()
+            return map(numpy.dtype, sig)
+        self.__in_sig = sig_to_dtype_sig(in_sig)
+        self.__out_sig = sig_to_dtype_sig(out_sig)
+
+        #cache the ranges to iterate when dispatching work
+        self.__in_indexes = range(len(self.__in_sig))
+        self.__out_indexes = range(len(self.__out_sig))
+
+        #convert the signatures into gr.io_signatures
+        def sig_to_gr_io_sigv(sig):
+            if not len(sig): return io_signature(0, 0, 0)
+            return io_signaturev(len(sig), len(sig), [s.itemsize for s in sig])
+        gr_in_sig = sig_to_gr_io_sigv(self.__in_sig)
+        gr_out_sig = sig_to_gr_io_sigv(self.__out_sig)
+
+        #create internal gateway block
+        self.__handler = gateway_handler()
+        self.__handler.init(self.__gr_block_handle)
+        self.__gateway = block_gateway(
+            self.__handler, name, gr_in_sig, gr_out_sig, work_type, factor)
+        self.__message = self.__gateway.gr_block_message()
+
+        #dict to keep references to all message handlers
+        self.__msg_handlers = {}
+
+        #register gr_block functions
+        prefix = 'gr_block__'
+        for attr in [x for x in dir(self.__gateway) if x.startswith(prefix)]:
+            setattr(self, attr.replace(prefix, ''), getattr(self.__gateway, attr))
+        self.pop_msg_queue = lambda: gr.gr_block_gw_pop_msg_queue_safe(self.__gateway)
+
+    def to_basic_block(self):
+        """
+        Makes this block connectable by hier/top block python
+        """
+        return self.__gateway.to_basic_block()
+
+    def __gr_block_handle(self):
+        """
+        Dispatch tasks according to the action type specified in the message.
+        """
+        if self.__message.action == gr_block_gw_message_type.ACTION_GENERAL_WORK:
+            self.__message.general_work_args_return_value = self.general_work(
+
+                input_items=[pointer_to_ndarray(
+                    self.__message.general_work_args_input_items[i],
+                    self.__in_sig[i],
+                    self.__message.general_work_args_ninput_items[i]
+                ) for i in self.__in_indexes],
+
+                output_items=[pointer_to_ndarray(
+                    self.__message.general_work_args_output_items[i],
+                    self.__out_sig[i],
+                    self.__message.general_work_args_noutput_items
+                ) for i in self.__out_indexes],
+            )
+
+        elif self.__message.action == gr_block_gw_message_type.ACTION_WORK:
+            self.__message.work_args_return_value = self.work(
+
+                input_items=[pointer_to_ndarray(
+                    self.__message.work_args_input_items[i],
+                    self.__in_sig[i],
+                    self.__message.work_args_ninput_items
+                ) for i in self.__in_indexes],
+
+                output_items=[pointer_to_ndarray(
+                    self.__message.work_args_output_items[i],
+                    self.__out_sig[i],
+                    self.__message.work_args_noutput_items
+                ) for i in self.__out_indexes],
+            )
+
+        elif self.__message.action == gr_block_gw_message_type.ACTION_FORECAST:
+            self.forecast(
+                noutput_items=self.__message.forecast_args_noutput_items,
+                ninput_items_required=self.__message.forecast_args_ninput_items_required,
+            )
+
+        elif self.__message.action == gr_block_gw_message_type.ACTION_START:
+            self.__message.start_args_return_value = self.start()
+
+        elif self.__message.action == gr_block_gw_message_type.ACTION_STOP:
+            self.__message.stop_args_return_value = self.stop()
+
+    def forecast(self, noutput_items, ninput_items_required):
+        """
+        forecast is only called from a general block
+        this is the default implementation
+        """
+        for ninput_item in ninput_items_required:
+            ninput_item = noutput_items + self.history() - 1;
+        return
+
+    def general_work(self, *args, **kwargs):
+        """general work to be overloaded in a derived class"""
+        raise NotImplementedError("general work not implemented")
+
+    def work(self, *args, **kwargs):
+        """work to be overloaded in a derived class"""
+        raise NotImplementedError("work not implemented")
+
+    def start(self): return True
+    def stop(self): return True
+
+    def set_msg_handler(self, which_port, handler_func):
+        handler = msg_handler()
+        handler.init(handler_func)
+        self.__gateway.set_msg_handler_feval(which_port, handler)
+        # Save handler object in class so it's not garbage collected
+        self.__msg_handlers[which_port] = handler
+
+########################################################################
+# Wrappers for the user to inherit from
+########################################################################
+class basic_block(gateway_block):
+    def __init__(self, name, in_sig, out_sig):
+        gateway_block.__init__(self,
+            name=name,
+            in_sig=in_sig,
+            out_sig=out_sig,
+            work_type=gr.GR_BLOCK_GW_WORK_GENERAL,
+            factor=1, #not relevant factor
+        )
+
+class sync_block(gateway_block):
+    def __init__(self, name, in_sig, out_sig):
+        gateway_block.__init__(self,
+            name=name,
+            in_sig=in_sig,
+            out_sig=out_sig,
+            work_type=gr.GR_BLOCK_GW_WORK_SYNC,
+            factor=1,
+        )
+
+class decim_block(gateway_block):
+    def __init__(self, name, in_sig, out_sig, decim):
+        gateway_block.__init__(self,
+            name=name,
+            in_sig=in_sig,
+            out_sig=out_sig,
+            work_type=gr.GR_BLOCK_GW_WORK_DECIM,
+            factor=decim,
+        )
+
+class interp_block(gateway_block):
+    def __init__(self, name, in_sig, out_sig, interp):
+        gateway_block.__init__(self,
+            name=name,
+            in_sig=in_sig,
+            out_sig=out_sig,
+            work_type=gr.GR_BLOCK_GW_WORK_INTERP,
+            factor=interp,
+        )
diff --git a/gnuradio-runtime/python/gnuradio/gr/gr_threading.py b/gnuradio-runtime/python/gnuradio/gr/gr_threading.py
new file mode 100644
index 0000000000..5d6f0fdaf9
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/gr_threading.py
@@ -0,0 +1,35 @@
+#
+# 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+from sys import version_info as _version_info
+
+# import patched version of standard threading module
+
+if _version_info[0:2] == (2, 3):
+    #print "Importing gr_threading_23"
+    from gr_threading_23 import *
+elif _version_info[0:2] == (2, 4):
+    #print "Importing gr_threading_24"
+    from gr_threading_24 import *
+else:
+    # assume the patch was applied...
+    #print "Importing system provided threading"
+    from threading import *
diff --git a/gnuradio-runtime/python/gnuradio/gr/gr_threading_23.py b/gnuradio-runtime/python/gnuradio/gr/gr_threading_23.py
new file mode 100644
index 0000000000..dee8034c1c
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/gr_threading_23.py
@@ -0,0 +1,724 @@
+"""Thread module emulating a subset of Java's threading model."""
+
+# This started life as the threading.py module of Python 2.3
+# It's been patched to fix a problem with join, where a KeyboardInterrupt
+# caused a lock to be left in the acquired state.
+
+import sys as _sys
+
+try:
+    import thread
+except ImportError:
+    del _sys.modules[__name__]
+    raise
+
+from StringIO import StringIO as _StringIO
+from time import time as _time, sleep as _sleep
+from traceback import print_exc as _print_exc
+
+# Rename some stuff so "from threading import *" is safe
+__all__ = ['activeCount', 'Condition', 'currentThread', 'enumerate', 'Event',
+           'Lock', 'RLock', 'Semaphore', 'BoundedSemaphore', 'Thread',
+           'Timer', 'setprofile', 'settrace']
+
+_start_new_thread = thread.start_new_thread
+_allocate_lock = thread.allocate_lock
+_get_ident = thread.get_ident
+ThreadError = thread.error
+del thread
+
+
+# Debug support (adapted from ihooks.py).
+# All the major classes here derive from _Verbose.  We force that to
+# be a new-style class so that all the major classes here are new-style.
+# This helps debugging (type(instance) is more revealing for instances
+# of new-style classes).
+
+_VERBOSE = False
+
+if __debug__:
+
+    class _Verbose(object):
+
+        def __init__(self, verbose=None):
+            if verbose is None:
+                verbose = _VERBOSE
+            self.__verbose = verbose
+
+        def _note(self, format, *args):
+            if self.__verbose:
+                format = format % args
+                format = "%s: %s\n" % (
+                    currentThread().getName(), format)
+                _sys.stderr.write(format)
+
+else:
+    # Disable this when using "python -O"
+    class _Verbose(object):
+        def __init__(self, verbose=None):
+            pass
+        def _note(self, *args):
+            pass
+
+# Support for profile and trace hooks
+
+_profile_hook = None
+_trace_hook = None
+
+def setprofile(func):
+    global _profile_hook
+    _profile_hook = func
+
+def settrace(func):
+    global _trace_hook
+    _trace_hook = func
+
+# Synchronization classes
+
+Lock = _allocate_lock
+
+def RLock(*args, **kwargs):
+    return _RLock(*args, **kwargs)
+
+class _RLock(_Verbose):
+
+    def __init__(self, verbose=None):
+        _Verbose.__init__(self, verbose)
+        self.__block = _allocate_lock()
+        self.__owner = None
+        self.__count = 0
+
+    def __repr__(self):
+        return "<%s(%s, %d)>" % (
+                self.__class__.__name__,
+                self.__owner and self.__owner.getName(),
+                self.__count)
+
+    def acquire(self, blocking=1):
+        me = currentThread()
+        if self.__owner is me:
+            self.__count = self.__count + 1
+            if __debug__:
+                self._note("%s.acquire(%s): recursive success", self, blocking)
+            return 1
+        rc = self.__block.acquire(blocking)
+        if rc:
+            self.__owner = me
+            self.__count = 1
+            if __debug__:
+                self._note("%s.acquire(%s): initial succes", self, blocking)
+        else:
+            if __debug__:
+                self._note("%s.acquire(%s): failure", self, blocking)
+        return rc
+
+    def release(self):
+        me = currentThread()
+        assert self.__owner is me, "release() of un-acquire()d lock"
+        self.__count = count = self.__count - 1
+        if not count:
+            self.__owner = None
+            self.__block.release()
+            if __debug__:
+                self._note("%s.release(): final release", self)
+        else:
+            if __debug__:
+                self._note("%s.release(): non-final release", self)
+
+    # Internal methods used by condition variables
+
+    def _acquire_restore(self, (count, owner)):
+        self.__block.acquire()
+        self.__count = count
+        self.__owner = owner
+        if __debug__:
+            self._note("%s._acquire_restore()", self)
+
+    def _release_save(self):
+        if __debug__:
+            self._note("%s._release_save()", self)
+        count = self.__count
+        self.__count = 0
+        owner = self.__owner
+        self.__owner = None
+        self.__block.release()
+        return (count, owner)
+
+    def _is_owned(self):
+        return self.__owner is currentThread()
+
+
+def Condition(*args, **kwargs):
+    return _Condition(*args, **kwargs)
+
+class _Condition(_Verbose):
+
+    def __init__(self, lock=None, verbose=None):
+        _Verbose.__init__(self, verbose)
+        if lock is None:
+            lock = RLock()
+        self.__lock = lock
+        # Export the lock's acquire() and release() methods
+        self.acquire = lock.acquire
+        self.release = lock.release
+        # If the lock defines _release_save() and/or _acquire_restore(),
+        # these override the default implementations (which just call
+        # release() and acquire() on the lock).  Ditto for _is_owned().
+        try:
+            self._release_save = lock._release_save
+        except AttributeError:
+            pass
+        try:
+            self._acquire_restore = lock._acquire_restore
+        except AttributeError:
+            pass
+        try:
+            self._is_owned = lock._is_owned
+        except AttributeError:
+            pass
+        self.__waiters = []
+
+    def __repr__(self):
+        return "<Condition(%s, %d)>" % (self.__lock, len(self.__waiters))
+
+    def _release_save(self):
+        self.__lock.release()           # No state to save
+
+    def _acquire_restore(self, x):
+        self.__lock.acquire()           # Ignore saved state
+
+    def _is_owned(self):
+        # Return True if lock is owned by currentThread.
+        # This method is called only if __lock doesn't have _is_owned().
+        if self.__lock.acquire(0):
+            self.__lock.release()
+            return False
+        else:
+            return True
+
+    def wait(self, timeout=None):
+        currentThread() # for side-effect
+        assert self._is_owned(), "wait() of un-acquire()d lock"
+        waiter = _allocate_lock()
+        waiter.acquire()
+        self.__waiters.append(waiter)
+        saved_state = self._release_save()
+        try:    # restore state no matter what (e.g., KeyboardInterrupt)
+            if timeout is None:
+                waiter.acquire()
+                if __debug__:
+                    self._note("%s.wait(): got it", self)
+            else:
+                # Balancing act:  We can't afford a pure busy loop, so we
+                # have to sleep; but if we sleep the whole timeout time,
+                # we'll be unresponsive.  The scheme here sleeps very
+                # little at first, longer as time goes on, but never longer
+                # than 20 times per second (or the timeout time remaining).
+                endtime = _time() + timeout
+                delay = 0.0005 # 500 us -> initial delay of 1 ms
+                while True:
+                    gotit = waiter.acquire(0)
+                    if gotit:
+                        break
+                    remaining = endtime - _time()
+                    if remaining <= 0:
+                        break
+                    delay = min(delay * 2, remaining, .05)
+                    _sleep(delay)
+                if not gotit:
+                    if __debug__:
+                        self._note("%s.wait(%s): timed out", self, timeout)
+                    try:
+                        self.__waiters.remove(waiter)
+                    except ValueError:
+                        pass
+                else:
+                    if __debug__:
+                        self._note("%s.wait(%s): got it", self, timeout)
+        finally:
+            self._acquire_restore(saved_state)
+
+    def notify(self, n=1):
+        currentThread() # for side-effect
+        assert self._is_owned(), "notify() of un-acquire()d lock"
+        __waiters = self.__waiters
+        waiters = __waiters[:n]
+        if not waiters:
+            if __debug__:
+                self._note("%s.notify(): no waiters", self)
+            return
+        self._note("%s.notify(): notifying %d waiter%s", self, n,
+                   n!=1 and "s" or "")
+        for waiter in waiters:
+            waiter.release()
+            try:
+                __waiters.remove(waiter)
+            except ValueError:
+                pass
+
+    def notifyAll(self):
+        self.notify(len(self.__waiters))
+
+
+def Semaphore(*args, **kwargs):
+    return _Semaphore(*args, **kwargs)
+
+class _Semaphore(_Verbose):
+
+    # After Tim Peters' semaphore class, but not quite the same (no maximum)
+
+    def __init__(self, value=1, verbose=None):
+        assert value >= 0, "Semaphore initial value must be >= 0"
+        _Verbose.__init__(self, verbose)
+        self.__cond = Condition(Lock())
+        self.__value = value
+
+    def acquire(self, blocking=1):
+        rc = False
+        self.__cond.acquire()
+        while self.__value == 0:
+            if not blocking:
+                break
+            if __debug__:
+                self._note("%s.acquire(%s): blocked waiting, value=%s",
+                           self, blocking, self.__value)
+            self.__cond.wait()
+        else:
+            self.__value = self.__value - 1
+            if __debug__:
+                self._note("%s.acquire: success, value=%s",
+                           self, self.__value)
+            rc = True
+        self.__cond.release()
+        return rc
+
+    def release(self):
+        self.__cond.acquire()
+        self.__value = self.__value + 1
+        if __debug__:
+            self._note("%s.release: success, value=%s",
+                       self, self.__value)
+        self.__cond.notify()
+        self.__cond.release()
+
+
+def BoundedSemaphore(*args, **kwargs):
+    return _BoundedSemaphore(*args, **kwargs)
+
+class _BoundedSemaphore(_Semaphore):
+    """Semaphore that checks that # releases is <= # acquires"""
+    def __init__(self, value=1, verbose=None):
+        _Semaphore.__init__(self, value, verbose)
+        self._initial_value = value
+
+    def release(self):
+        if self._Semaphore__value >= self._initial_value:
+            raise ValueError, "Semaphore released too many times"
+        return _Semaphore.release(self)
+
+
+def Event(*args, **kwargs):
+    return _Event(*args, **kwargs)
+
+class _Event(_Verbose):
+
+    # After Tim Peters' event class (without is_posted())
+
+    def __init__(self, verbose=None):
+        _Verbose.__init__(self, verbose)
+        self.__cond = Condition(Lock())
+        self.__flag = False
+
+    def isSet(self):
+        return self.__flag
+
+    def set(self):
+        self.__cond.acquire()
+        try:
+            self.__flag = True
+            self.__cond.notifyAll()
+        finally:
+            self.__cond.release()
+
+    def clear(self):
+        self.__cond.acquire()
+        try:
+            self.__flag = False
+        finally:
+            self.__cond.release()
+
+    def wait(self, timeout=None):
+        self.__cond.acquire()
+        try:
+            if not self.__flag:
+                self.__cond.wait(timeout)
+        finally:
+            self.__cond.release()
+
+# Helper to generate new thread names
+_counter = 0
+def _newname(template="Thread-%d"):
+    global _counter
+    _counter = _counter + 1
+    return template % _counter
+
+# Active thread administration
+_active_limbo_lock = _allocate_lock()
+_active = {}
+_limbo = {}
+
+
+# Main class for threads
+
+class Thread(_Verbose):
+
+    __initialized = False
+
+    def __init__(self, group=None, target=None, name=None,
+                 args=(), kwargs={}, verbose=None):
+        assert group is None, "group argument must be None for now"
+        _Verbose.__init__(self, verbose)
+        self.__target = target
+        self.__name = str(name or _newname())
+        self.__args = args
+        self.__kwargs = kwargs
+        self.__daemonic = self._set_daemon()
+        self.__started = False
+        self.__stopped = False
+        self.__block = Condition(Lock())
+        self.__initialized = True
+
+    def _set_daemon(self):
+        # Overridden in _MainThread and _DummyThread
+        return currentThread().isDaemon()
+
+    def __repr__(self):
+        assert self.__initialized, "Thread.__init__() was not called"
+        status = "initial"
+        if self.__started:
+            status = "started"
+        if self.__stopped:
+            status = "stopped"
+        if self.__daemonic:
+            status = status + " daemon"
+        return "<%s(%s, %s)>" % (self.__class__.__name__, self.__name, status)
+
+    def start(self):
+        assert self.__initialized, "Thread.__init__() not called"
+        assert not self.__started, "thread already started"
+        if __debug__:
+            self._note("%s.start(): starting thread", self)
+        _active_limbo_lock.acquire()
+        _limbo[self] = self
+        _active_limbo_lock.release()
+        _start_new_thread(self.__bootstrap, ())
+        self.__started = True
+        _sleep(0.000001)    # 1 usec, to let the thread run (Solaris hack)
+
+    def run(self):
+        if self.__target:
+            self.__target(*self.__args, **self.__kwargs)
+
+    def __bootstrap(self):
+        try:
+            self.__started = True
+            _active_limbo_lock.acquire()
+            _active[_get_ident()] = self
+            del _limbo[self]
+            _active_limbo_lock.release()
+            if __debug__:
+                self._note("%s.__bootstrap(): thread started", self)
+
+            if _trace_hook:
+                self._note("%s.__bootstrap(): registering trace hook", self)
+                _sys.settrace(_trace_hook)
+            if _profile_hook:
+                self._note("%s.__bootstrap(): registering profile hook", self)
+                _sys.setprofile(_profile_hook)
+
+            try:
+                self.run()
+            except SystemExit:
+                if __debug__:
+                    self._note("%s.__bootstrap(): raised SystemExit", self)
+            except:
+                if __debug__:
+                    self._note("%s.__bootstrap(): unhandled exception", self)
+                s = _StringIO()
+                _print_exc(file=s)
+                _sys.stderr.write("Exception in thread %s:\n%s\n" %
+                                 (self.getName(), s.getvalue()))
+            else:
+                if __debug__:
+                    self._note("%s.__bootstrap(): normal return", self)
+        finally:
+            self.__stop()
+            try:
+                self.__delete()
+            except:
+                pass
+
+    def __stop(self):
+        self.__block.acquire()
+        self.__stopped = True
+        self.__block.notifyAll()
+        self.__block.release()
+
+    def __delete(self):
+        _active_limbo_lock.acquire()
+        del _active[_get_ident()]
+        _active_limbo_lock.release()
+
+    def join(self, timeout=None):
+        assert self.__initialized, "Thread.__init__() not called"
+        assert self.__started, "cannot join thread before it is started"
+        assert self is not currentThread(), "cannot join current thread"
+        if __debug__:
+            if not self.__stopped:
+                self._note("%s.join(): waiting until thread stops", self)
+        self.__block.acquire()
+        try:
+            if timeout is None:
+                while not self.__stopped:
+                    self.__block.wait()
+                if __debug__:
+                    self._note("%s.join(): thread stopped", self)
+            else:
+                deadline = _time() + timeout
+                while not self.__stopped:
+                    delay = deadline - _time()
+                    if delay <= 0:
+                        if __debug__:
+                            self._note("%s.join(): timed out", self)
+                        break
+                    self.__block.wait(delay)
+                else:
+                    if __debug__:
+                        self._note("%s.join(): thread stopped", self)
+        finally:
+            self.__block.release()
+
+    def getName(self):
+        assert self.__initialized, "Thread.__init__() not called"
+        return self.__name
+
+    def setName(self, name):
+        assert self.__initialized, "Thread.__init__() not called"
+        self.__name = str(name)
+
+    def isAlive(self):
+        assert self.__initialized, "Thread.__init__() not called"
+        return self.__started and not self.__stopped
+
+    def isDaemon(self):
+        assert self.__initialized, "Thread.__init__() not called"
+        return self.__daemonic
+
+    def setDaemon(self, daemonic):
+        assert self.__initialized, "Thread.__init__() not called"
+        assert not self.__started, "cannot set daemon status of active thread"
+        self.__daemonic = daemonic
+
+# The timer class was contributed by Itamar Shtull-Trauring
+
+def Timer(*args, **kwargs):
+    return _Timer(*args, **kwargs)
+
+class _Timer(Thread):
+    """Call a function after a specified number of seconds:
+
+    t = Timer(30.0, f, args=[], kwargs={})
+    t.start()
+    t.cancel() # stop the timer's action if it's still waiting
+    """
+
+    def __init__(self, interval, function, args=[], kwargs={}):
+        Thread.__init__(self)
+        self.interval = interval
+        self.function = function
+        self.args = args
+        self.kwargs = kwargs
+        self.finished = Event()
+
+    def cancel(self):
+        """Stop the timer if it hasn't finished yet"""
+        self.finished.set()
+
+    def run(self):
+        self.finished.wait(self.interval)
+        if not self.finished.isSet():
+            self.function(*self.args, **self.kwargs)
+        self.finished.set()
+
+# Special thread class to represent the main thread
+# This is garbage collected through an exit handler
+
+class _MainThread(Thread):
+
+    def __init__(self):
+        Thread.__init__(self, name="MainThread")
+        self._Thread__started = True
+        _active_limbo_lock.acquire()
+        _active[_get_ident()] = self
+        _active_limbo_lock.release()
+        import atexit
+        atexit.register(self.__exitfunc)
+
+    def _set_daemon(self):
+        return False
+
+    def __exitfunc(self):
+        self._Thread__stop()
+        t = _pickSomeNonDaemonThread()
+        if t:
+            if __debug__:
+                self._note("%s: waiting for other threads", self)
+        while t:
+            t.join()
+            t = _pickSomeNonDaemonThread()
+        if __debug__:
+            self._note("%s: exiting", self)
+        self._Thread__delete()
+
+def _pickSomeNonDaemonThread():
+    for t in enumerate():
+        if not t.isDaemon() and t.isAlive():
+            return t
+    return None
+
+
+# Dummy thread class to represent threads not started here.
+# These aren't garbage collected when they die,
+# nor can they be waited for.
+# Their purpose is to return *something* from currentThread().
+# They are marked as daemon threads so we won't wait for them
+# when we exit (conform previous semantics).
+
+class _DummyThread(Thread):
+
+    def __init__(self):
+        Thread.__init__(self, name=_newname("Dummy-%d"))
+        self._Thread__started = True
+        _active_limbo_lock.acquire()
+        _active[_get_ident()] = self
+        _active_limbo_lock.release()
+
+    def _set_daemon(self):
+        return True
+
+    def join(self, timeout=None):
+        assert False, "cannot join a dummy thread"
+
+
+# Global API functions
+
+def currentThread():
+    try:
+        return _active[_get_ident()]
+    except KeyError:
+        ##print "currentThread(): no current thread for", _get_ident()
+        return _DummyThread()
+
+def activeCount():
+    _active_limbo_lock.acquire()
+    count = len(_active) + len(_limbo)
+    _active_limbo_lock.release()
+    return count
+
+def enumerate():
+    _active_limbo_lock.acquire()
+    active = _active.values() + _limbo.values()
+    _active_limbo_lock.release()
+    return active
+
+# Create the main thread object
+
+_MainThread()
+
+
+# Self-test code
+
+def _test():
+
+    class BoundedQueue(_Verbose):
+
+        def __init__(self, limit):
+            _Verbose.__init__(self)
+            self.mon = RLock()
+            self.rc = Condition(self.mon)
+            self.wc = Condition(self.mon)
+            self.limit = limit
+            self.queue = []
+
+        def put(self, item):
+            self.mon.acquire()
+            while len(self.queue) >= self.limit:
+                self._note("put(%s): queue full", item)
+                self.wc.wait()
+            self.queue.append(item)
+            self._note("put(%s): appended, length now %d",
+                       item, len(self.queue))
+            self.rc.notify()
+            self.mon.release()
+
+        def get(self):
+            self.mon.acquire()
+            while not self.queue:
+                self._note("get(): queue empty")
+                self.rc.wait()
+            item = self.queue.pop(0)
+            self._note("get(): got %s, %d left", item, len(self.queue))
+            self.wc.notify()
+            self.mon.release()
+            return item
+
+    class ProducerThread(Thread):
+
+        def __init__(self, queue, quota):
+            Thread.__init__(self, name="Producer")
+            self.queue = queue
+            self.quota = quota
+
+        def run(self):
+            from random import random
+            counter = 0
+            while counter < self.quota:
+                counter = counter + 1
+                self.queue.put("%s.%d" % (self.getName(), counter))
+                _sleep(random() * 0.00001)
+
+
+    class ConsumerThread(Thread):
+
+        def __init__(self, queue, count):
+            Thread.__init__(self, name="Consumer")
+            self.queue = queue
+            self.count = count
+
+        def run(self):
+            while self.count > 0:
+                item = self.queue.get()
+                print item
+                self.count = self.count - 1
+
+    NP = 3
+    QL = 4
+    NI = 5
+
+    Q = BoundedQueue(QL)
+    P = []
+    for i in range(NP):
+        t = ProducerThread(Q, NI)
+        t.setName("Producer-%d" % (i+1))
+        P.append(t)
+    C = ConsumerThread(Q, NI*NP)
+    for t in P:
+        t.start()
+        _sleep(0.000001)
+    C.start()
+    for t in P:
+        t.join()
+    C.join()
+
+if __name__ == '__main__':
+    _test()
diff --git a/gnuradio-runtime/python/gnuradio/gr/gr_threading_24.py b/gnuradio-runtime/python/gnuradio/gr/gr_threading_24.py
new file mode 100644
index 0000000000..8539bfc047
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/gr_threading_24.py
@@ -0,0 +1,793 @@
+"""Thread module emulating a subset of Java's threading model."""
+
+# This started life as the threading.py module of Python 2.4
+# It's been patched to fix a problem with join, where a KeyboardInterrupt
+# caused a lock to be left in the acquired state.
+
+import sys as _sys
+
+try:
+    import thread
+except ImportError:
+    del _sys.modules[__name__]
+    raise
+
+from time import time as _time, sleep as _sleep
+from traceback import format_exc as _format_exc
+from collections import deque
+
+# Rename some stuff so "from threading import *" is safe
+__all__ = ['activeCount', 'Condition', 'currentThread', 'enumerate', 'Event',
+           'Lock', 'RLock', 'Semaphore', 'BoundedSemaphore', 'Thread',
+           'Timer', 'setprofile', 'settrace', 'local']
+
+_start_new_thread = thread.start_new_thread
+_allocate_lock = thread.allocate_lock
+_get_ident = thread.get_ident
+ThreadError = thread.error
+del thread
+
+
+# Debug support (adapted from ihooks.py).
+# All the major classes here derive from _Verbose.  We force that to
+# be a new-style class so that all the major classes here are new-style.
+# This helps debugging (type(instance) is more revealing for instances
+# of new-style classes).
+
+_VERBOSE = False
+
+if __debug__:
+
+    class _Verbose(object):
+
+        def __init__(self, verbose=None):
+            if verbose is None:
+                verbose = _VERBOSE
+            self.__verbose = verbose
+
+        def _note(self, format, *args):
+            if self.__verbose:
+                format = format % args
+                format = "%s: %s\n" % (
+                    currentThread().getName(), format)
+                _sys.stderr.write(format)
+
+else:
+    # Disable this when using "python -O"
+    class _Verbose(object):
+        def __init__(self, verbose=None):
+            pass
+        def _note(self, *args):
+            pass
+
+# Support for profile and trace hooks
+
+_profile_hook = None
+_trace_hook = None
+
+def setprofile(func):
+    global _profile_hook
+    _profile_hook = func
+
+def settrace(func):
+    global _trace_hook
+    _trace_hook = func
+
+# Synchronization classes
+
+Lock = _allocate_lock
+
+def RLock(*args, **kwargs):
+    return _RLock(*args, **kwargs)
+
+class _RLock(_Verbose):
+
+    def __init__(self, verbose=None):
+        _Verbose.__init__(self, verbose)
+        self.__block = _allocate_lock()
+        self.__owner = None
+        self.__count = 0
+
+    def __repr__(self):
+        return "<%s(%s, %d)>" % (
+                self.__class__.__name__,
+                self.__owner and self.__owner.getName(),
+                self.__count)
+
+    def acquire(self, blocking=1):
+        me = currentThread()
+        if self.__owner is me:
+            self.__count = self.__count + 1
+            if __debug__:
+                self._note("%s.acquire(%s): recursive success", self, blocking)
+            return 1
+        rc = self.__block.acquire(blocking)
+        if rc:
+            self.__owner = me
+            self.__count = 1
+            if __debug__:
+                self._note("%s.acquire(%s): initial succes", self, blocking)
+        else:
+            if __debug__:
+                self._note("%s.acquire(%s): failure", self, blocking)
+        return rc
+
+    def release(self):
+        me = currentThread()
+        assert self.__owner is me, "release() of un-acquire()d lock"
+        self.__count = count = self.__count - 1
+        if not count:
+            self.__owner = None
+            self.__block.release()
+            if __debug__:
+                self._note("%s.release(): final release", self)
+        else:
+            if __debug__:
+                self._note("%s.release(): non-final release", self)
+
+    # Internal methods used by condition variables
+
+    def _acquire_restore(self, (count, owner)):
+        self.__block.acquire()
+        self.__count = count
+        self.__owner = owner
+        if __debug__:
+            self._note("%s._acquire_restore()", self)
+
+    def _release_save(self):
+        if __debug__:
+            self._note("%s._release_save()", self)
+        count = self.__count
+        self.__count = 0
+        owner = self.__owner
+        self.__owner = None
+        self.__block.release()
+        return (count, owner)
+
+    def _is_owned(self):
+        return self.__owner is currentThread()
+
+
+def Condition(*args, **kwargs):
+    return _Condition(*args, **kwargs)
+
+class _Condition(_Verbose):
+
+    def __init__(self, lock=None, verbose=None):
+        _Verbose.__init__(self, verbose)
+        if lock is None:
+            lock = RLock()
+        self.__lock = lock
+        # Export the lock's acquire() and release() methods
+        self.acquire = lock.acquire
+        self.release = lock.release
+        # If the lock defines _release_save() and/or _acquire_restore(),
+        # these override the default implementations (which just call
+        # release() and acquire() on the lock).  Ditto for _is_owned().
+        try:
+            self._release_save = lock._release_save
+        except AttributeError:
+            pass
+        try:
+            self._acquire_restore = lock._acquire_restore
+        except AttributeError:
+            pass
+        try:
+            self._is_owned = lock._is_owned
+        except AttributeError:
+            pass
+        self.__waiters = []
+
+    def __repr__(self):
+        return "<Condition(%s, %d)>" % (self.__lock, len(self.__waiters))
+
+    def _release_save(self):
+        self.__lock.release()           # No state to save
+
+    def _acquire_restore(self, x):
+        self.__lock.acquire()           # Ignore saved state
+
+    def _is_owned(self):
+        # Return True if lock is owned by currentThread.
+        # This method is called only if __lock doesn't have _is_owned().
+        if self.__lock.acquire(0):
+            self.__lock.release()
+            return False
+        else:
+            return True
+
+    def wait(self, timeout=None):
+        assert self._is_owned(), "wait() of un-acquire()d lock"
+        waiter = _allocate_lock()
+        waiter.acquire()
+        self.__waiters.append(waiter)
+        saved_state = self._release_save()
+        try:    # restore state no matter what (e.g., KeyboardInterrupt)
+            if timeout is None:
+                waiter.acquire()
+                if __debug__:
+                    self._note("%s.wait(): got it", self)
+            else:
+                # Balancing act:  We can't afford a pure busy loop, so we
+                # have to sleep; but if we sleep the whole timeout time,
+                # we'll be unresponsive.  The scheme here sleeps very
+                # little at first, longer as time goes on, but never longer
+                # than 20 times per second (or the timeout time remaining).
+                endtime = _time() + timeout
+                delay = 0.0005 # 500 us -> initial delay of 1 ms
+                while True:
+                    gotit = waiter.acquire(0)
+                    if gotit:
+                        break
+                    remaining = endtime - _time()
+                    if remaining <= 0:
+                        break
+                    delay = min(delay * 2, remaining, .05)
+                    _sleep(delay)
+                if not gotit:
+                    if __debug__:
+                        self._note("%s.wait(%s): timed out", self, timeout)
+                    try:
+                        self.__waiters.remove(waiter)
+                    except ValueError:
+                        pass
+                else:
+                    if __debug__:
+                        self._note("%s.wait(%s): got it", self, timeout)
+        finally:
+            self._acquire_restore(saved_state)
+
+    def notify(self, n=1):
+        assert self._is_owned(), "notify() of un-acquire()d lock"
+        __waiters = self.__waiters
+        waiters = __waiters[:n]
+        if not waiters:
+            if __debug__:
+                self._note("%s.notify(): no waiters", self)
+            return
+        self._note("%s.notify(): notifying %d waiter%s", self, n,
+                   n!=1 and "s" or "")
+        for waiter in waiters:
+            waiter.release()
+            try:
+                __waiters.remove(waiter)
+            except ValueError:
+                pass
+
+    def notifyAll(self):
+        self.notify(len(self.__waiters))
+
+
+def Semaphore(*args, **kwargs):
+    return _Semaphore(*args, **kwargs)
+
+class _Semaphore(_Verbose):
+
+    # After Tim Peters' semaphore class, but not quite the same (no maximum)
+
+    def __init__(self, value=1, verbose=None):
+        assert value >= 0, "Semaphore initial value must be >= 0"
+        _Verbose.__init__(self, verbose)
+        self.__cond = Condition(Lock())
+        self.__value = value
+
+    def acquire(self, blocking=1):
+        rc = False
+        self.__cond.acquire()
+        while self.__value == 0:
+            if not blocking:
+                break
+            if __debug__:
+                self._note("%s.acquire(%s): blocked waiting, value=%s",
+                           self, blocking, self.__value)
+            self.__cond.wait()
+        else:
+            self.__value = self.__value - 1
+            if __debug__:
+                self._note("%s.acquire: success, value=%s",
+                           self, self.__value)
+            rc = True
+        self.__cond.release()
+        return rc
+
+    def release(self):
+        self.__cond.acquire()
+        self.__value = self.__value + 1
+        if __debug__:
+            self._note("%s.release: success, value=%s",
+                       self, self.__value)
+        self.__cond.notify()
+        self.__cond.release()
+
+
+def BoundedSemaphore(*args, **kwargs):
+    return _BoundedSemaphore(*args, **kwargs)
+
+class _BoundedSemaphore(_Semaphore):
+    """Semaphore that checks that # releases is <= # acquires"""
+    def __init__(self, value=1, verbose=None):
+        _Semaphore.__init__(self, value, verbose)
+        self._initial_value = value
+
+    def release(self):
+        if self._Semaphore__value >= self._initial_value:
+            raise ValueError, "Semaphore released too many times"
+        return _Semaphore.release(self)
+
+
+def Event(*args, **kwargs):
+    return _Event(*args, **kwargs)
+
+class _Event(_Verbose):
+
+    # After Tim Peters' event class (without is_posted())
+
+    def __init__(self, verbose=None):
+        _Verbose.__init__(self, verbose)
+        self.__cond = Condition(Lock())
+        self.__flag = False
+
+    def isSet(self):
+        return self.__flag
+
+    def set(self):
+        self.__cond.acquire()
+        try:
+            self.__flag = True
+            self.__cond.notifyAll()
+        finally:
+            self.__cond.release()
+
+    def clear(self):
+        self.__cond.acquire()
+        try:
+            self.__flag = False
+        finally:
+            self.__cond.release()
+
+    def wait(self, timeout=None):
+        self.__cond.acquire()
+        try:
+            if not self.__flag:
+                self.__cond.wait(timeout)
+        finally:
+            self.__cond.release()
+
+# Helper to generate new thread names
+_counter = 0
+def _newname(template="Thread-%d"):
+    global _counter
+    _counter = _counter + 1
+    return template % _counter
+
+# Active thread administration
+_active_limbo_lock = _allocate_lock()
+_active = {}
+_limbo = {}
+
+
+# Main class for threads
+
+class Thread(_Verbose):
+
+    __initialized = False
+    # Need to store a reference to sys.exc_info for printing
+    # out exceptions when a thread tries to use a global var. during interp.
+    # shutdown and thus raises an exception about trying to perform some
+    # operation on/with a NoneType
+    __exc_info = _sys.exc_info
+
+    def __init__(self, group=None, target=None, name=None,
+                 args=(), kwargs={}, verbose=None):
+        assert group is None, "group argument must be None for now"
+        _Verbose.__init__(self, verbose)
+        self.__target = target
+        self.__name = str(name or _newname())
+        self.__args = args
+        self.__kwargs = kwargs
+        self.__daemonic = self._set_daemon()
+        self.__started = False
+        self.__stopped = False
+        self.__block = Condition(Lock())
+        self.__initialized = True
+        # sys.stderr is not stored in the class like
+        # sys.exc_info since it can be changed between instances
+        self.__stderr = _sys.stderr
+
+    def _set_daemon(self):
+        # Overridden in _MainThread and _DummyThread
+        return currentThread().isDaemon()
+
+    def __repr__(self):
+        assert self.__initialized, "Thread.__init__() was not called"
+        status = "initial"
+        if self.__started:
+            status = "started"
+        if self.__stopped:
+            status = "stopped"
+        if self.__daemonic:
+            status = status + " daemon"
+        return "<%s(%s, %s)>" % (self.__class__.__name__, self.__name, status)
+
+    def start(self):
+        assert self.__initialized, "Thread.__init__() not called"
+        assert not self.__started, "thread already started"
+        if __debug__:
+            self._note("%s.start(): starting thread", self)
+        _active_limbo_lock.acquire()
+        _limbo[self] = self
+        _active_limbo_lock.release()
+        _start_new_thread(self.__bootstrap, ())
+        self.__started = True
+        _sleep(0.000001)    # 1 usec, to let the thread run (Solaris hack)
+
+    def run(self):
+        if self.__target:
+            self.__target(*self.__args, **self.__kwargs)
+
+    def __bootstrap(self):
+        try:
+            self.__started = True
+            _active_limbo_lock.acquire()
+            _active[_get_ident()] = self
+            del _limbo[self]
+            _active_limbo_lock.release()
+            if __debug__:
+                self._note("%s.__bootstrap(): thread started", self)
+
+            if _trace_hook:
+                self._note("%s.__bootstrap(): registering trace hook", self)
+                _sys.settrace(_trace_hook)
+            if _profile_hook:
+                self._note("%s.__bootstrap(): registering profile hook", self)
+                _sys.setprofile(_profile_hook)
+
+            try:
+                self.run()
+            except SystemExit:
+                if __debug__:
+                    self._note("%s.__bootstrap(): raised SystemExit", self)
+            except:
+                if __debug__:
+                    self._note("%s.__bootstrap(): unhandled exception", self)
+                # If sys.stderr is no more (most likely from interpreter
+                # shutdown) use self.__stderr.  Otherwise still use sys (as in
+                # _sys) in case sys.stderr was redefined since the creation of
+                # self.
+                if _sys:
+                    _sys.stderr.write("Exception in thread %s:\n%s\n" %
+                                      (self.getName(), _format_exc()))
+                else:
+                    # Do the best job possible w/o a huge amt. of code to
+                    # approximate a traceback (code ideas from
+                    # Lib/traceback.py)
+                    exc_type, exc_value, exc_tb = self.__exc_info()
+                    try:
+                        print>>self.__stderr, (
+                            "Exception in thread " + self.getName() +
+                            " (most likely raised during interpreter shutdown):")
+                        print>>self.__stderr, (
+                            "Traceback (most recent call last):")
+                        while exc_tb:
+                            print>>self.__stderr, (
+                                '  File "%s", line %s, in %s' %
+                                (exc_tb.tb_frame.f_code.co_filename,
+                                    exc_tb.tb_lineno,
+                                    exc_tb.tb_frame.f_code.co_name))
+                            exc_tb = exc_tb.tb_next
+                        print>>self.__stderr, ("%s: %s" % (exc_type, exc_value))
+                    # Make sure that exc_tb gets deleted since it is a memory
+                    # hog; deleting everything else is just for thoroughness
+                    finally:
+                        del exc_type, exc_value, exc_tb
+            else:
+                if __debug__:
+                    self._note("%s.__bootstrap(): normal return", self)
+        finally:
+            self.__stop()
+            try:
+                self.__delete()
+            except:
+                pass
+
+    def __stop(self):
+        self.__block.acquire()
+        self.__stopped = True
+        self.__block.notifyAll()
+        self.__block.release()
+
+    def __delete(self):
+        "Remove current thread from the dict of currently running threads."
+
+        # Notes about running with dummy_thread:
+        #
+        # Must take care to not raise an exception if dummy_thread is being
+        # used (and thus this module is being used as an instance of
+        # dummy_threading).  dummy_thread.get_ident() always returns -1 since
+        # there is only one thread if dummy_thread is being used.  Thus
+        # len(_active) is always <= 1 here, and any Thread instance created
+        # overwrites the (if any) thread currently registered in _active.
+        #
+        # An instance of _MainThread is always created by 'threading'.  This
+        # gets overwritten the instant an instance of Thread is created; both
+        # threads return -1 from dummy_thread.get_ident() and thus have the
+        # same key in the dict.  So when the _MainThread instance created by
+        # 'threading' tries to clean itself up when atexit calls this method
+        # it gets a KeyError if another Thread instance was created.
+        #
+        # This all means that KeyError from trying to delete something from
+        # _active if dummy_threading is being used is a red herring.  But
+        # since it isn't if dummy_threading is *not* being used then don't
+        # hide the exception.
+
+        _active_limbo_lock.acquire()
+        try:
+            try:
+                del _active[_get_ident()]
+            except KeyError:
+                if 'dummy_threading' not in _sys.modules:
+                    raise
+        finally:
+            _active_limbo_lock.release()
+
+    def join(self, timeout=None):
+        assert self.__initialized, "Thread.__init__() not called"
+        assert self.__started, "cannot join thread before it is started"
+        assert self is not currentThread(), "cannot join current thread"
+        if __debug__:
+            if not self.__stopped:
+                self._note("%s.join(): waiting until thread stops", self)
+        self.__block.acquire()
+        try:
+            if timeout is None:
+                while not self.__stopped:
+                    self.__block.wait()
+                if __debug__:
+                    self._note("%s.join(): thread stopped", self)
+            else:
+                deadline = _time() + timeout
+                while not self.__stopped:
+                    delay = deadline - _time()
+                    if delay <= 0:
+                        if __debug__:
+                            self._note("%s.join(): timed out", self)
+                        break
+                    self.__block.wait(delay)
+                else:
+                    if __debug__:
+                        self._note("%s.join(): thread stopped", self)
+        finally:
+            self.__block.release()
+
+    def getName(self):
+        assert self.__initialized, "Thread.__init__() not called"
+        return self.__name
+
+    def setName(self, name):
+        assert self.__initialized, "Thread.__init__() not called"
+        self.__name = str(name)
+
+    def isAlive(self):
+        assert self.__initialized, "Thread.__init__() not called"
+        return self.__started and not self.__stopped
+
+    def isDaemon(self):
+        assert self.__initialized, "Thread.__init__() not called"
+        return self.__daemonic
+
+    def setDaemon(self, daemonic):
+        assert self.__initialized, "Thread.__init__() not called"
+        assert not self.__started, "cannot set daemon status of active thread"
+        self.__daemonic = daemonic
+
+# The timer class was contributed by Itamar Shtull-Trauring
+
+def Timer(*args, **kwargs):
+    return _Timer(*args, **kwargs)
+
+class _Timer(Thread):
+    """Call a function after a specified number of seconds:
+
+    t = Timer(30.0, f, args=[], kwargs={})
+    t.start()
+    t.cancel() # stop the timer's action if it's still waiting
+    """
+
+    def __init__(self, interval, function, args=[], kwargs={}):
+        Thread.__init__(self)
+        self.interval = interval
+        self.function = function
+        self.args = args
+        self.kwargs = kwargs
+        self.finished = Event()
+
+    def cancel(self):
+        """Stop the timer if it hasn't finished yet"""
+        self.finished.set()
+
+    def run(self):
+        self.finished.wait(self.interval)
+        if not self.finished.isSet():
+            self.function(*self.args, **self.kwargs)
+        self.finished.set()
+
+# Special thread class to represent the main thread
+# This is garbage collected through an exit handler
+
+class _MainThread(Thread):
+
+    def __init__(self):
+        Thread.__init__(self, name="MainThread")
+        self._Thread__started = True
+        _active_limbo_lock.acquire()
+        _active[_get_ident()] = self
+        _active_limbo_lock.release()
+        import atexit
+        atexit.register(self.__exitfunc)
+
+    def _set_daemon(self):
+        return False
+
+    def __exitfunc(self):
+        self._Thread__stop()
+        t = _pickSomeNonDaemonThread()
+        if t:
+            if __debug__:
+                self._note("%s: waiting for other threads", self)
+        while t:
+            t.join()
+            t = _pickSomeNonDaemonThread()
+        if __debug__:
+            self._note("%s: exiting", self)
+        self._Thread__delete()
+
+def _pickSomeNonDaemonThread():
+    for t in enumerate():
+        if not t.isDaemon() and t.isAlive():
+            return t
+    return None
+
+
+# Dummy thread class to represent threads not started here.
+# These aren't garbage collected when they die,
+# nor can they be waited for.
+# Their purpose is to return *something* from currentThread().
+# They are marked as daemon threads so we won't wait for them
+# when we exit (conform previous semantics).
+
+class _DummyThread(Thread):
+
+    def __init__(self):
+        Thread.__init__(self, name=_newname("Dummy-%d"))
+        self._Thread__started = True
+        _active_limbo_lock.acquire()
+        _active[_get_ident()] = self
+        _active_limbo_lock.release()
+
+    def _set_daemon(self):
+        return True
+
+    def join(self, timeout=None):
+        assert False, "cannot join a dummy thread"
+
+
+# Global API functions
+
+def currentThread():
+    try:
+        return _active[_get_ident()]
+    except KeyError:
+        ##print "currentThread(): no current thread for", _get_ident()
+        return _DummyThread()
+
+def activeCount():
+    _active_limbo_lock.acquire()
+    count = len(_active) + len(_limbo)
+    _active_limbo_lock.release()
+    return count
+
+def enumerate():
+    _active_limbo_lock.acquire()
+    active = _active.values() + _limbo.values()
+    _active_limbo_lock.release()
+    return active
+
+# Create the main thread object
+
+_MainThread()
+
+# get thread-local implementation, either from the thread
+# module, or from the python fallback
+
+try:
+    from thread import _local as local
+except ImportError:
+    from _threading_local import local
+
+
+# Self-test code
+
+def _test():
+
+    class BoundedQueue(_Verbose):
+
+        def __init__(self, limit):
+            _Verbose.__init__(self)
+            self.mon = RLock()
+            self.rc = Condition(self.mon)
+            self.wc = Condition(self.mon)
+            self.limit = limit
+            self.queue = deque()
+
+        def put(self, item):
+            self.mon.acquire()
+            while len(self.queue) >= self.limit:
+                self._note("put(%s): queue full", item)
+                self.wc.wait()
+            self.queue.append(item)
+            self._note("put(%s): appended, length now %d",
+                       item, len(self.queue))
+            self.rc.notify()
+            self.mon.release()
+
+        def get(self):
+            self.mon.acquire()
+            while not self.queue:
+                self._note("get(): queue empty")
+                self.rc.wait()
+            item = self.queue.popleft()
+            self._note("get(): got %s, %d left", item, len(self.queue))
+            self.wc.notify()
+            self.mon.release()
+            return item
+
+    class ProducerThread(Thread):
+
+        def __init__(self, queue, quota):
+            Thread.__init__(self, name="Producer")
+            self.queue = queue
+            self.quota = quota
+
+        def run(self):
+            from random import random
+            counter = 0
+            while counter < self.quota:
+                counter = counter + 1
+                self.queue.put("%s.%d" % (self.getName(), counter))
+                _sleep(random() * 0.00001)
+
+
+    class ConsumerThread(Thread):
+
+        def __init__(self, queue, count):
+            Thread.__init__(self, name="Consumer")
+            self.queue = queue
+            self.count = count
+
+        def run(self):
+            while self.count > 0:
+                item = self.queue.get()
+                print item
+                self.count = self.count - 1
+
+    NP = 3
+    QL = 4
+    NI = 5
+
+    Q = BoundedQueue(QL)
+    P = []
+    for i in range(NP):
+        t = ProducerThread(Q, NI)
+        t.setName("Producer-%d" % (i+1))
+        P.append(t)
+    C = ConsumerThread(Q, NI*NP)
+    for t in P:
+        t.start()
+        _sleep(0.000001)
+    C.start()
+    for t in P:
+        t.join()
+    C.join()
+
+if __name__ == '__main__':
+    _test()
diff --git a/gnuradio-runtime/python/gnuradio/gr/hier_block2.py b/gnuradio-runtime/python/gnuradio/gr/hier_block2.py
new file mode 100644
index 0000000000..31e4065a25
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/hier_block2.py
@@ -0,0 +1,132 @@
+#
+# Copyright 2006,2007 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+from runtime_swig import hier_block2_swig
+
+try:
+    import pmt
+except ImportError:
+    from gruel import pmt
+
+#
+# This hack forces a 'has-a' relationship to look like an 'is-a' one.
+#
+# It allows Python classes to subclass this one, while passing through
+# method calls to the C++ class shared pointer from SWIG.
+#
+# It also allows us to intercept method calls if needed
+#
+class hier_block2(object):
+    """
+    Subclass this to create a python hierarchical block.
+
+    This is a python wrapper around the C++ hierarchical block implementation.
+    Provides convenience functions and allows proper Python subclassing.
+    """
+
+    def __init__(self, name, input_signature, output_signature):
+        """
+        Create a hierarchical block with a given name and I/O signatures.
+        """
+	self._hb = hier_block2_swig(name, input_signature, output_signature)
+
+    def __getattr__(self, name):
+        """
+        Pass-through member requests to the C++ object.
+        """
+        if not hasattr(self, "_hb"):
+            raise RuntimeError("hier_block2: invalid state--did you forget to call gr.hier_block2.__init__ in a derived class?")
+	return getattr(self._hb, name)
+
+    def connect(self, *points):
+        """
+        Connect two or more block endpoints.  An endpoint is either a (block, port)
+        tuple or a block instance.  In the latter case, the port number is assumed
+        to be zero.
+
+        To connect the hierarchical block external inputs or outputs to internal block
+        inputs or outputs, use 'self' in the connect call.
+
+        If multiple arguments are provided, connect will attempt to wire them in series,
+        interpreting the endpoints as inputs or outputs as appropriate.
+        """
+
+        if len (points) < 1:
+            raise ValueError, ("connect requires at least one endpoint; %d provided." % (len (points),))
+	else:
+	    if len(points) == 1:
+		self._hb.primitive_connect(points[0].to_basic_block())
+	    else:
+		for i in range (1, len (points)):
+        	    self._connect(points[i-1], points[i])
+
+    def _connect(self, src, dst):
+        (src_block, src_port) = self._coerce_endpoint(src)
+        (dst_block, dst_port) = self._coerce_endpoint(dst)
+        self._hb.primitive_connect(src_block.to_basic_block(), src_port,
+                                   dst_block.to_basic_block(), dst_port)
+
+    def _coerce_endpoint(self, endp):
+        if hasattr(endp, 'to_basic_block'):
+            return (endp, 0)
+        else:
+            if hasattr(endp, "__getitem__") and len(endp) == 2:
+                return endp # Assume user put (block, port)
+            else:
+                raise ValueError("unable to coerce endpoint")
+
+    def disconnect(self, *points):
+        """
+        Disconnect two endpoints in the flowgraph.
+
+        To disconnect the hierarchical block external inputs or outputs to internal block
+        inputs or outputs, use 'self' in the connect call.
+
+        If more than two arguments are provided, they are disconnected successively.
+        """
+
+        if len (points) < 1:
+            raise ValueError, ("disconnect requires at least one endpoint; %d provided." % (len (points),))
+        else:
+            if len (points) == 1:
+                self._hb.primitive_disconnect(points[0].to_basic_block())
+            else:
+                for i in range (1, len (points)):
+                    self._disconnect(points[i-1], points[i])
+
+    def _disconnect(self, src, dst):
+        (src_block, src_port) = self._coerce_endpoint(src)
+        (dst_block, dst_port) = self._coerce_endpoint(dst)
+        self._hb.primitive_disconnect(src_block.to_basic_block(), src_port,
+                                      dst_block.to_basic_block(), dst_port)
+
+    def msg_connect(self, src, srcport, dst, dstport):
+        self.primitive_msg_connect(src.to_basic_block(), srcport, dst.to_basic_block(), dstport);
+
+    def msg_disconnect(self, src, srcport, dst, dstport):
+        self.primitive_msg_disconnect(src.to_basic_block(), srcport, dst.to_basic_block(), dstport);
+
+    def message_port_register_hier_in(self, portname):
+        self.primitive_message_port_register_hier_in(pmt.intern(portname));
+
+    def message_port_register_hier_out(self, portname):
+        self.primitive_message_port_register_hier_out(pmt.intern(portname));
+
diff --git a/gnuradio-runtime/python/gnuradio/gr/prefs.py b/gnuradio-runtime/python/gnuradio/gr/prefs.py
new file mode 100644
index 0000000000..25fa8cd6ae
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/prefs.py
@@ -0,0 +1,127 @@
+#
+# Copyright 2006,2009 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+import gnuradio_core as gsp
+_prefs_base = gsp.gr_prefs
+
+
+import ConfigParser
+import os
+import os.path
+import sys
+import glob
+
+
+def _user_prefs_filename():
+    return os.path.expanduser('~/.gnuradio/config.conf')
+
+def _sys_prefs_dirname():
+    return gsp.prefsdir()
+
+def _bool(x):
+    """
+    Try to coerce obj to a True or False
+    """
+    if isinstance(x, bool):
+        return x
+    if isinstance(x, (float, int)):
+        return bool(x)
+    raise TypeError, x
+
+
+class _prefs(_prefs_base):
+    """
+    Derive our 'real class' from the stubbed out base class that has support
+    for SWIG directors.  This allows C++ code to magically and transparently
+    invoke the methods in this python class.
+    """
+    def __init__(self):
+        _prefs_base.__init__(self)
+        self.cp = ConfigParser.RawConfigParser()
+        self.__getattr__ = lambda self, name: getattr(self.cp, name)
+
+    def _sys_prefs_filenames(self):
+        dir = _sys_prefs_dirname()
+        try:
+            fnames = glob.glob(os.path.join(dir, '*.conf'))
+        except (IOError, OSError):
+            return []
+        fnames.sort()
+        return fnames
+
+    def _read_files(self):
+        filenames = self._sys_prefs_filenames()
+        filenames.append(_user_prefs_filename())
+        #print "filenames: ", filenames
+        self.cp.read(filenames)
+
+    # ----------------------------------------------------------------
+    # These methods override the C++ virtual methods of the same name
+    # ----------------------------------------------------------------
+    def has_section(self, section):
+        return self.cp.has_section(section)
+
+    def has_option(self, section, option):
+        return self.cp.has_option(section, option)
+
+    def get_string(self, section, option, default_val):
+        try:
+            return self.cp.get(section, option)
+        except:
+            return default_val
+
+    def get_bool(self, section, option, default_val):
+        try:
+            return self.cp.getboolean(section, option)
+        except:
+            return default_val
+
+    def get_long(self, section, option, default_val):
+        try:
+            return self.cp.getint(section, option)
+        except:
+            return default_val
+
+    def get_double(self, section, option, default_val):
+        try:
+            return self.cp.getfloat(section, option)
+        except:
+            return default_val
+    # ----------------------------------------------------------------
+    #              End override of C++ virtual methods
+    # ----------------------------------------------------------------
+
+
+_prefs_db = _prefs()
+
+# if GR_DONT_LOAD_PREFS is set, don't load them.
+# (make check uses this to avoid interactions.)
+if os.getenv("GR_DONT_LOAD_PREFS", None) is None:
+    _prefs_db._read_files()
+
+
+_prefs_base.set_singleton(_prefs_db)    # tell C++ what instance to use
+
+def prefs():
+    """
+    Return the global preference data base
+    """
+    return _prefs_db
diff --git a/gnuradio-runtime/python/gnuradio/gr/pubsub.py b/gnuradio-runtime/python/gnuradio/gr/pubsub.py
new file mode 100644
index 0000000000..90568418fc
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/pubsub.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python
+#
+# Copyright 2008,2009 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+"""
+Abstract GNU Radio publisher/subscriber interface
+
+This is a proof of concept implementation, will likely change significantly.
+"""
+
+class pubsub(dict):
+    def __init__(self):
+	self._publishers = { }
+	self._subscribers = { }
+	self._proxies = { }
+
+    def __missing__(self, key, value=None):
+	dict.__setitem__(self, key, value)
+	self._publishers[key] = None
+	self._subscribers[key] = []
+	self._proxies[key] = None
+
+    def __setitem__(self, key, val):
+	if not self.has_key(key):
+	    self.__missing__(key, val)
+	elif self._proxies[key] is not None:
+	    (p, newkey) = self._proxies[key]
+	    p[newkey] = val
+	else:
+	    dict.__setitem__(self, key, val)
+	for sub in self._subscribers[key]:
+	    # Note this means subscribers will get called in the thread
+	    # context of the 'set' caller.
+	    sub(val)
+
+    def __getitem__(self, key):
+	if not self.has_key(key): self.__missing__(key)
+	if self._proxies[key] is not None:
+	    (p, newkey) = self._proxies[key]
+	    return p[newkey]
+	elif self._publishers[key] is not None:
+	    return self._publishers[key]()
+	else:
+	    return dict.__getitem__(self, key)
+
+    def publish(self, key, publisher):
+	if not self.has_key(key): self.__missing__(key)
+        if self._proxies[key] is not None:
+            (p, newkey) = self._proxies[key]
+            p.publish(newkey, publisher)
+        else:
+            self._publishers[key] = publisher
+
+    def subscribe(self, key, subscriber):
+	if not self.has_key(key): self.__missing__(key)
+        if self._proxies[key] is not None:
+            (p, newkey) = self._proxies[key]
+            p.subscribe(newkey, subscriber)
+        else:
+            self._subscribers[key].append(subscriber)
+
+    def unpublish(self, key):
+        if self._proxies[key] is not None:
+            (p, newkey) = self._proxies[key]
+            p.unpublish(newkey)
+        else:
+            self._publishers[key] = None
+
+    def unsubscribe(self, key, subscriber):
+        if self._proxies[key] is not None:
+            (p, newkey) = self._proxies[key]
+            p.unsubscribe(newkey, subscriber)
+        else:
+            self._subscribers[key].remove(subscriber)
+
+    def proxy(self, key, p, newkey=None):
+	if not self.has_key(key): self.__missing__(key)
+	if newkey is None: newkey = key
+	self._proxies[key] = (p, newkey)
+
+    def unproxy(self, key):
+        self._proxies[key] = None
+
+# Test code
+if __name__ == "__main__":
+    import sys
+    o = pubsub()
+
+    # Non-existent key gets auto-created with None value
+    print "Auto-created key 'foo' value:", o['foo']
+
+    # Add some subscribers
+    # First is a bare function
+    def print_len(x):
+	print "len=%i" % (len(x), )
+    o.subscribe('foo', print_len)
+
+    # The second is a class member function
+    class subber(object):
+	def __init__(self, param):
+	    self._param = param
+	def printer(self, x):
+	    print self._param, `x`
+    s = subber('param')
+    o.subscribe('foo', s.printer)
+
+    # The third is a lambda function
+    o.subscribe('foo', lambda x: sys.stdout.write('val='+`x`+'\n'))
+
+    # Update key 'foo', will notify subscribers
+    print "Updating 'foo' with three subscribers:"
+    o['foo'] = 'bar';
+
+    # Remove first subscriber
+    o.unsubscribe('foo', print_len)
+
+    # Update now will only trigger second and third subscriber
+    print "Updating 'foo' after removing a subscriber:"
+    o['foo'] = 'bar2';
+
+    # Publish a key as a function, in this case, a lambda function
+    o.publish('baz', lambda : 42)
+    print "Published value of 'baz':", o['baz']
+
+    # Unpublish the key
+    o.unpublish('baz')
+
+    # This will return None, as there is no publisher
+    print "Value of 'baz' with no publisher:", o['baz']
+
+    # Set 'baz' key, it gets cached
+    o['baz'] = 'bazzz'
+
+    # Now will return cached value, since no provider
+    print "Cached value of 'baz' after being set:", o['baz']
diff --git a/gnuradio-runtime/python/gnuradio/gr/qa_feval.py b/gnuradio-runtime/python/gnuradio/gr/qa_feval.py
new file mode 100755
index 0000000000..9018e12f36
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/qa_feval.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+#
+# Copyright 2006,2007,2010 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+from gnuradio import gr, gr_unittest
+
+class my_add2_dd(gr.feval_dd):
+    def eval(self, x):
+        return x + 2
+
+class my_add2_ll(gr.feval_ll):
+    def eval(self, x):
+        return x + 2
+
+class my_add2_cc(gr.feval_cc):
+    def eval(self, x):
+        return x + (2 - 2j)
+
+class my_feval(gr.feval):
+    def __init__(self):
+        gr.feval.__init__(self)
+        self.fired = False
+    def eval(self):
+        self.fired = True
+
+class test_feval(gr_unittest.TestCase):
+
+    def test_dd_1(self):
+        f = my_add2_dd()
+        src_data =        (0.0, 1.0, 2.0, 3.0, 4.0)
+        expected_result = (2.0, 3.0, 4.0, 5.0, 6.0)
+        # this is all in python...
+        actual_result = tuple([f.eval(x) for x in src_data])
+        self.assertEqual(expected_result, actual_result)
+
+    def test_dd_2(self):
+        f = my_add2_dd()
+        src_data =        (0.0, 1.0, 2.0, 3.0, 4.0)
+        expected_result = (2.0, 3.0, 4.0, 5.0, 6.0)
+        # this is python -> C++ -> python and back again...
+        actual_result = tuple([gr.feval_dd_example(f, x) for x in src_data])
+        self.assertEqual(expected_result, actual_result)
+
+
+    def test_ll_1(self):
+        f = my_add2_ll()
+        src_data =        (0, 1, 2, 3, 4)
+        expected_result = (2, 3, 4, 5, 6)
+        # this is all in python...
+        actual_result = tuple([f.eval(x) for x in src_data])
+        self.assertEqual(expected_result, actual_result)
+
+    def test_ll_2(self):
+        f = my_add2_ll()
+        src_data =        (0, 1, 2, 3, 4)
+        expected_result = (2, 3, 4, 5, 6)
+        # this is python -> C++ -> python and back again...
+        actual_result = tuple([gr.feval_ll_example(f, x) for x in src_data])
+        self.assertEqual(expected_result, actual_result)
+
+
+    def test_cc_1(self):
+        f = my_add2_cc()
+        src_data =        (0+1j, 2+3j, 4+5j, 6+7j)
+        expected_result = (2-1j, 4+1j, 6+3j, 8+5j)
+        # this is all in python...
+        actual_result = tuple([f.eval(x) for x in src_data])
+        self.assertEqual(expected_result, actual_result)
+
+    def test_cc_2(self):
+        f = my_add2_cc()
+        src_data =        (0+1j, 2+3j, 4+5j, 6+7j)
+        expected_result = (2-1j, 4+1j, 6+3j, 8+5j)
+        # this is python -> C++ -> python and back again...
+        actual_result = tuple([gr.feval_cc_example(f, x) for x in src_data])
+        self.assertEqual(expected_result, actual_result)
+
+    def test_void_1(self):
+        # this is all in python
+        f = my_feval()
+        f.eval()
+        self.assertEqual(True, f.fired)
+
+    def test_void_2(self):
+        # this is python -> C++ -> python and back again
+        f = my_feval()
+        gr.feval_example(f)
+        self.assertEqual(True, f.fired)
+
+
+if __name__ == '__main__':
+    gr_unittest.run(test_feval, "test_feval.xml")
diff --git a/gnuradio-runtime/python/gnuradio/gr/qa_kludged_imports.py b/gnuradio-runtime/python/gnuradio/gr/qa_kludged_imports.py
new file mode 100755
index 0000000000..f80188c9fc
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/qa_kludged_imports.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+#
+# Copyright 2005,2008,2010 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+from gnuradio import gr, gr_unittest
+
+class test_kludged_imports (gr_unittest.TestCase):
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_gru_import(self):
+        # make sure that this somewhat magic import works
+        from gnuradio import gru
+
+
+if __name__ == '__main__':
+    gr_unittest.run(test_kludged_imports, "test_kludged_imports.xml")
diff --git a/gnuradio-runtime/python/gnuradio/gr/qa_tag_utils.py b/gnuradio-runtime/python/gnuradio/gr/qa_tag_utils.py
new file mode 100755
index 0000000000..de1b5aa002
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/qa_tag_utils.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+#
+# Copyright 2007,2010 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+from gnuradio import gr, gr_unittest
+import tag_utils
+
+try:
+    import pmt_swig as pmt
+except ImportError:
+    import pmt
+
+class test_tag_utils (gr_unittest.TestCase):
+
+    def setUp (self):
+        self.tb = gr.top_block ()
+
+
+    def tearDown (self):
+        self.tb = None
+
+    def test_001(self):
+        t = gr.gr_tag_t()
+        t.offset = 10
+        t.key = pmt.string_to_symbol('key')
+        t.value = pmt.from_long(23)
+        t.srcid = pmt.from_bool(False)
+        pt = tag_utils.tag_to_python(t)
+        self.assertEqual(pt.key, 'key')
+        self.assertEqual(pt.value, 23)
+        self.assertEqual(pt.offset, 10)
+
+
+if __name__ == '__main__':
+    print 'hi'
+    gr_unittest.run(test_tag_utils, "test_tag_utils.xml")
+
diff --git a/gnuradio-runtime/python/gnuradio/gr/tag_utils.py b/gnuradio-runtime/python/gnuradio/gr/tag_utils.py
new file mode 100644
index 0000000000..1c9594d6d0
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/tag_utils.py
@@ -0,0 +1,57 @@
+#
+# Copyright 2003-2012 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+""" Conversion tools between stream tags and Python objects """
+
+try: import pmt
+except: from gruel import pmt
+
+try:
+    from gnuradio import gr
+except ImportError:
+    from runtime_swig import gr_tag_t
+
+class PythonTag(object):
+    " Python container for tags "
+    def __init__(self):
+        self.offset = None
+        self.key    = None
+        self.value  = None
+        self.srcid  = None
+
+def tag_to_python(tag):
+    """ Convert a stream tag to a Python-readable object """
+    newtag = PythonTag()
+    newtag.offset = tag.offset
+    newtag.key = pmt.to_python(tag.key)
+    newtag.value = pmt.to_python(tag.value)
+    newtag.srcid = pmt.to_python(tag.srcid)
+    return newtag
+
+def tag_to_pmt(tag):
+    """ Convert a Python-readable object to a stream tag """
+    newtag = gr_tag_t()
+    newtag.offset = tag.offset
+    newtag.key = pmt.to_python(tag.key)
+    newtag.value = pmt.from_python(tag.value)
+    newtag.srcid = pmt.from_python(tag.srcid)
+    return newtag
+
+
diff --git a/gnuradio-runtime/python/gnuradio/gr/top_block.py b/gnuradio-runtime/python/gnuradio/gr/top_block.py
new file mode 100644
index 0000000000..944e95e5ae
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr/top_block.py
@@ -0,0 +1,170 @@
+#
+# Copyright 2007 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+from runtime_swig import top_block_swig, \
+    top_block_wait_unlocked, top_block_run_unlocked
+
+#import gnuradio.gr.gr_threading as _threading
+import gr_threading as _threading
+
+#
+# There is no problem that can't be solved with an additional
+# level of indirection...
+#
+# This kludge allows ^C to interrupt top_block.run and top_block.wait
+#
+# The problem that we are working around is that Python only services
+# signals (e.g., KeyboardInterrupt) in its main thread.  If the main
+# thread is blocked in our C++ version of wait, even though Python's
+# SIGINT handler fires, and even though there may be other python
+# threads running, no one will know.  Thus instead of directly waiting
+# in the thread that calls wait (which is likely to be the Python main
+# thread), we create a separate thread that does the blocking wait,
+# and then use the thread that called wait to do a slow poll of an
+# event queue.  That thread, which is executing "wait" below is
+# interruptable, and if it sees a KeyboardInterrupt, executes a stop
+# on the top_block, then goes back to waiting for it to complete.
+# This ensures that the unlocked wait that was in progress (in the
+# _top_block_waiter thread) can complete, release its mutex and back
+# out.  If we don't do that, we are never able to clean up, and nasty
+# things occur like leaving the USRP transmitter sending a carrier.
+#
+# See also top_block.wait (below), which uses this class to implement
+# the interruptable wait.
+#
+class _top_block_waiter(_threading.Thread):
+    def __init__(self, tb):
+        _threading.Thread.__init__(self)
+        self.setDaemon(1)
+        self.tb = tb
+        self.event = _threading.Event()
+        self.start()
+
+    def run(self):
+        top_block_wait_unlocked(self.tb)
+        self.event.set()
+
+    def wait(self):
+        try:
+            while not self.event.isSet():
+                self.event.wait(0.100)
+        except KeyboardInterrupt:
+            self.tb.stop()
+            self.wait()
+
+
+#
+# This hack forces a 'has-a' relationship to look like an 'is-a' one.
+#
+# It allows Python classes to subclass this one, while passing through
+# method calls to the C++ class shared pointer from SWIG.
+#
+# It also allows us to intercept method calls if needed.
+#
+# This allows the 'run_locked' methods, which are defined in gr_top_block.i,
+# to release the Python global interpreter lock before calling the actual
+# method in gr_top_block
+#
+class top_block(object):
+    """
+    Top-level hierarchical block representing a flow-graph.
+
+    This is a python wrapper around the C++ implementation to allow
+    python subclassing.
+    """
+    def __init__(self, name="top_block"):
+	self._tb = top_block_swig(name)
+
+    def __getattr__(self, name):
+        if not hasattr(self, "_tb"):
+            raise RuntimeError("top_block: invalid state--did you forget to call gr.top_block.__init__ in a derived class?")
+	return getattr(self._tb, name)
+
+    def start(self, max_noutput_items=10000000):
+    	self._tb.start(max_noutput_items)
+
+    def stop(self):
+    	self._tb.stop()
+
+    def run(self, max_noutput_items=10000000):
+        self.start(max_noutput_items)
+        self.wait()
+
+    def wait(self):
+        _top_block_waiter(self._tb).wait()
+
+
+    # FIXME: these are duplicated from hier_block2.py; they should really be implemented
+    # in the original C++ class (gr_hier_block2), then they would all be inherited here
+
+    def connect(self, *points):
+        '''connect requires one or more arguments that can be coerced to endpoints.
+        If more than two arguments are provided, they are connected together successively.
+        '''
+        if len (points) < 1:
+            raise ValueError, ("connect requires at least one endpoint; %d provided." % (len (points),))
+	else:
+	    if len(points) == 1:
+		self._tb.primitive_connect(points[0].to_basic_block())
+	    else:
+		for i in range (1, len (points)):
+        	    self._connect(points[i-1], points[i])
+
+    def msg_connect(self, src, srcport, dst, dstport):
+        self.primitive_msg_connect(src.to_basic_block(), srcport, dst.to_basic_block(), dstport);
+    
+    def msg_disconnect(self, src, srcport, dst, dstport):
+        self.primitive_msg_disconnect(src.to_basic_block(), srcport, dst.to_basic_block(), dstport);
+
+    def _connect(self, src, dst):
+        (src_block, src_port) = self._coerce_endpoint(src)
+        (dst_block, dst_port) = self._coerce_endpoint(dst)
+        self._tb.primitive_connect(src_block.to_basic_block(), src_port,
+                                   dst_block.to_basic_block(), dst_port)
+
+    def _coerce_endpoint(self, endp):
+        if hasattr(endp, 'to_basic_block'):
+            return (endp, 0)
+        else:
+            if hasattr(endp, "__getitem__") and len(endp) == 2:
+                return endp # Assume user put (block, port)
+            else:
+                raise ValueError("unable to coerce endpoint")
+
+    def disconnect(self, *points):
+        '''disconnect requires one or more arguments that can be coerced to endpoints.
+        If more than two arguments are provided, they are disconnected successively.
+        '''
+        if len (points) < 1:
+            raise ValueError, ("disconnect requires at least one endpoint; %d provided." % (len (points),))
+        else:
+            if len(points) == 1:
+                self._tb.primitive_disconnect(points[0].to_basic_block())
+            else:
+                for i in range (1, len (points)):
+                    self._disconnect(points[i-1], points[i])
+
+    def _disconnect(self, src, dst):
+        (src_block, src_port) = self._coerce_endpoint(src)
+        (dst_block, dst_port) = self._coerce_endpoint(dst)
+        self._tb.primitive_disconnect(src_block.to_basic_block(), src_port,
+                                      dst_block.to_basic_block(), dst_port)
+
diff --git a/gnuradio-runtime/python/gnuradio/gr_unittest.py b/gnuradio-runtime/python/gnuradio/gr_unittest.py
new file mode 100755
index 0000000000..c729566e88
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr_unittest.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+#
+# Copyright 2004,2010 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+"""
+GNU radio specific extension of unittest.
+"""
+
+import unittest
+import gr_xmlrunner
+import sys, os, stat
+
+class TestCase(unittest.TestCase):
+    """A subclass of unittest.TestCase that adds additional assertions
+
+    Adds new methods assertComplexAlmostEqual,
+    assertComplexTuplesAlmostEqual and assertFloatTuplesAlmostEqual
+    """
+
+    def assertComplexAlmostEqual (self, first, second, places=7, msg=None):
+        """Fail if the two complex objects are unequal as determined by their
+           difference rounded to the given number of decimal places
+           (default 7) and comparing to zero.
+
+           Note that decimal places (from zero) is usually not the same
+           as significant digits (measured from the most signficant digit).
+       """
+        if round(second.real-first.real, places) != 0:
+            raise self.failureException, \
+                  (msg or '%s != %s within %s places' % (`first`, `second`, `places` ))
+        if round(second.imag-first.imag, places) != 0:
+            raise self.failureException, \
+                  (msg or '%s != %s within %s places' % (`first`, `second`, `places` ))
+
+    def assertComplexAlmostEqual2 (self, ref, x, abs_eps=1e-12, rel_eps=1e-6, msg=None):
+        """
+        Fail if the two complex objects are unequal as determined by...
+        """
+        if abs(ref - x) < abs_eps:
+            return
+
+        if abs(ref) > abs_eps:
+            if abs(ref-x)/abs(ref) > rel_eps:
+                raise self.failureException, \
+                      (msg or '%s != %s rel_error = %s rel_limit = %s' % (
+                    `ref`, `x`, abs(ref-x)/abs(ref), `rel_eps` ))
+        else:
+            raise self.failureException, \
+                      (msg or '%s != %s rel_error = %s rel_limit = %s' % (
+                    `ref`, `x`, abs(ref-x)/abs(ref), `rel_eps` ))
+
+
+
+    def assertComplexTuplesAlmostEqual (self, a, b, places=7, msg=None):
+        self.assertEqual (len(a), len(b))
+        for i in xrange (len(a)):
+            self.assertComplexAlmostEqual (a[i], b[i], places, msg)
+
+    def assertComplexTuplesAlmostEqual2 (self, ref, x,
+                                         abs_eps=1e-12, rel_eps=1e-6, msg=None):
+        self.assertEqual (len(ref), len(x))
+        for i in xrange (len(ref)):
+            try:
+                self.assertComplexAlmostEqual2 (ref[i], x[i], abs_eps, rel_eps, msg)
+            except self.failureException, e:
+                #sys.stderr.write("index = %d " % (i,))
+                #sys.stderr.write("%s\n" % (e,))
+                raise
+
+    def assertFloatTuplesAlmostEqual (self, a, b, places=7, msg=None):
+        self.assertEqual (len(a), len(b))
+        for i in xrange (len(a)):
+            self.assertAlmostEqual (a[i], b[i], places, msg)
+
+
+    def assertFloatTuplesAlmostEqual2 (self, ref, x,
+                                       abs_eps=1e-12, rel_eps=1e-6, msg=None):
+        self.assertEqual (len(ref), len(x))
+        for i in xrange (len(ref)):
+            try:
+                self.assertComplexAlmostEqual2 (ref[i], x[i], abs_eps, rel_eps, msg)
+            except self.failureException, e:
+                #sys.stderr.write("index = %d " % (i,))
+                #sys.stderr.write("%s\n" % (e,))
+                raise
+
+
+TestResult = unittest.TestResult
+TestSuite = unittest.TestSuite
+FunctionTestCase = unittest.FunctionTestCase
+TestLoader = unittest.TestLoader
+TextTestRunner = unittest.TextTestRunner
+TestProgram = unittest.TestProgram
+main = TestProgram
+
+def run(PUT, filename=None):
+    '''
+    Runs the unittest on a TestCase and produces an optional XML report
+    PUT:      the program under test and should be a gr_unittest.TestCase
+    filename: an optional filename to save the XML report of the tests
+              this will live in ./.unittests/python
+    '''
+
+    # Run this is given a file name
+    if(filename is not None):
+        basepath = "./.unittests"
+        path = basepath + "/python"
+
+        if not os.path.exists(basepath):
+            os.makedirs(basepath, 0750)
+
+        xmlrunner = None
+        # only proceed if .unittests is writable
+        st = os.stat(basepath)[stat.ST_MODE]
+        if(st & stat.S_IWUSR > 0):
+            # Test if path exists; if not, build it
+            if not os.path.exists(path):
+                os.makedirs(path, 0750)
+
+            # Just for safety: make sure we can write here, too
+            st = os.stat(path)[stat.ST_MODE]
+            if(st & stat.S_IWUSR > 0):
+                # Create an XML runner to filename
+                fout = file(path+"/"+filename, "w")
+                xmlrunner = gr_xmlrunner.XMLTestRunner(fout)
+
+        txtrunner = TextTestRunner(verbosity=1)
+
+        # Run the test; runner also creates XML output file
+        # FIXME: make xmlrunner output to screen so we don't have to do run and main
+        suite = TestLoader().loadTestsFromTestCase(PUT)
+
+        # use the xmlrunner if we can write the the directory
+        if(xmlrunner is not None):
+            xmlrunner.run(suite)
+
+        main()
+
+        # This will run and fail make check if problem
+        # but does not output to screen.
+        #main(testRunner = xmlrunner)
+
+    else:
+        # If no filename is given, just run the test
+        main()
+
+
+##############################################################################
+# Executing this module from the command line
+##############################################################################
+
+if __name__ == "__main__":
+    main(module=None)
diff --git a/gnuradio-runtime/python/gnuradio/gr_xmlrunner.py b/gnuradio-runtime/python/gnuradio/gr_xmlrunner.py
new file mode 100644
index 0000000000..31298197ff
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gr_xmlrunner.py
@@ -0,0 +1,387 @@
+"""
+XML Test Runner for PyUnit
+"""
+
+# Written by Sebastian Rittau <srittau@jroger.in-berlin.de> and placed in
+# the Public Domain. With contributions by Paolo Borelli and others.
+# Added to GNU Radio Oct. 3, 2010
+
+__version__ = "0.1"
+
+import os.path
+import re
+import sys
+import time
+import traceback
+import unittest
+from xml.sax.saxutils import escape
+
+try:
+    from StringIO import StringIO
+except ImportError:
+    from io import StringIO
+
+
+class _TestInfo(object):
+
+    """Information about a particular test.
+
+    Used by _XMLTestResult.
+
+    """
+
+    def __init__(self, test, time):
+        (self._class, self._method) = test.id().rsplit(".", 1)
+        self._time = time
+        self._error = None
+        self._failure = None
+
+    @staticmethod
+    def create_success(test, time):
+        """Create a _TestInfo instance for a successful test."""
+        return _TestInfo(test, time)
+
+    @staticmethod
+    def create_failure(test, time, failure):
+        """Create a _TestInfo instance for a failed test."""
+        info = _TestInfo(test, time)
+        info._failure = failure
+        return info
+
+    @staticmethod
+    def create_error(test, time, error):
+        """Create a _TestInfo instance for an erroneous test."""
+        info = _TestInfo(test, time)
+        info._error = error
+        return info
+
+    def print_report(self, stream):
+        """Print information about this test case in XML format to the
+        supplied stream.
+
+        """
+        stream.write('  <testcase classname="%(class)s" name="%(method)s" time="%(time).4f">' % \
+            {
+                "class": self._class,
+                "method": self._method,
+                "time": self._time,
+            })
+        if self._failure is not None:
+            self._print_error(stream, 'failure', self._failure)
+        if self._error is not None:
+            self._print_error(stream, 'error', self._error)
+        stream.write('</testcase>\n')
+
+    def _print_error(self, stream, tagname, error):
+        """Print information from a failure or error to the supplied stream."""
+        text = escape(str(error[1]))
+        stream.write('\n')
+        stream.write('    <%s type="%s">%s\n' \
+            % (tagname, _clsname(error[0]), text))
+        tb_stream = StringIO()
+        traceback.print_tb(error[2], None, tb_stream)
+        stream.write(escape(tb_stream.getvalue()))
+        stream.write('    </%s>\n' % tagname)
+        stream.write('  ')
+
+
+def _clsname(cls):
+    return cls.__module__ + "." + cls.__name__
+
+
+class _XMLTestResult(unittest.TestResult):
+
+    """A test result class that stores result as XML.
+
+    Used by XMLTestRunner.
+
+    """
+
+    def __init__(self, classname):
+        unittest.TestResult.__init__(self)
+        self._test_name = classname
+        self._start_time = None
+        self._tests = []
+        self._error = None
+        self._failure = None
+
+    def startTest(self, test):
+        unittest.TestResult.startTest(self, test)
+        self._error = None
+        self._failure = None
+        self._start_time = time.time()
+
+    def stopTest(self, test):
+        time_taken = time.time() - self._start_time
+        unittest.TestResult.stopTest(self, test)
+        if self._error:
+            info = _TestInfo.create_error(test, time_taken, self._error)
+        elif self._failure:
+            info = _TestInfo.create_failure(test, time_taken, self._failure)
+        else:
+            info = _TestInfo.create_success(test, time_taken)
+        self._tests.append(info)
+
+    def addError(self, test, err):
+        unittest.TestResult.addError(self, test, err)
+        self._error = err
+
+    def addFailure(self, test, err):
+        unittest.TestResult.addFailure(self, test, err)
+        self._failure = err
+
+    def print_report(self, stream, time_taken, out, err):
+        """Prints the XML report to the supplied stream.
+
+        The time the tests took to perform as well as the captured standard
+        output and standard error streams must be passed in.a
+
+        """
+        stream.write('<testsuite errors="%(e)d" failures="%(f)d" ' % \
+            { "e": len(self.errors), "f": len(self.failures) })
+        stream.write('name="%(n)s" tests="%(t)d" time="%(time).3f">\n' % \
+            {
+                "n": self._test_name,
+                "t": self.testsRun,
+                "time": time_taken,
+            })
+        for info in self._tests:
+            info.print_report(stream)
+        stream.write('  <system-out><![CDATA[%s]]></system-out>\n' % out)
+        stream.write('  <system-err><![CDATA[%s]]></system-err>\n' % err)
+        stream.write('</testsuite>\n')
+
+
+class XMLTestRunner(object):
+
+    """A test runner that stores results in XML format compatible with JUnit.
+
+    XMLTestRunner(stream=None) -> XML test runner
+
+    The XML file is written to the supplied stream. If stream is None, the
+    results are stored in a file called TEST-<module>.<class>.xml in the
+    current working directory (if not overridden with the path property),
+    where <module> and <class> are the module and class name of the test class.
+
+    """
+
+    def __init__(self, stream=None):
+        self._stream = stream
+        self._path = "."
+
+    def run(self, test):
+        """Run the given test case or test suite."""
+        class_ = test.__class__
+        classname = class_.__module__ + "." + class_.__name__
+        if self._stream == None:
+            filename = "TEST-%s.xml" % classname
+            stream = file(os.path.join(self._path, filename), "w")
+            stream.write('<?xml version="1.0" encoding="utf-8"?>\n')
+        else:
+            stream = self._stream
+
+        result = _XMLTestResult(classname)
+        start_time = time.time()
+
+        fss = _fake_std_streams()
+        fss.__enter__()
+        try:
+            test(result)
+            try:
+                out_s = sys.stdout.getvalue()
+            except AttributeError:
+                out_s = ""
+            try:
+                err_s = sys.stderr.getvalue()
+            except AttributeError:
+                err_s = ""
+        finally:
+            fss.__exit__(None, None, None)
+
+        time_taken = time.time() - start_time
+        result.print_report(stream, time_taken, out_s, err_s)
+        if self._stream is None:
+            stream.close()
+
+        return result
+
+    def _set_path(self, path):
+        self._path = path
+
+    path = property(lambda self: self._path, _set_path, None,
+            """The path where the XML files are stored.
+
+            This property is ignored when the XML file is written to a file
+            stream.""")
+
+
+class _fake_std_streams(object):
+
+    def __enter__(self):
+        self._orig_stdout = sys.stdout
+        self._orig_stderr = sys.stderr
+        #sys.stdout = StringIO()
+        #sys.stderr = StringIO()
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        sys.stdout = self._orig_stdout
+        sys.stderr = self._orig_stderr
+
+
+class XMLTestRunnerTest(unittest.TestCase):
+
+    def setUp(self):
+        self._stream = StringIO()
+
+    def _try_test_run(self, test_class, expected):
+
+        """Run the test suite against the supplied test class and compare the
+        XML result against the expected XML string. Fail if the expected
+        string doesn't match the actual string. All time attributes in the
+        expected string should have the value "0.000". All error and failure
+        messages are reduced to "Foobar".
+
+        """
+
+        runner = XMLTestRunner(self._stream)
+        runner.run(unittest.makeSuite(test_class))
+
+        got = self._stream.getvalue()
+        # Replace all time="X.YYY" attributes by time="0.000" to enable a
+        # simple string comparison.
+        got = re.sub(r'time="\d+\.\d+"', 'time="0.000"', got)
+        # Likewise, replace all failure and error messages by a simple "Foobar"
+        # string.
+        got = re.sub(r'(?s)<failure (.*?)>.*?</failure>', r'<failure \1>Foobar</failure>', got)
+        got = re.sub(r'(?s)<error (.*?)>.*?</error>', r'<error \1>Foobar</error>', got)
+        # And finally Python 3 compatibility.
+        got = got.replace('type="builtins.', 'type="exceptions.')
+
+        self.assertEqual(expected, got)
+
+    def test_no_tests(self):
+        """Regression test: Check whether a test run without any tests
+        matches a previous run.
+
+        """
+        class TestTest(unittest.TestCase):
+            pass
+        self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="0" time="0.000">
+  <system-out><![CDATA[]]></system-out>
+  <system-err><![CDATA[]]></system-err>
+</testsuite>
+""")
+
+    def test_success(self):
+        """Regression test: Check whether a test run with a successful test
+        matches a previous run.
+
+        """
+        class TestTest(unittest.TestCase):
+            def test_foo(self):
+                pass
+        self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
+  <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
+  <system-out><![CDATA[]]></system-out>
+  <system-err><![CDATA[]]></system-err>
+</testsuite>
+""")
+
+    def test_failure(self):
+        """Regression test: Check whether a test run with a failing test
+        matches a previous run.
+
+        """
+        class TestTest(unittest.TestCase):
+            def test_foo(self):
+                self.assert_(False)
+        self._try_test_run(TestTest, """<testsuite errors="0" failures="1" name="unittest.TestSuite" tests="1" time="0.000">
+  <testcase classname="__main__.TestTest" name="test_foo" time="0.000">
+    <failure type="exceptions.AssertionError">Foobar</failure>
+  </testcase>
+  <system-out><![CDATA[]]></system-out>
+  <system-err><![CDATA[]]></system-err>
+</testsuite>
+""")
+
+    def test_error(self):
+        """Regression test: Check whether a test run with a erroneous test
+        matches a previous run.
+
+        """
+        class TestTest(unittest.TestCase):
+            def test_foo(self):
+                raise IndexError()
+        self._try_test_run(TestTest, """<testsuite errors="1" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
+  <testcase classname="__main__.TestTest" name="test_foo" time="0.000">
+    <error type="exceptions.IndexError">Foobar</error>
+  </testcase>
+  <system-out><![CDATA[]]></system-out>
+  <system-err><![CDATA[]]></system-err>
+</testsuite>
+""")
+
+    def test_stdout_capture(self):
+        """Regression test: Check whether a test run with output to stdout
+        matches a previous run.
+
+        """
+        class TestTest(unittest.TestCase):
+            def test_foo(self):
+                sys.stdout.write("Test\n")
+        self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
+  <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
+  <system-out><![CDATA[Test
+]]></system-out>
+  <system-err><![CDATA[]]></system-err>
+</testsuite>
+""")
+
+    def test_stderr_capture(self):
+        """Regression test: Check whether a test run with output to stderr
+        matches a previous run.
+
+        """
+        class TestTest(unittest.TestCase):
+            def test_foo(self):
+                sys.stderr.write("Test\n")
+        self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
+  <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
+  <system-out><![CDATA[]]></system-out>
+  <system-err><![CDATA[Test
+]]></system-err>
+</testsuite>
+""")
+
+    class NullStream(object):
+        """A file-like object that discards everything written to it."""
+        def write(self, buffer):
+            pass
+
+    def test_unittests_changing_stdout(self):
+        """Check whether the XMLTestRunner recovers gracefully from unit tests
+        that change stdout, but don't change it back properly.
+
+        """
+        class TestTest(unittest.TestCase):
+            def test_foo(self):
+                sys.stdout = XMLTestRunnerTest.NullStream()
+
+        runner = XMLTestRunner(self._stream)
+        runner.run(unittest.makeSuite(TestTest))
+
+    def test_unittests_changing_stderr(self):
+        """Check whether the XMLTestRunner recovers gracefully from unit tests
+        that change stderr, but don't change it back properly.
+
+        """
+        class TestTest(unittest.TestCase):
+            def test_foo(self):
+                sys.stderr = XMLTestRunnerTest.NullStream()
+
+        runner = XMLTestRunner(self._stream)
+        runner.run(unittest.makeSuite(TestTest))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/gnuradio-runtime/python/gnuradio/gru/CMakeLists.txt b/gnuradio-runtime/python/gnuradio/gru/CMakeLists.txt
new file mode 100644
index 0000000000..c147981472
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gru/CMakeLists.txt
@@ -0,0 +1,36 @@
+# Copyright 2010-2011 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+
+include(GrPython)
+
+GR_PYTHON_INSTALL(FILES
+    __init__.py
+    freqz.py
+    gnuplot_freqz.py
+    hexint.py
+    listmisc.py
+    mathmisc.py
+    msgq_runner.py
+    os_read_exactly.py
+    seq_with_cursor.py
+    socket_stuff.py
+    daemon.py
+    DESTINATION ${GR_PYTHON_DIR}/gnuradio/gru
+    COMPONENT "core_python"
+)
diff --git a/gnuradio-runtime/python/gnuradio/gru/__init__.py b/gnuradio-runtime/python/gnuradio/gru/__init__.py
new file mode 100644
index 0000000000..4e41d03a74
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gru/__init__.py
@@ -0,0 +1,13 @@
+# make this a package
+
+# Import gru stuff
+from daemon import *
+from freqz import *
+from gnuplot_freqz import *
+from hexint import *
+from listmisc import *
+from mathmisc import *
+from msgq_runner import *
+from os_read_exactly import *
+from seq_with_cursor import *
+from socket_stuff import *
diff --git a/gnuradio-runtime/python/gnuradio/gru/daemon.py b/gnuradio-runtime/python/gnuradio/gru/daemon.py
new file mode 100644
index 0000000000..e04702152d
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gru/daemon.py
@@ -0,0 +1,102 @@
+#
+# Copyright 2008 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+import os, sys, signal
+
+# Turn application into a background daemon process.
+#
+# When this function returns:
+#
+# 1) The calling process is disconnected from its controlling terminal
+#    and will not exit when the controlling session exits
+# 2) If a pidfile name is provided, it is created and the new pid is
+#    written into it.
+# 3) If a logfile name is provided, it is opened and stdout/stderr are
+#    redirected to it.
+# 4) The process current working directory is changed to '/' to avoid
+#    pinning any filesystem mounts.
+# 5) The process umask is set to 0111.
+#
+# The return value is the new pid.
+#
+# To create GNU Radio applications that operate as daemons, add a call to this
+# function after all initialization but just before calling gr.top_block.run()
+# or .start().
+#
+# Daemonized GNU Radio applications may be stopped by sending them a
+# SIGINT, SIGKILL, or SIGTERM, e.g., using 'kill pid' from the command line.
+#
+# If your application uses gr.top_block.run(), the flowgraph will be stopped
+# and the function will return.  You should allow your daemon program to exit
+# at this point.
+#
+# If your application uses gr.top_block.start(), you are responsible for hooking
+# the Python signal handler (see 'signal' module) and calling gr.top_block.stop()
+# on your top block, and otherwise causing your daemon process to exit.
+#
+
+def daemonize(pidfile=None, logfile=None):
+    # fork() into background
+    try:
+	pid = os.fork()
+    except OSError, e:
+	raise Exception, "%s [%d]" % (e.strerror, e.errno)
+
+    if pid == 0:	# First child of first fork()
+	# Become session leader of new session
+	os.setsid()
+
+	# fork() into background again
+	try:
+	    pid = os.fork()
+	except OSError, e:
+	    raise Exception, "%s [%d]" % (e.strerror, e.errno)
+
+	if pid != 0:
+	    os._exit(0) # Second child of second fork()
+
+    else:		# Second child of first fork()
+	os._exit(0)
+
+    os.umask(0111)
+
+    # Write pid
+    pid = os.getpid()
+    if pidfile is not None:
+	open(pidfile, 'w').write('%d\n'%pid)
+
+    # Redirect streams
+    if logfile is not None:
+	lf = open(logfile, 'a+')
+	sys.stdout = lf
+	sys.stderr = lf
+
+    # Prevent pinning any filesystem mounts
+    os.chdir('/')
+
+    # Tell caller what pid to send future signals to
+    return pid
+
+if __name__ == "__main__":
+    import time
+    daemonize()
+    print "Hello, world, from daemon process."
+    time.sleep(20)
+    print "Goodbye, world, from daemon process."
diff --git a/gnuradio-runtime/python/gnuradio/gru/freqz.py b/gnuradio-runtime/python/gnuradio/gru/freqz.py
new file mode 100644
index 0000000000..60dca64a58
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gru/freqz.py
@@ -0,0 +1,344 @@
+#!/usr/bin/env python
+#
+# Copyright 2005,2007 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+# This code lifted from various parts of www.scipy.org -eb 2005-01-24
+
+# Copyright (c) 2001, 2002 Enthought, Inc.
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#   a. Redistributions of source code must retain the above copyright notice,
+#      this list of conditions and the following disclaimer.
+#   b. Redistributions in binary form must reproduce the above copyright
+#      notice, this list of conditions and the following disclaimer in the
+#      documentation and/or other materials provided with the distribution.
+#   c. Neither the name of the Enthought nor the names of its contributors
+#      may be used to endorse or promote products derived from this software
+#      without specific prior written permission.
+#
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+#
+
+__all__ = ['freqz']
+
+import numpy
+from numpy import *
+Num=numpy
+
+def atleast_1d(*arys):
+    """ Force a sequence of arrays to each be at least 1D.
+
+         Description:
+            Force an array to be at least 1D.  If an array is 0D, the
+            array is converted to a single row of values.  Otherwise,
+            the array is unaltered.
+         Arguments:
+            *arys -- arrays to be converted to 1 or more dimensional array.
+         Returns:
+            input array converted to at least 1D array.
+    """
+    res = []
+    for ary in arys:
+        ary = asarray(ary)
+        if len(ary.shape) == 0:
+            result = numpy.array([ary[0]])
+        else:
+            result = ary
+        res.append(result)
+    if len(res) == 1:
+        return res[0]
+    else:
+        return res
+
+
+def polyval(p,x):
+    """Evaluate the polynomial p at x.  If x is a polynomial then composition.
+
+    Description:
+
+      If p is of length N, this function returns the value:
+      p[0]*(x**N-1) + p[1]*(x**N-2) + ... + p[N-2]*x + p[N-1]
+
+      x can be a sequence and p(x) will be returned for all elements of x.
+      or x can be another polynomial and the composite polynomial p(x) will be
+      returned.
+    """
+    p = asarray(p)
+    if isinstance(x,poly1d):
+        y = 0
+    else:
+        x = asarray(x)
+        y = numpy.zeros(x.shape,x.typecode())
+    for i in range(len(p)):
+        y = x * y + p[i]
+    return y
+
+class poly1d:
+    """A one-dimensional polynomial class.
+
+    p = poly1d([1,2,3]) constructs the polynomial x**2 + 2 x + 3
+
+    p(0.5) evaluates the polynomial at the location
+    p.r  is a list of roots
+    p.c  is the coefficient array [1,2,3]
+    p.order is the polynomial order (after leading zeros in p.c are removed)
+    p[k] is the coefficient on the kth power of x (backwards from
+         sequencing the coefficient array.
+
+    polynomials can be added, substracted, multplied and divided (returns
+         quotient and remainder).
+    asarray(p) will also give the coefficient array, so polynomials can
+         be used in all functions that accept arrays.
+    """
+    def __init__(self, c_or_r, r=0):
+        if isinstance(c_or_r,poly1d):
+            for key in c_or_r.__dict__.keys():
+                self.__dict__[key] = c_or_r.__dict__[key]
+            return
+        if r:
+            c_or_r = poly(c_or_r)
+        c_or_r = atleast_1d(c_or_r)
+        if len(c_or_r.shape) > 1:
+            raise ValueError, "Polynomial must be 1d only."
+        c_or_r = trim_zeros(c_or_r, trim='f')
+        if len(c_or_r) == 0:
+            c_or_r = numpy.array([0])
+        self.__dict__['coeffs'] = c_or_r
+        self.__dict__['order'] = len(c_or_r) - 1
+
+    def __array__(self,t=None):
+        if t:
+            return asarray(self.coeffs,t)
+        else:
+            return asarray(self.coeffs)
+
+    def __coerce__(self,other):
+        return None
+
+    def __repr__(self):
+        vals = repr(self.coeffs)
+        vals = vals[6:-1]
+        return "poly1d(%s)" % vals
+
+    def __len__(self):
+        return self.order
+
+    def __str__(self):
+        N = self.order
+        thestr = "0"
+        for k in range(len(self.coeffs)):
+            coefstr ='%.4g' % abs(self.coeffs[k])
+            if coefstr[-4:] == '0000':
+                coefstr = coefstr[:-5]
+            power = (N-k)
+            if power == 0:
+                if coefstr != '0':
+                    newstr = '%s' % (coefstr,)
+                else:
+                    if k == 0:
+                        newstr = '0'
+                    else:
+                        newstr = ''
+            elif power == 1:
+                if coefstr == '0':
+                    newstr = ''
+                elif coefstr == '1':
+                    newstr = 'x'
+                else:
+                    newstr = '%s x' % (coefstr,)
+            else:
+                if coefstr == '0':
+                    newstr = ''
+                elif coefstr == '1':
+                    newstr = 'x**%d' % (power,)
+                else:
+                    newstr = '%s x**%d' % (coefstr, power)
+
+            if k > 0:
+                if newstr != '':
+                    if self.coeffs[k] < 0:
+                        thestr = "%s - %s" % (thestr, newstr)
+                    else:
+                        thestr = "%s + %s" % (thestr, newstr)
+            elif (k == 0) and (newstr != '') and (self.coeffs[k] < 0):
+                thestr = "-%s" % (newstr,)
+            else:
+                thestr = newstr
+        return _raise_power(thestr)
+
+
+    def __call__(self, val):
+        return polyval(self.coeffs, val)
+
+    def __mul__(self, other):
+        if isscalar(other):
+            return poly1d(self.coeffs * other)
+        else:
+            other = poly1d(other)
+            return poly1d(polymul(self.coeffs, other.coeffs))
+
+    def __rmul__(self, other):
+        if isscalar(other):
+            return poly1d(other * self.coeffs)
+        else:
+            other = poly1d(other)
+            return poly1d(polymul(self.coeffs, other.coeffs))
+
+    def __add__(self, other):
+        other = poly1d(other)
+        return poly1d(polyadd(self.coeffs, other.coeffs))
+
+    def __radd__(self, other):
+        other = poly1d(other)
+        return poly1d(polyadd(self.coeffs, other.coeffs))
+
+    def __pow__(self, val):
+        if not isscalar(val) or int(val) != val or val < 0:
+            raise ValueError, "Power to non-negative integers only."
+        res = [1]
+        for k in range(val):
+            res = polymul(self.coeffs, res)
+        return poly1d(res)
+
+    def __sub__(self, other):
+        other = poly1d(other)
+        return poly1d(polysub(self.coeffs, other.coeffs))
+
+    def __rsub__(self, other):
+        other = poly1d(other)
+        return poly1d(polysub(other.coeffs, self.coeffs))
+
+    def __div__(self, other):
+        if isscalar(other):
+            return poly1d(self.coeffs/other)
+        else:
+            other = poly1d(other)
+            return map(poly1d,polydiv(self.coeffs, other.coeffs))
+
+    def __rdiv__(self, other):
+        if isscalar(other):
+            return poly1d(other/self.coeffs)
+        else:
+            other = poly1d(other)
+            return map(poly1d,polydiv(other.coeffs, self.coeffs))
+
+    def __setattr__(self, key, val):
+        raise ValueError, "Attributes cannot be changed this way."
+
+    def __getattr__(self, key):
+        if key in ['r','roots']:
+            return roots(self.coeffs)
+        elif key in ['c','coef','coefficients']:
+            return self.coeffs
+        elif key in ['o']:
+            return self.order
+        else:
+            return self.__dict__[key]
+
+    def __getitem__(self, val):
+        ind = self.order - val
+        if val > self.order:
+            return 0
+        if val < 0:
+            return 0
+        return self.coeffs[ind]
+
+    def __setitem__(self, key, val):
+        ind = self.order - key
+        if key < 0:
+            raise ValueError, "Does not support negative powers."
+        if key > self.order:
+            zr = numpy.zeros(key-self.order,self.coeffs.typecode())
+            self.__dict__['coeffs'] = numpy.concatenate((zr,self.coeffs))
+            self.__dict__['order'] = key
+            ind = 0
+        self.__dict__['coeffs'][ind] = val
+        return
+
+    def integ(self, m=1, k=0):
+        return poly1d(polyint(self.coeffs,m=m,k=k))
+
+    def deriv(self, m=1):
+        return poly1d(polyder(self.coeffs,m=m))
+
+def freqz(b, a, worN=None, whole=0, plot=None):
+    """Compute frequency response of a digital filter.
+
+    Description:
+
+       Given the numerator (b) and denominator (a) of a digital filter compute
+       its frequency response.
+
+                  jw               -jw            -jmw
+           jw  B(e)    b[0] + b[1]e + .... + b[m]e
+        H(e) = ---- = ------------------------------------
+                  jw               -jw            -jnw
+               A(e)    a[0] + a[2]e + .... + a[n]e
+
+    Inputs:
+
+       b, a --- the numerator and denominator of a linear filter.
+       worN --- If None, then compute at 512 frequencies around the unit circle.
+                If a single integer, the compute at that many frequencies.
+                Otherwise, compute the response at frequencies given in worN
+       whole -- Normally, frequencies are computed from 0 to pi (upper-half of
+                unit-circle.  If whole is non-zero compute frequencies from 0
+                to 2*pi.
+
+    Outputs: (h,w)
+
+       h -- The frequency response.
+       w -- The frequencies at which h was computed.
+    """
+    b, a = map(atleast_1d, (b,a))
+    if whole:
+        lastpoint = 2*pi
+    else:
+        lastpoint = pi
+    if worN is None:
+        N = 512
+        w = Num.arange(0,lastpoint,lastpoint/N)
+    elif isinstance(worN, types.IntType):
+        N = worN
+        w = Num.arange(0,lastpoint,lastpoint/N)
+    else:
+        w = worN
+    w = atleast_1d(w)
+    zm1 = exp(-1j*w)
+    h = polyval(b[::-1], zm1) / polyval(a[::-1], zm1)
+    # if not plot is None:
+    #    plot(w, h)
+    return h, w
diff --git a/gnuradio-runtime/python/gnuradio/gru/gnuplot_freqz.py b/gnuradio-runtime/python/gnuradio/gru/gnuplot_freqz.py
new file mode 100755
index 0000000000..dd483e4277
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gru/gnuplot_freqz.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+#
+# Copyright 2005,2007 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+__all__ = ['gnuplot_freqz']
+
+import tempfile
+import os
+import math
+import numpy
+
+from gnuradio import gr
+from gnuradio.gru.freqz import freqz
+
+
+def gnuplot_freqz (hw, Fs=None, logfreq=False):
+
+    """hw is a tuple of the form (h, w) where h is sequence of complex
+    freq responses, and w is a sequence of corresponding frequency
+    points.  Plot the frequency response using gnuplot.  If Fs is
+    provide, use it as the sampling frequency, else use 2*pi.
+
+    Returns a handle to the gnuplot graph. When the handle is reclaimed
+    the graph is torn down."""
+
+    data_file = tempfile.NamedTemporaryFile ()
+    cmd_file = os.popen ('gnuplot', 'w')
+
+    h, w = hw
+    ampl = 20 * numpy.log10 (numpy.absolute (h) + 1e-9)
+    phase = map (lambda x: math.atan2 (x.imag, x.real), h)
+
+    if Fs:
+        w *= (Fs/(2*math.pi))
+
+    for freq, a, ph in zip (w, ampl, phase):
+        data_file.write ("%g\t%g\t%g\n" % (freq, a, ph))
+
+    data_file.flush ()
+
+    cmd_file.write ("set grid\n")
+    if logfreq:
+        cmd_file.write ("set logscale x\n")
+    else:
+        cmd_file.write ("unset logscale x\n")
+    cmd_file.write ("plot '%s' using 1:2 with lines\n" % (data_file.name,))
+    cmd_file.flush ()
+
+    return (cmd_file, data_file)
+
+
+def test_plot ():
+    sample_rate = 2.0e6
+    #taps = firdes.low_pass(1, sample_rate, 200000, 100000, firdes.WIN_HAMMING)
+    taps = (0.0007329441141337156, 0.0007755281985737383, 0.0005323155201040208,
+            -7.679847761841656e-19, -0.0007277769618667662, -0.001415981911122799,
+            -0.0017135187517851591, -0.001282231998629868, 1.61239866282397e-18,
+            0.0018589380197227001, 0.0035909228026866913, 0.004260237794369459,
+            0.00310456077568233, -3.0331308923229716e-18, -0.004244099836796522,
+            -0.007970594801008701, -0.009214458055794239, -0.006562007591128349,
+            4.714311174044374e-18, 0.008654761128127575, 0.01605774275958538,
+            0.01841980405151844, 0.013079923577606678, -6.2821650235090215e-18,
+            -0.017465557903051376, -0.032989680767059326, -0.03894065320491791,
+            -0.028868533670902252, 7.388111706347014e-18, 0.04517475143074989,
+            0.09890196472406387, 0.14991308748722076, 0.18646684288978577,
+            0.19974154233932495, 0.18646684288978577, 0.14991308748722076,
+            0.09890196472406387, 0.04517475143074989, 7.388111706347014e-18,
+            -0.028868533670902252, -0.03894065320491791, -0.032989680767059326,
+            -0.017465557903051376, -6.2821650235090215e-18, 0.013079923577606678,
+            0.01841980405151844, 0.01605774275958538, 0.008654761128127575,
+            4.714311174044374e-18, -0.006562007591128349, -0.009214458055794239,
+            -0.007970594801008701, -0.004244099836796522, -3.0331308923229716e-18,
+            0.00310456077568233, 0.004260237794369459, 0.0035909228026866913,
+            0.0018589380197227001, 1.61239866282397e-18, -0.001282231998629868,
+            -0.0017135187517851591, -0.001415981911122799, -0.0007277769618667662,
+            -7.679847761841656e-19, 0.0005323155201040208, 0.0007755281985737383,
+            0.0007329441141337156)
+
+    # print len (taps)
+    return gnuplot_freqz (freqz (taps, 1), sample_rate)
+
+if __name__ == '__main__':
+    handle = test_plot ()
+    raw_input ('Press Enter to continue: ')
diff --git a/gnuradio-runtime/python/gnuradio/gru/hexint.py b/gnuradio-runtime/python/gnuradio/gru/hexint.py
new file mode 100644
index 0000000000..0fb5ecde04
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gru/hexint.py
@@ -0,0 +1,44 @@
+#
+# 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+def hexint(mask):
+  """
+  Convert unsigned masks into signed ints.
+
+  This allows us to use hex constants like 0xf0f0f0f2 when talking to
+  our hardware and not get screwed by them getting treated as python
+  longs.
+  """
+  if mask >= 2**31:
+     return int(mask-2**32)
+  return mask
+
+def hexshort(mask):
+  """
+  Convert unsigned masks into signed shorts.
+
+  This allows us to use hex constants like 0x8000 when talking to
+  our hardware and not get screwed by them getting treated as python
+  longs.
+  """
+  if mask >= 2**15:
+    return int(mask-2**16)
+  return mask
diff --git a/gnuradio-runtime/python/gnuradio/gru/listmisc.py b/gnuradio-runtime/python/gnuradio/gru/listmisc.py
new file mode 100644
index 0000000000..9e70eb863c
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gru/listmisc.py
@@ -0,0 +1,29 @@
+#
+# 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+def list_reverse(x):
+    """
+    Return a copy of x that is reverse order.
+    """
+    r = list(x)
+    r.reverse()
+    return r
+
diff --git a/gnuradio-runtime/python/gnuradio/gru/mathmisc.py b/gnuradio-runtime/python/gnuradio/gru/mathmisc.py
new file mode 100644
index 0000000000..7e6f23a346
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gru/mathmisc.py
@@ -0,0 +1,33 @@
+#
+# 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+import math
+
+def gcd(a,b):
+    while b:
+        a,b = b, a % b
+    return a
+
+def lcm(a,b):
+    return a * b / gcd(a, b)
+
+def log2(x):
+    return math.log(x)/math.log(2)
diff --git a/gnuradio-runtime/python/gnuradio/gru/msgq_runner.py b/gnuradio-runtime/python/gnuradio/gru/msgq_runner.py
new file mode 100644
index 0000000000..767a74a717
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gru/msgq_runner.py
@@ -0,0 +1,82 @@
+#
+# Copyright 2009 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+"""
+Convenience class for dequeuing messages from a gr.msg_queue and
+invoking a callback.
+
+Creates a Python thread that does a blocking read on the supplied
+gr.msg_queue, then invokes callback each time a msg is received.
+
+If the msg type is not 0, then it is treated as a signal to exit
+its loop.
+
+If the callback raises an exception, and the runner was created
+with 'exit_on_error' equal to True, then the runner will store the
+exception and exit its loop, otherwise the exception is ignored.
+
+To get the exception that the callback raised, if any, call
+exit_error() on the object.
+
+To manually stop the runner, call stop() on the object.
+
+To determine if the runner has exited, call exited() on the object.
+"""
+
+from gnuradio import gr
+import gnuradio.gr.gr_threading as _threading
+
+class msgq_runner(_threading.Thread):
+
+    def __init__(self, msgq, callback, exit_on_error=False):
+        _threading.Thread.__init__(self)
+
+        self._msgq = msgq
+        self._callback = callback
+        self._exit_on_error = exit_on_error
+        self._done = False
+        self._exited = False
+        self._exit_error = None
+        self.setDaemon(1)
+        self.start()
+
+    def run(self):
+        while not self._done:
+            msg = self._msgq.delete_head()
+            if msg.type() != 0:
+                self.stop()
+            else:
+                try:
+                    self._callback(msg)
+                except Exception, e:
+                    if self._exit_on_error:
+                        self._exit_error = e
+                        self.stop()
+        self._exited = True
+
+    def stop(self):
+        self._done = True
+
+    def exited(self):
+        return self._exited
+
+    def exit_error(self):
+        return self._exit_error
diff --git a/gnuradio-runtime/python/gnuradio/gru/os_read_exactly.py b/gnuradio-runtime/python/gnuradio/gru/os_read_exactly.py
new file mode 100644
index 0000000000..40b053770e
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gru/os_read_exactly.py
@@ -0,0 +1,36 @@
+#
+# 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+import os
+
+def os_read_exactly(file_descriptor, nbytes):
+    """
+    Replacement for os.read that blocks until it reads exactly nbytes.
+
+    """
+    s = ''
+    while nbytes > 0:
+        sbuf = os.read(file_descriptor, nbytes)
+        if not(sbuf):
+            return ''
+        nbytes -= len(sbuf)
+        s = s + sbuf
+    return s
diff --git a/gnuradio-runtime/python/gnuradio/gru/seq_with_cursor.py b/gnuradio-runtime/python/gnuradio/gru/seq_with_cursor.py
new file mode 100644
index 0000000000..def3299b69
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gru/seq_with_cursor.py
@@ -0,0 +1,77 @@
+#
+# Copyright 2003,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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+# misc utilities
+
+import types
+import exceptions
+
+class seq_with_cursor (object):
+    __slots__ = [ 'items', 'index' ]
+
+    def __init__ (self, items, initial_index = None, initial_value = None):
+        assert len (items) > 0, "seq_with_cursor: len (items) == 0"
+        self.items = items
+        self.set_index (initial_index)
+        if initial_value is not None:
+            self.set_index_by_value(initial_value)
+
+    def set_index (self, initial_index):
+        if initial_index is None:
+            self.index = len (self.items) / 2
+        elif initial_index >= 0 and initial_index < len (self.items):
+            self.index = initial_index
+        else:
+            raise exceptions.ValueError
+
+    def set_index_by_value(self, v):
+        """
+        Set index to the smallest value such that items[index] >= v.
+        If there is no such item, set index to the maximum value.
+        """
+        self.set_index(0)              # side effect!
+        cv = self.current()
+        more = True
+        while cv < v and more:
+            cv, more = self.next()      # side effect!
+
+    def next (self):
+        new_index = self.index + 1
+        if new_index < len (self.items):
+            self.index = new_index
+            return self.items[new_index], True
+        else:
+            return self.items[self.index], False
+
+    def prev (self):
+        new_index = self.index - 1
+        if new_index >= 0:
+            self.index = new_index
+            return self.items[new_index], True
+        else:
+            return self.items[self.index], False
+
+    def current (self):
+        return self.items[self.index]
+
+    def get_seq (self):
+        return self.items[:]            # copy of items
+
diff --git a/gnuradio-runtime/python/gnuradio/gru/socket_stuff.py b/gnuradio-runtime/python/gnuradio/gru/socket_stuff.py
new file mode 100644
index 0000000000..489b6ab255
--- /dev/null
+++ b/gnuradio-runtime/python/gnuradio/gru/socket_stuff.py
@@ -0,0 +1,62 @@
+#
+# 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 3, 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., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+# random socket related stuff
+
+import socket
+import os
+import sys
+
+def tcp_connect_or_die(sock_addr):
+    """
+    
+    Args:
+        sock_addr: (host, port) to connect to (tuple)
+    
+    Returns:
+        : socket or exits
+    """
+    s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
+    try:
+        s.connect(sock_addr)
+    except socket.error, err:
+        sys.stderr.write('Failed to connect to %s: %s\n' %
+                         (sock_addr, os.strerror (err.args[0]),))
+        sys.exit(1)
+    return s
+
+def udp_connect_or_die(sock_addr):
+    """
+    
+    Args:
+        sock_addr: (host, port) to connect to (tuple)
+    
+    Returns:
+        : socket or exits
+    """
+    s = socket.socket (socket.AF_INET, socket.SOCK_DGRAM)
+    try:
+        s.connect(sock_addr)
+    except socket.error, err:
+        sys.stderr.write('Failed to connect to %s: %s\n' %
+                         (sock_addr, os.strerror (err.args[0]),))
+        sys.exit(1)
+    return s
-- 
cgit v1.2.3