diff options
-rw-r--r-- | gnuradio-runtime/python/gnuradio/gr/gateway.py | 7 | ||||
-rw-r--r-- | grc/base/Block.py | 2 | ||||
-rw-r--r-- | grc/base/Param.py | 4 | ||||
-rw-r--r-- | grc/blocks/epy_block.xml | 45 | ||||
-rw-r--r-- | grc/gui/ActionHandler.py | 16 | ||||
-rw-r--r-- | grc/gui/Actions.py | 1 | ||||
-rw-r--r-- | grc/gui/BlockTreeWindow.py | 2 | ||||
-rw-r--r-- | grc/gui/CMakeLists.txt | 1 | ||||
-rw-r--r-- | grc/gui/Constants.py | 14 | ||||
-rw-r--r-- | grc/gui/Dialogs.py | 23 | ||||
-rw-r--r-- | grc/gui/FlowGraph.py | 37 | ||||
-rw-r--r-- | grc/gui/Param.py | 119 | ||||
-rw-r--r-- | grc/gui/PropsDialog.py | 8 | ||||
-rw-r--r-- | grc/gui/external_editor.py | 93 | ||||
-rw-r--r-- | grc/python/Block.py | 96 | ||||
-rw-r--r-- | grc/python/CMakeLists.txt | 1 | ||||
-rw-r--r-- | grc/python/Generator.py | 27 | ||||
-rw-r--r-- | grc/python/Param.py | 19 | ||||
-rw-r--r-- | grc/python/epy_block_io.py | 97 |
19 files changed, 561 insertions, 51 deletions
diff --git a/gnuradio-runtime/python/gnuradio/gr/gateway.py b/gnuradio-runtime/python/gnuradio/gr/gateway.py index 2a27b8a9e0..2e46bca430 100644 --- a/gnuradio-runtime/python/gnuradio/gr/gateway.py +++ b/gnuradio-runtime/python/gnuradio/gr/gateway.py @@ -201,6 +201,13 @@ class gateway_block(object): # Save handler object in class so it's not garbage collected self.__msg_handlers[which_port] = handler + def in_sig(self): + return self.__in_sig + + def out_sig(self): + return self.__out_sig + + ######################################################################## # Wrappers for the user to inherit from ######################################################################## diff --git a/grc/base/Block.py b/grc/base/Block.py index b7918d1467..77c3145173 100644 --- a/grc/base/Block.py +++ b/grc/base/Block.py @@ -204,7 +204,7 @@ class Block(Element): block=self, n=odict({'name': 'Comment', 'key': 'comment', - 'type': 'multiline', + 'type': '_multiline', 'hide': 'part', 'value': '', 'tab': ADVANCED_PARAM_TAB diff --git a/grc/base/Param.py b/grc/base/Param.py index c2f413ccbe..b246d9f759 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 0000000000..2cd1cb5c92 --- /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/ActionHandler.py b/grc/gui/ActionHandler.py index ee01595a33..a2c48a7c52 100644 --- a/grc/gui/ActionHandler.py +++ b/grc/gui/ActionHandler.py @@ -58,9 +58,10 @@ class ActionHandler: platform: platform module """ self.clipboard = None + self.dialog = None for action in Actions.get_all_actions(): action.connect('activate', self._handle_action) #setup the main window - self.platform = platform; + self.platform = platform self.main_window = MainWindow(platform, self._handle_action) self.main_window.connect('delete-event', self._quit) self.main_window.connect('key-press-event', self._handle_key_press) @@ -425,10 +426,10 @@ class ActionHandler: elif action == Actions.BLOCK_PARAM_MODIFY: selected_block = self.get_flow_graph().get_selected_block() if selected_block: - dialog = PropsDialog(selected_block) + self.dialog = PropsDialog(selected_block) response = gtk.RESPONSE_APPLY while response == gtk.RESPONSE_APPLY: # rerun the dialog if Apply was hit - response = dialog.run() + response = self.dialog.run() if response in (gtk.RESPONSE_APPLY, gtk.RESPONSE_ACCEPT): self.get_flow_graph().update() self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data()) @@ -440,7 +441,14 @@ class ActionHandler: if response == gtk.RESPONSE_APPLY: # null action, that updates the main window Actions.ELEMENT_SELECT() - dialog.destroy() + self.dialog.destroy() + self.dialog = None + elif action == Actions.EXTERNAL_UPDATE: + self.get_page().get_state_cache().save_new_state(self.get_flow_graph().export_data()) + self.get_flow_graph().update() + if self.dialog is not None: + self.dialog.update_gui(force=True) + self.get_page().set_saved(False) ################################################## # View Parser Errors ################################################## diff --git a/grc/gui/Actions.py b/grc/gui/Actions.py index 20929344c0..c3ae6c971f 100644 --- a/grc/gui/Actions.py +++ b/grc/gui/Actions.py @@ -170,6 +170,7 @@ class ToggleAction(gtk.ToggleAction, _ActionBase): # Actions ######################################################################## PAGE_CHANGE = Action() +EXTERNAL_UPDATE = Action() FLOW_GRAPH_NEW = Action( label='_New', tooltip='Create a new flow graph', diff --git a/grc/gui/BlockTreeWindow.py b/grc/gui/BlockTreeWindow.py index 631272b03c..1cc4d9018b 100644 --- a/grc/gui/BlockTreeWindow.py +++ b/grc/gui/BlockTreeWindow.py @@ -125,7 +125,7 @@ class BlockTreeWindow(gtk.VBox): if treestore is None: treestore = self.treestore if categories is None: categories = self._categories - if isinstance(category, str): category = category.split('/') + if isinstance(category, (str, unicode)): category = category.split('/') category = tuple(filter(lambda x: x, category)) #tuple is hashable #add category and all sub categories for i, cat_name in enumerate(category): diff --git a/grc/gui/CMakeLists.txt b/grc/gui/CMakeLists.txt index 08aaf3e4df..99140df7c4 100644 --- a/grc/gui/CMakeLists.txt +++ b/grc/gui/CMakeLists.txt @@ -19,6 +19,7 @@ ######################################################################## GR_PYTHON_INSTALL(FILES + external_editor.py Block.py Colors.py Constants.py diff --git a/grc/gui/Constants.py b/grc/gui/Constants.py index 980396f85d..741c6fda95 100644 --- a/grc/gui/Constants.py +++ b/grc/gui/Constants.py @@ -17,16 +17,18 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -import pygtk +import os +import sys +import pygtk pygtk.require('2.0') import gtk -import os -import sys + from gnuradio import gr -_gr_prefs = gr.prefs() +prefs = gr.prefs() GR_PREFIX = gr.prefix() +EDITOR = prefs.get_string('grc', 'editor', '') # default path for the open/save dialogs DEFAULT_FILE_PATH = os.getcwd() @@ -49,7 +51,7 @@ DEFAULT_BLOCKS_WINDOW_WIDTH = 100 DEFAULT_REPORTS_WINDOW_WIDTH = 100 try: # ugly, but matches current code style - raw = _gr_prefs.get_string('grc', 'canvas_default_size', '1280, 1024') + raw = prefs.get_string('grc', 'canvas_default_size', '1280, 1024') DEFAULT_CANVAS_SIZE = tuple(int(x.strip('() ')) for x in raw.split(',')) if len(DEFAULT_CANVAS_SIZE) != 2 or not all(300 < x < 4096 for x in DEFAULT_CANVAS_SIZE): raise Exception() @@ -59,7 +61,7 @@ except: # flow-graph canvas fonts try: # ugly, but matches current code style - FONT_SIZE = _gr_prefs.get_long('grc', 'canvas_font_size', 8) + FONT_SIZE = prefs.get_long('grc', 'canvas_font_size', 8) if FONT_SIZE <= 0: raise Exception() except: diff --git a/grc/gui/Dialogs.py b/grc/gui/Dialogs.py index 6c01219dee..631dc0fd98 100644 --- a/grc/gui/Dialogs.py +++ b/grc/gui/Dialogs.py @@ -20,8 +20,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import pygtk pygtk.require('2.0') import gtk -import Utils -import Actions + +from . import Utils, Actions, Constants class SimpleTextDisplay(gtk.TextView): @@ -234,3 +234,22 @@ def MissingXTermDialog(xterm): "\n" "(This message is shown only once)").format(xterm) ) + + +def ChooseEditorDialog(): + file_dialog = gtk.FileChooserDialog( + 'Open a Data File...', None, + gtk.FILE_CHOOSER_ACTION_OPEN, + ('gtk-cancel', gtk.RESPONSE_CANCEL, 'gtk-open', gtk.RESPONSE_OK) + ) + file_dialog.set_select_multiple(False) + file_dialog.set_local_only(True) + file_dialog.set_current_folder('/usr/bin') + response = file_dialog.run() + + if response == gtk.RESPONSE_OK: + file_path = file_dialog.get_filename() + Constants.prefs.set_string('grc', 'editor', file_path) + Constants.prefs.save() + Constants.EDITOR = file_path + file_dialog.destroy() diff --git a/grc/gui/FlowGraph.py b/grc/gui/FlowGraph.py index fc6a711572..b27f0153db 100644 --- a/grc/gui/FlowGraph.py +++ b/grc/gui/FlowGraph.py @@ -18,12 +18,16 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ import random +import functools from itertools import chain from operator import methodcaller +import gobject + from . import Actions, Colors, Utils, Messages, Bars -from .Constants import SCROLL_PROXIMITY_SENSITIVITY, SCROLL_DISTANCE -from .Element import Element +from . Element import Element +from . Constants import SCROLL_PROXIMITY_SENSITIVITY, SCROLL_DISTANCE +from . external_editor import ExternalEditor class FlowGraph(Element): @@ -55,6 +59,35 @@ class FlowGraph(Element): self._context_menu = Bars.ContextMenu() self.get_context_menu = lambda: self._context_menu + self._external_updaters = {} + + def install_external_editor(self, param): + target = (param.get_parent().get_id(), param.get_key()) + + if target in self._external_updaters: + editor = self._external_updaters[target] + else: + updater = functools.partial( + self.handle_external_editor_change, target=target) + editor = self._external_updaters[target] = ExternalEditor( + name=target[0], value=param.get_value(), + callback=functools.partial(gobject.idle_add, updater) + ) + editor.start() + editor.open_editor() + + def handle_external_editor_change(self, new_value, target): + try: + block_id, param_key = target + self.get_block(block_id).get_param(param_key).set_value(new_value) + + except (IndexError, ValueError): # block no longer exists + self._external_updaters[target].stop() + del self._external_updaters[target] + return + Actions.EXTERNAL_UPDATE() + + ########################################################################### # Access Drawing Area ########################################################################### diff --git a/grc/gui/Param.py b/grc/gui/Param.py index ca0a8c60e5..5f83689023 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, Dialogs +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)) @@ -161,6 +173,70 @@ class MultiLineEntryParam(InputParam): pass # no tooltips for old GTK +# try: +# import gtksourceview +# lang_manager = gtksourceview.SourceLanguagesManager() +# py_lang = lang_manager.get_language_from_mime_type('text/x-python') +# +# class PythonEditorParam(InputParam): +# expand = True +# +# def __init__(self, *args, **kwargs): +# InputParam.__init__(self, *args, **kwargs) +# +# buf = self._buffer = gtksourceview.SourceBuffer() +# buf.set_language(py_lang) +# buf.set_highlight(True) +# buf.set_text(self.param.get_value()) +# buf.connect('changed', self._mark_changed) +# +# view = self._view = gtksourceview.SourceView(self._buffer) +# view.connect('focus-out-event', self._apply_change) +# view.connect('key-press-event', self._handle_key_press) +# view.set_tabs_width(4) +# view.set_insert_spaces_instead_of_tabs(True) +# view.set_auto_indent(True) +# view.set_border_width(2) +# +# scroll = gtk.ScrolledWindow() +# scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) +# scroll.add_with_viewport(view) +# self.pack_start(scroll, True) +# +# def get_text(self): +# buf = self._buffer +# return buf.get_text(buf.get_start_iter(), +# buf.get_end_iter()).strip() +# +# except ImportError: +# print "Package 'gtksourceview' not found. No Syntax highlighting." +# PythonEditorParam = MultiLineEntryParam + +class PythonEditorParam(InputParam): + + def __init__(self, *args, **kwargs): + InputParam.__init__(self, *args, **kwargs) + button = self._button = gtk.Button('Open in Editor') + button.connect('clicked', self.open_editor) + self.pack_start(button, True) + + def open_editor(self, widget=None): + if not os.path.exists(Constants.EDITOR): + Dialogs.ChooseEditorDialog() + flowgraph = self.param.get_parent().get_parent() + flowgraph.install_external_editor(self.param) + + def get_text(self): + pass # we never update the value from here + + def set_color(self, color): + # self._button.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse(color)) + self._button.modify_text(gtk.STATE_NORMAL, Colors.PARAM_ENTRY_TEXT_COLOR) + + def _apply_change(self, *args): + pass + + class EnumParam(InputParam): """Provide an entry box for Enum types with a drop down menu.""" @@ -172,7 +248,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 +275,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 +288,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 +319,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 +332,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 +383,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,9 +405,12 @@ class Param(Element): elif self.get_options(): input_widget = EnumEntryParam(self, *args, **kwargs) - elif self.get_type() == 'multiline': + elif self.get_type() == '_multiline': input_widget = MultiLineEntryParam(self, *args, **kwargs) + elif self.get_type() == '_multiline_python_external': + input_widget = PythonEditorParam(self, *args, **kwargs) + else: input_widget = EntryParam(self, *args, **kwargs) @@ -335,4 +423,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/gui/PropsDialog.py b/grc/gui/PropsDialog.py index abf242691f..f5a136e634 100644 --- a/grc/gui/PropsDialog.py +++ b/grc/gui/PropsDialog.py @@ -127,7 +127,7 @@ class PropsDialog(gtk.Dialog): # Connect events self.connect('key-press-event', self._handle_key_press) - self.connect('show', self._update_gui) + self.connect('show', self.update_gui) self.connect('response', self._handle_response) self.show_all() # show all (performs initial gui update) @@ -158,12 +158,12 @@ class PropsDialog(gtk.Dialog): # update for the block self._block.rewrite() self._block.validate() - self._update_gui() + self.update_gui() def _activate_apply(self, *args): self.set_response_sensitive(gtk.RESPONSE_APPLY, True) - def _update_gui(self, *args): + def update_gui(self, widget=None, force=False): """ Repopulate the parameters boxes (if changed). Update all the input parameters. @@ -173,7 +173,7 @@ class PropsDialog(gtk.Dialog): Hide the box if there are no docs. """ # update the params box - if self._params_changed(): + if force or self._params_changed(): # hide params box before changing for tab, label, vbox in self._params_boxes: vbox.hide_all() diff --git a/grc/gui/external_editor.py b/grc/gui/external_editor.py new file mode 100644 index 0000000000..3322556ce7 --- /dev/null +++ b/grc/gui/external_editor.py @@ -0,0 +1,93 @@ +""" +Copyright 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 +""" + +import os +import sys +import time +import threading +import tempfile +import subprocess + +import Constants + + +class ExternalEditor(threading.Thread): + + def __init__(self, name, value, callback): + threading.Thread.__init__(self) + self.daemon = True + self._stop_event = threading.Event() + + self.callback = callback + self.tempfile = self._create_tempfile(name, value) + + def _create_tempfile(self, name, value): + fp = tempfile.NamedTemporaryFile(mode='w', suffix='.py', + prefix=name + '_') + fp.write(value) + fp.flush() + return fp + + @property + def filename(self): + return self.tempfile.name + + def open_editor(self): + proc = subprocess.Popen( + args=(Constants.EDITOR, self.filename) + ) + proc.poll() + return proc + + def stop(self): + self._stop_event.set() + + def run(self): + filename = self.filename + # print "file monitor: started for", filename + last_change = os.path.getmtime(filename) + try: + while not self._stop_event.is_set(): + mtime = os.path.getmtime(filename) + if mtime > last_change: + # print "file monitor: reload trigger for", filename + last_change = mtime + with open(filename) as fp: + data = fp.read() + self.callback(data) + time.sleep(1) + + except Exception as e: + print >> sys.stderr, "file monitor crashed:", str(e) + else: + # print "file monitor: done with", filename + pass + + +if __name__ == '__main__': + def p(data): + print data + + Constants.EDITOR = '/usr/bin/gedit' + editor = ExternalEditor("test", "content", p) + editor.open_editor() + editor.start() + time.sleep(15) + editor.stop() + editor.join() diff --git a/grc/python/Block.py b/grc/python/Block.py index 5289d5765e..f5f406449e 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.get_param('_source_code').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(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 41d965e89c..3f9e273146 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 d60befe3fa..f064c7528f 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 codecs.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 27e5b76320..746f677e46 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 0000000000..8d3ce1caa1 --- /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) |