diff options
author | Sebastian Koslowski <sebastian.koslowski@gmail.com> | 2016-05-03 17:13:08 +0200 |
---|---|---|
committer | Johnathan Corgan <johnathan@corganlabs.com> | 2017-06-29 09:16:49 -0700 |
commit | 7f7fa2f91467fdb2b11312be8562e7b51fdeb199 (patch) | |
tree | 24268bac15b9920d2a15ddbb45eaf3b9b03718a1 /grc/core/blocks | |
parent | 44cae388881821942e691a4d69a923bbd8d347db (diff) |
grc: added yaml/mako support
Includes basic converter from XML/Cheetah to YAML/Mako based block format.
Diffstat (limited to 'grc/core/blocks')
-rw-r--r-- | grc/core/blocks/__init__.py | 37 | ||||
-rw-r--r-- | grc/core/blocks/_build.py | 69 | ||||
-rw-r--r-- | grc/core/blocks/_flags.py | 39 | ||||
-rw-r--r-- | grc/core/blocks/_templates.py | 77 | ||||
-rw-r--r-- | grc/core/blocks/block.py | 416 | ||||
-rw-r--r-- | grc/core/blocks/dummy.py | 54 | ||||
-rw-r--r-- | grc/core/blocks/embedded_python.py | 242 | ||||
-rw-r--r-- | grc/core/blocks/virtual.py | 76 |
8 files changed, 1010 insertions, 0 deletions
diff --git a/grc/core/blocks/__init__.py b/grc/core/blocks/__init__.py new file mode 100644 index 0000000000..e4a085d477 --- /dev/null +++ b/grc/core/blocks/__init__.py @@ -0,0 +1,37 @@ +# Copyright 2016 Free Software Foundation, Inc. +# This file is part of GNU Radio +# +# GNU Radio Companion is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# GNU Radio Companion 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 + +from __future__ import absolute_import + +from ._flags import Flags +from ._templates import MakoTemplates + +from .block import Block + +from ._build import build + + +build_ins = {} + + +def register_build_in(cls): + build_ins[cls.key] = cls + return cls + +from .dummy import DummyBlock +from .embedded_python import EPyBlock, EPyModule +from .virtual import VirtualSink, VirtualSource diff --git a/grc/core/blocks/_build.py b/grc/core/blocks/_build.py new file mode 100644 index 0000000000..9a50086cea --- /dev/null +++ b/grc/core/blocks/_build.py @@ -0,0 +1,69 @@ +# Copyright 2016 Free Software Foundation, Inc. +# This file is part of GNU Radio +# +# GNU Radio Companion is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# GNU Radio Companion 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 + +from __future__ import absolute_import + +import re + +from .block import Block +from ._flags import Flags +from ._templates import MakoTemplates + + +def build(id, label='', category='', flags='', documentation='', + checks=None, value=None, + parameters=None, inputs=None, outputs=None, templates=None, **kwargs): + block_id = id + + cls = type(block_id, (Block,), {}) + cls.key = block_id + + cls.label = label or block_id.title() + cls.category = [cat.strip() for cat in category.split('/') if cat.strip()] + + cls.flags = Flags(flags) + if re.match(r'options$|variable|virtual', block_id): + cls.flags += Flags.NOT_DSP + Flags.DISABLE_BYPASS + + cls.documentation = {'': documentation.strip('\n\t ').replace('\\\n', '')} + + cls.checks = [_single_mako_expr(check, block_id) for check in (checks or [])] + + cls.parameters_data = parameters or [] + cls.inputs_data = inputs or [] + cls.outputs_data = outputs or [] + cls.extra_data = kwargs + + templates = templates or {} + cls.templates = MakoTemplates( + imports=templates.get('imports', ''), + make=templates.get('make', ''), + callbacks=templates.get('callbacks', []), + var_make=templates.get('var_make', ''), + ) + # todo: MakoTemplates.compile() to check for errors + + cls.value = _single_mako_expr(value, block_id) + + return cls + + +def _single_mako_expr(value, block_id): + match = re.match(r'\s*\$\{\s*(.*?)\s*\}\s*', str(value)) + if value and not match: + raise ValueError('{} is not a mako substitution in {}'.format(value, block_id)) + return match.group(1) if match else None diff --git a/grc/core/blocks/_flags.py b/grc/core/blocks/_flags.py new file mode 100644 index 0000000000..ffea2ad569 --- /dev/null +++ b/grc/core/blocks/_flags.py @@ -0,0 +1,39 @@ +# Copyright 2016 Free Software Foundation, Inc. +# This file is part of GNU Radio +# +# GNU Radio Companion is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# GNU Radio Companion 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 + +from __future__ import absolute_import + +import six + + +class Flags(six.text_type): + + THROTTLE = 'throttle' + DISABLE_BYPASS = 'disable_bypass' + NEED_QT_GUI = 'need_qt_gui' + DEPRECATED = 'deprecated' + NOT_DSP = 'not_dsp' + + def __getattr__(self, item): + return item in self + + def __add__(self, other): + if not isinstance(other, six.string_types): + return NotImplemented + return self.__class__(str(self) + other) + + __iadd__ = __add__ diff --git a/grc/core/blocks/_templates.py b/grc/core/blocks/_templates.py new file mode 100644 index 0000000000..0b15166423 --- /dev/null +++ b/grc/core/blocks/_templates.py @@ -0,0 +1,77 @@ +# Copyright 2016 Free Software Foundation, Inc. +# This file is part of GNU Radio +# +# GNU Radio Companion is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# GNU Radio Companion 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 +""" +This dict class holds a (shared) cache of compiled mako templates. +These + +""" +from __future__ import absolute_import, print_function + +from mako.template import Template +from mako.exceptions import SyntaxException + +from ..errors import TemplateError + + +class MakoTemplates(dict): + + _template_cache = {} + + def __init__(self, _bind_to=None, *args, **kwargs): + self.instance = _bind_to + dict.__init__(self, *args, **kwargs) + + def __get__(self, instance, owner): + if instance is None or self.instance is not None: + return self + copy = self.__class__(_bind_to=instance, **self) + if getattr(instance.__class__, 'templates', None) is self: + setattr(instance, 'templates', copy) + return copy + + @classmethod + def compile(cls, text): + text = str(text) + try: + template = Template(text) + except SyntaxException as error: + raise TemplateError(text, *error.args) + + cls._template_cache[text] = template + return template + + def _get_template(self, text): + try: + return self._template_cache[str(text)] + except KeyError: + return self.compile(text) + + def render(self, item): + text = self.get(item) + if not text: + return '' + namespace = self.instance.namespace_templates + + try: + if isinstance(text, list): + templates = (self._get_template(t) for t in text) + return [template.render(**namespace) for template in templates] + else: + template = self._get_template(text) + return template.render(**namespace) + except Exception as error: + raise TemplateError(error, text) diff --git a/grc/core/blocks/block.py b/grc/core/blocks/block.py new file mode 100644 index 0000000000..e6695083a1 --- /dev/null +++ b/grc/core/blocks/block.py @@ -0,0 +1,416 @@ +""" +Copyright 2008-2015 Free Software Foundation, Inc. +This file is part of GNU Radio + +GNU Radio Companion is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +GNU Radio Companion 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 +""" + +from __future__ import absolute_import + +import ast +import collections +import itertools + +import six +from six.moves import range + +from ._templates import MakoTemplates +from ._flags import Flags + +from ..Constants import ADVANCED_PARAM_TAB +from ..base import Element +from ..utils.descriptors import lazy_property + + +def _get_elem(iterable, key): + items = list(iterable) + for item in items: + if item.key == key: + return item + return ValueError('Key "{}" not found in {}.'.format(key, items)) + + +class Block(Element): + + is_block = True + + STATE_LABELS = ['disabled', 'enabled', 'bypassed'] + + key = '' + label = '' + category = '' + flags = Flags('') + documentation = {'': ''} + + value = None + checks = [] + + templates = MakoTemplates() + parameters_data = [] + inputs_data = [] + outputs_data = [] + + extra_data = {} + + # region Init + def __init__(self, parent): + """Make a new block from nested data.""" + super(Block, self).__init__(parent) + self.params = self._init_params() + self.sinks = self._init_ports(self.inputs_data, direction='sink') + self.sources = self._init_ports(self.outputs_data, direction='source') + + self.active_sources = [] # on rewrite + self.active_sinks = [] # on rewrite + + self.states = {'state': True} + + def _init_params(self): + is_dsp_block = not self.flags.not_dsp + has_inputs = bool(self.inputs_data) + has_outputs = bool(self.outputs_data) + + params = collections.OrderedDict() + param_factory = self.parent_platform.make_param + + def add_param(id, **kwargs): + params[id] = param_factory(self, id=id, **kwargs) + + add_param(id='id', name='ID', dtype='id', + hide='none' if (self.key == 'options' or self.is_variable) else 'part') + + if is_dsp_block: + add_param(id='alias', name='Block Alias', dtype='string', + hide='part', category=ADVANCED_PARAM_TAB) + + if has_outputs or has_inputs: + add_param(id='affinity', name='Core Affinity', dtype='int_vector', + hide='part', category=ADVANCED_PARAM_TAB) + + if has_outputs: + add_param(id='minoutbuf', name='Min Output Buffer', dtype='int', + hide='part', value='0', category=ADVANCED_PARAM_TAB) + add_param(id='maxoutbuf', name='Max Output Buffer', dtype='int', + hide='part', value='0', category=ADVANCED_PARAM_TAB) + + base_params_n = {} + for param_data in self.parameters_data: + param_id = param_data['id'] + if param_id in params: + raise Exception('Param id "{}" is not unique'.format(param_id)) + + base_key = param_data.get('base_key', None) + param_data_ext = base_params_n.get(base_key, {}).copy() + param_data_ext.update(param_data) + + add_param(**param_data_ext) + base_params_n[param_id] = param_data_ext + + add_param(id='comment', name='Comment', dtype='_multiline', hide='part', + value='', category=ADVANCED_PARAM_TAB) + return params + + def _init_ports(self, ports_n, direction): + ports = [] + port_factory = self.parent_platform.make_port + port_ids = set() + + def make_stream_port_id(_pool=itertools.count()): + return {'sink': 'in', 'source': 'out'}[direction] + str(next(_pool)) + + for i, port_data in enumerate(ports_n): + port_id = port_data.setdefault('id', make_stream_port_id()) + if port_id in port_ids: + raise Exception('Port id "{}" already exists in {}s'.format(port_id, direction)) + port_ids.add(port_id) + + port = port_factory(parent=self, direction=direction, **port_data) + ports.append(port) + return ports + # endregion + + # region Rewrite_and_Validation + def rewrite(self): + """ + Add and remove ports to adjust for the nports. + """ + Element.rewrite(self) + + def rekey(ports): + """Renumber non-message/message ports""" + domain_specific_port_index = collections.defaultdict(int) + for port in ports: + if not port.key.isdigit(): + continue + domain = port.domain + port.key = str(domain_specific_port_index[domain]) + domain_specific_port_index[domain] += 1 + + # Adjust nports + for ports in (self.sources, self.sinks): + self._rewrite_nports(ports) + rekey(ports) + + # disconnect hidden ports + self.parent_flowgraph.disconnect(*[p for p in self.ports() if p.hidden]) + + self.active_sources = [p for p in self.sources if not p.hidden] + self.active_sinks = [p for p in self.sinks if not p.hidden] + + def _rewrite_nports(self, ports): + for port in ports: + if hasattr(port, 'master_port'): # Not a master port and no left-over clones + continue + nports = port.multiplicity + for clone in port.clones[nports-1:]: + # Remove excess connections + self.parent_flowgraph.disconnect(clone) + port.remove_clone(clone) + ports.remove(clone) + # Add more cloned ports + for j in range(1 + len(port.clones), nports): + clone = port.add_clone() + ports.insert(ports.index(port) + j, clone) + + def validate(self): + """ + Validate this block. + Call the base class validate. + Evaluate the checks: each check must evaluate to True. + """ + Element.validate(self) + self._run_checks() + self._validate_generate_mode_compat() + self._validate_var_value() + + def _run_checks(self): + """Evaluate the checks""" + for check in self.checks: + try: + if not self.evaluate(check): + self.add_error_message('Check "{}" failed.'.format(check)) + except: + self.add_error_message('Check "{}" did not evaluate.'.format(check)) + + def _validate_generate_mode_compat(self): + """check if this is a GUI block and matches the selected generate option""" + current_generate_option = self.parent.get_option('generate_options') + + def check_generate_mode(label, flag, valid_options): + block_requires_mode = ( + flag in self.flags or self.label.upper().startswith(label) + ) + if block_requires_mode and current_generate_option not in valid_options: + self.add_error_message("Can't generate this block in mode: {} ".format( + repr(current_generate_option))) + + check_generate_mode('QT GUI', Flags.NEED_QT_GUI, ('qt_gui', 'hb_qt_gui')) + + def _validate_var_value(self): + """or variables check the value (only if var_value is used)""" + if self.is_variable and self.value != 'value': + try: + self.parent_flowgraph.evaluate(self.value, local_namespace=self.namespace) + except Exception as err: + self.add_error_message('Value "{}" cannot be evaluated:\n{}'.format(self.value, err)) + # endregion + + # region Properties + + def __str__(self): + return 'Block - {} - {}({})'.format(self.name, self.label, self.key) + + def __repr__(self): + try: + name = self.name + except Exception: + name = self.key + return 'block[' + name + ']' + + @property + def name(self): + return self.params['id'].value + + @lazy_property + def is_virtual_or_pad(self): + return self.key in ("virtual_source", "virtual_sink", "pad_source", "pad_sink") + + @lazy_property + def is_variable(self): + return bool(self.value) + + @lazy_property + def is_import(self): + return self.key == 'import' + + @property + def comment(self): + return self.params['comment'].value + + @property + def state(self): + """Gets the block's current state.""" + state = self.states['state'] + return state if state in self.STATE_LABELS else 'enabled' + + @state.setter + def state(self, value): + """Sets the state for the block.""" + self.states['state'] = value + + # Enable/Disable Aliases + @property + def enabled(self): + """Get the enabled state of the block""" + return self.state != 'disabled' + + # endregion + + ############################################## + # Getters (old) + ############################################## + def get_var_make(self): + return self.templates.render('var_make') + + def get_var_value(self): + return self.templates.render('var_value') + + def get_callbacks(self): + """ + Get a list of function callbacks for this block. + + Returns: + a list of strings + """ + def make_callback(callback): + if 'self.' in callback: + return callback + return 'self.{}.{}'.format(self.name, callback) + return [make_callback(c) for c in self.templates.render('callbacks')] + + def is_virtual_sink(self): + return self.key == 'virtual_sink' + + def is_virtual_source(self): + return self.key == 'virtual_source' + + # Block bypassing + def get_bypassed(self): + """ + Check if the block is bypassed + """ + return self.state == 'bypassed' + + def set_bypassed(self): + """ + Bypass the block + + Returns: + True if block chagnes state + """ + if self.state != 'bypassed' and self.can_bypass(): + self.state = 'bypassed' + return True + return False + + def can_bypass(self): + """ Check the number of sinks and sources and see if this block can be bypassed """ + # Check to make sure this is a single path block + # Could possibly support 1 to many blocks + if len(self.sources) != 1 or len(self.sinks) != 1: + return False + if not (self.sources[0].dtype == self.sinks[0].dtype): + return False + if self.flags.disable_bypass: + return False + return True + + def ports(self): + return itertools.chain(self.sources, self.sinks) + + def active_ports(self): + return itertools.chain(self.active_sources, self.active_sinks) + + def children(self): + return itertools.chain(six.itervalues(self.params), self.ports()) + + ############################################## + # Access + ############################################## + + def get_sink(self, key): + return _get_elem(self.sinks, key) + + def get_source(self, key): + return _get_elem(self.sources, key) + + ############################################## + # Resolve + ############################################## + @property + def namespace(self): + return {key: param.get_evaluated() for key, param in six.iteritems(self.params)} + + @property + def namespace_templates(self): + return {key: param.template_arg for key, param in six.iteritems(self.params)} + + def evaluate(self, expr): + return self.parent_flowgraph.evaluate(expr, self.namespace) + + ############################################## + # Import/Export Methods + ############################################## + def export_data(self): + """ + Export this block's params to nested data. + + Returns: + a nested data odict + """ + data = collections.OrderedDict() + if self.key != 'options': + data['name'] = self.name + data['id'] = self.key + data['parameters'] = collections.OrderedDict(sorted( + (param_id, param.value) for param_id, param in self.params.items() + if param_id != 'id' + )) + data['states'] = collections.OrderedDict(sorted(self.states.items())) + return data + + def import_data(self, name, states, parameters, **_): + """ + Import this block's params from nested data. + Any param keys that do not exist will be ignored. + Since params can be dynamically created based another param, + call rewrite, and repeat the load until the params stick. + """ + self.params['id'].value = name + self.states.update(states) + + def get_hash(): + return hash(tuple(hash(v) for v in self.params.values())) + + pre_rewrite_hash = -1 + while pre_rewrite_hash != get_hash(): + for key, value in six.iteritems(parameters): + try: + self.params[key].set_value(value) + except KeyError: + continue + # Store hash and call rewrite + pre_rewrite_hash = get_hash() + self.rewrite() diff --git a/grc/core/blocks/dummy.py b/grc/core/blocks/dummy.py new file mode 100644 index 0000000000..6a5ec2fa72 --- /dev/null +++ b/grc/core/blocks/dummy.py @@ -0,0 +1,54 @@ +# Copyright 2016 Free Software Foundation, Inc. +# This file is part of GNU Radio +# +# GNU Radio Companion is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# GNU Radio Companion 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 + +from __future__ import absolute_import + +from . import Block, register_build_in + + +@register_build_in +class DummyBlock(Block): + + is_dummy_block = True + + label = 'Missing Block' + key = '_dummy' + + def __init__(self, parent, missing_block_id, parameters, **_): + self.key = missing_block_id + super(DummyBlock, self).__init__(parent=parent) + + param_factory = self.parent_platform.make_param + for param_id in parameters: + self.params.setdefault(param_id, param_factory(parent=self, id=param_id, dtype='string')) + + def is_valid(self): + return False + + @property + def enabled(self): + return False + + def add_missing_port(self, port_id, direction): + port = self.parent_platform.make_port( + parent=self, direction=direction, id=port_id, name='?', dtype='', + ) + if port.is_source: + self.sources.append(port) + else: + self.sinks.append(port) + return port diff --git a/grc/core/blocks/embedded_python.py b/grc/core/blocks/embedded_python.py new file mode 100644 index 0000000000..0b5a7a21c5 --- /dev/null +++ b/grc/core/blocks/embedded_python.py @@ -0,0 +1,242 @@ +# Copyright 2015-16 Free Software Foundation, Inc. +# This file is part of GNU Radio +# +# GNU Radio Companion is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# GNU Radio Companion 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 + +from __future__ import absolute_import + +from ast import literal_eval +from textwrap import dedent + +from . import Block, register_build_in +from ._templates import MakoTemplates + +from .. import utils +from ..base import Element + + +DEFAULT_CODE = '''\ +""" +Embedded Python Blocks: + +Each time 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): # other base classes are basic_block, decim_block, interp_block + """Embedded Python Block example - a simple multiply const""" + + def __init__(self, example_param=1.0): # only default arguments here + """arguments to this function show up as parameters in GRC""" + gr.sync_block.__init__( + self, + name='Embedded Python Block', # will show up in GRC + in_sig=[np.complex64], + out_sig=[np.complex64] + ) + # if an attribute with the same name as a parameter is found, + # a callback is registered (properties work, too). + self.example_param = example_param + + def work(self, input_items, output_items): + """example: multiply with constant""" + output_items[0][:] = input_items[0] * self.example_param + return len(output_items[0]) +''' + +DOC = """ +This block represents an arbitrary GNU Radio Python Block. + +Its source code can be accessed through the parameter 'Code' which opens your editor. \ +Each time you save changes in the editor, GRC will update the block. \ +This includes the number, names and defaults of the parameters, \ +the ports (stream and message) and the block name and documentation. + +Block Documentation: +(will be replaced the docstring of your block class) +""" + + +@register_build_in +class EPyBlock(Block): + + key = 'epy_block' + label = 'Python Block' + documentation = {'': DOC} + + parameters_data = [dict( + label='Code', + id='_source_code', + dtype='_multiline_python_external', + value=DEFAULT_CODE, + hide='part', + )] + inputs_data = [] + outputs_data = [] + + def __init__(self, flow_graph, **kwargs): + super(EPyBlock, self).__init__(flow_graph, **kwargs) + self.states['_io_cache'] = '' + + self._epy_source_hash = -1 + self._epy_reload_error = None + + def rewrite(self): + Element.rewrite(self) + + param_src = self.params['_source_code'] + + src = param_src.get_value() + src_hash = hash((self.name, src)) + if src_hash == self._epy_source_hash: + return + + try: + blk_io = utils.epy_block_io.extract(src) + + except Exception as e: + self._epy_reload_error = ValueError(str(e)) + try: # Load last working block io + blk_io_args = literal_eval(self.states['_io_cache']) + if len(blk_io_args) == 6: + blk_io_args += ([],) # add empty callbacks + blk_io = utils.epy_block_io.BlockIO(*blk_io_args) + except Exception: + return + else: + self._epy_reload_error = None # Clear previous errors + self.states['_io_cache'] = repr(tuple(blk_io)) + + # print "Rewriting embedded python block {!r}".format(self.name) + self._epy_source_hash = src_hash + + self.label = blk_io.name or blk_io.cls + self.documentation = {'': blk_io.doc} + + self.templates['imports'] = 'import ' + self.name + self.templates['make'] = '{mod}.{cls}({args})'.format( + mod=self.name, + cls=blk_io.cls, + args=', '.join('{0}=${{ {0} }}'.format(key) for key, _ in blk_io.params)) + self.templates['callbacks'] = [ + '{0} = ${{ {0} }}'.format(attr) for attr in blk_io.callbacks + ] + + self._update_params(blk_io.params) + self._update_ports('in', self.sinks, blk_io.sinks, 'sink') + self._update_ports('out', self.sources, blk_io.sources, 'source') + + super(EPyBlock, self).rewrite() + + def _update_params(self, params_in_src): + param_factory = self.parent_platform.make_param + params = {} + for param in list(self.params): + if hasattr(param, '__epy_param__'): + params[param.key] = param + del self.params[param.key] + + for id_, value in params_in_src: + try: + param = params[id_] + if param.default == param.value: + param.set_value(value) + param.default = str(value) + except KeyError: # need to make a new param + param = param_factory( + parent=self, id=id_, dtype='raw', value=value, + name=id_.replace('_', ' ').title(), + ) + setattr(param, '__epy_param__', True) + self.params[id_] = param + + def _update_ports(self, label, ports, port_specs, direction): + port_factory = self.parent_platform.make_port + ports_to_remove = list(ports) + iter_ports = iter(ports) + ports_new = [] + port_current = next(iter_ports, None) + for key, port_type, vlen in port_specs: + reuse_port = ( + port_current is not None and + port_current.dtype == port_type and + port_current.vlen == vlen and + (key.isdigit() or port_current.key == key) + ) + if reuse_port: + ports_to_remove.remove(port_current) + port, port_current = port_current, next(iter_ports, None) + else: + n = dict(name=label + str(key), dtype=port_type, id=key) + if port_type == 'message': + n['name'] = key + n['optional'] = '1' + if vlen > 1: + n['vlen'] = str(vlen) + port = port_factory(self, direction=direction, **n) + ports_new.append(port) + # replace old port list with new one + del ports[:] + ports.extend(ports_new) + # remove excess port connections + self.parent_flowgraph.disconnect(*ports_to_remove) + + def validate(self): + super(EPyBlock, self).validate() + if self._epy_reload_error: + self.params['_source_code'].add_error_message(str(self._epy_reload_error)) + + +@register_build_in +class EPyModule(Block): + key = 'epy_module' + label = 'Python Module' + documentation = {'': dedent(""" + This block lets you embed a python module in your flowgraph. + + Code you put in this module is accessible in other blocks using the ID of this + block. Example: + + If you put + + a = 2 + + def double(arg): + return 2 * arg + + in a Python Module Block with the ID 'stuff' you can use code like + + stuff.a # evals to 2 + stuff.double(3) # evals to 6 + + to set parameters of other blocks in your flowgraph. + """)} + + parameters_data = [dict( + label='Code', + id='source_code', + dtype='_multiline_python_external', + value='# this module will be imported in the into your flowgraph', + hide='part', + )] + + templates = MakoTemplates( + imports='import ${ id } # embedded python module', + ) diff --git a/grc/core/blocks/virtual.py b/grc/core/blocks/virtual.py new file mode 100644 index 0000000000..a10853ad1b --- /dev/null +++ b/grc/core/blocks/virtual.py @@ -0,0 +1,76 @@ +# Copyright 2016 Free Software Foundation, Inc. +# This file is part of GNU Radio +# +# GNU Radio Companion is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# GNU Radio Companion 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 + +from __future__ import absolute_import + +import itertools + +from . import Block, register_build_in + + +@register_build_in +class VirtualSink(Block): + count = itertools.count() + + key = 'virtual_sink' + label = 'Virtual Sink' + documentation = {'': ''} + + parameters_data = [dict( + label='Stream ID', + id='stream_id', + dtype='stream_id', + )] + inputs_data = [dict( + domain='stream', + dtype='' + )] + + def __init__(self, parent, **kwargs): + super(VirtualSink, self).__init__(parent, **kwargs) + self.params['id'].hide = 'all' + + @property + def stream_id(self): + return self.params['stream_id'].value + + +@register_build_in +class VirtualSource(Block): + count = itertools.count() + + key = 'virtual_source' + label = 'Virtual Source' + documentation = {'': ''} + + parameters_data = [dict( + label='Stream ID', + id='stream_id', + dtype='stream_id', + )] + outputs_data = [dict( + domain='stream', + dtype='' + )] + + def __init__(self, parent, **kwargs): + super(VirtualSource, self).__init__(parent, **kwargs) + self.params['id'].hide = 'all' + + @property + def stream_id(self): + return self.params['stream_id'].value |