diff options
author | Josh Morman <jmorman@perspectalabs.com> | 2019-08-30 09:47:42 -0400 |
---|---|---|
committer | devnulling <devnulling@users.noreply.github.com> | 2020-02-10 13:33:07 -0800 |
commit | 3232eeb534b5ac1ab503f7f36254227b1cfba1f9 (patch) | |
tree | c950d0e841644ed584737e905e315d778fe26f3e | |
parent | 838d081918f8e5ee3f28273237d8b74a8c0b9d9b (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.txt | 1 | ||||
-rw-r--r-- | gr-blocks/examples/py_snippets_demo.grc | 290 | ||||
-rw-r--r-- | grc/blocks/grc.tree.yml | 1 | ||||
-rw-r--r-- | grc/blocks/snippet.block.yml | 45 | ||||
-rw-r--r-- | grc/core/FlowGraph.py | 41 | ||||
-rw-r--r-- | grc/core/base.py | 1 | ||||
-rw-r--r-- | grc/core/blocks/block.py | 4 | ||||
-rw-r--r-- | grc/core/generator/flow_graph.py.mako | 43 | ||||
-rw-r--r-- | grc/core/generator/top_block.py | 2 | ||||
-rw-r--r-- | grc/core/params/param.py | 10 |
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) |