[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)
- [Commit-gnuradio] [gnuradio] branch master updated (6a88efb -> a18e480), git, 2015/11/11
- [Commit-gnuradio] [gnuradio] 01/08: runtime: add accessors for in_sig and out_sig in python blocks, git, 2015/11/11
- [Commit-gnuradio] [gnuradio] 04/08: grc: open and update params from external editor, git, 2015/11/11
- [Commit-gnuradio] [gnuradio] 02/08: grc: mark param type multiline as protected, git, 2015/11/11
- [Commit-gnuradio] [gnuradio] 05/08: grc: update PropsDialog on external param change, git, 2015/11/11
- [Commit-gnuradio] [gnuradio] 03/08: grc: add embedded python block definition and support in GRC,
git <=
- [Commit-gnuradio] [gnuradio] 06/08: grc: fix output file encoding, git, 2015/11/11
- [Commit-gnuradio] [gnuradio] 08/08: Merge remote-tracking branch 'gnuradio-wg-grc/master_grcwg', git, 2015/11/11
- [Commit-gnuradio] [gnuradio] 07/08: Merge branch 'maint', git, 2015/11/11