commit-gnuradio
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[Commit-gnuradio] [gnuradio] 03/08: grc: add embedded python block defin


From: git
Subject: [Commit-gnuradio] [gnuradio] 03/08: grc: add embedded python block definition and support in GRC
Date: Wed, 11 Nov 2015 23:08:07 +0000 (UTC)

This is an automated email from the git hooks/post-receive script.

jcorgan pushed a commit to branch master
in repository gnuradio.

commit c9a5fabe17efe8af1c9435f746ed55c5f5790917
Author: Sebastian Koslowski <address@hidden>
Date:   Mon Jul 20 22:24:53 2015 +0200

    grc: add embedded python block definition and support in GRC
---
 grc/base/Param.py          |  4 ++
 grc/blocks/epy_block.xml   | 45 +++++++++++++++++++++
 grc/gui/Param.py           | 52 ++++++++++++++++++-------
 grc/python/Block.py        | 96 ++++++++++++++++++++++++++++++++++++++++++++-
 grc/python/CMakeLists.txt  |  1 +
 grc/python/Generator.py    | 27 +++++++++----
 grc/python/Param.py        | 19 +++++----
 grc/python/epy_block_io.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++
 8 files changed, 310 insertions(+), 31 deletions(-)

diff --git a/grc/base/Param.py b/grc/base/Param.py
index c2f413c..b246d9f 100644
--- a/grc/base/Param.py
+++ b/grc/base/Param.py
@@ -111,6 +111,7 @@ class Param(Element):
             if self.get_value() not in self.get_option_keys():
                 raise Exception, 'The value "%s" is not in the possible values 
of "%s".'%(self.get_value(), self.get_option_keys())
         else: self._value = value or ''
+        self._default = value
 
     def validate(self):
         """
@@ -153,6 +154,9 @@ class Param(Element):
 
     def set_value(self, value): self._value = str(value) #must be a string
 
+    def value_is_default(self):
+        return self._default == self._value
+
     def get_type(self): return 
self.get_parent().resolve_dependencies(self._type)
     def get_tab_label(self): return self._tab_label
     def is_enum(self): return self._type == 'enum'
diff --git a/grc/blocks/epy_block.xml b/grc/blocks/epy_block.xml
new file mode 100644
index 0000000..2cd1cb5
--- /dev/null
+++ b/grc/blocks/epy_block.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0"?>
+<block>
+    <name>Embedded Python Block</name>
+    <key>epy_block</key>
+    <category>Misc</category>
+    <import></import>
+    <make></make>
+    <param><!-- Cache the last working block IO to keep FG sane -->
+        <name>Block Io</name>
+        <key>_io_cache</key>
+        <type>string</type>
+    <hide>all</hide>
+    </param>
+    <param>
+        <name>Code</name>
+        <key>_source_code</key>
+    <value>"""
+Embedded Python Blocks:
+
+Each this file is saved, GRC will instantiate the first class it finds to get
+ports and parameters of your block. The arguments to __init__  will be the
+parameters. All of them are required to have default values!
+"""
+import numpy as np
+from gnuradio import gr
+
+class blk(gr.sync_block):
+    def __init__(self, factor=1.0):  # only default arguments here
+        gr.sync_block.__init__(
+            self,
+            name='Embedded Python Block',
+            in_sig=[np.complex64],
+            out_sig=[np.complex64]
+        )
+        self.factor = factor
+
+    def work(self, input_items, output_items):
+        output_items[0][:] = input_items[0] * self.factor
+        return len(output_items[0])
+</value>
+    <type>_multiline_python_external</type>
+    <hide>part</hide>
+    </param>
+    <doc>Doc me, baby!</doc>
+</block>
diff --git a/grc/gui/Param.py b/grc/gui/Param.py
index ec4f7d5..6bd45fa 100644
--- a/grc/gui/Param.py
+++ b/grc/gui/Param.py
@@ -17,14 +17,14 @@ along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 """
 
-import Utils
-from Element import Element
-from . Constants import PARAM_FONT, GR_PREFIX
+import os
+
 import pygtk
 pygtk.require('2.0')
 import gtk
-import Colors
-import os
+
+from . import Colors, Utils, Constants
+from . Element import Element
 
 
 class InputParam(gtk.HBox):
@@ -44,8 +44,15 @@ class InputParam(gtk.HBox):
         self._have_pending_changes = False
         #connect events
         self.connect('show', self._update_gui)
-    def set_color(self, color): pass
-    def set_tooltip_text(self, text): pass
+
+    def set_color(self, color):
+        pass
+
+    def set_tooltip_text(self, text):
+        pass
+
+    def get_text(self):
+        raise NotImplementedError()
 
     def _update_gui(self, *args):
         """
@@ -115,10 +122,14 @@ class EntryParam(InputParam):
         self._input.connect('focus-out-event', self._apply_change)
         self._input.connect('key-press-event', self._handle_key_press)
         self.pack_start(self._input, True)
-    def get_text(self): return self._input.get_text()
+
+    def get_text(self):
+        return self._input.get_text()
+
     def set_color(self, color):
         self._input.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse(color))
         self._input.modify_text(gtk.STATE_NORMAL, 
Colors.PARAM_ENTRY_TEXT_COLOR)
+
     def set_tooltip_text(self, text):
         try:
             self._input.set_tooltip_text(text)
@@ -147,8 +158,9 @@ class MultiLineEntryParam(InputParam):
         self.pack_start(self._sw, True)
 
     def get_text(self):
-        return self._buffer.get_text(self._buffer.get_start_iter(),
-                                     self._buffer.get_end_iter()).strip()
+        buf = self._buffer
+        return buf.get_text(buf.get_start_iter(),
+                            buf.get_end_iter()).strip()
 
     def set_color(self, color):
         self._view.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse(color))
@@ -172,7 +184,10 @@ class EnumParam(InputParam):
         self._input.connect('changed', self._editing_callback)
         self._input.connect('changed', self._apply_change)
         self.pack_start(self._input, False)
-    def get_text(self): return 
self.param.get_option_keys()[self._input.get_active()]
+
+    def get_text(self):
+        return self.param.get_option_keys()[self._input.get_active()]
+
     def set_tooltip_text(self, text):
         try:
             self._input.set_tooltip_text(text)
@@ -196,9 +211,11 @@ class EnumEntryParam(InputParam):
         self._input.get_child().connect('focus-out-event', self._apply_change)
         self._input.get_child().connect('key-press-event', 
self._handle_key_press)
         self.pack_start(self._input, False)
+
     def get_text(self):
         if self._input.get_active() == -1: return 
self._input.get_child().get_text()
         return self.param.get_option_keys()[self._input.get_active()]
+
     def set_tooltip_text(self, text):
         try:
             if self._input.get_active() == -1: #custom entry
@@ -207,6 +224,7 @@ class EnumEntryParam(InputParam):
                 self._input.set_tooltip_text(text)
         except AttributeError:
             pass  # no tooltips for old GTK
+
     def set_color(self, color):
         if self._input.get_active() == -1: #custom entry, use color
             self._input.get_child().modify_base(gtk.STATE_NORMAL, 
gtk.gdk.color_parse(color))
@@ -237,7 +255,7 @@ class FileParam(EntryParam):
         if self.param.get_key() == 'qt_qss_theme':
             dirname = os.path.dirname(dirname)  # trim filename
             if not os.path.exists(dirname):
-               dirname = os.path.join(GR_PREFIX, '/share/gnuradio/themes')
+               dirname = os.path.join(Constants.GR_PREFIX, 
'/share/gnuradio/themes')
         if not os.path.exists(dirname):
             dirname = os.getcwd()  # fix bad paths
 
@@ -250,6 +268,8 @@ class FileParam(EntryParam):
                 gtk.FILE_CHOOSER_ACTION_SAVE, 
('gtk-cancel',gtk.RESPONSE_CANCEL, 'gtk-save',gtk.RESPONSE_OK))
             file_dialog.set_do_overwrite_confirmation(True)
             file_dialog.set_current_name(basename) #show the current filename
+        else:
+            raise ValueError("Can't open file chooser dialog for type " + 
repr(self.param.get_type()))
         file_dialog.set_current_folder(dirname) #current directory
         file_dialog.set_select_multiple(False)
         file_dialog.set_local_only(True)
@@ -299,7 +319,8 @@ Error:
 class Param(Element):
     """The graphical parameter."""
 
-    def __init__(self): Element.__init__(self)
+    def __init__(self):
+        Element.__init__(self)
 
     def get_input(self, *args, **kwargs):
         """
@@ -320,7 +341,7 @@ class Param(Element):
         elif self.get_options():
             input_widget = EnumEntryParam(self, *args, **kwargs)
 
-        elif self.get_type() == '_multiline':
+        elif self.get_type() in ('_multiline', '_multiline_python_external'):
             input_widget = MultiLineEntryParam(self, *args, **kwargs)
 
         else:
@@ -335,4 +356,5 @@ class Param(Element):
         Returns:
             a pango markup string
         """
-        return Utils.parse_template(PARAM_MARKUP_TMPL, param=self, 
font=PARAM_FONT)
+        return Utils.parse_template(PARAM_MARKUP_TMPL,
+                                    param=self, font=Constants.PARAM_FONT)
diff --git a/grc/python/Block.py b/grc/python/Block.py
index 5289d57..accfd21 100644
--- a/grc/python/Block.py
+++ b/grc/python/Block.py
@@ -21,11 +21,13 @@ import itertools
 import collections
 
 from .. base.Constants import BLOCK_FLAG_NEED_QT_GUI, BLOCK_FLAG_NEED_WX_GUI
+from .. base.odict import odict
+
 from .. base.Block import Block as _Block
 from .. gui.Block import Block as _GUIBlock
 
 from . FlowGraph import _variable_matcher
-import extract_docs
+from . import epy_block_io, extract_docs
 
 
 class Block(_Block, _GUIBlock):
@@ -59,6 +61,9 @@ class Block(_Block, _GUIBlock):
         )
         _GUIBlock.__init__(self)
 
+        self._epy_source_hash = -1  # for epy blocks
+        self._epy_reload_error = None
+
     def get_bus_structure(self, direction):
         if direction == 'source':
             bus_structure = self._bus_structure_source;
@@ -111,12 +116,16 @@ class Block(_Block, _GUIBlock):
 
         check_generate_mode('WX GUI', BLOCK_FLAG_NEED_WX_GUI, ('wx_gui',))
         check_generate_mode('QT GUI', BLOCK_FLAG_NEED_QT_GUI, ('qt_gui', 
'hb_qt_gui'))
+        if self._epy_reload_error:
+            self.add_error_message(str(self._epy_reload_error))
 
     def rewrite(self):
         """
         Add and remove ports to adjust for the nports.
         """
         _Block.rewrite(self)
+        # Check and run any custom rewrite function for this block
+        getattr(self, 'rewrite_' + self._key, lambda: None)()
 
         # adjust nports, disconnect hidden ports
         for ports in (self.get_sources(), self.get_sinks()):
@@ -216,3 +225,88 @@ class Block(_Block, _GUIBlock):
 
     def is_virtual_source(self):
         return self.get_key() == 'virtual_source'
+
+    ###########################################################################
+    # Custom rewrite functions
+    ###########################################################################
+
+    def rewrite_epy_block(self):
+        flowgraph = self.get_parent()
+        platform = flowgraph.get_parent()
+        param_blk = self.get_param('_io_cache')
+        param_src = self.get_param('_source_code')
+
+        src = param_src.get_value()
+        src_hash = hash(src)
+        if src_hash == self._epy_source_hash:
+            return
+
+        try:
+            blk_io = epy_block_io.extract(src)
+
+        except Exception as e:
+            self._epy_reload_error = ValueError('Source code eval:\n' + str(e))
+            try:  # load last working block io
+                blk_io = epy_block_io.BlockIO(*eval(param_blk.get_value()))
+            except:
+                return
+        else:
+            self._epy_reload_error = None  # clear previous errors
+            param_blk.set_value(repr(tuple(blk_io)))
+
+        # print "Rewriting embedded python block {!r}".format(self.get_id())
+
+        self._epy_source_hash = src_hash
+        self._name = blk_io.name or blk_io.cls
+        self._doc = blk_io.doc
+        self._imports[0] = 'from {} import {}'.format(self.get_id(), 
blk_io.cls)
+        self._make = '{}({})'.format(blk_io.cls, ', '.join(
+            '{0}=${0}'.format(key) for key, _ in blk_io.params))
+
+        params = dict()
+        for param in list(self._params):
+            if hasattr(param, '__epy_param__'):
+                params[param.get_key()] = param
+                self._params.remove(param)
+
+        for key, value in blk_io.params:
+            if key in params:
+                param = params[key]
+                if not param.value_is_default():
+                    param.set_value(value)
+            else:
+                name = key.replace('_', ' ').title()
+                n = odict(dict(name=name, key=key, type='raw', value=value))
+                param = platform.Param(block=self, n=n)
+                setattr(param, '__epy_param__', True)
+            self._params.append(param)
+
+        def update_ports(label, ports, port_specs, direction):
+            ports_to_remove = list(ports)
+            iter_ports = iter(ports)
+            ports_new = list()
+            port_current = next(iter_ports, None)
+            for key, port_type in port_specs:
+                reuse_port = (
+                    port_current is not None and
+                    port_current.get_type() == port_type and
+                    (key.isdigit() or port_current.get_key() == key)
+                )
+                if reuse_port:
+                    ports_to_remove.remove(port_current)
+                    port, port_current = port_current, next(iter_ports, None)
+                else:
+                    n = odict(dict(name=label + str(key), type=port_type, 
key=key))
+                    port = platform.Port(block=self, n=n, dir=direction)
+                ports_new.append(port)
+            # replace old port list with new one
+            del ports[:]
+            ports.extend(ports_new)
+            # remove excess port connections
+            for port in ports_to_remove:
+                for connection in port.get_connections():
+                    flowgraph.remove_element(connection)
+
+        update_ports('in', self.get_sinks(), blk_io.sinks, 'sink')
+        update_ports('out', self.get_sources(), blk_io.sources, 'source')
+        _Block.rewrite(self)
diff --git a/grc/python/CMakeLists.txt b/grc/python/CMakeLists.txt
index 41d965e..3f9e273 100644
--- a/grc/python/CMakeLists.txt
+++ b/grc/python/CMakeLists.txt
@@ -21,6 +21,7 @@
 GR_PYTHON_INSTALL(FILES
     expr_utils.py
     extract_docs.py
+    epy_block_io.py
     Block.py
     Connection.py
     Constants.py
diff --git a/grc/python/Generator.py b/grc/python/Generator.py
index d60befe..0b9469b 100644
--- a/grc/python/Generator.py
+++ b/grc/python/Generator.py
@@ -79,7 +79,7 @@ class TopBlockGenerator(object):
         self._flow_graph = flow_graph
         self._generate_options = 
self._flow_graph.get_option('generate_options')
         self._mode = TOP_BLOCK_FILE_MODE
-        dirname = os.path.dirname(file_path)
+        dirname = self._dirname = os.path.dirname(file_path)
         # handle the case where the directory is read-only
         # in this case, use the system's temp directory
         if not os.access(dirname, os.W_OK):
@@ -108,12 +108,14 @@ class TopBlockGenerator(object):
                                       "This is usually undesired. Consider "
                                       "removing the throttle block.")
         # generate
-        with codecs.open(self.get_file_path(), 'w', encoding = 'utf-8') as fp:
-            fp.write(self._build_python_code_from_template())
-        try:
-            os.chmod(self.get_file_path(), self._mode)
-        except:
-            pass
+        for filename, data in self._build_python_code_from_template():
+            with open(filename, 'w', encoding='utf-8') as fp:
+                fp.write(data)
+            if filename == self.get_file_path():
+                try:
+                    os.chmod(filename, self._mode)
+                except:
+                    pass
 
     def get_popen(self):
         """
@@ -148,6 +150,8 @@ class TopBlockGenerator(object):
         Returns:
             a string of python code
         """
+        output = list()
+
         title = self._flow_graph.get_option('title') or 
self._flow_graph.get_option('id').replace('_', ' ').title()
         imports = self._flow_graph.get_imports()
         variables = self._flow_graph.get_variables()
@@ -174,6 +178,12 @@ class TopBlockGenerator(object):
         # List of regular blocks (all blocks minus the special ones)
         blocks = filter(lambda b: b not in (imports + parameters), blocks)
 
+        for block in blocks:
+            if block.get_key() == 'epy_block':
+                file_path = os.path.join(self._dirname, block.get_id() + '.py')
+                src = block.get_param('_source_code').get_value()
+                output.append((file_path, src))
+
         # Filter out virtual sink connections
         cf = lambda c: not (c.is_bus() or c.is_msg() or 
c.get_sink().get_parent().is_virtual_sink())
         connections = filter(cf, self._flow_graph.get_enabled_connections())
@@ -258,7 +268,8 @@ class TopBlockGenerator(object):
         }
         # build the template
         t = Template(open(FLOW_GRAPH_TEMPLATE, 'r').read(), namespace)
-        return str(t)
+        output.append((self.get_file_path(), str(t)))
+        return output
 
 
 class HierBlockGenerator(TopBlockGenerator):
diff --git a/grc/python/Param.py b/grc/python/Param.py
index 50723ed..746f677 100644
--- a/grc/python/Param.py
+++ b/grc/python/Param.py
@@ -17,6 +17,11 @@ along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 """
 
+import ast
+import re
+
+from gnuradio import gr
+
 from .. base.Param import Param as _Param
 from .. gui.Param import Param as _GUIParam
 
@@ -24,8 +29,6 @@ import Constants
 from Constants import VECTOR_TYPES, COMPLEX_TYPES, REAL_TYPES, INT_TYPES
 
 from gnuradio import eng_notation
-import re
-from gnuradio import gr
 
 _check_id_matcher = re.compile('^[a-z|A-Z]\w*$')
 _show_id_matcher = re.compile('^(variable\w*|parameter|options|notebook)$')
@@ -62,7 +65,7 @@ class Param(_Param, _GUIParam):
         'complex', 'real', 'float', 'int',
         'complex_vector', 'real_vector', 'float_vector', 'int_vector',
         'hex', 'string', 'bool',
-        'file_open', 'file_save', '_multiline',
+        'file_open', 'file_save', '_multiline', '_multiline_python_external',
         'id', 'stream_id',
         'grid_pos', 'notebook', 'gui_hint',
         'import',
@@ -266,7 +269,7 @@ class Param(_Param, _GUIParam):
         #########################
         # String Types
         #########################
-        elif t in ('string', 'file_open', 'file_save', '_multiline'):
+        elif t in ('string', 'file_open', 'file_save', '_multiline', 
'_multiline_python_external'):
             #do not check if file/directory exists, that is a runtime issue
             try:
                 e = self.get_parent().get_parent().evaluate(v)
@@ -274,8 +277,10 @@ class Param(_Param, _GUIParam):
                     raise Exception()
             except:
                 self._stringify_flag = True
-                e = v
-            return str(e)
+                e = str(v)
+            if t == '_multiline_python_external':
+                ast.parse(e)  # raises SyntaxError
+            return e
         #########################
         # Unique ID Type
         #########################
@@ -405,7 +410,7 @@ class Param(_Param, _GUIParam):
         """
         v = self.get_value()
         t = self.get_type()
-        if t in ('string', 'file_open', 'file_save', '_multiline'):  # string 
types
+        if t in ('string', 'file_open', 'file_save', '_multiline', 
'_multiline_python_external'):  # string types
             if not self._init: self.evaluate()
             if self._stringify_flag: return '"%s"'%v.replace('"', '\"')
             else: return v
diff --git a/grc/python/epy_block_io.py b/grc/python/epy_block_io.py
new file mode 100644
index 0000000..8d3ce1c
--- /dev/null
+++ b/grc/python/epy_block_io.py
@@ -0,0 +1,97 @@
+
+import inspect
+import collections
+
+from gnuradio import gr
+import pmt
+
+
+TYPE_MAP = {
+    'complex64': 'complex', 'complex': 'complex',
+    'float32': 'float', 'float': 'float',
+    'int32': 'int', 'uint32': 'int',
+    'int16': 'short', 'uint16': 'short',
+    'int8': 'byte', 'uint8': 'byte',
+}
+
+BlockIO = collections.namedtuple('BlockIO', 'name cls params sinks sources 
doc')
+
+
+def _ports(sigs, msgs):
+    ports = list()
+    for i, dtype in enumerate(sigs):
+        port_type = TYPE_MAP.get(dtype.name, None)
+        if not port_type:
+            raise ValueError("Can't map {0:!r} to GRC port type".format(dtype))
+        ports.append((str(i), port_type))
+    for msg_key in msgs:
+        if msg_key == 'system':
+            continue
+        ports.append((msg_key, 'message'))
+    return ports
+
+
+def _blk_class(source_code):
+    ns = {}
+    try:
+        exec source_code in ns
+    except Exception as e:
+        raise ValueError("Can't interpret source code: " + str(e))
+    for var in ns.itervalues():
+        if inspect.isclass(var)and issubclass(var, gr.gateway.gateway_block):
+            break
+    else:
+        raise ValueError('No python block class found in code')
+    return var
+
+
+def extract(cls):
+    if not inspect.isclass(cls):
+        cls = _blk_class(cls)
+
+    spec = inspect.getargspec(cls.__init__)
+    defaults = map(repr, spec.defaults or ())
+    doc = cls.__doc__ or cls.__init__.__doc__ or ''
+    cls_name = cls.__name__
+
+    if len(defaults) + 1 != len(spec.args):
+        raise ValueError("Need all default values")
+
+    try:
+        instance = cls()
+    except Exception as e:
+        raise RuntimeError("Can't create an instance of your block: " + str(e))
+
+    name = instance.name()
+    params = list(zip(spec.args[1:], defaults))
+
+    sinks = _ports(instance.in_sig(),
+                      pmt.to_python(instance.message_ports_in()))
+    sources = _ports(instance.out_sig(),
+                        pmt.to_python(instance.message_ports_out()))
+
+    return BlockIO(name, cls_name, params, sinks, sources, doc)
+
+
+if __name__ == '__main__':
+    blk_code = """
+import numpy as np
+from gnuradio import gr
+import pmt
+
+class blk(gr.sync_block):
+    def __init__(self, param1=None, param2=None):
+        "Test Docu"
+        gr.sync_block.__init__(
+            self,
+            name='Embedded Python Block',
+            in_sig = (np.float32,),
+            out_sig = (np.float32,np.complex64,),
+        )
+        self.message_port_register_in(pmt.intern('msg_in'))
+        self.message_port_register_out(pmt.intern('msg_out'))
+
+    def work(self, inputs_items, output_items):
+        return 10
+    """
+    print extract(blk_code)



reply via email to

[Prev in Thread] Current Thread [Next in Thread]