summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gnuradio-runtime/python/gnuradio/gr/gateway.py7
-rw-r--r--grc/base/Block.py2
-rw-r--r--grc/base/Param.py4
-rw-r--r--grc/blocks/epy_block.xml45
-rw-r--r--grc/gui/ActionHandler.py16
-rw-r--r--grc/gui/Actions.py1
-rw-r--r--grc/gui/BlockTreeWindow.py2
-rw-r--r--grc/gui/CMakeLists.txt1
-rw-r--r--grc/gui/Constants.py14
-rw-r--r--grc/gui/Dialogs.py23
-rw-r--r--grc/gui/FlowGraph.py37
-rw-r--r--grc/gui/Param.py119
-rw-r--r--grc/gui/PropsDialog.py8
-rw-r--r--grc/gui/external_editor.py93
-rw-r--r--grc/python/Block.py96
-rw-r--r--grc/python/CMakeLists.txt1
-rw-r--r--grc/python/Generator.py27
-rw-r--r--grc/python/Param.py19
-rw-r--r--grc/python/epy_block_io.py97
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)