summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosh Morman <jmorman@perspectalabs.com>2019-08-30 09:47:42 -0400
committerdevnulling <devnulling@users.noreply.github.com>2020-02-10 13:33:07 -0800
commit3232eeb534b5ac1ab503f7f36254227b1cfba1f9 (patch)
treec950d0e841644ed584737e905e315d778fe26f3e
parent838d081918f8e5ee3f28273237d8b74a8c0b9d9b (diff)
grc: add python snippets to GRC
This feature adds the ability to insert arbitrary code into the python flowgraph. It gives a little more low-level flexibility for quickly modifying flowgraphs and adding custom bits of code rather than having to go and edit the generated py file One example is synchronizing multiple USRP objects - sometimes you want different sync than what is offered in the multi-usrp object, so you can put a bit of code in the snippet block to do the custom synchronization
-rw-r--r--gr-blocks/examples/CMakeLists.txt1
-rw-r--r--gr-blocks/examples/py_snippets_demo.grc290
-rw-r--r--grc/blocks/grc.tree.yml1
-rw-r--r--grc/blocks/snippet.block.yml45
-rw-r--r--grc/core/FlowGraph.py41
-rw-r--r--grc/core/base.py1
-rw-r--r--grc/core/blocks/block.py4
-rw-r--r--grc/core/generator/flow_graph.py.mako43
-rw-r--r--grc/core/generator/top_block.py2
-rw-r--r--grc/core/params/param.py10
10 files changed, 432 insertions, 6 deletions
diff --git a/gr-blocks/examples/CMakeLists.txt b/gr-blocks/examples/CMakeLists.txt
index 44e2ca0d8a..c2a9b10897 100644
--- a/gr-blocks/examples/CMakeLists.txt
+++ b/gr-blocks/examples/CMakeLists.txt
@@ -9,6 +9,7 @@ install(
FILES
matrix_multiplexer.grc
peak_detector2.grc
+ py_snippets_demo.grc
selector.grc
test_stream_mux_tags.grc
vector_source_with_tags.grc
diff --git a/gr-blocks/examples/py_snippets_demo.grc b/gr-blocks/examples/py_snippets_demo.grc
new file mode 100644
index 0000000000..ebf070b4aa
--- /dev/null
+++ b/gr-blocks/examples/py_snippets_demo.grc
@@ -0,0 +1,290 @@
+options:
+ parameters:
+ author: Josh Morman
+ catch_exceptions: 'True'
+ category: '[GRC Hier Blocks]'
+ cmake_opt: ''
+ comment: ''
+ copyright: Copyright 2020
+ description: Show simple examples of Python Snippets inserted into flowgraph
+ gen_cmake: 'On'
+ gen_linking: dynamic
+ generate_options: qt_gui
+ hier_block_src_path: '.:'
+ id: snippets_demo
+ max_nouts: '0'
+ output_language: python
+ placement: (0,0)
+ qt_qss_theme: ''
+ realtime_scheduling: ''
+ run: 'True'
+ run_command: '{python} -u {filename}'
+ run_options: prompt
+ sizing_mode: fixed
+ thread_safe_setters: ''
+ title: Python Snippets Example Flowgraph
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [8, 8]
+ rotation: 0
+ state: enabled
+
+blocks:
+- name: samp_rate
+ id: variable
+ parameters:
+ comment: ''
+ value: '32000'
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [184, 12]
+ rotation: 0
+ state: enabled
+- name: foo_mod
+ id: epy_module
+ parameters:
+ alias: ''
+ comment: ''
+ source_code: "# this module will be imported in the into your flowgraph\n\ndef\
+ \ add(a,b):\n return a+b\n"
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [850, 264]
+ rotation: 0
+ state: true
+- name: head_block
+ id: blocks_head
+ parameters:
+ affinity: ''
+ alias: ''
+ comment: ''
+ maxoutbuf: '0'
+ minoutbuf: '0'
+ num_items: '12'
+ type: float
+ vlen: '1'
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [937, 351]
+ rotation: 0
+ state: true
+- name: s0
+ id: snippet
+ parameters:
+ alias: ''
+ code: 's = ''After start - 500 priority''
+
+ print(s)'
+ comment: ''
+ priority: '500'
+ section: main_after_start
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [263, 115]
+ rotation: 0
+ state: true
+- name: s1
+ id: snippet
+ parameters:
+ alias: ''
+ code: 's = ''After start - 200 priority''
+
+ print(s)'
+ comment: ''
+ priority: '200'
+ section: main_after_start
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [264, 208]
+ rotation: 0
+ state: true
+- name: s2
+ id: snippet
+ parameters:
+ alias: ''
+ code: 's = ''After start - negative priority''
+
+ print(s)'
+ comment: ''
+ priority: '-400'
+ section: main_after_start
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [267, 298]
+ rotation: 0
+ state: true
+- name: s3
+ id: snippet
+ parameters:
+ alias: ''
+ code: 's = ''After start - 0 priority''
+
+ print(s)'
+ comment: ''
+ priority: '0'
+ section: main_after_start
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [270, 371]
+ rotation: 0
+ state: true
+- name: s4
+ id: snippet
+ parameters:
+ alias: ''
+ code: 's = ''After Init - 100 priority''
+
+ print(s)'
+ comment: ''
+ priority: '100'
+ section: main_after_init
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [549, 88]
+ rotation: 0
+ state: true
+- name: s5
+ id: snippet
+ parameters:
+ alias: ''
+ code: 'x = 4
+
+ y = foo_mod.add(x,x)
+
+ print(y)
+
+ s = ''After init - 200 priority''
+
+ print(s)'
+ comment: "This snippet shows how to call a \nmethod from an embedded python \n\
+ module"
+ priority: '200'
+ section: main_after_init
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [550, 209]
+ rotation: 0
+ state: true
+- name: s6
+ id: snippet
+ parameters:
+ alias: ''
+ code: 's = ''After Stop - 500 priority''
+
+ print(s)'
+ comment: ''
+ priority: '500'
+ section: main_after_stop
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [865, 52]
+ rotation: 0
+ state: true
+- name: s7
+ id: snippet
+ parameters:
+ alias: ''
+ code: 's = ''After Stop - 200 priority''
+
+ print(s)
+
+ print(self.vsink.data())'
+ comment: 'This snippet shows an example of calling
+
+ a method on another block from within the
+
+ flowgraph - (use at your own risk)'
+ priority: '200'
+ section: main_after_stop
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [1120, 55]
+ rotation: 0
+ state: true
+- name: throttle_block
+ id: blocks_throttle
+ parameters:
+ affinity: ''
+ alias: ''
+ comment: ''
+ ignoretag: 'True'
+ maxoutbuf: '0'
+ minoutbuf: '0'
+ samples_per_second: samp_rate
+ type: float
+ vlen: '1'
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [752, 384]
+ rotation: 0
+ state: enabled
+- name: vsink
+ id: blocks_vector_sink_x
+ parameters:
+ affinity: ''
+ alias: ''
+ comment: ''
+ reserve_items: '1024'
+ type: float
+ vlen: '1'
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [1118, 337]
+ rotation: 0
+ state: true
+- name: vsrc
+ id: blocks_vector_source_x
+ parameters:
+ affinity: ''
+ alias: ''
+ comment: ''
+ maxoutbuf: '0'
+ minoutbuf: '0'
+ repeat: 'True'
+ tags: '[]'
+ type: float
+ vector: list(range(10))
+ vlen: '1'
+ states:
+ bus_sink: false
+ bus_source: false
+ bus_structure: null
+ coordinate: [531, 390]
+ rotation: 0
+ state: true
+
+connections:
+- [head_block, '0', vsink, '0']
+- [throttle_block, '0', head_block, '0']
+- [vsrc, '0', throttle_block, '0']
+
+metadata:
+ file_format: 1
diff --git a/grc/blocks/grc.tree.yml b/grc/blocks/grc.tree.yml
index f27fb6ae47..fc2fded9ab 100644
--- a/grc/blocks/grc.tree.yml
+++ b/grc/blocks/grc.tree.yml
@@ -13,6 +13,7 @@
- epy_module
- note
- import
+ - snippet
- Variables:
- variable
- variable_struct
diff --git a/grc/blocks/snippet.block.yml b/grc/blocks/snippet.block.yml
new file mode 100644
index 0000000000..17d8da0ff8
--- /dev/null
+++ b/grc/blocks/snippet.block.yml
@@ -0,0 +1,45 @@
+id: snippet
+label: Python Snippet
+flags: [ python ]
+
+parameters:
+- id: section
+ label: Section of Flowgraph
+ dtype: string
+ options: ['main_after_init', 'main_after_start', 'main_after_stop' ]
+ option_labels: ['Main - After Init', 'Main - After Start', 'Main - After Stop']
+- id: priority
+ label: Priority
+ dtype: int
+ hide: ${'part' if priority <= 0 else 'none'}
+- id: code
+ label: Code Snippet
+ dtype: _multiline
+
+templates:
+ var_make: ${code}
+
+documentation: |-
+ CAUTION: This is an ADVANCED feature and can lead to unintended consequences in the rendering of a flowgraph. Use at your own risk.
+
+ Insert a snippet of Python code directly into the flowgraph at the end of the specified section. \
+ For each snippet a function is generated with the block name of the snippet (use GRC Show Block IDs option to modify). These functions are\
+ then grouped into their respective sections in the rendered flowgraph.
+
+ The purpose of the python snippets is to be able to exercise features from within GRC that are not entirely supported by the block callbacks, \
+ methods and mechanisms to generate the code. One example of this would be calling UHD timed commands before starting the flowgraph
+
+ Indents will be handled upon insertion into the python flowgraph
+
+ Example 1:
+ epy_mod_0.some_function(self.some_block.some_property)
+
+ Will place the function call in the generated .py file using the name of the appropriate embedded python block in the proper scope
+ The scope is relative to the blocks in the flowgraph, e.g. to reference a block, it should be identified as self.block
+
+ Example 2:
+ print('The flowgraph has been stopped')
+
+ With section selected as 'Main - After Stop', will place the print statement after the flowgraph has been stopped.
+
+file_format: 1
diff --git a/grc/core/FlowGraph.py b/grc/core/FlowGraph.py
index 21c3cdb59d..b0141f8705 100644
--- a/grc/core/FlowGraph.py
+++ b/grc/core/FlowGraph.py
@@ -81,6 +81,47 @@ class FlowGraph(Element):
parameters = [b for b in self.iter_enabled_blocks() if b.key == 'parameter']
return parameters
+ def get_snippets(self):
+ """
+ Get a set of all code snippets (Python) in this flow graph namespace.
+
+ Returns:
+ a list of code snippets
+ """
+ return [b for b in self.iter_enabled_blocks() if b.key == 'snippet']
+
+ def get_snippets_dict(self, section=None):
+ """
+ Get a dictionary of code snippet information for a particular section.
+
+ Args:
+ section: string specifier of section of snippets to return, section=None returns all
+
+ Returns:
+ a list of code snippets dicts
+ """
+ snippets = self.get_snippets()
+ if not snippets:
+ return []
+
+ output = []
+ for snip in snippets:
+ d ={}
+ sect = snip.params['section'].value
+ d['section'] = sect
+ d['priority'] = snip.params['priority'].value
+ d['lines'] = snip.params['code'].value.splitlines()
+ d['def'] = 'def snipfcn_{}(self):'.format(snip.name)
+ d['call'] = 'snipfcn_{}(tb)'.format(snip.name)
+ if not section or sect == section:
+ output.append(d)
+
+ # Sort by descending priority
+ if section:
+ output = sorted(output, key=lambda x: x['priority'], reverse=True)
+
+ return output
+
def get_monitors(self):
"""
Get a list of all ControlPort monitors
diff --git a/grc/core/base.py b/grc/core/base.py
index dae9ea229b..a5de8837d6 100644
--- a/grc/core/base.py
+++ b/grc/core/base.py
@@ -139,6 +139,7 @@ class Element(object):
is_param = False
is_variable = False
is_import = False
+ is_snippet = False
def get_raw(self, name):
descriptor = getattr(self.__class__, name, None)
diff --git a/grc/core/blocks/block.py b/grc/core/blocks/block.py
index ff1c8befc3..e51b730cdd 100644
--- a/grc/core/blocks/block.py
+++ b/grc/core/blocks/block.py
@@ -293,6 +293,10 @@ class Block(Element):
def is_import(self):
return self.key == 'import'
+ @lazy_property
+ def is_snippet(self):
+ return self.key == 'snippet'
+
@property
def comment(self):
return self.params['comment'].value
diff --git a/grc/core/generator/flow_graph.py.mako b/grc/core/generator/flow_graph.py.mako
index 7e731d1a9d..3635c550db 100644
--- a/grc/core/generator/flow_graph.py.mako
+++ b/grc/core/generator/flow_graph.py.mako
@@ -42,6 +42,7 @@ if __name__ == '__main__':
##${imp.replace(" # grc-generated hier_block", "")}
${imp}
% endfor
+
########################################################
##Create Class
## Write the class declaration for a top or hier block.
@@ -219,6 +220,7 @@ gr.io_signaturev(${len(io_sigs)}, ${len(io_sigs)}, [${', '.join(size_strs)}])\
${ connection.rstrip() }
% endfor
% endif
+
########################################################
## QT sink close method reimplementation
########################################################
@@ -266,6 +268,32 @@ gr.io_signaturev(${len(io_sigs)}, ${len(io_sigs)}, [${', '.join(size_strs)}])\
% endfor
% endif
% endfor
+\
+% for snip in flow_graph.get_snippets_dict():
+
+${indent(snip['def'])}
+% for line in snip['lines']:
+ ${indent(line)}
+% endfor
+% endfor
+\
+<%
+snippet_sections = ['main_after_init', 'main_after_start', 'main_after_stop']
+snippets = {}
+for section in snippet_sections:
+ snippets[section] = flow_graph.get_snippets_dict(section)
+%>
+\
+%for section in snippet_sections:
+%if snippets[section]:
+
+def snippets_${section}(tb):
+ % for snip in snippets[section]:
+ ${indent(snip['call'])}
+ % endfor
+%endif
+%endfor
+
########################################################
##Create Main
## For top block code, generate a main routine.
@@ -335,9 +363,11 @@ def main(top_block_cls=${class_name}, options=None):
qapp = Qt.QApplication(sys.argv)
tb = top_block_cls(${ ', '.join(params_eq_list) })
+ ${'snippets_main_after_init(tb)' if snippets['main_after_init'] else ''}
% if flow_graph.get_option('run'):
tb.start(${flow_graph.get_option('max_nouts') or ''})
% endif
+ ${'snippets_main_after_start(tb)' if snippets['main_after_start'] else ''}
% if flow_graph.get_option('qt_qss_theme'):
tb.setStyleSheetFromFile("${ flow_graph.get_option('qt_qss_theme') }")
% endif
@@ -356,6 +386,7 @@ def main(top_block_cls=${class_name}, options=None):
def quitting():
tb.stop()
tb.wait()
+ ${'snippets_main_after_stop(tb)' if snippets['main_after_stop'] else ''}
qapp.aboutToQuit.connect(quitting)
% for m in monitors:
% if m.params['en'].get_value() == 'True':
@@ -368,6 +399,7 @@ def main(top_block_cls=${class_name}, options=None):
def killProc(signum, frame, tb):
tb.stop()
tb.wait()
+ ${'snippets_main_after_stop(tb)' if snippets['main_after_stop'] else ''}
serverProc.terminate()
serverProc.kill()
time.sleep(1)
@@ -383,20 +415,23 @@ def main(top_block_cls=${class_name}, options=None):
url = "http://localhost:" + port + "/bokehgui")
# Create Top Block instance
tb = top_block_cls(doc)
+ ${'snippets_main_after_init(tb)' if snippets['main_after_init'] else ''}
try:
tb.start()
+ ${'snippets_main_after_start(tb)' if snippets['main_after_start'] else ''}
signal.signal(signal.SIGTERM, functools.partial(killProc, tb=tb))
session.loop_until_closed()
finally:
print("Exiting the simulation. Stopping Bokeh Server")
tb.stop()
tb.wait()
+ ${'snippets_main_after_stop(tb)' if snippets['main_after_stop'] else ''}
finally:
serverProc.terminate()
serverProc.kill()
% elif generate_options == 'no_gui':
tb = top_block_cls(${ ', '.join(params_eq_list) })
-
+ ${'snippets_main_after_init(tb)' if snippets['main_after_init'] else ''}
def sig_handler(sig=None, frame=None):
% for m in monitors:
% if m.params['en'].get_value() == 'True':
@@ -405,6 +440,7 @@ def main(top_block_cls=${class_name}, options=None):
% endfor
tb.stop()
tb.wait()
+ ${'snippets_main_after_stop(tb)' if snippets['main_after_stop'] else ''}
sys.exit(0)
signal.signal(signal.SIGINT, sig_handler)
@@ -412,6 +448,7 @@ def main(top_block_cls=${class_name}, options=None):
% if flow_graph.get_option('run_options') == 'prompt':
tb.start(${ flow_graph.get_option('max_nouts') or '' })
+ ${'snippets_main_after_start(tb)' if snippets['main_after_start'] else ''}
% for m in monitors:
% if m.params['en'].get_value() == 'True':
tb.${m.name}.start()
@@ -422,8 +459,10 @@ def main(top_block_cls=${class_name}, options=None):
except EOFError:
pass
tb.stop()
+ ## ${'snippets_main_after_stop(tb)' if snippets['main_after_stop'] else ''}
% elif flow_graph.get_option('run_options') == 'run':
tb.start(${flow_graph.get_option('max_nouts') or ''})
+ ${'snippets_main_after_start(tb)' if snippets['main_after_start'] else ''}
% for m in monitors:
% if m.params['en'].get_value() == 'True':
tb.${m.name}.start()
@@ -431,6 +470,7 @@ def main(top_block_cls=${class_name}, options=None):
% endfor
% endif
tb.wait()
+ ${'snippets_main_after_stop(tb)' if snippets['main_after_stop'] else ''}
% for m in monitors:
% if m.params['en'].get_value() == 'True':
tb.${m.name}.stop()
@@ -438,7 +478,6 @@ def main(top_block_cls=${class_name}, options=None):
% endfor
% endif
-
if __name__ == '__main__':
main()
% endif
diff --git a/grc/core/generator/top_block.py b/grc/core/generator/top_block.py
index 27d2428888..bf7ebcc224 100644
--- a/grc/core/generator/top_block.py
+++ b/grc/core/generator/top_block.py
@@ -191,7 +191,7 @@ class TopBlockGenerator(object):
blocks = [
b for b in fg.blocks
- if b.enabled and not (b.get_bypassed() or b.is_import or b in parameters or b.key == 'options')
+ if b.enabled and not (b.get_bypassed() or b.is_import or b.is_snippet or b in parameters or b.key == 'options')
]
blocks = expr_utils.sort_objects(blocks, operator.attrgetter('name'), _get_block_sort_text)
diff --git a/grc/core/params/param.py b/grc/core/params/param.py
index 816588efe2..ef8d7df291 100644
--- a/grc/core/params/param.py
+++ b/grc/core/params/param.py
@@ -245,9 +245,13 @@ class Param(Element):
elif dtype in ('string', 'file_open', 'file_save', '_multiline', '_multiline_python_external'):
# Do not check if file/directory exists, that is a runtime issue
try:
- value = self.parent_flowgraph.evaluate(expr)
- if not isinstance(value, str):
- raise Exception()
+ # Do not evaluate multiline strings (code snippets or comments)
+ if dtype not in ['_multiline','_multiline_python_external']:
+ value = self.parent_flowgraph.evaluate(expr)
+ if not isinstance(value, str):
+ raise Exception()
+ else:
+ value = str(expr)
except Exception:
self._stringify_flag = True
value = str(expr)