main - feat: copy GRC from latest stable GNURadio

This commit is contained in:
Yoel Bassin 2025-04-25 16:36:21 +03:00
commit 1c3acd7922
209 changed files with 31325 additions and 0 deletions

4
grc/00-grc-docs.conf.in Normal file
View File

@ -0,0 +1,4 @@
# This should link to the wiki page-per-block documentation, when the block label (e.g., FFT) is appended to the end of the string
[grc-docs]
wiki_block_docs_url_prefix = @GRC_DOCS_URL_PREFIX@

194
grc/CMakeLists.txt Normal file
View File

@ -0,0 +1,194 @@
# Copyright 2011,2013,2017 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
########################################################################
# Setup dependencies
########################################################################
include(GrPython)
message(STATUS "")
GR_PYTHON_CHECK_MODULE_RAW(
"PyYAML >= 3.11"
"import yaml; assert yaml.__version__ >= '3.11'"
PYYAML_FOUND
)
GR_PYTHON_CHECK_MODULE(
"mako >= ${GR_MAKO_MIN_VERSION}"
mako
"LooseVersion(mako.__version__) >= LooseVersion('${GR_MAKO_MIN_VERSION}')"
MAKO_FOUND
)
GR_PYTHON_CHECK_MODULE_RAW(
"pygobject >= 2.28.6"
"import gi; assert gi.version_info >= (2, 28, 6)"
PYGI_FOUND
)
GR_PYTHON_CHECK_MODULE_RAW(
"Gtk (GI) >= 3.10.8"
"import gi; gi.require_version('Gtk', '3.0'); \
from gi.repository import Gtk; Gtk.check_version(3, 10, 8)"
GTK_GI_FOUND
)
GR_PYTHON_CHECK_MODULE_RAW(
"Cairo (GI) >= 1.0"
"import gi; gi.require_foreign('cairo', 'Context')" # Cairo 1.13.0
CAIRO_GI_FOUND
)
GR_PYTHON_CHECK_MODULE_RAW(
"PangoCairo (GI) >= 1.0"
"import gi; gi.require_version('PangoCairo', '1.0')" # pangocairo 1.36.3
PANGOCAIRO_GI_FOUND
)
GR_PYTHON_CHECK_MODULE_RAW(
"numpy"
"import numpy"
NUMPY_FOUND
)
GR_PYTHON_CHECK_MODULE_RAW(
"jsonschema"
"import jsonschema"
JSONSCHEMA_FOUND
)
########################################################################
# Register component
########################################################################
include(GrComponent)
if(NOT CMAKE_CROSSCOMPILING)
set(grc_python_deps
PYYAML_FOUND
MAKO_FOUND
PYGI_FOUND
GTK_GI_FOUND
CAIRO_GI_FOUND
PANGOCAIRO_GI_FOUND
NUMPY_FOUND
)
endif(NOT CMAKE_CROSSCOMPILING)
GR_REGISTER_COMPONENT("gnuradio-companion" ENABLE_GRC
ENABLE_GNURADIO_RUNTIME
ENABLE_PYTHON
${grc_python_deps}
)
if(ENABLE_GRC)
GR_REGISTER_COMPONENT("JSON/YAML config blocks" ENABLE_JSONYAML_BLOCKS
ENABLE_GRC
JSONSCHEMA_FOUND
)
########################################################################
# Create and install the grc and grc-docs conf file
########################################################################
file(TO_NATIVE_PATH ${CMAKE_INSTALL_PREFIX}/${GRC_BLOCKS_DIR} blocksdir)
file(TO_NATIVE_PATH ${CMAKE_INSTALL_PREFIX}/${GRC_EXAMPLES_DIR} examplesdir)
if(CMAKE_INSTALL_PREFIX STREQUAL "/usr")
# linux binary installs: append blocks and examples dir with prefix /usr/local
set(blocksdir ${blocksdir}:/usr/local/${GRC_BLOCKS_DIR})
set(examplesdir ${examplesdir}:/usr/local/${GRC_EXAMPLES_DIR})
endif(CMAKE_INSTALL_PREFIX STREQUAL "/usr")
if(UNIX)
find_program(GRC_XTERM_EXE
NAMES x-terminal-emulator gnome-terminal konsole xfce4-terminal urxvt xterm foot
HINTS ENV PATH
DOC "graphical terminal emulator used in GRC's no-gui-mode"
)
if(NOT GRC_XTERM_EXE)
set(GRC_XTERM_EXE "x-terminal-emulator")
endif()
else() # APPLE CYGWIN
set(GRC_XTERM_EXE "xterm")
endif()
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/grc.conf.in
${CMAKE_CURRENT_BINARY_DIR}/grc.conf
@ONLY)
install(
FILES ${CMAKE_CURRENT_BINARY_DIR}/grc.conf
DESTINATION ${GR_PREFSDIR}
)
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/00-grc-docs.conf.in
${CMAKE_CURRENT_BINARY_DIR}/00-grc-docs.conf
@ONLY)
install(
FILES ${CMAKE_CURRENT_BINARY_DIR}/00-grc-docs.conf
DESTINATION ${GR_PREFSDIR}
)
########################################################################
# Install (+ compile) python sources and data files
########################################################################
file(GLOB py_files "*.py")
GR_PYTHON_INSTALL(
FILES ${py_files}
DESTINATION "${GR_PYTHON_DIR}/gnuradio/grc"
)
GR_PYTHON_INSTALL(
DIRECTORY core
DESTINATION "${GR_PYTHON_DIR}/gnuradio/grc"
FILES_MATCHING REGEX "\\.(py|dtd|grc|tmpl|png|mako)$"
)
GR_PYTHON_INSTALL(
DIRECTORY gui
DESTINATION "${GR_PYTHON_DIR}/gnuradio/grc"
FILES_MATCHING REGEX "\\.(py|dtd|grc|tmpl|png|mako)$"
)
GR_PYTHON_INSTALL(
DIRECTORY gui_qt
DESTINATION "${GR_PYTHON_DIR}/gnuradio/grc"
FILES_MATCHING REGEX "\\.(py|yml|grc|mo|png|ui)$"
)
GR_PYTHON_INSTALL(
DIRECTORY converter
DESTINATION "${GR_PYTHON_DIR}/gnuradio/grc"
FILES_MATCHING REGEX "\\.(py|dtd|grc|tmpl|png|mako)$"
)
########################################################################
# Append NSIS commands to set environment variables
########################################################################
if(WIN32)
file(TO_NATIVE_PATH ${GR_PKG_DOC_DIR} GR_DOC_DIR)
string(REPLACE "\\" "\\\\" GR_DOC_DIR ${GR_DOC_DIR})
file(TO_NATIVE_PATH ${GRC_BLOCKS_DIR} GRC_BLOCKS_PATH)
string(REPLACE "\\" "\\\\" GRC_BLOCKS_PATH ${GRC_BLOCKS_PATH})
file(TO_NATIVE_PATH ${GR_PYTHON_DIR} GR_PYTHON_POSTFIX)
string(REPLACE "\\" "\\\\" GR_PYTHON_POSTFIX ${GR_PYTHON_POSTFIX})
endif(WIN32)
########################################################################
# Add subdirectories
########################################################################
add_subdirectory(blocks)
add_subdirectory(scripts)
add_subdirectory(tests)
endif(ENABLE_GRC)

0
grc/__init__.py Normal file
View File

9
grc/__main__.py Normal file
View File

@ -0,0 +1,9 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from .main import main
main()

1
grc/blocks/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
variable_struct.xml

48
grc/blocks/CMakeLists.txt Normal file
View File

@ -0,0 +1,48 @@
# Copyright 2011,2018 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
########################################################################
include(GrPython)
file(GLOB yml_files "*.yml")
macro(REPLACE_IN_FILE _yml_block match replace)
set(yml_block_src "${CMAKE_CURRENT_SOURCE_DIR}/${_yml_block}")
set(yml_block "${CMAKE_CURRENT_BINARY_DIR}/${_yml_block}")
list(REMOVE_ITEM yml_files "${yml_block_src}")
file(READ "${yml_block_src}" yml_block_src_text)
string(REPLACE "${match}" "${replace}"
yml_block_text "${yml_block_src_text}")
file(WRITE "${yml_block}" "${yml_block_text}")
list(APPEND generated_yml_files "${yml_block}")
endmacro()
macro(GEN_BLOCK_YML _generator _yml_block)
set(generator ${CMAKE_CURRENT_SOURCE_DIR}/${_generator})
set(yml_block ${CMAKE_CURRENT_BINARY_DIR}/${_yml_block})
list(APPEND generated_yml_files ${yml_block})
add_custom_command(
DEPENDS ${generator} OUTPUT ${yml_block}
COMMAND ${PYTHON_EXECUTABLE} ${generator} ${yml_block}
)
endmacro()
GEN_BLOCK_YML(variable_struct.block.yml.py variable_struct.block.yml)
add_custom_target(grc_generated_yml ALL DEPENDS ${generated_yml_files})
if(NOT ENABLE_JSONYAML_BLOCKS)
list(REMOVE_ITEM yml_files "json_config.block.yml")
list(REMOVE_ITEM yml_files "yaml_config.block.yml")
endif()
install(
FILES ${yml_files} ${generated_yml_files}
DESTINATION ${GRC_BLOCKS_DIR}
)

24
grc/blocks/grc.tree.yml Normal file
View File

@ -0,0 +1,24 @@
'[Core]':
- Misc:
- options
- pad_source
- pad_sink
- virtual_source
- virtual_sink
- bus_sink
- bus_source
- bus_structure_sink
- bus_structure_source
- epy_block
- epy_module
- note
- import
- snippet
- Variables:
- variable
- variable_struct
- variable_config
- variable_function_probe
- json_config
- yaml_config
- parameter

View File

@ -0,0 +1,21 @@
id: import_
label: Import
flags: [ python ]
parameters:
- id: imports
label: Import
dtype: import
templates:
imports: ${imports}
documentation: |-
Import additional python modules into the namespace.
Examples:
from gnuradio.filter import firdes
import math,cmath
from math import pi
file_format: 1

View File

@ -0,0 +1,58 @@
id: json_config
label: JSON Config
flags: [ show_id, python ]
parameters:
- id: config_file
label: Config File
dtype: file_open
default: ""
- id: schema_file
label: Config Schema
dtype: file_open
default: ""
# Not my favorite thing because it doesn't explicitly close the file, but
# it should be okay. The garbage collector will take care of it.
value: "${ json.load(open(config_file)) }"
asserts:
- ${ schema_file == "" or validate(json.load(open(config_file)), json.load(open(schema_file))) is None }
templates:
imports: |-
import json
from jsonschema import validate
# Note that yaml makes it really hard to insert spaces at the beginning of the line.
# The "\" char below is used to escape the space at the beginning of a line.
var_make: "with open(${config_file}) as fid:\n\
\ self.${id} = ${id} = json.load(fid)\n\
self.${id}_schema = ${id}_schema = ${schema_file}\n\
if ${id}_schema:\n\
\ with open(${id}_schema) as fid:\n\
\ validate(${id}, json.load(fid))"
documentation: |-
This block represents a json config file that is read in as a dictionary.
The values can be used directly when instantiating blocks. For example,
Sample Rate: yaml_config["samp_rate"]
Optionally, a json schema can be specified to validate the configuration.
For example, you could have a json file that contains:
{
"samp_rate": 1e6,
}
And a schema that contains
{
"type": "object",
"properties": {
"samp_rate": {"type": "number", "exclusiveMinimum": 0}
}
}
If the id of this block is json_config_0, then you can access the samp rate
in other blocks as json_config_0["samp_rate"]
file_format: 1

View File

@ -0,0 +1,12 @@
id: message
label: Message
color: "#FFFFFF"
multiple_connections_per_input: true
multiple_connections_per_output: true
templates:
- type: [message, message]
connect: self.msg_connect(${ make_port_sig(source) }, ${ make_port_sig(sink) })
cpp_connect: hier_block2::msg_connect(${ make_port_sig(source) }, ${ make_port_sig(sink) })

10
grc/blocks/note.block.yml Normal file
View File

@ -0,0 +1,10 @@
id: note
label: Note
flags: [ python, cpp ]
parameters:
- id: note
label: Note
dtype: string
file_format: 1

View File

@ -0,0 +1,179 @@
id: options
label: Options
flags: ['python', 'cpp']
parameters:
- id: title
label: Title
dtype: string
hide: ${ ('none' if title else 'part') }
- id: author
label: Author
dtype: string
hide: ${ ('none' if author else 'part') }
- id: copyright
label: Copyright
dtype: string
hide: ${ ('none' if copyright else 'part') }
- id: description
label: Description
dtype: string
hide: ${ ('none' if description else 'part') }
- id: output_language
label: Output Language
dtype: enum
default: python
options: [python, cpp]
option_labels: [Python, C++]
- id: generate_options
label: Generate Options
dtype: enum
default: qt_gui
options: [qt_gui, bokeh_gui, no_gui, hb, hb_qt_gui]
option_labels: [QT GUI, Bokeh GUI, No GUI, Hier Block, Hier Block (QT GUI)]
- id: gen_linking
label: Linking
dtype: enum
default: dynamic
options: [dynamic, static]
option_labels: [ Dynamic, Static ]
hide: 'all'
- id: gen_cmake
label: Generate CMakeLists.txt
dtype: enum
default: 'On'
options: ['On', 'Off']
hide: ${ ('part' if output_language == 'cpp' else 'all') }
- id: cmake_opt
label: CMake options
dtype: string
default: ''
hide: ${ ('part' if output_language == 'cpp' else 'all') }
- id: category
label: Category
dtype: string
default: '[GRC Hier Blocks]'
hide: ${ ('none' if generate_options.startswith('hb') else 'all') }
- id: run_options
label: Run Options
dtype: enum
default: prompt
options: [run, prompt]
option_labels: [Run to Completion, Prompt for Exit]
hide: ${ ('none' if generate_options == 'no_gui' else 'all') }
- id: placement
label: Widget Placement
dtype: int_vector
default: (0,0)
hide: ${ ('part' if generate_options == 'bokeh_gui' else 'all') }
- id: window_size
label: Window size
dtype: int_vector
default: (1000,1000)
hide: ${ ('part' if generate_options == 'bokeh_gui' else 'all') }
- id: sizing_mode
label: Sizing Mode
dtype: enum
default: fixed
options: [fixed, stretch_both, scale_width, scale_height, scale_both]
option_labels: [Fixed, Stretch Both, Scale Width, Scale Height, Scale Both]
hide: ${ ('part' if generate_options == 'bokeh_gui' else 'all') }
- id: run
label: Run
dtype: bool
default: 'True'
options: ['True', 'False']
option_labels: [Autostart, 'Off']
hide: ${ ('all' if generate_options not in ('qt_gui', 'bokeh_gui') else ('part'
if run else 'none')) }
- id: max_nouts
label: Max Number of Output
dtype: int
default: '0'
hide: ${ ('all' if generate_options.startswith('hb') else ('none' if max_nouts
else 'part')) }
- id: realtime_scheduling
label: Realtime Scheduling
dtype: enum
options: ['', '1']
option_labels: ['Off', 'On']
hide: ${ ('all' if generate_options.startswith('hb') else ('none' if realtime_scheduling
else 'part')) }
- id: qt_qss_theme
label: QSS Theme
dtype: file_open
hide: ${ ('all' if generate_options != 'qt_gui' else ('none' if qt_qss_theme else
'part')) }
- id: thread_safe_setters
label: Thread-safe setters
category: Advanced
dtype: enum
options: ['', '1']
option_labels: ['Off', 'On']
hide: part
- id: catch_exceptions
label: Catch Block Exceptions
category: Advanced
dtype: enum
options: ['False', 'True']
option_labels: ['Off', 'On']
default: 'True'
hide: part
- id: run_command
label: Run Command
category: Advanced
dtype: string
default: '{python} -u {filename}'
hide: ${ ('all' if generate_options.startswith('hb') else 'part') }
- id: hier_block_src_path
label: Hier Block Source Path
category: Advanced
dtype: string
default: '.:'
hide: part
asserts:
- ${ len(placement) == 4 or len(placement) == 2 }
- ${ all(i >= 0 for i in placement) }
templates:
imports: |-
from gnuradio import gr
from gnuradio.filter import firdes
from gnuradio.fft import window
import sys
import signal
% if generate_options == 'qt_gui':
from PyQt5 import Qt
% endif
% if not generate_options.startswith('hb'):
from argparse import ArgumentParser
from gnuradio.eng_arg import eng_float, intx
from gnuradio import eng_notation
% endif
callbacks:
- 'if ${run}: self.start()
else: self.stop(); self.wait()'
cpp_templates:
includes: ['#include <gnuradio/top_block.h>']
documentation: |-
The options block sets special parameters for the flow graph. Only one option block is allowed per flow graph.
Title, author, and description parameters are for identification purposes.
The window size controls the dimensions of the flow graph editor. The window size (width, height) must be between (300, 300) and (4096, 4096).
The generate options controls the type of code generated. Non-graphical flow graphs should avoid using graphical sinks or graphical variable controls.
In a graphical application, run can be controlled by a variable to start and stop the flowgraph at runtime.
The id of this block determines the name of the generated file and the name of the class. For example, an id of my_block will generate the file my_block.py and class my_block(gr....
The category parameter determines the placement of the block in the block selection window. The category only applies when creating hier blocks. To put hier blocks into the root category, enter / for the category.
The Max Number of Output is the maximum number of output items allowed for any block in the flowgraph; to disable this set the max_nouts equal to 0.Use this to adjust the maximum latency a flowgraph can exhibit.
file_format: 1

View File

@ -0,0 +1,56 @@
id: pad_sink
label: Pad Sink
flags: [ python, cpp ]
parameters:
- id: label
label: Label
dtype: string
default: out
- id: type
label: Input Type
dtype: enum
options: [complex, float, int, short, byte, bit, message, '']
option_labels: [Complex, Float, Int, Short, Byte, Bits, Message, Wildcard]
option_attributes:
size: [gr.sizeof_gr_complex, gr.sizeof_float, gr.sizeof_int, gr.sizeof_short,
gr.sizeof_char, gr.sizeof_char, '0', '0']
cpp_size: [sizeof(gr_complex), sizeof(float), sizeof(int), sizeof(short),
sizeof(char), sizeof(char), '0', '0']
hide: part
- id: vlen
label: Vector Length
dtype: int
default: '1'
hide: ${ 'part' if vlen == 1 else 'none' }
- id: num_streams
label: Num Streams
dtype: int
default: '1'
hide: part
- id: optional
label: Optional
dtype: bool
default: 'False'
options: ['True', 'False']
option_labels: [Optional, Required]
hide: part
inputs:
- domain: stream
dtype: ${ type }
vlen: ${ vlen }
multiplicity: ${ num_streams }
optional: ${optional}
asserts:
- ${ vlen > 0 }
- ${ num_streams > 0 }
documentation: |-
The inputs of this block will become the outputs to this flow graph when it is instantiated as a hierarchical block.
Pad sink will be ordered alphabetically by their ids. The first pad sink will have an index of 0.
file_format: 1

View File

@ -0,0 +1,56 @@
id: pad_source
label: Pad Source
flags: [ python, cpp ]
parameters:
- id: label
label: Label
dtype: string
default: in
- id: type
label: Output Type
dtype: enum
options: [complex, float, int, short, byte, bit, message, '']
option_labels: [Complex, Float, Int, Short, Byte, Bits, Message, Wildcard]
option_attributes:
size: [gr.sizeof_gr_complex, gr.sizeof_float, gr.sizeof_int, gr.sizeof_short,
gr.sizeof_char, gr.sizeof_char, '0', '0']
cpp_size: [sizeof(gr_complex), sizeof(float), sizeof(int), sizeof(short),
sizeof(char), sizeof(char), '0', '0']
hide: part
- id: vlen
label: Vector Length
dtype: int
default: '1'
hide: ${ 'part' if vlen == 1 else 'none' }
- id: num_streams
label: Num Streams
dtype: int
default: '1'
hide: part
- id: optional
label: Optional
dtype: bool
default: 'False'
options: ['True', 'False']
option_labels: [Optional, Required]
hide: part
outputs:
- domain: stream
dtype: ${ type }
vlen: ${ vlen }
multiplicity: ${ num_streams }
optional: ${optional}
asserts:
- ${ vlen > 0 }
- ${ num_streams > 0 }
documentation: |-
The outputs of this block will become the inputs to this flow graph when it is instantiated as a hierarchical block.
Pad sources will be ordered alphabetically by their ids. The first pad source will have an index of 0.
file_format: 1

View File

@ -0,0 +1,60 @@
id: parameter
label: Parameter
flags: [ show_id, python, cpp ]
parameters:
- id: label
label: Label
dtype: string
hide: ${ ('none' if label else 'part') }
- id: type
label: Type
dtype: enum
options: ['', complex, eng_float, intx, long, str]
option_labels: [None, Complex, Float, Int, Long, String]
option_attributes:
type: [raw, complex, real, int, int, string]
hide: ${ ('none' if type else 'part') }
- id: value
label: Value
dtype: ${ type.type }
default: '0'
- id: short_id
label: Short ID
dtype: string
hide: ${ 'all' if not type else ('none' if short_id else 'part') }
- id: hide
label: Show
dtype: enum
options: [none, part]
option_labels: [Always, Only in Properties]
hide: part
asserts:
- ${ len(short_id) in (0, 1) }
- ${ short_id == '' or short_id.isalpha() }
templates:
var_make: self.${id} = ${id}
make: ${value}
cpp_templates:
var_make: ${id} = ${value};
make: ${value}
documentation: |-
This block represents a parameter to the flow graph. A parameter can be used to pass command line arguments into a top block. Or, parameters can pass arguments into an instantiated hierarchical block.
The parameter value cannot depend on any variables.
Leave the label blank to use the parameter id as the label.
When type is not None, this parameter also becomes a command line option of the form:
-[short_id] --[id] [value]
The Short ID field may be left blank.
To disable showing the parameter on the hierarchical block in GRC, use Only in Properties option in the Show field.
file_format: 1

View File

@ -0,0 +1,46 @@
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', 'init_before_blocks' ]
option_labels: ['Main - After Init', 'Main - After Start', 'Main - After Stop', 'Init - Before Blocks']
- id: priority
label: Priority
dtype: int
default: "0"
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

View File

@ -0,0 +1,11 @@
id: stream
label: Stream
color: "#000000"
multiple_connections_per_input: false
multiple_connections_per_output: true
templates:
- type: [stream, stream]
connect: self.connect(${ make_port_sig(source) }, ${ make_port_sig(sink) })
cpp_connect: hier_block2::connect(${ make_port_sig(source) }, ${ make_port_sig(sink) })

View File

@ -0,0 +1,25 @@
id: variable
label: Variable
flags: [ show_id, python, cpp ]
parameters:
- id: value
label: Value
dtype: raw
default: '0'
value: ${ value }
templates:
var_make: self.${id} = ${id} = ${value}
callbacks:
- self.set_${id}(${value})
cpp_templates:
var_make: ${id} = ${value};
callbacks:
- this->set_${id}(${value})
documentation: |-
This block maps a value to a unique variable. This variable block has no graphical representation.
file_format: 1

View File

@ -0,0 +1,59 @@
id: variable_config
label: Variable Config
flags: [ show_id, python ]
parameters:
- id: value
label: Default Value
dtype: ${ type }
default: '0'
- id: type
label: Type
dtype: enum
default: real
options: [real, int, bool, string]
option_labels: [Float, Int, Bool, String]
option_attributes:
get: [getfloat, getint, getboolean, get]
- id: config_file
label: Config File
dtype: file_open
default: default
- id: section
label: Section
dtype: string
default: main
- id: option
label: Option
dtype: string
default: key
- id: writeback
label: WriteBack
dtype: raw
default: None
value: ${ value }
templates:
imports: import configparser
var_make: 'self._${id}_config = configparser.ConfigParser()
self._${id}_config.read(${config_file})
try: ${id} = self._${id}_config.${type.get}(${section}, ${option})
except: ${id} = ${value}
self.${id} = ${id}'
callbacks:
- self.set_${id}(${value})
- "self._${id}_config = configparser.ConfigParser()\nself._${id}_config.read(${config_file})\n\
if not self._${id}_config.has_section(${section}):\n\tself._${id}_config.add_section(${section})\n\
self._${id}_config.set(${section}, ${option}, str(${writeback}))\nself._${id}_config.write(open(${config_file},\
\ 'w'))"
documentation: |-
This block represents a variable that can be read from a config file.
To save the value back into the config file: enter the name of another variable into the writeback param. When the other variable is changed at runtime, the config file will be re-written.
file_format: 1

View File

@ -0,0 +1,66 @@
id: variable_function_probe
label: Function Probe
flags: [ show_id, python ]
parameters:
- id: block_id
label: Block ID
dtype: name
default: my_block_0
- id: function_name
label: Function Name
dtype: name
default: get_number
- id: function_args
label: Function Args
dtype: raw
hide: ${ ('none' if function_args else 'part') }
- id: poll_rate
label: Poll Rate (Hz)
dtype: real
default: '10'
- id: value
label: Initial Value
dtype: raw
default: '0'
hide: part
value: ${ value }
templates:
imports: |-
import time
import threading
var_make: self.${id} = ${id} = ${value}
make: |+
def _${id}_probe():
self.flowgraph_started.wait()
while True:
<% obj = 'self' + ('.' + block_id if block_id else '') %>
val = ${obj}.${function_name}(${function_args})
try:
try:
self.doc.add_next_tick_callback(functools.partial(self.set_${id},val))
except AttributeError:
self.set_${id}(val)
except AttributeError:
pass
time.sleep(1.0 / (${poll_rate}))
_${id}_thread = threading.Thread(target=_${id}_probe)
_${id}_thread.daemon = True
_${id}_thread.start()
callbacks:
- self.set_${id}(${value})
documentation: |-
Periodically probe a function and set its value to this variable.
Set the values for block ID, function name, and function args appropriately: Block ID should be the ID of another block in this flow graph. An empty Block ID references the flow graph itself. Function name should be the name of a class method on that block. Function args are the parameters passed into that function. For a function with no arguments, leave function args blank. When passing a string for the function arguments, quote the string literal: '"arg"'.
The values will used literally, and generated into the following form:
self.block_id.function_name(function_args)
or, if the Block ID is empty,
self.function_name(function_args)
To poll a stream for a level, use this with the probe signal block.
file_format: 1

View File

@ -0,0 +1,100 @@
#!/usr/bin/env python
MAX_NUM_FIELDS = 20
HEADER = """\
id: variable_struct
label: Struct Variable
flags: [ show_id ]
parameters:
"""
FUNCTIONCALL = """\
field{0}:value{0},\
"""
TEMPLATES = """\
templates:
imports: "def struct(data): return type('Struct', (object,), data)()"
var_make: |-
self.${{id}} = ${{id}} = struct({{
% for i in range({0}):
<%
field = context.get('field' + str(i))
value = context.get('value' + str(i))
%>
% if len(str(field)) > 2:
${{field}}: ${{value}},
% endif
% endfor
}})
"""
FIELD0 = """\
- id: field0
label: Field 0
category: Fields
dtype: string
default: field0
hide: part
"""
FIELDS = """\
- id: field{0}
label: Field {0}
category: Fields
dtype: string
hide: part
"""
VALUES = """\
- id: value{0}
label: ${{field{0}}}
dtype: raw
default: '0'
hide: ${{ 'none' if field{0} else 'all' }}
"""
ASSERTS = """\
- ${{ (str(field{0}) or "a")[0].isalpha() }}
- ${{ (str(field{0}) or "a").isalnum() }}
"""
FOOTER = """\
documentation: |-
This is a simple struct/record like variable.
Attribute/field names can be specified in the tab 'Fields'.
For each non-empty field a parameter with type raw is shown.
Value access via the dot operator, e.g. "variable_struct_0.field0"
file_format: 1
"""
def make_yml(num_fields):
return ''.join((
HEADER.format(num_fields),
FIELD0, ''.join(FIELDS.format(i) for i in range(1, num_fields)),
''.join(VALUES.format(i) for i in range(num_fields)),'value: ${struct({',
''.join(FUNCTIONCALL.format(i) for i in range(num_fields)),'})}\n\nasserts:\n',
''.join(ASSERTS.format(i) for i in range(num_fields)),
''.join(TEMPLATES.format(num_fields)),
FOOTER
))
if __name__ == '__main__':
import sys
try:
filename = sys.argv[1]
except IndexError:
filename = __file__[:-3]
data = make_yml(MAX_NUM_FIELDS)
with open(filename, 'wb') as fp:
fp.write(data.encode())

View File

@ -0,0 +1,58 @@
id: yaml_config
label: YAML Config
flags: [ show_id, python ]
parameters:
- id: config_file
label: Config File
dtype: file_open
default: ""
- id: schema_file
label: Config Schema
dtype: file_open
default: ""
# Not my favorite thing because it doesn't explicitly close the file, but
# it should be okay. The garbage collector will take care of it.
value: "${ yaml.safe_load(open(config_file)) }"
asserts:
- ${ schema_file == "" or validate(yaml.safe_load(open(config_file)), json.load(open(schema_file))) is None }
templates:
imports: |-
import json
import yaml
from jsonschema import validate
var_make: "with open(${config_file}) as fid:\n\
\ self.${id} = ${id} = yaml.safe_load(fid)\n\
self.${id}_schema = ${id}_schema = ${schema_file}\n\
if ${id}_schema:\n\
\ with open(${id}_schema) as fid:\n\
\ validate(${id}, json.load(fid))"
documentation: |-
This block represents a yaml config file that is read in as a dictionary.
The values can be used directly when instantiating blocks. For example,
Sample Rate: yaml_config["samp_rate"]
Optionally, a json schema can be specified to validate the configuration.
It may sound odd to use a json schema for a yaml file, but it works and
jsonschema is a rich specification.
For example, you could have a yaml file that contains:
samp_rate: 1e6
And a schema that contains
{
"type": "object",
"properties": {
"samp_rate": {"type": "number", "exclusiveMinimum": 0}
}
}
If the id of this block is yaml_config_0, then you can access the samp rate
in other blocks as yaml_config_0["samp_rate"]
file_format: 1

65
grc/compiler.py Executable file
View File

@ -0,0 +1,65 @@
# Copyright 2016 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
import argparse
import os
import subprocess
from gnuradio import gr
from .core import Messages
from .core.platform import Platform
def argument_parser():
parser = argparse.ArgumentParser(description=(
"Compile a GRC file (.grc) into a GNU Radio Python program and run it."
))
parser.add_argument("-o", "--output", metavar='DIR', default='.',
help="Output directory for compiled program [default=%(default)s]")
parser.add_argument("-u", "--user-lib-dir", action='store_true', default=False,
help="Output to default hier_block library (overwrites -o)")
parser.add_argument("-r", "--run", action="store_true", default=False,
help="Run the program after compiling [default=%(default)s]")
parser.add_argument(metavar="GRC_FILE", dest='grc_files', nargs='+',
help=".grc file to compile")
return parser
def main(args=None):
args = args or argument_parser().parse_args()
platform = Platform(
name='GNU Radio Companion Compiler',
prefs=gr.prefs(),
version=gr.version(),
version_parts=(gr.major_version(),
gr.api_version(), gr.minor_version())
)
platform.build_library()
output_dir = args.output if not args.user_lib_dir else platform.config.hier_block_lib_dir
try:
# recursive mkdir: os.makedirs doesn't work with .. paths, resolve with realpath
os.makedirs(os.path.realpath(output_dir), exist_ok=True)
except Exception as e:
exit(str(e))
Messages.send_init(platform)
flow_graph = file_path = None
for grc_file in args.grc_files:
os.path.exists(grc_file) or exit('Error: missing ' + grc_file)
Messages.send('\n')
platform.config.hier_block_lib_dir = output_dir
flow_graph, file_path = platform.load_and_generate_flow_graph(
os.path.abspath(grc_file), os.path.abspath(output_dir))
if not file_path:
exit('Compilation error')
if file_path and args.run:
run_command_args = flow_graph.get_run_command(file_path, split=True)
subprocess.call(run_command_args)

View File

@ -0,0 +1,8 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from .main import Converter

View File

@ -0,0 +1,8 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
# TODO: implement cli

58
grc/converter/block.dtd Normal file
View File

@ -0,0 +1,58 @@
<!--
Copyright 2008 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
-->
<!--
gnuradio_python.blocks.dtd
Josh Blum
The document type definition for blocks.
-->
<!--
Top level element.
A block contains a name, ...parameters list, and list of IO ports.
-->
<!ELEMENT block (name, key, category?, throttle?, flags?, import*, var_make?, var_value?,
make, callback*, param_tab_order?, param*, bus_sink?, bus_source?, check*,
sink*, source*, bus_structure_sink?, bus_structure_source?, doc?, grc_source?)>
<!--
Sub level elements.
-->
<!ELEMENT param_tab_order (tab+)>
<!ELEMENT param (base_key?, name, key, value?, type?, hide?, option*, tab?)>
<!ELEMENT option (name, key, opt*)>
<!ELEMENT sink (name, type, vlen?, domain?, nports?, optional?, hide?)>
<!ELEMENT source (name, type, vlen?, domain?, nports?, optional?, hide?)>
<!--
Bottom level elements.
Character data only.
-->
<!ELEMENT category (#PCDATA)>
<!ELEMENT import (#PCDATA)>
<!ELEMENT doc (#PCDATA)>
<!ELEMENT grc_source (#PCDATA)>
<!ELEMENT tab (#PCDATA)>
<!ELEMENT name (#PCDATA)>
<!ELEMENT base_key (#PCDATA)>
<!ELEMENT key (#PCDATA)>
<!ELEMENT check (#PCDATA)>
<!ELEMENT bus_sink (#PCDATA)>
<!ELEMENT bus_source (#PCDATA)>
<!ELEMENT opt (#PCDATA)>
<!ELEMENT type (#PCDATA)>
<!ELEMENT hide (#PCDATA)>
<!ELEMENT vlen (#PCDATA)>
<!ELEMENT domain (#PCDATA)>
<!ELEMENT nports (#PCDATA)>
<!ELEMENT bus_structure_sink (#PCDATA)>
<!ELEMENT bus_structure_source (#PCDATA)>
<!ELEMENT var_make (#PCDATA)>
<!ELEMENT var_value (#PCDATA)>
<!ELEMENT make (#PCDATA)>
<!ELEMENT value (#PCDATA)>
<!ELEMENT callback (#PCDATA)>
<!ELEMENT optional (#PCDATA)>
<!ELEMENT throttle (#PCDATA)>
<!ELEMENT flags (#PCDATA)>

219
grc/converter/block.py Normal file
View File

@ -0,0 +1,219 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
"""
Converter for legacy block definitions in XML format
- Cheetah expressions that can not be converted are passed to Cheetah for now
- Instead of generating a Block subclass directly a string representation is
used and evaluated. This is slower / lamer but allows us to show the user
how a converted definition would look like
"""
from collections import OrderedDict, defaultdict
from itertools import chain
from ..core.io import yaml
from . import cheetah_converter, xml
current_file_format = 1
reserved_block_keys = ('import', ) # todo: add more keys
def from_xml(filename):
"""Load block description from xml file"""
element, version_info = xml.load(filename, 'block.dtd')
try:
data = convert_block_xml(element)
except NameError:
raise ValueError('Conversion failed', filename)
return data
def dump(data, stream):
out = yaml.dump(data)
replace = [
('parameters:', '\nparameters:'),
('inputs:', '\ninputs:'),
('outputs:', '\noutputs:'),
('templates:', '\ntemplates:'),
('documentation:', '\ndocumentation:'),
('file_format:', '\nfile_format:'),
]
for r in replace:
out = out.replace(*r)
prefix = '# auto-generated by grc.converter\n\n'
stream.write(prefix + out)
no_value = object()
dummy = cheetah_converter.DummyConverter()
def convert_block_xml(node):
converter = cheetah_converter.Converter(names={
param_node.findtext('key'): {
opt_node.text.split(':')[0]
for opt_node in next(param_node.iterfind('option'), param_node).iterfind('opt')
} for param_node in node.iterfind('param')
})
block_id = node.findtext('key')
if block_id in reserved_block_keys:
block_id += '_'
data = OrderedDict()
data['id'] = block_id
data['label'] = node.findtext('name') or no_value
data['category'] = node.findtext('category') or no_value
data['flags'] = [n.text for n in node.findall('flags')]
data['flags'] += ['show_id'] if block_id.startswith('variable') else []
if not data['flags']:
data['flags'] = no_value
data['parameters'] = [convert_param_xml(param_node, converter.to_python_dec)
for param_node in node.iterfind('param')] or no_value
# data['params'] = {p.pop('key'): p for p in data['params']}
data['inputs'] = [convert_port_xml(port_node, converter.to_python_dec)
for port_node in node.iterfind('sink')] or no_value
data['outputs'] = [convert_port_xml(port_node, converter.to_python_dec)
for port_node in node.iterfind('source')] or no_value
data['value'] = (
converter.to_python_dec(node.findtext('var_value')) or
('${ value }' if block_id.startswith('variable') else no_value)
)
data['asserts'] = [converter.to_python_dec(check_node.text)
for check_node in node.iterfind('check')] or no_value
data['templates'] = convert_templates(
node, converter.to_mako, block_id) or no_value
docs = node.findtext('doc')
if docs:
docs = docs.strip().replace('\\\n', '')
data['documentation'] = yaml.MultiLineString(docs)
data['file_format'] = current_file_format
data = OrderedDict((key, value)
for key, value in data.items() if value is not no_value)
auto_hide_params_for_item_sizes(data)
return data
def auto_hide_params_for_item_sizes(data):
item_size_templates = []
vlen_templates = []
for port in chain(*[data.get(direction, []) for direction in ['inputs', 'outputs']]):
for key in ['dtype', 'multiplicity']:
item_size_templates.append(str(port.get(key, '')))
vlen_templates.append(str(port.get('vlen', '')))
item_size_templates = ' '.join(
value for value in item_size_templates if '${' in value)
vlen_templates = ' '.join(
value for value in vlen_templates if '${' in value)
for param in data.get('parameters', []):
if param['id'] in item_size_templates:
param.setdefault('hide', 'part')
if param['id'] in vlen_templates:
param.setdefault('hide', "${ 'part' if vlen == 1 else 'none' }")
def convert_templates(node, convert, block_id=''):
templates = OrderedDict()
imports = '\n'.join(convert(import_node.text)
for import_node in node.iterfind('import'))
if '\n' in imports:
imports = yaml.MultiLineString(imports)
templates['imports'] = imports or no_value
templates['var_make'] = convert(
node.findtext('var_make') or '') or no_value
make = convert(node.findtext('make') or '')
if make:
check_mako_template(block_id, make)
if '\n' in make:
make = yaml.MultiLineString(make)
templates['make'] = make or no_value
templates['callbacks'] = [
convert(cb_node.text) for cb_node in node.iterfind('callback')
] or no_value
return OrderedDict((key, value) for key, value in templates.items() if value is not no_value)
def convert_param_xml(node, convert):
param = OrderedDict()
param['id'] = node.findtext('key').strip()
param['label'] = node.findtext('name').strip()
param['category'] = node.findtext('tab') or no_value
param['dtype'] = convert(node.findtext('type') or '')
param['default'] = node.findtext('value') or no_value
options = yaml.ListFlowing(on.findtext('key')
for on in node.iterfind('option'))
option_labels = yaml.ListFlowing(on.findtext(
'name') for on in node.iterfind('option'))
param['options'] = options or no_value
if not all(str(o).title() == l for o, l in zip(options, option_labels)):
param['option_labels'] = option_labels
attributes = defaultdict(yaml.ListFlowing)
for option_n in node.iterfind('option'):
for opt_n in option_n.iterfind('opt'):
key, value = opt_n.text.split(':', 2)
attributes[key].append(value)
param['option_attributes'] = dict(attributes) or no_value
param['hide'] = convert(node.findtext('hide')) or no_value
return OrderedDict((key, value) for key, value in param.items() if value is not no_value)
def convert_port_xml(node, convert):
port = OrderedDict()
label = node.findtext('name')
# default values:
port['label'] = label if label not in ('in', 'out') else no_value
dtype = convert(node.findtext('type'))
# TODO: detect dyn message ports
port['domain'] = domain = 'message' if dtype == 'message' else 'stream'
if domain == 'message':
port['id'], port['label'] = label, no_value
else:
port['dtype'] = dtype
vlen = node.findtext('vlen')
port['vlen'] = int(vlen) if vlen and vlen.isdigit(
) else convert(vlen) or no_value
port['multiplicity'] = convert(node.findtext('nports')) or no_value
port['optional'] = bool(node.findtext('optional')) or no_value
port['hide'] = convert(node.findtext('hide')) or no_value
return OrderedDict((key, value) for key, value in port.items() if value is not no_value)
def check_mako_template(block_id, expr):
import sys
from mako.template import Template
try:
Template(expr)
except Exception as error:
print(block_id, expr, type(error), error,
'', sep='\n', file=sys.stderr)

View File

@ -0,0 +1,15 @@
<!--
Copyright 2008 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
-->
<!--
block_tree.dtd
Josh Blum
The document type definition for a block tree category listing.
-->
<!ELEMENT cat (name, cat*, block*)>
<!ELEMENT name (#PCDATA)>
<!ELEMENT block (#PCDATA)>

View File

@ -0,0 +1,44 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
"""
Converter for legacy block tree definitions in XML format
"""
from ..core.io import yaml
from . import xml
def from_xml(filename):
"""Load block tree description from xml file"""
element, version_info = xml.load(filename, 'block_tree.dtd')
try:
data = convert_category_node(element)
except NameError:
raise ValueError('Conversion failed', filename)
return data
def dump(data, stream):
out = yaml.dump(data, indent=2)
prefix = '# auto-generated by grc.converter\n\n'
stream.write(prefix + out)
def convert_category_node(node):
"""convert nested <cat> tags to nested lists dicts"""
assert node.tag == 'cat'
name, elements = '', []
for child in node:
if child.tag == 'name':
name = child.text.strip()
elif child.tag == 'block':
elements.append(child.text.strip())
elif child.tag == 'cat':
elements.append(convert_category_node(child))
return {name: elements}

View File

@ -0,0 +1,269 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
import collections
import re
import string
delims = {'(': ')', '[': ']', '{': '}', '': ', #\\*:'}
identifier_start = '_' + string.ascii_letters + ''.join(delims.keys())
string_delims = '"\''
cheetah_substitution = re.compile(
r'^\$((?P<d1>\()|(?P<d2>\{)|(?P<d3>\[)|)'
r'(?P<arg>[_a-zA-Z][_a-zA-Z0-9]*(?:\.[_a-zA-Z][_a-zA-Z0-9]*)?)(?P<eval>\(\))?'
r'(?(d1)\)|(?(d2)\}|(?(d3)\]|)))$'
)
cheetah_inline_if = re.compile(
r'#if (?P<cond>.*) then (?P<then>.*?) ?else (?P<else>.*?) ?(#|$)')
class Python(object):
start = ''
end = ''
nested_start = ''
nested_end = ''
eval = ''
type = str # yaml_output.Eval
class FormatString(Python):
start = '{'
end = '}'
nested_start = '{'
nested_end = '}'
eval = ':eval'
type = str
class Mako(Python):
start = '${'
end = '}'
nested_start = ''
nested_end = ''
type = str
class Converter(object):
def __init__(self, names):
self.stats = collections.defaultdict(int)
self.names = set(names)
self.extended = set(self._iter_identifiers(names))
@staticmethod
def _iter_identifiers(names):
if not isinstance(names, dict):
names = {name: {} for name in names}
for key, sub_keys in names.items():
yield key
for sub_key in sub_keys:
yield '{}.{}'.format(key, sub_key)
def to_python(self, expr):
return self.convert(expr=expr, spec=Python)
def to_python_dec(self, expr):
converted = self.convert(expr=expr, spec=Python)
if converted and converted != expr:
converted = '${ ' + converted.strip() + ' }'
return converted
def to_format_string(self, expr):
return self.convert(expr=expr, spec=FormatString)
def to_mako(self, expr):
return self.convert(expr=expr, spec=Mako)
def convert(self, expr, spec=Python):
if not expr:
return ''
elif '$' not in expr:
return expr
try:
return self.convert_simple(expr, spec)
except ValueError:
pass
try:
if '#if' in expr and '\n' not in expr:
expr = self.convert_inline_conditional(expr, spec)
return self.convert_hard(expr, spec)
except ValueError:
return 'Cheetah! ' + expr
def convert_simple(self, expr, spec=Python):
match = cheetah_substitution.match(expr)
if not match:
raise ValueError('Not a simple substitution: ' + expr)
identifier = match.group('arg')
if identifier not in self.extended:
raise NameError('Unknown substitution {!r}'.format(identifier))
if match.group('eval'):
identifier += spec.eval
out = spec.start + identifier + spec.end
if '$' in out or '#' in out:
raise ValueError('Failed to convert: ' + expr)
self.stats['simple'] += 1
return spec.type(out)
def convert_hard(self, expr, spec=Python):
lines = '\n'.join(self.convert_hard_line(line, spec)
for line in expr.split('\n'))
if spec == Mako:
# no line-continuation before a mako control structure
lines = re.sub(r'\\\n(\s*%)', r'\n\1', lines)
return lines
def convert_hard_line(self, expr, spec=Python):
if spec == Mako:
if '#set' in expr:
ws, set_, statement = expr.partition('#set ')
return ws + '<% ' + self.to_python(statement) + ' %>'
if '#if' in expr:
ws, if_, condition = expr.partition('#if ')
return ws + '% if ' + self.to_python(condition) + ':'
if '#else if' in expr:
ws, elif_, condition = expr.partition('#else if ')
return ws + '% elif ' + self.to_python(condition) + ':'
if '#else' in expr:
return expr.replace('#else', '% else:')
if '#end if' in expr:
return expr.replace('#end if', '% endif')
if '#slurp' in expr:
expr = expr.split('#slurp', 1)[0] + '\\'
return self.convert_hard_replace(expr, spec)
def convert_hard_replace(self, expr, spec=Python):
counts = collections.Counter()
def all_delims_closed():
for opener_, closer_ in delims.items():
if counts[opener_] != counts[closer_]:
return False
return True
def extra_close():
for opener_, closer_ in delims.items():
if counts[opener_] < counts[closer_]:
return True
return False
out = []
delim_to_find = False
pos = 0
char = ''
in_string = None
while pos < len(expr):
prev, char = char, expr[pos]
counts.update(char)
if char in string_delims:
if not in_string:
in_string = char
elif char == in_string:
in_string = None
out.append(char)
pos += 1
continue
if in_string:
out.append(char)
pos += 1
continue
if char == '$':
pass # no output
elif prev == '$':
if char not in identifier_start: # not a substitution
out.append('$' + char) # now print the $ we skipped over
elif not delim_to_find: # start of a substitution
try:
delim_to_find = delims[char]
out.append(spec.start)
except KeyError:
if char in identifier_start:
delim_to_find = delims['']
out.append(spec.start)
out.append(char)
counts.clear()
counts.update(char)
else: # nested substitution: simply match known variable names
found = False
for known_identifier in self.names:
if expr[pos:].startswith(known_identifier):
found = True
break
if found:
out.append(spec.nested_start)
out.append(known_identifier)
out.append(spec.nested_end)
pos += len(known_identifier)
continue
elif delim_to_find and char in delim_to_find and all_delims_closed(): # end of substitution
out.append(spec.end)
if char in delims['']:
out.append(char)
delim_to_find = False
# end of substitution
elif delim_to_find and char in ')]}' and extra_close():
out.append(spec.end)
out.append(char)
delim_to_find = False
else:
out.append(char)
pos += 1
if delim_to_find == delims['']:
out.append(spec.end)
out = ''.join(out)
# fix: eval stuff
out = re.sub(r'(?P<arg>' + r'|'.join(self.extended) +
r')\(\)', r'\g<arg>', out)
self.stats['hard'] += 1
return spec.type(out)
def convert_inline_conditional(self, expr, spec=Python):
if spec == FormatString:
raise ValueError('No conditionals in format strings: ' + expr)
matcher = r'\g<then> if \g<cond> else \g<else>'
if spec == Python:
matcher = '(' + matcher + ')'
expr = cheetah_inline_if.sub(matcher, expr)
return spec.type(self.convert_hard(expr, spec))
class DummyConverter(object):
def __init__(self, names={}):
pass
def to_python(self, expr):
return expr
def to_format_string(self, expr):
return expr
def to_mako(self, expr):
return expr

View File

@ -0,0 +1,27 @@
<!--
Copyright 2008 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
-->
<!--
flow_graph.dtd
Josh Blum
The document type definition for flow graph xml files.
-->
<!ELEMENT flow_graph (timestamp?, block*, connection*)> <!-- optional timestamp -->
<!ELEMENT timestamp (#PCDATA)>
<!-- Block -->
<!ELEMENT block (key, param*, bus_sink?, bus_source?)>
<!ELEMENT param (key, value)>
<!ELEMENT key (#PCDATA)>
<!ELEMENT value (#PCDATA)>
<!ELEMENT bus_sink (#PCDATA)>
<!ELEMENT bus_source (#PCDATA)>
<!-- Connection -->
<!ELEMENT connection (source_block_id, sink_block_id, source_key, sink_key)>
<!ELEMENT source_block_id (#PCDATA)>
<!ELEMENT sink_block_id (#PCDATA)>
<!ELEMENT source_key (#PCDATA)>
<!ELEMENT sink_key (#PCDATA)>

121
grc/converter/flow_graph.py Normal file
View File

@ -0,0 +1,121 @@
# Copyright 2017,2018 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
import ast
from collections import OrderedDict
from ..core.io import yaml
from . import xml
def from_xml(filename):
"""Load flow graph from xml file"""
element, version_info = xml.load(filename, 'flow_graph.dtd')
data = convert_flow_graph_xml(element)
try:
file_format = int(version_info['format'])
except KeyError:
file_format = _guess_file_format_1(data)
data['metadata'] = {'file_format': file_format}
return data
def dump(data, stream):
out = yaml.dump(data, indent=2)
replace = [
('blocks:', '\nblocks:'),
('connections:', '\nconnections:'),
('metadata:', '\nmetadata:'),
]
for r in replace:
out = out.replace(*r)
prefix = '# auto-generated by grc.converter\n\n'
stream.write(prefix + out)
def convert_flow_graph_xml(node):
blocks = [
convert_block(block_data)
for block_data in node.findall('block')
]
options = next(b for b in blocks if b['id'] == 'options')
blocks.remove(options)
options.pop('id')
connections = [
convert_connection(connection)
for connection in node.findall('connection')
]
flow_graph = OrderedDict()
flow_graph['options'] = options
flow_graph['blocks'] = blocks
flow_graph['connections'] = connections
return flow_graph
def convert_block(data):
block_id = data.findtext('key')
params = OrderedDict(sorted(
(param.findtext('key'), param.findtext('value'))
for param in data.findall('param')
))
if block_id == "import":
params["imports"] = params.pop("import")
states = OrderedDict()
x, y = ast.literal_eval(params.pop('_coordinate', '(10, 10)'))
states['coordinate'] = yaml.ListFlowing([x, y])
states['rotation'] = int(params.pop('_rotation', '0'))
enabled = params.pop('_enabled', 'True')
states['state'] = (
'enabled' if enabled in ('1', 'True') else
'bypassed' if enabled == '2' else
'disabled'
)
block = OrderedDict()
if block_id != 'options':
block['name'] = params.pop('id')
block['id'] = block_id
block['parameters'] = params
block['states'] = states
return block
def convert_connection(data):
src_blk_id = data.findtext('source_block_id')
src_port_id = data.findtext('source_key')
snk_blk_id = data.findtext('sink_block_id')
snk_port_id = data.findtext('sink_key')
if src_port_id.isdigit():
src_port_id = src_port_id
if snk_port_id.isdigit():
snk_port_id = snk_port_id
return yaml.ListFlowing([src_blk_id, src_port_id, snk_blk_id, snk_port_id])
def _guess_file_format_1(data):
"""Try to guess the file format for flow-graph files without version tag"""
def has_numeric_port_ids(src_id, src_port_id, snk_id, snk_port_id):
return src_port_id.isdigit() and snk_port_id.isdigit()
try:
if any(not has_numeric_port_ids(*con) for con in data['connections']):
return 1
except (TypeError, KeyError):
pass
return 0

160
grc/converter/main.py Normal file
View File

@ -0,0 +1,160 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from codecs import open
import json
import logging
import os
from ..main import get_state_directory
from ..core import Constants
from . import block_tree, block
path = os.path
logger = logging.getLogger(__name__)
excludes = [
'qtgui_',
'.grc_gnuradio/',
os.path.join(get_state_directory(), Constants.GRC_SUBDIR),
'blks2',
'wxgui',
'epy_block.xml',
'virtual_sink.xml',
'virtual_source.xml',
'dummy.xml',
'variable_struct.xml', # todo: re-implement as class
'digital_constellation', # todo: fix template
]
class Converter(object):
def __init__(self, search_path: str, output_dir: str = os.path.join(get_state_directory(), Constants.GRC_SUBDIR)):
self.search_path = search_path
self.output_dir = os.path.expanduser(output_dir)
logger.info("Saving converted files to {}".format(self.output_dir))
self._force = False
converter_module_path = path.dirname(__file__)
self._converter_mtime = max(path.getmtime(path.join(converter_module_path, module))
for module in os.listdir(converter_module_path)
if not module.endswith('flow_graph.py'))
self.cache_file = os.path.join(self.output_dir, '_cache.json')
self.cache = {}
def run(self, force=False):
self._force = force
try:
logger.debug(
"Loading block cache from: {}".format(self.cache_file))
with open(self.cache_file, encoding='utf-8') as cache_file:
self.cache = byteify(json.load(cache_file))
except (IOError, ValueError):
self.cache = {}
self._force = True
need_cache_write = False
if not path.isdir(self.output_dir):
os.makedirs(self.output_dir)
if self._force:
for name in os.listdir(self.output_dir):
os.remove(os.path.join(self.output_dir, name))
for xml_file in self.iter_files_in_block_path():
if xml_file.endswith("block_tree.xml"):
changed = self.load_category_tree_xml(xml_file)
elif xml_file.endswith('domain.xml'):
continue
else:
changed = self.load_block_xml(xml_file)
if changed:
need_cache_write = True
if need_cache_write:
logger.debug('Saving %d entries to json cache', len(self.cache))
with open(self.cache_file, 'w', encoding='utf-8') as cache_file:
json.dump(self.cache, cache_file)
def load_block_xml(self, xml_file, force=False):
"""Load block description from xml file"""
if any(part in xml_file for part in excludes) and not force:
logger.warn('Skipping {} because name is blacklisted!'
.format(xml_file))
return False
elif any(part in xml_file for part in excludes) and force:
logger.warn('Forcing conversion of blacklisted file: {}'
.format(xml_file))
block_id_from_xml = path.basename(xml_file)[:-4]
yml_file = path.join(self.output_dir, block_id_from_xml + '.block.yml')
if not self.needs_conversion(xml_file, yml_file):
return # yml file up-to-date
logger.info('Converting block %s', path.basename(xml_file))
data = block.from_xml(xml_file)
if block_id_from_xml != data['id']:
logger.warning('block_id and filename differ')
self.cache[yml_file] = data
with open(yml_file, 'w', encoding='utf-8') as yml_file:
block.dump(data, yml_file)
return True
def load_category_tree_xml(self, xml_file):
"""Validate and parse category tree file and add it to list"""
module_name = path.basename(
xml_file)[:-len('block_tree.xml')].rstrip('._-')
yml_file = path.join(self.output_dir, module_name + '.tree.yml')
if not self.needs_conversion(xml_file, yml_file):
return # yml file up-to-date
logger.info('Converting module %s', path.basename(xml_file))
data = block_tree.from_xml(xml_file)
self.cache[yml_file] = data
with open(yml_file, 'w', encoding='utf-8') as yml_file:
block_tree.dump(data, yml_file)
return True
def needs_conversion(self, source, destination):
"""Check if source has already been converted and destination is up-to-date"""
if self._force or not path.exists(destination):
return True
xml_time = path.getmtime(source)
yml_time = path.getmtime(destination)
return yml_time < xml_time or yml_time < self._converter_mtime
def iter_files_in_block_path(self, suffix='.xml'):
"""Iterator for block descriptions and category trees"""
for block_path in self.search_path:
if path.isfile(block_path):
yield block_path
elif path.isdir(block_path):
for root, _, files in os.walk(block_path, followlinks=True):
for name in files:
if name.endswith(suffix):
yield path.join(root, name)
else:
logger.warning(
'Invalid entry in search path: {}'.format(block_path))
def byteify(data):
if isinstance(data, dict):
return {byteify(key): byteify(value) for key, value in data.items()}
elif isinstance(data, list):
return [byteify(element) for element in data]
else:
return data

73
grc/converter/xml.py Normal file
View File

@ -0,0 +1,73 @@
# Copyright 2017 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
import re
from os import path
try:
# raise ImportError()
from lxml import etree
HAVE_LXML = True
except ImportError:
import xml.etree.ElementTree as etree
HAVE_LXML = False
_validator_cache = {None: lambda xml: True}
def _get_validator(dtd=None):
validator = _validator_cache.get(dtd)
if not validator:
if not path.isabs(dtd):
dtd = path.join(path.dirname(__file__), dtd)
validator = _validator_cache[dtd] = etree.DTD(dtd).validate
return validator
def load_lxml(filename, document_type_def=None):
"""Load block description from xml file"""
try:
xml_tree = etree.parse(filename)
_get_validator(document_type_def)
element = xml_tree.getroot()
except etree.LxmlError:
raise ValueError("Failed to parse or validate {}".format(filename))
version_info = {}
for inst in xml_tree.xpath('/processing-instruction()'):
if inst.target == 'grc':
version_info.update(inst.attrib)
return element, version_info
def load_stdlib(filename, document_type_def=None):
"""Load block description from xml file"""
if isinstance(filename, str):
with open(filename, 'rb') as xml_file:
data = xml_file.read().decode('utf-8')
else: # Already opened
data = filename.read().decode('utf-8')
try:
element = etree.fromstring(data)
except etree.ParseError:
raise ValueError("Failed to parse {}".format(filename))
version_info = {}
for body in re.findall(r'<\?(.*?)\?>', data):
inst = etree.fromstring('<' + body + '/>')
if inst.tag == 'grc':
version_info.update(inst.attrib)
return element, version_info
load = load_lxml if HAVE_LXML else load_stdlib

79
grc/core/Config.py Normal file
View File

@ -0,0 +1,79 @@
"""Copyright 2024 The GNU Radio Contributors
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
import os
from os.path import expanduser, normpath, expandvars, exists
from collections import OrderedDict
from ..main import get_state_directory, get_config_file_path
from . import Constants
class Config(object):
name = 'GNU Radio Companion (no gui)'
license = __doc__.strip()
website = 'https://www.gnuradio.org/'
hier_block_lib_dir = os.environ.get('GRC_HIER_PATH', get_state_directory())
def __init__(self, version, version_parts=None, name=None, prefs=None):
self._gr_prefs = prefs if prefs else DummyPrefs()
self.version = version
self.version_parts = version_parts or version[1:].split(
'-', 1)[0].split('.')[:3]
self.enabled_components = self._gr_prefs.get_string(
'grc', 'enabled_components', '')
if name:
self.name = name
@property
def block_paths(self):
paths_sources = (
self.hier_block_lib_dir,
os.environ.get('GRC_BLOCKS_PATH', ''),
self._gr_prefs.get_string('grc', 'local_blocks_path', ''),
self._gr_prefs.get_string('grc', 'global_blocks_path', ''),
)
collected_paths = sum((paths.split(os.pathsep)
for paths in paths_sources), [])
valid_paths = [normpath(expanduser(expandvars(path)))
for path in collected_paths if exists(path)]
# Deduplicate paths to avoid warnings about finding blocks twice, but
# preserve order of paths
valid_paths = list(OrderedDict.fromkeys(valid_paths))
return valid_paths
@property
def example_paths(self):
return [self._gr_prefs.get_string('grc', 'examples_path', '')]
@property
def default_flow_graph(self):
user_default = (
os.environ.get('GRC_DEFAULT_FLOW_GRAPH') or
self._gr_prefs.get_string('grc', 'default_flow_graph', '') or
os.path.join(self.hier_block_lib_dir, 'default_flow_graph.grc')
)
return user_default if exists(user_default) else Constants.DEFAULT_FLOW_GRAPH
class DummyPrefs(object):
def get_string(self, category, item, default):
return str(default)
def set_string(self, category, item, value):
pass
def get_long(self, category, item, default):
return int(default)
def save(self):
pass

182
grc/core/Connection.py Normal file
View File

@ -0,0 +1,182 @@
"""
Copyright 2008-2015 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
import collections
from .base import Element
from .Constants import ALIASES_OF
from .utils.descriptors import lazy_property
class Connection(Element):
"""
Stores information about a connection between two block ports. This class
knows:
- Where the source and sink ports are (on which blocks)
- The domain (message, stream, ...)
- Which parameters are associated with this connection
"""
is_connection = True
category = []
documentation = {'': ''}
doc_url = ''
def __init__(self, parent, source, sink):
"""
Make a new connection given the parent and 2 ports.
Args:
parent: the parent of this element (a flow graph)
source: a port (any direction)
sink: a port (any direction)
@throws Error cannot make connection
Returns:
a new connection
"""
Element.__init__(self, parent)
if not source.is_source:
source, sink = sink, source
if not source.is_source:
raise ValueError('Connection could not isolate source')
if not sink.is_sink:
raise ValueError('Connection could not isolate sink')
self.source_port = source
self.sink_port = sink
# Unlike the blocks, connection parameters are defined in the connection
# domain definition files, as all connections within the same domain
# share the same properties.
param_factory = self.parent_platform.make_param
conn_parameters = self.parent_platform.connection_params.get(self.type, {})
self.params = collections.OrderedDict(
(data['id'], param_factory(parent=self, **data))
for data in conn_parameters
)
def __str__(self):
return 'Connection (\n\t{}\n\t\t{}\n\t{}\n\t\t{}\n)'.format(
self.source_block, self.source_port, self.sink_block, self.sink_port,
)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.source_port == other.source_port and self.sink_port == other.sink_port
def __hash__(self):
return hash((self.source_port, self.sink_port))
def __iter__(self):
return iter((self.source_port, self.sink_port))
def children(self):
""" This includes the connection parameters """
return self.params.values()
@lazy_property
def source_block(self):
return self.source_port.parent_block
@lazy_property
def sink_block(self):
return self.sink_port.parent_block
@property
def type(self):
return self.source_port.domain, self.sink_port.domain
@property
def enabled(self):
"""
Get the enabled state of this connection.
Returns:
true if source and sink blocks are enabled
"""
return self.source_block.enabled and self.sink_block.enabled
@property
def label(self):
""" Returns a label for dialogs """
src_domain, sink_domain = [
self.parent_platform.domains[d].name for d in self.type]
return f'Connection ({src_domain}{sink_domain})'
@property
def namespace_templates(self):
"""Returns everything we want to have available in the template rendering"""
return {key: param.template_arg for key, param in self.params.items()}
def validate(self):
"""
Validate the connections.
The ports must match in io size.
"""
Element.validate(self)
platform = self.parent_platform
if self.type not in platform.connection_templates:
self.add_error_message('No connection known between domains "{}" and "{}"'
''.format(*self.type))
source_dtype = self.source_port.dtype
sink_dtype = self.sink_port.dtype
if source_dtype != sink_dtype and source_dtype not in ALIASES_OF.get(
sink_dtype, set()
):
self.add_error_message('Source IO type "{}" does not match sink IO type "{}".'.format(
source_dtype, sink_dtype))
source_size = self.source_port.item_size
sink_size = self.sink_port.item_size
if source_size != sink_size:
self.add_error_message(
'Source IO size "{}" does not match sink IO size "{}".'.format(source_size, sink_size))
##############################################
# Import/Export Methods
##############################################
def export_data(self):
"""
Export this connection's info.
Returns:
A tuple with connection info, and parameters.
"""
# See if we need to use file format version 2:
if self.params:
return {
'src_blk_id': self.source_block.name,
'src_port_id': self.source_port.key,
'snk_blk_id': self.sink_block.name,
'snk_port_id': self.sink_port.key,
'params': collections.OrderedDict(sorted(
(param_id, param.value)
for param_id, param in self.params.items())),
}
# If there's no reason to do otherwise, we can export info as
# FLOW_GRAPH_FILE_FORMAT_VERSION 1 format:
return [
self.source_block.name, self.source_port.key,
self.sink_block.name, self.sink_port.key,
]
def import_data(self, params):
"""
Import connection parameters.
"""
for key, value in params.items():
try:
self.params[key].set_value(value)
except KeyError:
continue

147
grc/core/Constants.py Normal file
View File

@ -0,0 +1,147 @@
"""
Copyright 2008-2016 Free Software Foundation, Inc.
Copyright 2021 GNU Radio contributors
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
import os
import numbers
import stat
import numpy
# Data files
DATA_DIR = os.path.dirname(__file__)
BLOCK_DTD = os.path.join(DATA_DIR, 'block.dtd')
DEFAULT_FLOW_GRAPH = os.path.join(DATA_DIR, 'default_flow_graph.grc')
DEFAULT_FLOW_GRAPH_ID = 'default'
PROJECT_DEFAULT_DIR = 'gnuradio'
GRC_SUBDIR = 'grc'
CACHE_FILE_NAME = 'cache_v2.json'
FALLBACK_CACHE_FILE = os.path.expanduser(f'~/.cache/{PROJECT_DEFAULT_DIR}/{GRC_SUBDIR}/{CACHE_FILE_NAME}')
EXAMPLE_CACHE_FILE_NAME = 'example_cache.json'
FALLBACK_EXAMPLE_CACHE_FILE = os.path.expanduser(f'~/.cache/{PROJECT_DEFAULT_DIR}/{GRC_SUBDIR}/{EXAMPLE_CACHE_FILE_NAME}')
BLOCK_DESCRIPTION_FILE_FORMAT_VERSION = 1
# File format versions:
# This constant is the max known version. If a version higher than this shows
# up, we assume we can't handle it.
# 0: undefined / legacy
# 1: non-numeric message port keys (label is used instead)
# 2: connection info is stored as dictionary
FLOW_GRAPH_FILE_FORMAT_VERSION = 2
# Param tabs
DEFAULT_PARAM_TAB = "General"
ADVANCED_PARAM_TAB = "Advanced"
DEFAULT_BLOCK_MODULE_NAME = '(no module specified)'
# Port domains
GR_STREAM_DOMAIN = "stream"
GR_MESSAGE_DOMAIN = "message"
DEFAULT_DOMAIN = GR_STREAM_DOMAIN
# File creation modes
TOP_BLOCK_FILE_MODE = stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | \
stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH
HIER_BLOCK_FILE_MODE = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH
PARAM_TYPE_NAMES = {
'raw', 'enum',
'complex', 'real', 'float', 'int', 'short', 'byte',
'complex_vector', 'real_vector', 'float_vector', 'int_vector',
'hex', 'string', 'bool',
'file_open', 'file_save', 'dir_select', '_multiline', '_multiline_python_external',
'id', 'stream_id', 'name',
'gui_hint',
'import',
}
PARAM_TYPE_MAP = {
'complex': numbers.Complex,
'float': numbers.Real,
'real': numbers.Real,
'int': numbers.Integral,
}
# Define types, native python + numpy
VECTOR_TYPES = (tuple, list, set, numpy.ndarray)
# Updating colors. Using the standard color palette from:
# http://www.google.com/design/spec/style/color.html#color-color-palette
# Most are based on the main, primary color standard. Some are within
# that color's spectrum when it was deemed necessary.
GRC_COLOR_BROWN = '#795548'
GRC_COLOR_BLUE = '#2196F3'
GRC_COLOR_LIGHT_GREEN = '#8BC34A'
GRC_COLOR_GREEN = '#4CAF50'
GRC_COLOR_AMBER = '#FFC107'
GRC_COLOR_PURPLE = '#9C27B0'
GRC_COLOR_CYAN = '#00BCD4'
GRC_COLOR_GR_ORANGE = '#FF6905'
GRC_COLOR_ORANGE = '#F57C00'
GRC_COLOR_LIME = '#CDDC39'
GRC_COLOR_TEAL = '#009688'
GRC_COLOR_YELLOW = '#FFEB3B'
GRC_COLOR_PINK = '#F50057'
GRC_COLOR_PURPLE_A100 = '#EA80FC'
GRC_COLOR_PURPLE_A400 = '#D500F9'
GRC_COLOR_DARK_GREY = '#72706F'
GRC_COLOR_GREY = '#BDBDBD'
GRC_COLOR_WHITE = '#FFFFFF'
CORE_TYPES = ( # name, key, sizeof, color
('Complex Float 64', 'fc64', 16, GRC_COLOR_BROWN),
('Complex Float 32', 'fc32', 8, GRC_COLOR_BLUE),
('Complex Integer 64', 'sc64', 16, GRC_COLOR_LIGHT_GREEN),
('Complex Integer 32', 'sc32', 8, GRC_COLOR_GREEN),
('Complex Integer 16', 'sc16', 4, GRC_COLOR_AMBER),
('Complex Integer 8', 'sc8', 2, GRC_COLOR_PURPLE),
('Float 64', 'f64', 8, GRC_COLOR_CYAN),
('Float 32', 'f32', 4, GRC_COLOR_ORANGE),
('Integer 64', 's64', 8, GRC_COLOR_LIME),
('Integer 32', 's32', 4, GRC_COLOR_TEAL),
('Integer 16', 's16', 2, GRC_COLOR_YELLOW),
('Integer 8', 's8', 1, GRC_COLOR_PURPLE_A400),
('Bits (unpacked byte)', 'bit', 1, GRC_COLOR_PURPLE_A100),
('Async Message', 'message', 0, GRC_COLOR_GREY),
('Bus Connection', 'bus', 0, GRC_COLOR_WHITE),
('Wildcard', '', 0, GRC_COLOR_WHITE),
)
ALIAS_TYPES = {
'complex': (8, GRC_COLOR_BLUE),
'float': (4, GRC_COLOR_ORANGE),
'int': (4, GRC_COLOR_TEAL),
'short': (2, GRC_COLOR_YELLOW),
'byte': (1, GRC_COLOR_PURPLE_A400),
'bits': (1, GRC_COLOR_PURPLE_A100),
}
ALIASES_OF = {
'complex': {'fc32'},
'float': {'f32'},
'int': {'s32'},
'short': {'s16', 'sc16'},
'byte': {'s8', 'sc8'},
'bits': {'bit'},
'fc32': {'complex'},
'f32': {'float'},
's32': {'int'},
's16': {'short'},
'sc16': {'short'},
's8': {'byte'},
'sc8': {'byte'},
'bit': {'bits'},
}
TYPE_TO_SIZEOF = {key: sizeof for name, key, sizeof, color in CORE_TYPES}
TYPE_TO_SIZEOF.update((key, sizeof)
for key, (sizeof, _) in ALIAS_TYPES.items())

578
grc/core/FlowGraph.py Normal file
View File

@ -0,0 +1,578 @@
# Copyright 2008-2015 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
import collections
import itertools
import sys
import types
import logging
import shlex
import yaml
from operator import methodcaller, attrgetter
from typing import (List, Set, Optional, Iterator, Iterable, Tuple, Union, OrderedDict)
from . import Messages
from .base import Element
from .blocks import Block
from .params import Param
from .utils import expr_utils
log = logging.getLogger(__name__)
class FlowGraph(Element):
is_flow_graph = True
def __init__(self, parent: Element):
"""
Make a flow graph from the arguments.
Args:
parent: a platforms with blocks and element factories
Returns:
the flow graph object
"""
Element.__init__(self, parent)
self.options_block: Block = self.parent_platform.make_block(self, 'options')
self.blocks = [self.options_block]
self.connections = set()
self._eval_cache = {}
self.namespace = {}
self.imported_names = []
self.grc_file_path = ''
def __str__(self) -> str:
return f"FlowGraph - {self.get_option('title')}({self.get_option('id')})"
def imports(self) -> List[str]:
"""
Get a set of all import statements (Python) in this flow graph namespace.
Returns:
a list of import statements
"""
return [block.templates.render('imports') for block in self.iter_enabled_blocks()]
def get_variables(self) -> List[str]:
"""
Get a list of all variables (Python) in this flow graph namespace.
Exclude parameterized variables.
Returns:
a sorted list of variable blocks in order of dependency (indep -> dep)
"""
variables = [block for block in self.iter_enabled_blocks()
if block.is_variable]
return expr_utils.sort_objects(variables, attrgetter('name'), methodcaller('get_var_make'))
def get_parameters(self) -> List[Element]:
"""
Get a list of all parameterized variables in this flow graph namespace.
Returns:
a list of parameterized variables
"""
parameters = [b for b in self.iter_enabled_blocks()
if b.key == 'parameter']
return parameters
def _get_snippets(self) -> List[Element]:
"""
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) -> List[dict]:
"""
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 len(d['lines']):
Messages.send_warning("Ignoring empty snippet from canvas")
else:
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) -> List[Element]:
"""
Get a list of all ControlPort monitors
"""
monitors = [b for b in self.iter_enabled_blocks()
if 'ctrlport_monitor' in b.key]
return monitors
def get_python_modules(self) -> Iterator[Tuple[str, str]]:
"""Iterate over custom code block ID and Source"""
for block in self.iter_enabled_blocks():
if block.key == 'epy_module':
yield block.name, block.params['source_code'].get_value()
def iter_enabled_blocks(self) -> Iterator[Element]:
"""
Get an iterator of all blocks that are enabled and not bypassed.
"""
return (block for block in self.blocks if block.enabled)
def get_enabled_blocks(self) -> List[Element]:
"""
Get a list of all blocks that are enabled and not bypassed.
Returns:
a list of blocks
"""
return list(self.iter_enabled_blocks())
def get_bypassed_blocks(self) -> List[Element]:
"""
Get a list of all blocks that are bypassed.
Returns:
a list of blocks
"""
return [block for block in self.blocks if block.get_bypassed()]
def get_enabled_connections(self) -> List[Element]:
"""
Get a list of all connections that are enabled.
Returns:
a list of connections
"""
return [connection for connection in self.connections if connection.enabled]
def get_option(self, key) -> Param.EvaluationType:
"""
Get the option for a given key.
The option comes from the special options block.
Args:
key: the param key for the options block
Returns:
the value held by that param
"""
return self.options_block.params[key].get_evaluated()
def get_run_command(self, file_path, split=False) -> Union[str, List[str]]:
run_command = self.get_option('run_command')
try:
run_command = run_command.format(
python=shlex.quote(sys.executable),
filename=shlex.quote(file_path))
return shlex.split(run_command) if split else run_command
except Exception as e:
raise ValueError(f"Can't parse run command {repr(run_command)}: {e}")
def get_imported_names(self) -> Set[str]:
"""
Get a list of imported names.
These names may not be used as id's
Returns:
a list of imported names
"""
return self.imported_names
##############################################
# Access Elements
##############################################
def get_block(self, name) -> Block:
for block in self.blocks:
if block.name == name:
return block
raise KeyError(f'No block with name {repr(name)}')
def get_elements(self) -> List[Element]:
elements = list(self.blocks)
elements.extend(self.connections)
return elements
def children(self) -> Iterable[Element]:
return itertools.chain(self.blocks, self.connections)
def rewrite(self):
"""
Flag the namespace to be renewed.
"""
self._renew_namespace()
Element.rewrite(self)
def _reload_imports(self, namespace: dict) -> dict:
"""
Load imports; be tolerant about import errors
"""
for expr in self.imports():
try:
exec(expr, namespace)
except ImportError:
# We do not have a good way right now to determine if an import is for a
# hier block, these imports will fail as they are not in the search path
# this is ok behavior, unfortunately we could be hiding other import bugs
pass
except Exception:
log.exception(f"Failed to evaluate import expression \"{expr}\"", exc_info=True)
pass
return namespace
def _reload_modules(self, namespace: dict) -> dict:
for id, expr in self.get_python_modules():
try:
module = types.ModuleType(id)
exec(expr, module.__dict__)
namespace[id] = module
except Exception:
log.exception(f'Failed to evaluate expression in module {id}', exc_info=True)
pass
return namespace
def _reload_parameters(self, namespace: dict) -> dict:
"""
Load parameters. Be tolerant of evaluation failures.
"""
np = {} # params don't know each other
for parameter_block in self.get_parameters():
try:
value = eval(
parameter_block.params['value'].to_code(), namespace)
np[parameter_block.name] = value
except Exception:
log.exception(f'Failed to evaluate parameter block {parameter_block.name}', exc_info=True)
pass
namespace.update(np) # Merge param namespace
return namespace
def _reload_variables(self, namespace: dict) -> dict:
"""
Load variables. Be tolerant of evaluation failures.
"""
for variable_block in self.get_variables():
try:
variable_block.rewrite()
value = eval(variable_block.value, namespace,
variable_block.namespace)
namespace[variable_block.name] = value
# rewrite on subsequent blocks depends on an updated self.namespace
self.namespace.update(namespace)
# The following Errors may happen, but that doesn't matter as they are displayed in the gui
except (TypeError, FileNotFoundError, AttributeError, yaml.YAMLError):
pass
except Exception:
log.exception(f'Failed to evaluate variable block {variable_block.name}', exc_info=True)
return namespace
def _renew_namespace(self) -> None:
# Before renewing the namespace, clear it
# to get rid of entries of blocks that
# are no longer valid ( deleted, disabled, ...)
self.namespace.clear()
namespace = self._reload_imports({})
self.imported_names = set(namespace.keys())
namespace = self._reload_modules(namespace)
namespace = self._reload_parameters(namespace)
# We need the updated namespace to evaluate the variable blocks
# otherwise sometimes variable_block rewrite / eval fails
self.namespace.update(namespace)
namespace = self._reload_variables(namespace)
self._eval_cache.clear()
def evaluate(self, expr: str, namespace: Optional[dict] = None, local_namespace: Optional[dict] = None):
"""
Evaluate the expression within the specified global and local namespaces
"""
# Evaluate
if not expr:
raise Exception('Cannot evaluate empty statement.')
if namespace is not None:
return eval(expr, namespace, local_namespace)
else:
return self._eval_cache.setdefault(expr, eval(expr, self.namespace, local_namespace))
##############################################
# Add/remove stuff
##############################################
def new_block(self, block_id, **kwargs) -> Block:
"""
Get a new block of the specified key.
Add the block to the list of elements.
Args:
block_id: the block key
Returns:
the new block or None if not found
"""
if block_id == 'options':
return self.options_block
try:
block = self.parent_platform.make_block(self, block_id, **kwargs)
self.blocks.append(block)
except KeyError:
block = None
return block
def connect(self, porta, portb, params=None):
"""
Create a connection between porta and portb.
Args:
porta: a port
portb: another port
@throw Exception bad connection
Returns:
the new connection
"""
connection = self.parent_platform.Connection(
parent=self, source=porta, sink=portb)
if params:
connection.import_data(params)
self.connections.add(connection)
return connection
def disconnect(self, *ports) -> None:
to_be_removed = [con for con in self.connections
if any(port in con for port in ports)]
for con in to_be_removed:
self.remove_element(con)
def remove_element(self, element) -> None:
"""
Remove the element from the list of elements.
If the element is a port, remove the whole block.
If the element is a block, remove its connections.
If the element is a connection, just remove the connection.
"""
if element is self.options_block:
return
if element.is_port:
element = element.parent_block # remove parent block
if element in self.blocks:
# Remove block, remove all involved connections
self.disconnect(*element.ports())
self.blocks.remove(element)
elif element in self.connections:
self.connections.remove(element)
##############################################
# Import/Export Methods
##############################################
def export_data(self) -> OrderedDict[str, str]:
"""
Export this flow graph to nested data.
Export all block and connection data.
Returns:
a nested data odict
"""
def block_order(b):
return not b.is_variable, b.name # todo: vars still first ?!?
def get_file_format_version(data) -> int:
"""Determine file format version based on available data"""
if any(isinstance(c, dict) for c in data['connections']):
return 2
return 1
def sort_connection_key(connection_info) -> List[str]:
if isinstance(connection_info, dict):
return [connection_info.get(key) for key in ('src_blk_id', 'src_port_id', 'snk_blk_id', 'snk_port_id')]
return connection_info
data = collections.OrderedDict()
data['options'] = self.options_block.export_data()
data['blocks'] = [b.export_data() for b in sorted(self.blocks, key=block_order)
if b is not self.options_block]
data['connections'] = sorted(
(c.export_data() for c in self.connections),
key=sort_connection_key)
data['metadata'] = {
'file_format': get_file_format_version(data),
'grc_version': self.parent_platform.config.version
}
return data
def _build_depending_hier_block(self, block_id) -> Optional[Block]:
# we're before the initial fg update(), so no evaluated values!
# --> use raw value instead
path_param = self.options_block.params['hier_block_src_path']
file_path = self.parent_platform.find_file_in_paths(
filename=block_id + '.grc',
paths=path_param.get_value(),
cwd=self.grc_file_path
)
if file_path: # grc file found. load and get block
self.parent_platform.load_and_generate_flow_graph(
file_path, hier_only=True)
return self.new_block(block_id) # can be None
def import_data(self, data) -> bool:
"""
Import blocks and connections into this flow graph.
Clear this flow graph of all previous blocks and connections.
Any blocks or connections in error will be ignored.
Args:
data: the nested data odict
Returns:
connection_error bool signifying whether a connection error happened.
"""
# Remove previous elements
del self.blocks[:]
self.connections.clear()
file_format = data['metadata']['file_format']
# build the blocks
self.options_block.import_data(name='', **data.get('options', {}))
self.blocks.append(self.options_block)
for block_data in data.get('blocks', []):
block_id = block_data['id']
block = (
self.new_block(block_id) or
self._build_depending_hier_block(block_id) or
self.new_block(block_id='_dummy',
missing_block_id=block_id, **block_data)
)
block.import_data(**block_data)
self.rewrite()
# build the connections
def verify_and_get_port(key, block, dir):
ports = block.sinks if dir == 'sink' else block.sources
for port in ports:
if key == port.key or key + '0' == port.key:
break
if not key.isdigit() and port.dtype == '' and key == port.name:
break
else:
if block.is_dummy_block:
port = block.add_missing_port(key, dir)
else:
raise LookupError(f"{dir} key {key} not in {dir} block keys")
return port
had_connect_errors = False
_blocks = {block.name: block for block in self.blocks}
# TODO: Add better error handling if no connections exist in the flowgraph file.
for connection_info in data.get('connections', []):
# First unpack the connection info, which can be in different formats.
# FLOW_GRAPH_FILE_FORMAT_VERSION 1: Connection info is a 4-tuple
if isinstance(connection_info, (list, tuple)) and len(connection_info) == 4:
src_blk_id, src_port_id, snk_blk_id, snk_port_id = connection_info
conn_params = {}
# FLOW_GRAPH_FILE_FORMAT_VERSION 2: Connection info is a dictionary
elif isinstance(connection_info, dict):
src_blk_id = connection_info.get('src_blk_id')
src_port_id = connection_info.get('src_port_id')
snk_blk_id = connection_info.get('snk_blk_id')
snk_port_id = connection_info.get('snk_port_id')
conn_params = connection_info.get('params', {})
else:
Messages.send_error_load('Invalid connection format detected!')
had_connect_errors = True
continue
try:
source_block = _blocks[src_blk_id]
sink_block = _blocks[snk_blk_id]
# fix old, numeric message ports keys
if file_format < 1:
src_port_id, snk_port_id = _update_old_message_port_keys(
src_port_id, snk_port_id, source_block, sink_block)
# build the connection
source_port = verify_and_get_port(
src_port_id, source_block, 'source')
sink_port = verify_and_get_port(
snk_port_id, sink_block, 'sink')
self.connect(source_port, sink_port, conn_params)
except (KeyError, LookupError) as e:
Messages.send_error_load(
f"""Connection between {src_blk_id}({src_port_id}) and {snk_blk_id}({snk_port_id}) could not be made
\t{e}""")
had_connect_errors = True
for block in self.blocks:
if block.is_dummy_block:
block.rewrite() # Make ports visible
# Flowgraph errors depending on disabled blocks are not displayed
# in the error dialog box
# So put a message into the Property window of the dummy block
block.add_error_message(f'Block id "{block.key}" not found.')
self.rewrite() # global rewrite
return had_connect_errors
def _update_old_message_port_keys(source_key, sink_key, source_block, sink_block) -> Tuple[str, str]:
"""
Backward compatibility for message port keys
Message ports use their names as key (like in the 'connect' method).
Flowgraph files from former versions still have numeric keys stored for
message connections. These have to be replaced by the name of the
respective port. The correct message port is deduced from the integer
value of the key (assuming the order has not changed).
The connection ends are updated only if both ends translate into a
message port.
"""
try:
# get ports using the "old way" (assuming linear indexed keys)
source_port = source_block.sources[int(source_key)]
sink_port = sink_block.sinks[int(sink_key)]
if source_port.dtype == "message" and sink_port.dtype == "message":
source_key, sink_key = source_port.key, sink_port.key
except (ValueError, IndexError):
pass
return source_key, sink_key # do nothing

147
grc/core/Messages.py Normal file
View File

@ -0,0 +1,147 @@
# Copyright 2007, 2015 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
import traceback
import sys
# A list of functions that can receive a message.
MESSENGERS_LIST = list()
_indent = ''
# Global FlowGraph Error and the file that caused it
flowgraph_error = None
flowgraph_error_file = None
def register_messenger(messenger):
"""
Append the given messenger to the list of messengers.
Args:
messenger: a method that takes a string
"""
MESSENGERS_LIST.append(messenger)
def set_indent(level=0):
global _indent
_indent = ' ' * level
def send(message):
"""
Give the message to each of the messengers.
Args:
message: a message string
"""
for messenger in MESSENGERS_LIST:
messenger(_indent + message)
# register stdout by default
register_messenger(sys.stdout.write)
###########################################################################
# Special functions for specific program functionalities
###########################################################################
def send_init(platform):
msg = "<<< Welcome to {config.name} {config.version} >>>\n\n" \
"Block paths:\n\t{paths}\n"
send(msg.format(
config=platform.config,
paths="\n\t".join(platform.config.block_paths)))
def send_xml_errors_if_any(xml_failures):
if xml_failures:
send('\nXML parser: Found {0} erroneous XML file{1} while loading the '
'block tree (see "Help/Parser errors" for details)\n'.format(
len(xml_failures), 's' if len(xml_failures) > 1 else ''))
def send_start_load(file_path):
send('\nLoading: "%s"\n' % file_path)
def send_error_msg_load(error):
send('>>> Error: %s\n' % error)
def send_error_load(error):
send_error_msg_load(error)
traceback.print_exc()
def send_end_load():
send('>>> Done\n')
def send_fail_load(error):
send('Error: %s\n>>> Failure\n' % error)
traceback.print_exc()
def send_start_gen(file_path):
send('\nGenerating: "%s"\n' % file_path)
def send_auto_gen(file_path):
send('>>> Generating: "%s"\n' % file_path)
def send_fail_gen(error):
send('Generate Error: %s\n>>> Failure\n' % error)
traceback.print_exc()
def send_start_exec(file_path):
send('\nExecuting: %s\n' % file_path)
def send_verbose_exec(verbose):
send(verbose)
def send_end_exec(code=0):
send('\n>>> Done%s\n' % (" (return code %s)" % code if code else ""))
def send_fail_save(file_path):
send('>>> Error: Cannot save: %s\n' % file_path)
def send_fail_connection(msg=''):
send('>>> Error: Cannot create connection.\n' +
('\t{}\n'.format(msg) if msg else ''))
def send_fail_load_preferences(prefs_file_path):
send('>>> Error: Cannot load preferences file: "%s"\n' % prefs_file_path)
def send_fail_save_preferences(prefs_file_path):
send('>>> Error: Cannot save preferences file: "%s"\n' % prefs_file_path)
def send_warning(warning):
send('>>> Warning: %s\n' % warning)
def send_flowgraph_error_report(flowgraph):
""" verbose error report for flowgraphs """
error_list = flowgraph.get_error_messages()
if not error_list:
return
send('*' * 50 + '\n')
summary_msg = '{} errors from flowgraph:\n'.format(len(error_list))
send(summary_msg)
for err in error_list:
send(err)
send('\n' + '*' * 50 + '\n')

0
grc/core/__init__.py Normal file
View File

153
grc/core/base.py Normal file
View File

@ -0,0 +1,153 @@
# Copyright 2008, 2009, 2015, 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
import weakref
from .utils.descriptors import lazy_property
class Element(object):
def __init__(self, parent=None):
self._parent = weakref.ref(parent) if parent else lambda: None
self._error_messages = []
##################################################
# Element Validation API
##################################################
def validate(self):
"""
Validate this element and call validate on all children.
Call this base method before adding error messages in the subclass.
"""
for child in self.children():
child.validate()
def is_valid(self):
"""
Is this element valid?
Returns:
true when the element is enabled and has no error messages or is bypassed
"""
if not self.enabled or self.get_bypassed():
return True
return not next(self.iter_error_messages(), False)
def add_error_message(self, msg):
"""
Add an error message to the list of errors.
Args:
msg: the error message string
"""
self._error_messages.append(msg)
def get_error_messages(self):
"""
Get the list of error messages from this element and all of its children.
Do not include the error messages from disabled or bypassed children.
Cleverly indent the children error messages for printing purposes.
Returns:
a list of error message strings
"""
return [msg if elem is self else "{}:\n\t{}".format(elem, msg.replace("\n", "\n\t"))
for elem, msg in self.iter_error_messages()]
def iter_error_messages(self):
"""
Iterate over error messages. Yields tuples of (element, message)
"""
for msg in self._error_messages:
yield self, msg
for child in self.children():
if not child.enabled or child.get_bypassed():
continue
for element_msg in child.iter_error_messages():
yield element_msg
def rewrite(self):
"""
Rewrite this element and call rewrite on all children.
Call this base method before rewriting the element.
"""
del self._error_messages[:]
for child in self.children():
child.rewrite()
@property
def enabled(self):
return True
def get_bypassed(self):
return False
##############################################
# Tree-like API
##############################################
@property
def parent(self):
return self._parent()
def get_parent_by_type(self, cls):
parent = self.parent
if parent is None:
return None
elif isinstance(parent, cls):
return parent
else:
return parent.get_parent_by_type(cls)
@lazy_property
def parent_platform(self):
from .platform import Platform
return self.get_parent_by_type(Platform)
@lazy_property
def parent_flowgraph(self):
from .FlowGraph import FlowGraph
return self.get_parent_by_type(FlowGraph)
@lazy_property
def parent_block(self):
from .blocks import Block
return self.get_parent_by_type(Block)
def reset_parents_by_type(self):
"""Reset all lazy properties"""
for name, obj in vars(Element): # explicitly only in Element, not subclasses
if isinstance(obj, lazy_property):
delattr(self, name)
def children(self):
return
yield # empty generator
##############################################
# Type testing
##############################################
is_flow_graph = False
is_block = False
is_dummy_block = False
is_connection = False
is_port = False
is_param = False
is_variable = False
is_import = False
is_snippet = False
def get_raw(self, name):
descriptor = getattr(self.__class__, name, None)
if not descriptor:
raise ValueError("No evaluated property '{}' found".format(name))
return getattr(self, descriptor.name_raw, None) or getattr(self, descriptor.name, None)
def set_evaluated(self, name, value):
descriptor = getattr(self.__class__, name, None)
if not descriptor:
raise ValueError("No evaluated property '{}' found".format(name))
self.__dict__[descriptor.name] = value

View File

@ -0,0 +1,27 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from ._flags import Flags
from ._templates import MakoTemplates
from .block import Block
from ._build import build
build_ins = {}
def register_build_in(cls):
cls.loaded_from = '(build-in)'
build_ins[cls.key] = cls
return cls
from .dummy import DummyBlock
from .embedded_python import EPyBlock, EPyModule
from .virtual import VirtualSink, VirtualSource

170
grc/core/blocks/_build.py Normal file
View File

@ -0,0 +1,170 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
import itertools
import re
from typing import Type
from ..Constants import ADVANCED_PARAM_TAB
from ..utils import to_list
from ..Messages import send_warning
from .block import Block
from ._flags import Flags
from ._templates import MakoTemplates
def build(id,
label='',
category='',
flags='',
documentation='',
value=None,
asserts=None,
parameters=None,
inputs=None,
outputs=None,
templates=None,
cpp_templates=None,
doc_url=None,
**kwargs) -> Type[Block]:
block_id = id
cls = type(str(block_id), (Block,), {})
cls.key = block_id
cls.label = label or block_id.title()
cls.category = [cat.strip() for cat in category.split('/') if cat.strip()]
cls.flags = Flags(flags)
if re.match(r'options$|variable|virtual', block_id):
cls.flags.set(Flags.NOT_DSP, Flags.DISABLE_BYPASS)
cls.documentation = {'': documentation.strip('\n\t ').replace('\\\n', '')}
cls.asserts = [_single_mako_expr(a, block_id) for a in to_list(asserts)]
cls.inputs_data = build_ports(inputs, 'sink') if inputs else []
cls.outputs_data = build_ports(outputs, 'source') if outputs else []
cls.parameters_data = build_params(parameters or [],
bool(cls.inputs_data), bool(cls.outputs_data), cls.flags, block_id)
cls.extra_data = kwargs
templates = templates or {}
cls.templates = MakoTemplates(
imports=templates.get('imports', ''),
make=templates.get('make', ''),
callbacks=templates.get('callbacks', []),
var_make=templates.get('var_make', ''),
)
cpp_templates = cpp_templates or {}
cls.cpp_templates = MakoTemplates(
includes=cpp_templates.get('includes', []),
make=cpp_templates.get('make', ''),
callbacks=cpp_templates.get('callbacks', []),
var_make=cpp_templates.get('var_make', ''),
link=cpp_templates.get('link', []),
packages=cpp_templates.get('packages', []),
translations=cpp_templates.get('translations', []),
declarations=cpp_templates.get('declarations', ''),
)
cls.doc_url = doc_url if doc_url else ""
# todo: MakoTemplates.compile() to check for errors
cls.value = _single_mako_expr(value, block_id)
return cls
def build_ports(ports_raw, direction):
ports = []
port_ids = set()
stream_port_ids = itertools.count()
for i, port_params in enumerate(ports_raw):
port = port_params.copy()
port['direction'] = direction
port_id = port.setdefault('id', str(next(stream_port_ids)))
if port_id in port_ids:
raise Exception(
'Port id "{}" already exists in {}s'.format(port_id, direction))
port_ids.add(port_id)
ports.append(port)
return ports
def build_params(params_raw, have_inputs, have_outputs, flags, block_id):
params = []
def add_param(**data):
params.append(data)
if flags.SHOW_ID in flags:
add_param(id='id', name='ID', dtype='id', hide='none')
else:
add_param(id='id', name='ID', dtype='id', hide='all')
if not flags.not_dsp:
add_param(id='alias', name='Block Alias', dtype='string',
hide='part', category=ADVANCED_PARAM_TAB)
if have_outputs or have_inputs:
add_param(id='affinity', name='Core Affinity', dtype='int_vector',
hide='part', category=ADVANCED_PARAM_TAB)
if have_outputs:
add_param(id='minoutbuf', name='Min Output Buffer', dtype='int',
hide='part', default='0', category=ADVANCED_PARAM_TAB)
add_param(id='maxoutbuf', name='Max Output Buffer', dtype='int',
hide='part', default='0', category=ADVANCED_PARAM_TAB)
base_params_n = {}
for param_data in params_raw:
param_id = param_data['id']
if param_id in params:
raise Exception('Param id "{}" is not unique'.format(param_id))
base_key = param_data.get('base_key', None)
param_data_ext = base_params_n.get(base_key, {}).copy()
param_data_ext.update(param_data)
if 'option_attributes' in param_data:
_validate_option_attributes(param_data_ext, block_id)
add_param(**param_data_ext)
base_params_n[param_id] = param_data_ext
add_param(id='comment', name='Comment', dtype='_multiline', hide='part',
default='', category=ADVANCED_PARAM_TAB)
return params
def _single_mako_expr(value, block_id):
if not value:
return None
value = value.strip()
if not (value.startswith('${') and value.endswith('}')):
raise ValueError(
'{} is not a mako substitution in {}'.format(value, block_id))
return value[2:-1].strip()
def _validate_option_attributes(param_data, block_id):
if param_data['dtype'] != 'enum':
send_warning(
'{} - option_attributes are for enums only, ignoring'.format(block_id))
del param_data['option_attributes']
else:
for key in list(param_data['option_attributes'].keys()):
if key in dir(str):
del param_data['option_attributes'][key]
send_warning(
'{} - option_attribute "{}" overrides str, ignoring'.format(block_id, key))

36
grc/core/blocks/_flags.py Normal file
View File

@ -0,0 +1,36 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
class Flags(object):
THROTTLE = 'throttle'
DISABLE_BYPASS = 'disable_bypass'
NEED_QT_GUI = 'need_qt_gui'
DEPRECATED = 'deprecated'
NOT_DSP = 'not_dsp'
SHOW_ID = 'show_id'
HAS_PYTHON = 'python'
HAS_CPP = 'cpp'
def __init__(self, flags=None):
if flags is None:
flags = set()
if isinstance(flags, str):
flags = (f.strip() for f in flags.replace(',', '').split())
self.data = set(flags)
def __getattr__(self, item):
return item in self
def __contains__(self, item):
return item in self.data
def __str__(self):
return ', '.join(self.data)
def set(self, *flags):
self.data.update(flags)

View File

@ -0,0 +1,79 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
"""
This dict class holds a (shared) cache of compiled mako templates.
These
"""
from mako.template import Template
from mako.exceptions import SyntaxException
from ..errors import TemplateError
# The utils dict contains convenience functions
# that can be called from any template
def no_quotes(string, fallback=None):
if len(string) > 2:
if str(string)[0] + str(string)[-1] in ("''", '""'):
return str(string)[1:-1]
return str(fallback if fallback else string)
utils = {'no_quotes': no_quotes}
class MakoTemplates(dict):
_template_cache = {}
def __init__(self, _bind_to=None, *args, **kwargs):
self.instance = _bind_to
dict.__init__(self, *args, **kwargs)
def __get__(self, instance, owner):
if instance is None or self.instance is not None:
return self
copy = self.__class__(_bind_to=instance, **self)
if getattr(instance.__class__, 'templates', None) is self:
setattr(instance, 'templates', copy)
return copy
@classmethod
def compile(cls, text):
text = str(text)
try:
template = Template(text, strict_undefined=True)
except SyntaxException as error:
raise TemplateError(text, *error.args)
cls._template_cache[text] = template
return template
def _get_template(self, text):
try:
return self._template_cache[str(text)]
except KeyError:
return self.compile(text)
def render(self, item):
text = self.get(item)
if not text:
return ''
namespace = self.instance.namespace_templates
namespace = {**namespace, **utils}
try:
if isinstance(text, list):
templates = (self._get_template(t) for t in text)
return [template.render(**namespace) for template in templates]
else:
template = self._get_template(text)
return template.render(**namespace)
except Exception as error:
raise TemplateError(error, text)

789
grc/core/blocks/block.py Normal file
View File

@ -0,0 +1,789 @@
"""
Copyright 2008-2020 Free Software Foundation, Inc.
Copyright 2021 GNU Radio contributors
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
import collections
import itertools
import copy
import re
import ast
import typing
from ._templates import MakoTemplates, no_quotes
from ._flags import Flags
from ..base import Element
from ..params import Param
from ..utils.descriptors import lazy_property
def _get_elem(iterable, key):
items = list(iterable)
for item in items:
if item.key == key:
return item
return ValueError('Key "{}" not found in {}.'.format(key, items))
class Block(Element):
is_block = True
STATE_LABELS = ['disabled', 'enabled', 'bypassed']
key = ''
label = ''
category = []
vtype = '' # This is only used for variables when we want C++ output
flags = Flags('')
documentation = {'': ''}
doc_url = ''
value = None
asserts = []
templates = MakoTemplates()
parameters_data = []
inputs_data = []
outputs_data = []
extra_data = {}
loaded_from = '(unknown)'
def __init__(self, parent):
"""Make a new block from nested data."""
super(Block, self).__init__(parent)
param_factory = self.parent_platform.make_param
port_factory = self.parent_platform.make_port
self.params: typing.OrderedDict[str, Param] = collections.OrderedDict(
(data['id'], param_factory(parent=self, **data)) for data in self.parameters_data)
if self.key == 'options':
self.params['id'].hide = 'part'
self.sinks = [port_factory(parent=self, **params)
for params in self.inputs_data]
self.sources = [port_factory(parent=self, **params)
for params in self.outputs_data]
self.active_sources = [] # on rewrite
self.active_sinks = [] # on rewrite
self.states = {'state': 'enabled', 'bus_source': False,
'bus_sink': False, 'bus_structure': None}
self.block_namespace = {}
self.deprecated = self.is_deprecated()
if Flags.HAS_CPP in self.flags and self.enabled and not (self.is_virtual_source() or self.is_virtual_sink()):
# This is a workaround to allow embedded python blocks/modules to load as there is
# currently 'cpp' in the flags by default caused by the other built-in blocks
if hasattr(self, 'cpp_templates'):
# The original template, in case we have to edit it when transpiling to C++
self.orig_cpp_templates = self.cpp_templates
self.current_bus_structure = {'source': None, 'sink': None}
def get_bus_structure(self, direction):
if direction == 'source':
bus_structure = self.bus_structure_source
else:
bus_structure = self.bus_structure_sink
if not bus_structure:
return None
try:
clean_bus_structure = self.evaluate(bus_structure)
return clean_bus_structure
except Exception:
return None
# region Rewrite_and_Validation
def rewrite(self):
"""
Add and remove ports to adjust for the nports.
"""
Element.rewrite(self)
def rekey(ports):
"""Renumber non-message/message ports"""
domain_specific_port_index = collections.defaultdict(int)
for port in ports:
if not port.key.isdigit():
continue
domain = port.domain
port.key = str(domain_specific_port_index[domain])
domain_specific_port_index[domain] += 1
# Adjust nports
for ports in (self.sources, self.sinks):
self._rewrite_nports(ports)
rekey(ports)
self.update_bus_logic()
# disconnect hidden ports
self.parent_flowgraph.disconnect(
*[p for p in self.ports() if p.hidden])
self.active_sources = [p for p in self.sources if not p.hidden]
self.active_sinks = [p for p in self.sinks if not p.hidden]
# namespaces may have changed, update them
self.block_namespace.clear()
imports = ""
try:
imports = self.templates.render('imports')
exec(imports, self.block_namespace)
except ImportError:
# We do not have a good way right now to determine if an import is for a
# hier block, these imports will fail as they are not in the search path
# this is ok behavior, unfortunately we could be hiding other import bugs
pass
except Exception:
self.add_error_message(
f'Failed to evaluate import expression {imports!r}')
def update_bus_logic(self):
###############################
# Bus Logic
###############################
for direc in {'source', 'sink'}:
if direc == 'source':
ports = self.sources
ports_gui = self.filter_bus_port(self.sources)
bus_state = self.bus_source
else:
ports = self.sinks
ports_gui = self.filter_bus_port(self.sinks)
bus_state = self.bus_sink
# Remove the bus ports
removed_bus_ports = []
removed_bus_connections = []
if 'bus' in map(lambda a: a.dtype, ports):
for port in ports_gui:
for c in self.parent_flowgraph.connections:
if port is c.source_port or port is c.sink_port:
removed_bus_ports.append(port)
removed_bus_connections.append(c)
ports.remove(port)
if (bus_state):
struct = self.form_bus_structure(direc)
self.current_bus_structure[direc] = struct
# Hide ports that are not part of the bus structure
# TODO: Blocks where it is desired to only have a subset
# of ports included in the bus still has some issues
for idx, port in enumerate(ports):
if any([idx in bus for bus in self.current_bus_structure[direc]]):
if (port.stored_hidden_state is None):
port.stored_hidden_state = port.hidden
port.hidden = True
# Add the Bus Ports to the list of ports
for i in range(len(struct)):
# self.sinks = [port_factory(parent=self, **params) for params in self.inputs_data]
port = self.parent.parent.make_port(self, direction=direc, id=str(
len(ports)), label='bus', dtype='bus', bus_struct=struct[i])
ports.append(port)
for (saved_port, connection) in zip(removed_bus_ports, removed_bus_connections):
if port.key == saved_port.key:
self.parent_flowgraph.connections.remove(
connection)
if saved_port.is_source:
connection.source_port = port
if saved_port.is_sink:
connection.sink_port = port
self.parent_flowgraph.connections.add(connection)
else:
self.current_bus_structure[direc] = None
# Re-enable the hidden property of the ports
for port in ports:
if (port.stored_hidden_state is not None):
port.hidden = port.stored_hidden_state
port.stored_hidden_state = None
def _rewrite_nports(self, ports):
for port in ports:
if hasattr(port, 'master_port'): # Not a master port and no left-over clones
port.dtype = port.master_port.dtype
port.vlen = port.master_port.vlen
continue
nports = port.multiplicity
for clone in port.clones[nports - 1:]:
# Remove excess connections
self.parent_flowgraph.disconnect(clone)
port.remove_clone(clone)
ports.remove(clone)
# Add more cloned ports
for j in range(1 + len(port.clones), nports):
clone = port.add_clone()
ports.insert(ports.index(port) + j, clone)
def validate(self):
"""
Validate this block.
Call the base class validate.
Evaluate the checks: each check must evaluate to True.
"""
Element.validate(self)
self._run_asserts()
self._validate_generate_mode_compat()
self._validate_output_language_compat()
self._validate_var_value()
def _run_asserts(self):
"""Evaluate the checks"""
for expr in self.asserts:
try:
if not self.evaluate(expr):
self.add_error_message(
'Assertion "{}" failed.'.format(expr))
except Exception:
self.add_error_message(
'Assertion "{}" did not evaluate.'.format(expr))
def _validate_generate_mode_compat(self):
"""check if this is a GUI block and matches the selected generate option"""
current_generate_option = self.parent.get_option('generate_options')
def check_generate_mode(label, flag, valid_options):
block_requires_mode = (
flag in self.flags or self.label.upper().startswith(label)
)
if block_requires_mode and current_generate_option not in valid_options:
self.add_error_message("Can't generate this block in mode: {} ".format(
repr(current_generate_option)))
check_generate_mode('QT GUI', Flags.NEED_QT_GUI,
('qt_gui', 'hb_qt_gui'))
def _validate_output_language_compat(self):
"""check if this block supports the selected output language"""
current_output_language = self.parent.get_option('output_language')
if current_output_language == 'cpp':
if 'cpp' not in self.flags:
self.add_error_message(
"This block does not support C++ output.")
if self.key == 'parameter':
if not self.params['type'].value:
self.add_error_message(
"C++ output requires you to choose a parameter type.")
def _validate_var_value(self):
"""or variables check the value (only if var_value is used)"""
if self.is_variable and self.value != 'value':
try:
self.parent_flowgraph.evaluate(
self.value, local_namespace=self.namespace)
except Exception as err:
self.add_error_message(
'Value "{}" cannot be evaluated:\n{}'.format(self.value, err))
# endregion
# region Properties
def __str__(self):
return 'Block - {} - {}({})'.format(self.name, self.label, self.key)
def __repr__(self):
try:
name = self.name
except Exception:
name = self.key
return 'block[' + name + ']'
@property
def name(self):
return self.params['id'].value
@lazy_property
def is_virtual_or_pad(self):
return self.key in ("virtual_source", "virtual_sink", "pad_source", "pad_sink")
@lazy_property
def is_variable(self):
return bool(self.value)
@lazy_property
def is_import(self):
return self.key == 'import'
@lazy_property
def is_snippet(self):
return self.key == 'snippet'
@property
def comment(self):
return self.params['comment'].value
@property
def state(self):
"""Gets the block's current state."""
state = self.states['state']
return state if state in self.STATE_LABELS else 'enabled'
@state.setter
def state(self, value):
"""Sets the state for the block."""
self.states['state'] = value
# Enable/Disable Aliases
@property
def enabled(self):
"""Get the enabled state of the block"""
return self.state != 'disabled'
@property
def bus_sink(self):
"""Gets the block's current Toggle Bus Sink state."""
return self.states['bus_sink']
@bus_sink.setter
def bus_sink(self, value):
"""Sets the Toggle Bus Sink state for the block."""
self.states['bus_sink'] = value
@property
def bus_source(self):
"""Gets the block's current Toggle Bus Sink state."""
return self.states['bus_source']
@bus_source.setter
def bus_source(self, value):
"""Sets the Toggle Bus Source state for the block."""
self.states['bus_source'] = value
@property
def bus_structure_source(self):
"""Gets the block's current source bus structure."""
try:
bus_structure = self.params['bus_structure_source'].value or None
except Exception:
bus_structure = None
return bus_structure
@property
def bus_structure_sink(self):
"""Gets the block's current source bus structure."""
try:
bus_structure = self.params['bus_structure_sink'].value or None
except Exception:
bus_structure = None
return bus_structure
# endregion
##############################################
# Getters (old)
##############################################
def get_var_make(self):
return self.templates.render('var_make')
def get_cpp_var_make(self):
return self.cpp_templates.render('var_make')
def get_var_value(self):
return self.templates.render('var_value')
def get_callbacks(self):
"""
Get a list of function callbacks for this block.
Returns:
a list of strings
"""
def make_callback(callback):
if 'self.' in callback:
return callback
return 'self.{}.{}'.format(self.name, callback)
return [make_callback(c) for c in self.templates.render('callbacks')]
def get_cpp_callbacks(self):
"""
Get a list of C++ function callbacks for this block.
Returns:
a list of strings
"""
def make_callback(callback):
if self.is_variable:
return callback
if 'this->' in callback:
return callback
return 'this->{}->{}'.format(self.name, callback)
return [make_callback(c) for c in self.cpp_templates.render('callbacks')]
def format_expr(self, py_type):
"""
Evaluate the value of the variable block and decide its type.
Returns:
None
"""
value = self.params['value'].value
self.cpp_templates = copy.copy(self.orig_cpp_templates)
# Determine the lvalue type
def get_type(element: str, vtype: typing.Optional[type] = None) -> str:
evaluated = None
try:
evaluated = ast.literal_eval(element)
if vtype is None:
vtype = type(evaluated)
except ValueError or SyntaxError as excp:
if vtype is None:
print(excp)
simple_types = {int: "long", float: "double", bool: "bool", complex: "gr_complex", str: "std::string"}
if vtype in simple_types:
return simple_types[vtype]
elif vtype == list:
try:
# For container types we must also determine the type of the template parameter(s)
return f"std::vector<{get_type(str(evaluated[0]), type(evaluated[0]))}>"
except IndexError: # empty list
return 'std::vector<std::string>'
elif vtype == dict:
try:
# For container types we must also determine the type of the template parameter(s)
key, val = next(iter(evaluated.entries()))
return f"std::map<{get_type(str(key), type(key))}, {get_type(str(val), type(val))}>"
except IndexError: # empty dict
return 'std::map<std::string, std::string>'
# Get the lvalue type
self.vtype = get_type(value, py_type)
# The r-value for these types must be transformed to create legal C++ syntax.
if self.vtype in ['bool', 'gr_complex'] or 'std::map' in self.vtype or 'std::vector' in self.vtype:
evaluated = ast.literal_eval(value)
self.cpp_templates['var_make'] = self.cpp_templates['var_make'].replace(
'${value}', self.get_cpp_value(evaluated))
if 'string' in self.vtype:
self.cpp_templates['includes'].append('#include <string>')
def get_cpp_value(self, pyval, vtype: typing.Optional[type] = None) -> str:
"""
Convert an evaluated variable value from Python to C++ with a defined type.
Returns:
string representation of the C++ value
"""
if vtype is None:
vtype = type(pyval)
else:
assert vtype == type(pyval)
if vtype == int or vtype == float:
val_str = str(pyval)
# Check for PI and replace with C++ constant
pi_re = r'^(math|numpy|np|scipy|sp)\.pi$'
if re.match(pi_re, str(pyval)):
val_str = re.sub(
pi_re, 'boost::math::constants::pi<double>()', val_str)
self.cpp_templates['includes'].append(
'#include <boost/math/constants/constants.hpp>')
return str(pyval)
elif vtype == bool:
return str(pyval).lower()
elif vtype == complex:
self.cpp_templates['includes'].append(
'#include <gnuradio/gr_complex.h>')
evaluated = ast.literal_eval(str(pyval).strip())
return '{' + str(evaluated.real) + ', ' + str(evaluated.imag) + '}'
elif vtype == list:
self.cpp_templates['includes'].append('#include <vector>')
if len(pyval) == 0:
return '{}'
item_type = type(pyval[0])
elements = [str(self.get_cpp_value(element, item_type)) for element in pyval]
return '{' + ', '.join(elements) + '}'
elif vtype == dict:
self.cpp_templates['includes'].append('#include <map>')
key_type, val_type = next(iter(pyval.entries()))
key_type, val_type = type(key_type), type(val_type)
entries = ['{' + self.get_cpp_value(key, key_type) + ', ' + self.get_cpp_value(val, val_type) + '}'
for key, val in pyval.entries()]
return '{' + ', '.join(entries) + '}'
elif vtype == str:
self.cpp_templates['includes'].append('#include <string>')
value = pyval.strip()
if value in ['""', "''"]:
return '""'
return f'"{no_quotes(value)}"'
raise TypeError(f"Unsupported C++ vtype: {vtype}")
def is_virtual_sink(self):
return self.key == 'virtual_sink'
def is_virtual_source(self):
return self.key == 'virtual_source'
def is_deprecated(self):
"""
Check whether the block is deprecated.
For now, we just check the category name for presence of "deprecated".
As it might be desirable in the future to have such "tags" be stored
explicitly, we're taking the detour of introducing a property.
"""
if not self.category:
return False
try:
return (self.flags.deprecated or
any("deprecated".casefold() in cat.casefold()
for cat in self.category))
except Exception as exception:
print(exception.message)
return False
# Block bypassing
def get_bypassed(self):
"""
Check if the block is bypassed
"""
return self.state == 'bypassed'
def set_bypassed(self):
"""
Bypass the block
Returns:
True if block changes state
"""
if self.state != 'bypassed' and self.can_bypass():
self.state = 'bypassed'
return True
return False
def can_bypass(self):
"""
Check the number of sinks and sources and see if this block can be bypassed
"""
# Check to make sure this is a single path block
# Could possibly support 1 to many blocks
if len(self.sources) != 1 or len(self.sinks) != 1:
return False
if not (self.sources[0].dtype == self.sinks[0].dtype):
return False
if self.flags.disable_bypass:
return False
return True
def ports(self):
return itertools.chain(self.sources, self.sinks)
def active_ports(self):
return itertools.chain(self.active_sources, self.active_sinks)
def children(self):
return itertools.chain(self.params.values(), self.ports())
def connections(self):
block_connections = []
for port in self.ports():
block_connections = block_connections + list(port.connections())
return block_connections
##############################################
# Access
##############################################
def get_sink(self, key):
return _get_elem(self.sinks, key)
def get_source(self, key):
return _get_elem(self.sources, key)
##############################################
# Resolve
##############################################
@property
def namespace(self):
# update block namespace
self.block_namespace.update({key: param.get_evaluated() for key, param in self.params.items()})
return self.block_namespace
@property
def namespace_templates(self):
return {key: param.template_arg for key, param in self.params.items()}
def evaluate(self, expr):
return self.parent_flowgraph.evaluate(expr, self.namespace)
##############################################
# Import/Export Methods
##############################################
def export_data(self):
"""
Export this block's params to nested data.
Returns:
a nested data odict
"""
data = collections.OrderedDict()
if self.key != 'options':
data['name'] = self.name
data['id'] = self.key
data['parameters'] = collections.OrderedDict(sorted(
(param_id, param.value)
for param_id, param in self.params.items()
if (param_id != 'id' or self.key == 'options')
))
data['states'] = collections.OrderedDict(sorted(self.states.items()))
return data
def import_data(self, name, states, parameters, **_):
"""
Import this block's params from nested data.
Any param keys that do not exist will be ignored.
Since params can be dynamically created based another param,
call rewrite, and repeat the load until the params stick.
"""
self.params['id'].value = name
self.states.update(states)
def get_hash():
return hash(tuple(hash(v) for v in self.params.values()))
pre_rewrite_hash = -1
while pre_rewrite_hash != get_hash():
for key, value in parameters.items():
try:
self.params[key].set_value(value)
except KeyError:
continue
# Store hash and call rewrite
pre_rewrite_hash = get_hash()
self.rewrite()
##############################################
# Controller Modify
##############################################
def filter_bus_port(self, ports):
buslist = [p for p in ports if p.dtype == 'bus']
return buslist or ports
def type_controller_modify(self, direction):
"""
Change the type controller.
Args:
direction: +1 or -1
Returns:
true for change
"""
changed = False
type_param = None
for param in filter(lambda p: p.is_enum(), self.get_params()):
children = self.get_ports() + self.get_params()
# Priority to the type controller
if param.get_key() in ' '.join(map(lambda p: p._type, children)):
type_param = param
# Use param if type param is unset
if not type_param:
type_param = param
if type_param:
# Try to increment the enum by direction
try:
keys = type_param.get_option_keys()
old_index = keys.index(type_param.get_value())
new_index = (old_index + direction + len(keys)) % len(keys)
type_param.set_value(keys[new_index])
changed = True
except Exception:
pass
return changed
def form_bus_structure(self, direc):
if direc == 'source':
ports = self.sources
bus_structure = self.get_bus_structure('source')
else:
ports = self.sinks
bus_structure = self.get_bus_structure('sink')
struct = [range(len(ports))]
# struct = list(range(len(ports)))
# TODO for more complicated port structures, this code is needed but not working yet
if any([p.multiplicity for p in ports]):
structlet = []
last = 0
# group the ports with > n inputs together on the bus
cnt = 0
idx = 0
for p in ports:
if p.domain == 'message':
continue
if cnt > 0:
cnt -= 1
continue
if p.multiplicity > 1:
cnt = p.multiplicity - 1
structlet.append([idx + j for j in range(p.multiplicity)])
else:
structlet.append([idx])
struct = structlet
if bus_structure:
struct = bus_structure
self.current_bus_structure[direc] = struct
return struct
def bussify(self, direc):
if direc == 'source':
ports = self.sources
if ports:
ports_gui = self.filter_bus_port(self.sources)
self.bus_structure = self.get_bus_structure('source')
self.bus_source = not self.bus_source
else:
ports = self.sinks
if ports:
ports_gui = self.filter_bus_port(self.sinks)
self.bus_structure = self.get_bus_structure('sink')
self.bus_sink = not self.bus_sink
# Disconnect all the connections when toggling the bus state
for port in ports:
l_connections = list(port.connections())
for connect in l_connections:
self.parent.remove_element(connect)
self.update_bus_logic()

47
grc/core/blocks/dummy.py Normal file
View File

@ -0,0 +1,47 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from . import Block, register_build_in
from ._build import build_params
@register_build_in
class DummyBlock(Block):
is_dummy_block = True
label = 'Missing Block'
key = '_dummy'
def __init__(self, parent, missing_block_id, parameters, **_):
self.key = missing_block_id
self.parameters_data = build_params(
[], False, False, self.flags, self.key)
super(DummyBlock, self).__init__(parent=parent)
param_factory = self.parent_platform.make_param
for param_id in parameters:
self.params.setdefault(param_id, param_factory(
parent=self, id=param_id, dtype='string'))
def is_valid(self):
return False
@property
def enabled(self):
return False
def add_missing_port(self, port_id, direction):
port = self.parent_platform.make_port(
parent=self, direction=direction, id=port_id, name='?', dtype='',
)
if port.is_source:
self.sources.append(port)
else:
self.sinks.append(port)
return port

View File

@ -0,0 +1,248 @@
# Copyright 2015-16 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from ast import literal_eval
from textwrap import dedent
from . import Block, register_build_in
from ._templates import MakoTemplates
from ._flags import Flags
from .. import utils
from ..base import Element
from ._build import build_params
DEFAULT_CODE = '''\
"""
Embedded Python Blocks:
Each time this file is saved, GRC will instantiate the first class it finds
to get ports and parameters of your block. The arguments to __init__ will
be the parameters. All of them are required to have default values!
"""
import numpy as np
from gnuradio import gr
class blk(gr.sync_block): # other base classes are basic_block, decim_block, interp_block
"""Embedded Python Block example - a simple multiply const"""
def __init__(self, example_param=1.0): # only default arguments here
"""arguments to this function show up as parameters in GRC"""
gr.sync_block.__init__(
self,
name='Embedded Python Block', # will show up in GRC
in_sig=[np.complex64],
out_sig=[np.complex64]
)
# if an attribute with the same name as a parameter is found,
# a callback is registered (properties work, too).
self.example_param = example_param
def work(self, input_items, output_items):
"""example: multiply with constant"""
output_items[0][:] = input_items[0] * self.example_param
return len(output_items[0])
'''
DOC = """
This block represents an arbitrary GNU Radio Python Block.
Its source code can be accessed through the parameter 'Code' which opens your editor. \
Each time you save changes in the editor, GRC will update the block. \
This includes the number, names and defaults of the parameters, \
the ports (stream and message) and the block name and documentation.
Block Documentation:
(will be replaced the docstring of your block class)
"""
@register_build_in
class EPyBlock(Block):
key = 'epy_block'
label = 'Python Block'
exempt_from_id_validation = True # Exempt epy block from blacklist id validation
documentation = {'': DOC}
parameters_data = build_params(
params_raw=[
dict(label='Code', id='_source_code', dtype='_multiline_python_external',
default=DEFAULT_CODE, hide='part')
], have_inputs=True, have_outputs=True, flags=Block.flags, block_id=key
)
inputs_data = []
outputs_data = []
def __init__(self, flow_graph, **kwargs):
super(EPyBlock, self).__init__(flow_graph, **kwargs)
self.states['_io_cache'] = ''
self.module_name = self.name
self._epy_source_hash = -1
self._epy_reload_error = None
def rewrite(self):
Element.rewrite(self)
param_src = self.params['_source_code']
src = param_src.get_value()
src_hash = hash((self.name, src))
if src_hash == self._epy_source_hash:
return
try:
blk_io = utils.epy_block_io.extract(src)
except Exception as e:
self._epy_reload_error = ValueError(str(e))
try: # Load last working block io
blk_io_args = literal_eval(self.states['_io_cache'])
if len(blk_io_args) == 6:
blk_io_args += ([],) # add empty callbacks
blk_io = utils.epy_block_io.BlockIO(*blk_io_args)
except Exception:
return
else:
self._epy_reload_error = None # Clear previous errors
self.states['_io_cache'] = repr(tuple(blk_io))
# print "Rewriting embedded python block {!r}".format(self.name)
self._epy_source_hash = src_hash
self.label = blk_io.name or blk_io.cls
self.documentation = {'': blk_io.doc}
self.module_name = "{}_{}".format(
self.parent_flowgraph.get_option("id"), self.name)
self.templates['imports'] = 'import {} as {} # embedded python block'.format(
self.module_name, self.name)
self.templates['make'] = '{mod}.{cls}({args})'.format(
mod=self.name,
cls=blk_io.cls,
args=', '.join('{0}=${{ {0} }}'.format(key) for key, _ in blk_io.params))
self.templates['callbacks'] = [
'{0} = ${{ {0} }}'.format(attr) for attr in blk_io.callbacks
]
self._update_params(blk_io.params)
self._update_ports('in', self.sinks, blk_io.sinks, 'sink')
self._update_ports('out', self.sources, blk_io.sources, 'source')
super(EPyBlock, self).rewrite()
def _update_params(self, params_in_src):
param_factory = self.parent_platform.make_param
params = {}
for key, value in self.params.copy().items():
if hasattr(value, '__epy_param__'):
params[key] = value
del self.params[key]
for id_, value in params_in_src:
try:
param = params[id_]
if param.default == param.value:
param.set_value(value)
param.default = str(value)
except KeyError: # need to make a new param
param = param_factory(
parent=self, id=id_, dtype='raw', value=value,
name=id_.replace('_', ' ').title(),
)
setattr(param, '__epy_param__', True)
self.params[id_] = param
def _update_ports(self, label, ports, port_specs, direction):
port_factory = self.parent_platform.make_port
ports_to_remove = list(ports)
iter_ports = iter(ports)
ports_new = []
port_current = next(iter_ports, None)
for key, port_type, vlen in port_specs:
reuse_port = (
port_current is not None and
port_current.dtype == port_type and
port_current.vlen == vlen and
(key.isdigit() or port_current.key == key)
)
if reuse_port:
ports_to_remove.remove(port_current)
port, port_current = port_current, next(iter_ports, None)
else:
n = dict(name=label + str(key), dtype=port_type, id=key)
if port_type == 'message':
n['name'] = key
n['optional'] = '1'
if vlen > 1:
n['vlen'] = str(vlen)
port = port_factory(self, direction=direction, **n)
ports_new.append(port)
# replace old port list with new one
del ports[:]
ports.extend(ports_new)
# remove excess port connections
self.parent_flowgraph.disconnect(*ports_to_remove)
def validate(self):
super(EPyBlock, self).validate()
if self._epy_reload_error:
self.params['_source_code'].add_error_message(
str(self._epy_reload_error))
@register_build_in
class EPyModule(Block):
key = 'epy_module'
label = 'Python Module'
exempt_from_id_validation = True # Exempt epy module from blacklist id validation
documentation = {'': dedent("""
This block lets you embed a python module in your flowgraph.
Code you put in this module is accessible in other blocks using the ID of this
block. Example:
If you put
a = 2
def double(arg):
return 2 * arg
in a Python Module Block with the ID 'stuff' you can use code like
stuff.a # evals to 2
stuff.double(3) # evals to 6
to set parameters of other blocks in your flowgraph.
""")}
flags = Flags(Flags.SHOW_ID)
parameters_data = build_params(
params_raw=[
dict(label='Code', id='source_code', dtype='_multiline_python_external',
default='# this module will be imported in the into your flowgraph',
hide='part')
], have_inputs=False, have_outputs=False, flags=flags, block_id=key
)
def __init__(self, flow_graph, **kwargs):
super(EPyModule, self).__init__(flow_graph, **kwargs)
self.module_name = self.name
def rewrite(self):
super(EPyModule, self).rewrite()
self.module_name = "{}_{}".format(
self.parent_flowgraph.get_option("id"), self.name)
self.templates['imports'] = 'import {} as {} # embedded python module'.format(
self.module_name, self.name)

View File

@ -0,0 +1,62 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
import itertools
from . import Block, register_build_in
from ._build import build_params
@register_build_in
class VirtualSink(Block):
count = itertools.count()
key = 'virtual_sink'
label = 'Virtual Sink'
flags = Block.flags
flags.set('cpp')
parameters_data = build_params(
params_raw=[
dict(label='Stream ID', id='stream_id', dtype='stream_id')],
have_inputs=False, have_outputs=False, flags=flags, block_id=key
)
inputs_data = [dict(domain='stream', dtype='', direction='sink', id="0")]
def __init__(self, parent, **kwargs):
super(VirtualSink, self).__init__(parent, **kwargs)
self.params['id'].hide = 'all'
@property
def stream_id(self):
return self.params['stream_id'].value
@register_build_in
class VirtualSource(Block):
count = itertools.count()
key = 'virtual_source'
label = 'Virtual Source'
flags = Block.flags
flags.set('cpp')
parameters_data = build_params(
params_raw=[
dict(label='Stream ID', id='stream_id', dtype='stream_id')],
have_inputs=False, have_outputs=False, flags=flags, block_id=key
)
outputs_data = [dict(domain='stream', dtype='',
direction='source', id="0")]
def __init__(self, parent, **kwargs):
super(VirtualSource, self).__init__(parent, **kwargs)
self.params['id'].hide = 'all'
@property
def stream_id(self):
return self.params['stream_id'].value

106
grc/core/cache.py Normal file
View File

@ -0,0 +1,106 @@
# Copyright 2017 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
import json
import logging
import os
import time
from .io import yaml
logger = logging.getLogger(__name__)
class Cache(object):
def __init__(self, filename, version=None, log=True):
self.cache_file = filename
self.version = version
self.log = log
self.cache = {}
self._cachetime = None
self.need_cache_write = True
self._accessed_items = set()
try:
os.makedirs(os.path.dirname(filename))
except OSError:
pass
try:
self._converter_mtime = os.path.getmtime(filename)
except OSError:
self._converter_mtime = -1
def load(self):
try:
self.need_cache_write = False
if self.log:
logger.debug(f"Loading cache from: {self.cache_file}")
with open(self.cache_file, encoding='utf-8') as cache_file:
cache = json.load(cache_file)
cacheversion = cache.get("version", None)
if self.log:
logger.debug(f"Cache version {cacheversion}")
self._cachetime = cache.get("cached-at", 0)
if cacheversion == self.version:
if self.log:
logger.debug("Loaded cache")
self.cache = cache["cache"]
else:
if self.log:
logger.info(f"Outdated cache {self.cache_file} found, "
"will be overwritten.")
raise ValueError()
except (IOError, ValueError):
self.need_cache_write = True
def get_or_load(self, filename):
self._accessed_items.add(filename)
modtime = os.path.getmtime(filename)
if modtime <= self._converter_mtime:
try:
cached = self.cache[filename]
if int(cached["cached-at"] + 0.5) >= modtime:
return cached["data"]
if self.log:
logger.info(f"Cache for {filename} outdated, loading yaml")
except KeyError:
pass
with open(filename, encoding='utf-8') as fp:
data = yaml.safe_load(fp)
self.cache[filename] = {
"cached-at": int(time.time()),
"data": data
}
self.need_cache_write = True
return data
def save(self):
if not self.need_cache_write:
return
if self.log:
logger.debug('Saving %d entries to json cache', len(self.cache))
# Dumping to binary file is only supported for Python3 >= 3.6
with open(self.cache_file, 'w', encoding='utf8') as cache_file:
cache_content = {
"version": self.version,
"cached-at": self._cachetime,
"cache": self.cache
}
cache_file.write(
json.dumps(cache_content, ensure_ascii=False))
def prune(self):
for filename in (set(self.cache) - self._accessed_items):
del self.cache[filename]
def __enter__(self):
self.load()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.save()

View File

@ -0,0 +1,32 @@
###################################################
# Default Flow Graph:
# Include an options block and a variable for sample rate
###################################################
options:
parameters:
id: 'default'
title: 'Not titled yet'
states:
coordinate:
- 8
- 8
rotation: 0
state: enabled
blocks:
- name: samp_rate
id: variable
parameters:
comment: ''
value: '32000'
states:
coordinate:
- 200
- 12
rotation: 0
state: enabled
metadata:
file_format: 1
grc_version: 3.8.0

17
grc/core/errors.py Normal file
View File

@ -0,0 +1,17 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
class GRCError(Exception):
"""Generic error class"""
class BlockLoadError(GRCError):
"""Error during block loading"""
class TemplateError(BlockLoadError):
"""Mako Template Error"""

27
grc/core/flow_graph.dtd Normal file
View File

@ -0,0 +1,27 @@
<!--
Copyright 2008 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
-->
<!--
flow_graph.dtd
Josh Blum
The document type definition for flow graph xml files.
-->
<!ELEMENT flow_graph (timestamp?, block*, connection*)> <!-- optional timestamp -->
<!ELEMENT timestamp (#PCDATA)>
<!-- Block -->
<!ELEMENT block (key, param*, bus_sink?, bus_source?)>
<!ELEMENT param (key, value)>
<!ELEMENT key (#PCDATA)>
<!ELEMENT value (#PCDATA)>
<!ELEMENT bus_sink (#PCDATA)>
<!ELEMENT bus_source (#PCDATA)>
<!-- Connection -->
<!ELEMENT connection (source_block_id, sink_block_id, source_key, sink_key)>
<!ELEMENT source_block_id (#PCDATA)>
<!ELEMENT sink_block_id (#PCDATA)>
<!ELEMENT source_key (#PCDATA)>
<!ELEMENT sink_key (#PCDATA)>

View File

@ -0,0 +1,191 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from ..utils import expr_utils
from operator import methodcaller, attrgetter
class FlowGraphProxy(object): # TODO: move this in a refactored Generator
def __init__(self, fg):
self.orignal_flowgraph = fg
def __getattr__(self, item):
return getattr(self.orignal_flowgraph, item)
def get_hier_block_stream_io(self, direction):
"""
Get a list of stream io signatures for this flow graph.
Args:
direction: a string of 'in' or 'out'
Returns:
a list of dicts with: type, label, vlen, size, optional
"""
return [p for p in self.get_hier_block_io(direction) if p['type'] != "message"]
def get_hier_block_message_io(self, direction):
"""
Get a list of message io signatures for this flow graph.
Args:
direction: a string of 'in' or 'out'
Returns:
a list of dicts with: type, label, vlen, size, optional
"""
return [p for p in self.get_hier_block_io(direction) if p['type'] == "message"]
def get_hier_block_io(self, direction):
"""
Get a list of io ports for this flow graph.
Args:
direction: a string of 'in' or 'out'
Returns:
a list of dicts with: type, label, vlen, size, optional
"""
pads = self.get_pad_sources() if direction in ('sink', 'in') else \
self.get_pad_sinks() if direction in ('source', 'out') else []
ports = []
for pad in pads:
type_param = pad.params['type']
master = {
'label': str(pad.params['label'].get_evaluated()),
'type': str(pad.params['type'].get_evaluated()),
'vlen': str(pad.params['vlen'].get_value()),
'size': type_param.options.attributes[type_param.get_value()]['size'],
'cpp_size': type_param.options.attributes[type_param.get_value()]['cpp_size'],
'optional': bool(pad.params['optional'].get_evaluated()),
}
num_ports = pad.params['num_streams'].get_evaluated()
if num_ports > 1:
for i in range(num_ports):
clone = master.copy()
clone['label'] += str(i)
ports.append(clone)
else:
ports.append(master)
return ports
def get_pad_sources(self):
"""
Get a list of pad source blocks sorted by id order.
Returns:
a list of pad source blocks in this flow graph
"""
pads = [b for b in self.get_enabled_blocks() if b.key == 'pad_source']
return sorted(pads, key=lambda x: x.name)
def get_pad_sinks(self):
"""
Get a list of pad sink blocks sorted by id order.
Returns:
a list of pad sink blocks in this flow graph
"""
pads = [b for b in self.get_enabled_blocks() if b.key == 'pad_sink']
return sorted(pads, key=lambda x: x.name)
def get_pad_port_global_key(self, port):
"""
Get the key for a port of a pad source/sink to use in connect()
This takes into account that pad blocks may have multiple ports
Returns:
the key (str)
"""
key_offset = 0
pads = self.get_pad_sources() if port.is_source else self.get_pad_sinks()
for pad in pads:
# using the block param 'type' instead of the port domain here
# to emphasize that hier block generation is domain agnostic
is_message_pad = pad.params['type'].get_evaluated() == "message"
if port.parent == pad:
if is_message_pad:
key = pad.params['label'].get_value()
else:
key = str(key_offset + int(port.key))
return key
else:
# assuming we have either only sources or sinks
if not is_message_pad:
key_offset += len(pad.sinks) + len(pad.sources)
return -1
def get_cpp_variables(self):
"""
Get a list of all variables (C++) in this flow graph namespace.
Exclude parameterized variables.
Returns:
a sorted list of variable blocks in order of dependency (indep -> dep)
"""
variables = [block for block in self.iter_enabled_blocks()
if block.is_variable]
return expr_utils.sort_objects(variables, attrgetter('name'), methodcaller('get_cpp_var_make'))
def includes(self):
"""
Get a set of all include statements (C++) in this flow graph namespace.
Returns:
a list of #include statements
"""
return [block.cpp_templates.render('includes') for block in self.iter_enabled_blocks() if not (block.is_virtual_sink() or block.is_virtual_source())]
def links(self):
"""
Get a set of all libraries to link against (C++) in this flow graph namespace.
Returns:
a list of GNU Radio modules
"""
return [block.cpp_templates.render('link') for block in self.iter_enabled_blocks() if not (block.is_virtual_sink() or block.is_virtual_source())]
def packages(self):
"""
Get a set of all packages to find (C++) ( especially for oot modules ) in this flow graph namespace.
Returns:
a list of required packages
"""
return [block.cpp_templates.render('packages') for block in self.iter_enabled_blocks() if not (block.is_virtual_sink() or block.is_virtual_source())]
def get_hier_block_io(flow_graph, direction, domain=None):
"""
Get a list of io ports for this flow graph.
Returns a list of dicts with: type, label, vlen, size, optional
"""
pads = flow_graph.get_pad_sources() if direction in ('sink', 'in') else \
flow_graph.get_pad_sinks() if direction in ('source', 'out') else []
ports = []
for pad in pads:
type_param = pad.params['type']
master = {
'label': str(pad.params['label'].get_evaluated()),
'type': str(pad.params['type'].get_evaluated()),
'vlen': str(pad.params['vlen'].get_value()),
'size': type_param.options.attributes[type_param.get_value()]['size'],
'optional': bool(pad.params['optional'].get_evaluated()),
}
num_ports = pad.params['num_streams'].get_evaluated()
if num_ports > 1:
for i in range(num_ports):
clone = master.copy()
clone['label'] += str(i)
ports.append(clone)
else:
ports.append(master)
if domain is not None:
ports = [p for p in ports if p.domain == domain]
return ports

View File

@ -0,0 +1,51 @@
# Copyright 2008-2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from .hier_block import HierBlockGenerator, QtHierBlockGenerator
from .top_block import TopBlockGenerator
from .cpp_top_block import CppTopBlockGenerator
from .cpp_hier_block import CppHierBlockGenerator
class Generator(object):
"""Adaptor for various generators (uses generate_options)"""
def __init__(self, flow_graph, output_dir):
"""
Initialize the generator object.
Determine the file to generate.
Args:
flow_graph: the flow graph object
output_dir: the output path for generated files
"""
self.generate_options = flow_graph.get_option('generate_options')
self.output_language = flow_graph.get_option('output_language')
if self.output_language == 'python':
if self.generate_options == 'hb':
generator_cls = HierBlockGenerator
elif self.generate_options == 'hb_qt_gui':
generator_cls = QtHierBlockGenerator
else:
generator_cls = TopBlockGenerator
elif self.output_language == 'cpp':
if self.generate_options == 'hb':
generator_cls = CppHierBlockGenerator
elif self.generate_options == 'hb_qt_gui':
pass
else:
generator_cls = CppTopBlockGenerator
self._generator = generator_cls(flow_graph, output_dir)
def __getattr__(self, item):
"""get all other attrib from actual generator object"""
return getattr(self._generator, item)

View File

@ -0,0 +1,7 @@
# Copyright 2008-2015 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from .Generator import Generator

View File

@ -0,0 +1,217 @@
import collections
import os
import codecs
from .cpp_top_block import CppTopBlockGenerator
from .. import Constants
from ..io import yaml
class CppHierBlockGenerator(CppTopBlockGenerator):
"""Extends the top block generator to also generate a block YML file"""
def __init__(self, flow_graph, output_dir):
"""
Initialize the hier block generator object.
Args:
flow_graph: the flow graph object
output_dir: the path for written files
"""
platform = flow_graph.parent
if output_dir is None:
output_dir = platform.config.hier_block_lib_dir
if not os.path.exists(output_dir):
os.mkdir(output_dir)
CppTopBlockGenerator.__init__(self, flow_graph, output_dir)
self._mode = Constants.HIER_BLOCK_FILE_MODE
self.file_path_yml = self.file_path + '.block.yml'
def write(self):
"""generate output and write it to files"""
CppTopBlockGenerator.write(self)
data = yaml.dump(self._build_block_n_from_flow_graph_io())
replace = [
('parameters:', '\nparameters:'),
('inputs:', '\ninputs:'),
('outputs:', '\noutputs:'),
('asserts:', '\nasserts:'),
('\ntemplates:', '\n\ntemplates:'),
('cpp_templates:', '\ncpp_templates:'),
('documentation:', '\ndocumentation:'),
('file_format:', '\nfile_format:'),
]
for r in replace:
data = data.replace(*r)
with codecs.open(self.file_path_yml, 'w', encoding='utf-8') as fp:
fp.write(data)
# Windows only supports S_IREAD and S_IWRITE, other flags are ignored
os.chmod(self.file_path_yml, self._mode)
def _build_block_n_from_flow_graph_io(self):
"""
Generate a block YML nested data from the flow graph IO
Returns:
a yml node tree
"""
# Extract info from the flow graph
block_id = self._flow_graph.get_option('id')
parameters = self._flow_graph.get_parameters()
def var_or_value(name):
if name in (p.name for p in parameters):
return "${" + name + " }"
return name
# Build the nested data
data = collections.OrderedDict()
data['id'] = block_id
data['label'] = (
self._flow_graph.get_option('title') or
self._flow_graph.get_option('id').replace('_', ' ').title()
)
data['category'] = self._flow_graph.get_option('category')
data['flags'] = ['cpp']
# Parameters
data['parameters'] = []
for param_block in parameters:
p = collections.OrderedDict()
p['id'] = param_block.name
p['label'] = param_block.params['label'].get_value() or param_block.name
p['dtype'] = param_block.params['value'].dtype
p['default'] = param_block.params['value'].get_value()
p['hide'] = param_block.params['hide'].get_value()
data['parameters'].append(p)
# Ports
for direction in ('inputs', 'outputs'):
data[direction] = []
for port in get_hier_block_io(self._flow_graph, direction):
p = collections.OrderedDict()
if port.domain == Constants.GR_MESSAGE_DOMAIN:
p['id'] = port.id
p['label'] = port.parent.params['label'].value
if port.domain != Constants.DEFAULT_DOMAIN:
p['domain'] = port.domain
p['dtype'] = port.dtype
if port.domain != Constants.GR_MESSAGE_DOMAIN:
p['vlen'] = var_or_value(port.vlen)
if port.optional:
p['optional'] = True
data[direction].append(p)
t = data['templates'] = collections.OrderedDict()
t['import'] = "from {0} import {0} # grc-generated hier_block".format(
self._flow_graph.get_option('id'))
# Make data
if parameters:
t['make'] = '{cls}(\n {kwargs},\n)'.format(
cls=block_id,
kwargs=',\n '.join(
'{key}=${{ {key} }}'.format(key=param.name) for param in parameters
),
)
else:
t['make'] = '{cls}()'.format(cls=block_id)
# Self-connect if there aren't any ports
if not data['inputs'] and not data['outputs']:
t['make'] += '\nthis->connect(this->${id});'
# Callback data
t['callbacks'] = [
'set_{key}(${{ {key} }})'.format(key=param_block.name) for param_block in parameters
]
t_cpp = data['cpp_templates'] = collections.OrderedDict()
t_cpp['includes'] = []
t_cpp['includes'].append(
'#include "{id}/{id}.hpp"'.format(id=self._flow_graph.get_option('id')))
# Make data
if parameters:
t_cpp['make'] = '{cls}(\n {kwargs},\n)'.format(
cls=block_id,
kwargs=',\n '.join(
'{key}=${{ {key} }}'.format(key=param.name) for param in parameters
),
)
else:
t_cpp['make'] = 'this->${{id}} = {cls}_sptr(make_{cls}());'.format(
cls=block_id)
t_cpp['declarations'] = '{cls}_sptr ${{id}};'.format(cls=block_id)
# Callback data
t_cpp['callbacks'] = [
'set_{key}(${{ {key} }})'.format(key=param_block.name) for param_block in parameters
]
# Documentation
data['documentation'] = "\n".join(field for field in (
self._flow_graph.get_option('author'),
self._flow_graph.get_option('description'),
self.file_path
) if field)
data['grc_source'] = str(self._flow_graph.grc_file_path)
data['file_format'] = 1
return data
class CppQtHierBlockGenerator(CppHierBlockGenerator):
def _build_block_n_from_flow_graph_io(self):
n = CppHierBlockGenerator._build_block_n_from_flow_graph_io(self)
block_n = collections.OrderedDict()
# insert flags after category
for key, value in n['block'].items():
block_n[key] = value
if key == 'category':
block_n['flags'] = 'need_qt_gui'
if not block_n['name'].upper().startswith('QT GUI'):
block_n['name'] = 'QT GUI ' + block_n['name']
gui_hint_param = collections.OrderedDict()
gui_hint_param['name'] = 'GUI Hint'
gui_hint_param['key'] = 'gui_hint'
gui_hint_param['value'] = ''
gui_hint_param['type'] = 'gui_hint'
gui_hint_param['hide'] = 'part'
block_n['param'].append(gui_hint_param)
block_n['make'] += (
"\n#set $win = 'self.%s' % $id"
"\n${gui_hint()($win)}"
)
return {'block': block_n}
def get_hier_block_io(flow_graph, direction, domain=None):
"""
Get a list of io ports for this flow graph.
Returns a list of dicts with: type, label, vlen, size, optional
"""
pads = flow_graph.get_pad_sources(
) if direction == 'inputs' else flow_graph.get_pad_sinks()
for pad in pads:
for port in (pad.sources if direction == 'inputs' else pad.sinks):
if domain and port.domain != domain:
continue
yield port

View File

@ -0,0 +1,72 @@
#####################
# GNU Radio C++ Flow Graph CMakeLists.txt
#
# Title: ${title}
% if flow_graph.get_option('author'):
# Author: ${flow_graph.get_option('author')}
% endif
% if flow_graph.get_option('description'):
# Description: ${flow_graph.get_option('description')}
% endif
# GNU Radio version: ${config.version}
#####################
<%
class_name = flow_graph.get_option('id')
version_list = config.version_parts
short_version = '.'.join(version_list[0:2])
%>\
cmake_minimum_required(VERSION 3.16)
set(CMAKE_CXX_STANDARD 14)
project(${class_name})
find_package(Gnuradio "${short_version}" COMPONENTS
% for component in config.enabled_components.split(";"):
% if component.startswith("gr-"):
% if not component in ['gr-utils', 'gr-ctrlport']:
${component.replace("gr-", "")}
% endif
% endif
% endfor
)
% if generate_options == 'qt_gui':
find_package(Qt5Widgets REQUIRED)
set(CMAKE_AUTOMOC TRUE)
% endif
% if cmake_tuples:
% for key, val in cmake_tuples:
set(${key} ${val})
% endfor
% endif
% if flow_graph.get_option('gen_linking') == 'static':
set(BUILD_SHARED_LIBS false)
set(CMAKE_EXE_LINKER_FLAGS " -static")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
% endif
% for package in packages:
% if package:
find_package(${package})
% endif
% endfor
add_executable(${class_name} ${class_name}.cpp)
target_link_libraries(${class_name}
gnuradio::gnuradio-blocks
% if generate_options == 'qt_gui':
gnuradio::gnuradio-qtgui
% endif
% if parameters:
Boost::program_options
% endif
% for link in links:
% if link:
${link}
% endif
% endfor
)

View File

@ -0,0 +1,189 @@
<%def name="doubleindent(code)">${ '\n '.join(str(code).splitlines()) }</%def>\
/********************
GNU Radio C++ Flow Graph Source File
Title: ${title}
% if flow_graph.get_option('author'):
Author: ${flow_graph.get_option('author')}
% endif
% if flow_graph.get_option('description'):
Description: ${flow_graph.get_option('description')}
% endif
GNU Radio version: ${config.version}
********************/
#include "${flow_graph.get_option('id')}.hpp"
% if flow_graph.get_option('realtime_scheduling'):
#include <gnuradio/realtime.h>
% endif
% if parameters:
namespace po = boost::program_options;
% endif
using namespace gr;
<%
class_name = flow_graph.get_option('id') + ('_' if flow_graph.get_option('id') == 'top_block' else '')
param_str = ", ".join((param.vtype + " " + param.name) for param in parameters)
param_str_without_types = ", ".join(param.name for param in parameters)
initializer_str = ",\n ".join((param.name + "(" + param.name + ")") for param in parameters)
if generate_options == 'qt_gui':
initializer_str = 'QWidget()' + (',\n ' if len(parameters) > 0 else '') + initializer_str
if len(initializer_str) > 0:
initializer_str = '\n: ' + initializer_str
%>\
${class_name}::${class_name} (${param_str}) ${initializer_str} {
% if generate_options == 'qt_gui':
this->setWindowTitle("${title}");
// check_set_qss
// set icon
this->top_scroll_layout = new QVBoxLayout();
this->setLayout(this->top_scroll_layout);
this->top_scroll = new QScrollArea();
this->top_scroll->setFrameStyle(QFrame::NoFrame);
this->top_scroll_layout->addWidget(this->top_scroll);
this->top_scroll->setWidgetResizable(true);
this->top_widget = new QWidget();
this->top_scroll->setWidget(this->top_widget);
this->top_layout = new QVBoxLayout(this->top_widget);
this->top_grid_layout = new QGridLayout();
this->top_layout->addLayout(this->top_grid_layout);
this->settings = new QSettings("gnuradio/flowgraphs", "${class_name}");
this->restoreGeometry(this->settings->value("geometry").toByteArray());
% endif
% if flow_graph.get_option('thread_safe_setters'):
## self._lock = threading.RLock()
% endif
this->tb = gr::make_top_block("${title}");
% if blocks:
// Blocks:
% for blk, blk_make, declarations in blocks:
${doubleindent(blk_make)}
## % if 'alias' in blk.params and blk.params['alias'].get_evaluated():
## ${blk.name}.set_block_alias("${blk.params['alias'].get_evaluated()}")
## % endif
## % if 'affinity' in blk.params and blk.params['affinity'].get_evaluated():
## ${blk.name}.set_processor_affinity("${blk.params['affinity'].get_evaluated()}")
## % endif
## % if len(blk.sources) > 0 and 'minoutbuf' in blk.params and int(blk.params['minoutbuf'].get_evaluated()) > 0:
## ${blk.name}.set_min_output_buffer(${blk.params['minoutbuf'].get_evaluated()})
## % endif
## % if len(blk.sources) > 0 and 'maxoutbuf' in blk.params and int(blk.params['maxoutbuf'].get_evaluated()) > 0:
## ${blk.name}.set_max_output_buffer(${blk.params['maxoutbuf'].get_evaluated()})
## % endif
% endfor
% endif
% if connections:
// Connections:
% for connection in connections:
${connection.rstrip()};
% endfor
% endif
}
${class_name}::~${class_name} () {
}
// Callbacks:
% for var in parameters + variables:
${var.vtype} ${class_name}::get_${var.name} () const {
return this->${var.name};
}
void ${class_name}::set_${var.name} (${var.vtype} ${var.name}) {
% if flow_graph.get_option('thread_safe_setters'):
## with self._lock:
return;
% else:
this->${var.name} = ${var.name};
% for callback in callbacks[var.name]:
${callback};
% endfor
% endif
}
% endfor
% if generate_options == 'qt_gui':
void ${class_name}::closeEvent(QCloseEvent *event) {
this->settings->setValue("geometry",this->saveGeometry());
event->accept();
}
% endif
int main (int argc, char **argv) {
% if parameters:
% for parameter in parameters:
${parameter.vtype} ${parameter.name} = ${parameter.cpp_templates.render('make')};
% endfor
po::options_description desc("Options");
desc.add_options()
("help", "display help")
% for parameter in parameters:
("${parameter.name}", po::value<${parameter.vtype}>(&${parameter.name}), "${parameter.label}")
% endfor
;
po::variables_map vm;
po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm);
if (vm.count("help")) {
std::cout << desc << std::endl;
return 0;
}
% endif
% if flow_graph.get_option('realtime_scheduling'):
if (enable_realtime_scheduling() != RT_OK) {
std::cout << "Error: failed to enable real-time scheduling." << std::endl;
}
% endif
% if generate_options == 'no_gui':
${class_name}* top_block = new ${class_name}(${param_str_without_types});
## TODO: params
% if flow_graph.get_option('run_options') == 'prompt':
top_block->tb->start();
% for m in monitors:
(top_block->${m.name}).start();
% endfor
std::cout << "Press Enter to quit: ";
std::cin.ignore();
top_block->tb->stop();
% elif flow_graph.get_option('run_options') == 'run':
top_block->tb->start();
% endif
% for m in monitors:
(top_block->${m.name}).start();
% endfor
top_block->tb->wait();
% elif generate_options == 'qt_gui':
QApplication app(argc, argv);
${class_name}* top_block = new ${class_name}(${param_str_without_types});
top_block->tb->start();
top_block->show();
app.exec();
% endif
return 0;
}
% if generate_options == 'qt_gui':
#include "moc_${class_name}.cpp"
% endif

View File

@ -0,0 +1,193 @@
<%def name="indent(code)">${ ' ' + '\n '.join(str(code).splitlines()) }</%def>\
<%def name="doubleindent(code)">${ '\n '.join(str(code).splitlines()) }</%def>\
#ifndef ${flow_graph.get_option('id').upper()}_HPP
#define ${flow_graph.get_option('id').upper()}_HPP
/********************
GNU Radio C++ Flow Graph Header File
Title: ${title}
% if flow_graph.get_option('author'):
Author: ${flow_graph.get_option('author')}
% endif
% if flow_graph.get_option('description'):
Description: ${flow_graph.get_option('description')}
% endif
GNU Radio version: ${config.version}
********************/
/********************
** Create includes
********************/
% for inc in includes:
${inc}
% endfor
% if generate_options == 'qt_gui':
#include <QVBoxLayout>
#include <QScrollArea>
#include <QWidget>
#include <QGridLayout>
#include <QSettings>
#include <QApplication>
#include <QCloseEvent>
% endif
% if parameters:
#include <boost/program_options.hpp>
% endif
using namespace gr;
<%
class_name = flow_graph.get_option('id') + ('_' if flow_graph.get_option('id') == 'top_block' else '')
param_str = ", ".join((param.vtype + " " + param.name) for param in parameters)
%>\
% if generate_options == 'no_gui':
class ${class_name} {
% elif generate_options.startswith('hb'):
class ${class_name} : public hier_block2 {
% elif generate_options == 'qt_gui':
class ${class_name} : public QWidget {
Q_OBJECT
% endif
private:
% if generate_options == 'qt_gui':
QVBoxLayout *top_scroll_layout;
QScrollArea *top_scroll;
QWidget *top_widget;
QVBoxLayout *top_layout;
QGridLayout *top_grid_layout;
QSettings *settings;
void closeEvent(QCloseEvent *event);
% endif
% for block, make, declarations in blocks:
% if declarations:
${indent(declarations)}
% endif
% endfor
% if parameters:
// Parameters:
% for param in parameters:
${param.vtype} ${param.cpp_templates.render('var_make')}
% endfor
% endif
% if variables:
// Variables:
% for var in variables:
${var.vtype} ${var.cpp_templates.render('var_make')}
% endfor
% endif
public:
% if generate_options.startswith('hb'):
typedef std::shared_ptr<${class_name}> sptr;
static sptr make(${param_str});
% else:
top_block_sptr tb;
% endif
${class_name}(${param_str});
~${class_name}();
% for var in parameters + variables:
${var.vtype} get_${var.name} () const;
void set_${var.name}(${var.vtype} ${var.name});
% endfor
};
% if generate_options.startswith('hb'):
<% in_sigs = flow_graph.get_hier_block_stream_io('in') %>
<% out_sigs = flow_graph.get_hier_block_stream_io('out') %>
<%def name="make_io_sig(io_sigs)">\
<% size_strs = [ '%s*%s'%(io_sig['cpp_size'], io_sig['vlen']) for io_sig in io_sigs] %>\
% if len(io_sigs) == 0:
gr::io_signature::make(0, 0, 0)\
% elif len(io_sigs) == 1:
gr::io_signature::make(1, 1, ${size_strs[0]})\
% else:
gr::io_signaturev(${len(io_sigs)}, ${len(io_sigs)}, [${', '.join(size_strs)}])\
% endif
</%def>\
${class_name}::${class_name} (${param_str}) : hier_block2("${title}",
${make_io_sig(in_sigs)},
${make_io_sig(out_sigs)}
) {
% for pad in flow_graph.get_hier_block_message_io('in'):
message_port_register_hier_in("${pad['label']}")
% endfor
% for pad in flow_graph.get_hier_block_message_io('out'):
message_port_register_hier_out("${pad['label']}")
% endfor
% if flow_graph.get_option('thread_safe_setters'):
## self._lock = threading.RLock()
% endif
% if blocks:
// Blocks:
% for blk, blk_make, declarations in blocks:
{
${doubleindent(blk_make)}
## % if 'alias' in blk.params and blk.params['alias'].get_evaluated():
## ${blk.name}.set_block_alias("${blk.params['alias'].get_evaluated()}")
## % endif
## % if 'affinity' in blk.params and blk.params['affinity'].get_evaluated():
## ${blk.name}.set_processor_affinity("${blk.params['affinity'].get_evaluated()}")
## % endif
## % if len(blk.sources) > 0 and 'minoutbuf' in blk.params and int(blk.params['minoutbuf'].get_evaluated()) > 0:
## ${blk.name}.set_min_output_buffer(${blk.params['minoutbuf'].get_evaluated()})
## % endif
## % if len(blk.sources) > 0 and 'maxoutbuf' in blk.params and int(blk.params['maxoutbuf'].get_evaluated()) > 0:
## ${blk.name}.set_max_output_buffer(${blk.params['maxoutbuf'].get_evaluated()})
## % endif
}
% endfor
% endif
% if connections:
// Connections:
% for connection in connections:
${connection.rstrip()};
% endfor
% endif
}
${class_name}::~${class_name} () {
}
// Callbacks:
% for var in parameters + variables:
${var.vtype} ${class_name}::get_${var.name} () const {
return this->${var.name};
}
void ${class_name}::set_${var.name} (${var.vtype} ${var.name}) {
% if flow_graph.get_option('thread_safe_setters'):
## with self._lock:
return;
% else:
this->${var.name} = ${var.name};
% for callback in callbacks[var.name]:
${callback};
% endfor
% endif
}
% endfor
${class_name}::sptr
${class_name}::make(${param_str})
{
return gnuradio::make_block_sptr<${class_name}>(
${", ".join(param.name for param in parameters)});
}
% endif
#endif

View File

@ -0,0 +1,499 @@
import codecs
import yaml
import operator
import os
import tempfile
import re
from mako.template import Template
from .. import Messages, blocks
from ..Constants import TOP_BLOCK_FILE_MODE
from .FlowGraphProxy import FlowGraphProxy
from ..utils import expr_utils
from .top_block import TopBlockGenerator
DATA_DIR = os.path.dirname(__file__)
HEADER_TEMPLATE = os.path.join(DATA_DIR, 'cpp_templates/flow_graph.hpp.mako')
SOURCE_TEMPLATE = os.path.join(DATA_DIR, 'cpp_templates/flow_graph.cpp.mako')
CMAKE_TEMPLATE = os.path.join(DATA_DIR, 'cpp_templates/CMakeLists.txt.mako')
header_template = Template(filename=HEADER_TEMPLATE)
source_template = Template(filename=SOURCE_TEMPLATE)
cmake_template = Template(filename=CMAKE_TEMPLATE)
class CppTopBlockGenerator(object):
def __init__(self, flow_graph, output_dir):
"""
Initialize the top block generator object.
Args:
flow_graph: the flow graph object
output_dir: the path for written files
"""
self._flow_graph = FlowGraphProxy(flow_graph)
self._generate_options = self._flow_graph.get_option(
'generate_options')
self._mode = TOP_BLOCK_FILE_MODE
# Handle the case where the directory is read-only
# In this case, use the system's temp directory
if not os.access(output_dir, os.W_OK):
output_dir = tempfile.gettempdir()
filename = self._flow_graph.get_option('id')
self.file_path = os.path.join(output_dir, filename)
self.output_dir = output_dir
def _warnings(self):
throttling_blocks = [b for b in self._flow_graph.get_enabled_blocks()
if b.flags.throttle]
if not throttling_blocks and not self._generate_options.startswith('hb'):
Messages.send_warning("This flow graph may not have flow control: "
"no audio or RF hardware blocks found. "
"Add a Misc->Throttle block to your flow "
"graph to avoid CPU congestion.")
if len(throttling_blocks) > 1:
keys = set([b.key for b in throttling_blocks])
if len(keys) > 1 and 'blocks_throttle' in keys:
Messages.send_warning("This flow graph contains a throttle "
"block and another rate limiting block, "
"e.g. a hardware source or sink. "
"This is usually undesired. Consider "
"removing the throttle block.")
deprecated_block_keys = {
b.name for b in self._flow_graph.get_enabled_blocks() if b.flags.deprecated}
for key in deprecated_block_keys:
Messages.send_warning("The block {!r} is deprecated.".format(key))
def write(self):
"""create directory, generate output and write it to files"""
self._warnings()
fg = self._flow_graph
platform = fg.parent
self.title = fg.get_option('title') or fg.get_option(
'id').replace('_', ' ').title()
variables = fg.get_cpp_variables()
parameters = fg.get_parameters()
monitors = fg.get_monitors()
self._variable_types()
self._parameter_types()
self.namespace = {
'flow_graph': fg,
'variables': variables,
'parameters': parameters,
'monitors': monitors,
'generate_options': self._generate_options,
'config': platform.config
}
if not os.path.exists(self.file_path):
os.makedirs(self.file_path)
for filename, data in self._build_cpp_header_code_from_template():
with codecs.open(filename, 'w', encoding='utf-8') as fp:
fp.write(data)
if not self._generate_options.startswith('hb'):
if not os.path.exists(os.path.join(self.file_path, 'build')):
os.makedirs(os.path.join(self.file_path, 'build'))
for filename, data in self._build_cpp_source_code_from_template():
with codecs.open(filename, 'w', encoding='utf-8') as fp:
fp.write(data)
if fg.get_option('gen_cmake') == 'On':
for filename, data in self._build_cmake_code_from_template():
with codecs.open(filename, 'w', encoding='utf-8') as fp:
fp.write(data)
def _build_cpp_source_code_from_template(self):
"""
Convert the flow graph to a C++ source file.
Returns:
a string of C++ code
"""
filename = self._flow_graph.get_option('id') + '.cpp'
file_path = os.path.join(self.file_path, filename)
output = []
flow_graph_code = source_template.render(
title=self.title,
includes=self._includes(),
blocks=self._blocks(),
callbacks=self._callbacks(),
connections=self._connections(),
**self.namespace
)
# strip trailing white-space
flow_graph_code = "\n".join(line.rstrip()
for line in flow_graph_code.split("\n"))
output.append((file_path, flow_graph_code))
return output
def _build_cpp_header_code_from_template(self):
"""
Convert the flow graph to a C++ header file.
Returns:
a string of C++ code
"""
filename = self._flow_graph.get_option('id') + '.hpp'
file_path = os.path.join(self.file_path, filename)
output = []
flow_graph_code = header_template.render(
title=self.title,
includes=self._includes(),
blocks=self._blocks(),
callbacks=self._callbacks(),
connections=self._connections(),
**self.namespace
)
# strip trailing white-space
flow_graph_code = "\n".join(line.rstrip()
for line in flow_graph_code.split("\n"))
output.append((file_path, flow_graph_code))
return output
def _build_cmake_code_from_template(self):
"""
Convert the flow graph to a CMakeLists.txt file.
Returns:
a string of CMake code
"""
filename = 'CMakeLists.txt'
file_path = os.path.join(self.file_path, filename)
cmake_tuples = []
cmake_opt = self._flow_graph.get_option("cmake_opt")
cmake_opt = " " + cmake_opt # To make sure we get rid of the "-D"s when splitting
for opt_string in cmake_opt.split(" -D"):
opt_string = opt_string.strip()
if opt_string:
cmake_tuples.append(tuple(opt_string.split("=")))
output = []
flow_graph_code = cmake_template.render(
title=self.title,
includes=self._includes(),
blocks=self._blocks(),
callbacks=self._callbacks(),
connections=self._connections(),
links=self._links(),
cmake_tuples=cmake_tuples,
packages=self._packages(),
**self.namespace
)
# strip trailing white-space
flow_graph_code = "\n".join(line.rstrip()
for line in flow_graph_code.split("\n"))
output.append((file_path, flow_graph_code))
return output
def _links(self):
fg = self._flow_graph
links = fg.links()
seen = set()
for link_list in links:
if link_list:
for link in link_list:
seen.add(link)
return list(seen)
def _packages(self):
fg = self._flow_graph
packages = fg.packages()
seen = set()
for package_list in packages:
if package_list:
for package in package_list:
seen.add(package)
return list(seen)
def _includes(self):
fg = self._flow_graph
includes = fg.includes()
seen = set()
output = []
def is_duplicate(line):
if line.startswith('#include') and line in seen:
return True
seen.add(line)
return False
for block_ in includes:
for include_ in block_:
if not include_:
continue
line = include_.rstrip()
if not is_duplicate(line):
output.append(line)
return output
def _blocks(self):
fg = self._flow_graph
parameters = fg.get_parameters()
# List of blocks not including variables and parameters and disabled
def _get_block_sort_text(block):
code = block.cpp_templates.render('declarations')
try:
# Newer gui markup w/ qtgui
code += block.params['gui_hint'].get_value()
except Exception:
pass
return code
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' or b.is_virtual_source() or b.is_virtual_sink())
]
blocks = expr_utils.sort_objects(
blocks, operator.attrgetter('name'), _get_block_sort_text)
blocks_make = []
for block in blocks:
translations = block.cpp_templates.render('translations')
make = block.cpp_templates.render('make')
declarations = block.cpp_templates.render('declarations')
if translations:
translations = yaml.safe_load(translations)
else:
translations = {}
translations.update({
r"gr\.sizeof_([\w_]+)": r"sizeof(\1)",
r"'([^']*)'": r'"\1"',
r"True": r"true",
r"False": r"false",
})
for key in translations:
make = re.sub(key.replace("\\\\", "\\"),
translations[key], make)
declarations = declarations.replace(key, translations[key])
if make:
blocks_make.append((block, make, declarations))
elif 'qt' in block.key:
# The QT Widget blocks are technically variables,
# but they contain some code we don't want to miss
blocks_make.append(('', make, declarations))
return blocks_make
def _variable_types(self):
fg = self._flow_graph
variables = fg.get_cpp_variables()
type_translation = {'real': 'double', 'float': 'float', 'int': 'int', 'bool': 'bool',
'complex_vector': 'std::vector<gr_complex>', 'real_vector': 'std::vector<double>',
'float_vector': 'std::vector<float>', 'int_vector': 'std::vector<int>',
'string': 'std::string', 'complex': 'gr_complex'}
# If the type is explicitly specified, translate to the corresponding C++ type
for var in list(variables):
if var.params['value'].dtype != 'raw':
var.vtype = type_translation[var.params['value'].dtype]
variables.remove(var)
# If the type is 'raw', we'll need to evaluate the variable to infer the type.
# Create an executable fragment of code containing all 'raw' variables in
# order to infer the lvalue types.
#
# Note that this differs from using ast.literal_eval() as literal_eval evaluates one
# variable at a time. The code fragment below evaluates all variables together which
# allows the variables to reference each other (i.e. a = b * c).
prog = 'def get_decl_types():\n'
prog += '\tvar_types = {}\n'
for var in variables:
prog += '\t' + str(var.params['id'].value) + \
'=' + str(var.params['value'].value) + '\n'
prog += '\tvar_types = {}\n'
for var in variables:
prog += '\tvar_types[\'' + str(var.params['id'].value) + \
'\'] = type(' + str(var.params['id'].value) + ')\n'
prog += '\treturn var_types'
# Execute the code fragment in a separate namespace and retrieve the lvalue types
var_types = {}
namespace = {}
try:
exec(prog, namespace)
var_types = namespace['get_decl_types']()
except Exception as excp:
print('Failed to get parameter lvalue types: %s' % (excp))
# Format the rvalue of each variable expression
for var in variables:
var.format_expr(var_types[str(var.params['id'].value)])
def _parameter_types(self):
fg = self._flow_graph
parameters = fg.get_parameters()
for param in parameters:
type_translation = {'eng_float': 'double', 'intx': 'long', 'str': 'std::string', 'complex': 'gr_complex'}
param.vtype = type_translation[param.params['type'].value]
cpp_value = param.get_cpp_value(param.params['value'].value)
# Update 'make' and 'var_make' entries in the cpp_templates dictionary
d = param.cpp_templates
cpp_expr = d['var_make'].replace('${value}', cpp_value)
d.update({'make': cpp_value, 'var_make': cpp_expr})
param.cpp_templates = d
def _callbacks(self):
fg = self._flow_graph
variables = fg.get_cpp_variables()
parameters = fg.get_parameters()
# List of variable names
var_ids = [var.name for var in parameters + variables]
replace_dict = dict((var_id, 'this->' + var_id) for var_id in var_ids)
callbacks_all = []
for block in fg.iter_enabled_blocks():
if not (block.is_virtual_sink() or block.is_virtual_source()):
callbacks_all.extend(expr_utils.expr_replace(
cb, replace_dict) for cb in block.get_cpp_callbacks())
# Map var id to callbacks
def uses_var_id(callback):
used = expr_utils.get_variable_dependencies(callback, [var_id])
# callback might contain var_id itself
return used and ('this->' + var_id in callback)
callbacks = {}
for var_id in var_ids:
callbacks[var_id] = [
callback for callback in callbacks_all if uses_var_id(callback)]
return callbacks
def _connections(self):
fg = self._flow_graph
templates = {key: Template(text)
for key, text in fg.parent_platform.cpp_connection_templates.items()}
def make_port_sig(port):
if port.parent.key in ('pad_source', 'pad_sink'):
block = 'self()'
key = fg.get_pad_port_global_key(port)
else:
block = 'this->' + port.parent_block.name
key = port.key
if not key.isdigit():
# TODO What use case is this supporting?
toks = re.findall(r'\d+', key)
if len(toks) > 0:
key = toks[0]
else:
# Assume key is a string
key = '"' + key + '"'
return '{block}, {key}'.format(block=block, key=key)
connections = fg.get_enabled_connections()
# Get the virtual blocks and resolve their connections
connection_factory = fg.parent_platform.Connection
virtual_source_connections = [c for c in connections if isinstance(
c.source_block, blocks.VirtualSource)]
for connection in virtual_source_connections:
sink = connection.sink_port
for source in connection.source_port.resolve_virtual_source():
resolved = connection_factory(
fg.orignal_flowgraph, source, sink)
connections.append(resolved)
virtual_connections = [c for c in connections if (isinstance(
c.source_block, blocks.VirtualSource) or isinstance(c.sink_block, blocks.VirtualSink))]
for connection in virtual_connections:
# Remove the virtual connection
connections.remove(connection)
# Bypassing blocks: Need to find all the enabled connections for the block using
# the *connections* object rather than get_connections(). Create new connections
# that bypass the selected block and remove the existing ones. This allows adjacent
# bypassed blocks to see the newly created connections to downstream blocks,
# allowing them to correctly construct bypass connections.
bypassed_blocks = fg.get_bypassed_blocks()
for block in bypassed_blocks:
# Get the upstream connection (off of the sink ports)
# Use *connections* not get_connections()
source_connection = [
c for c in connections if c.sink_port == block.sinks[0]]
# The source connection should never have more than one element.
assert (len(source_connection) == 1)
# Get the source of the connection.
source_port = source_connection[0].source_port
# Loop through all the downstream connections
for sink in (c for c in connections if c.source_port == block.sources[0]):
if not sink.enabled:
# Ignore disabled connections
continue
connection = connection_factory(
fg.orignal_flowgraph, source_port, sink.sink_port)
connections.append(connection)
# Remove this sink connection
connections.remove(sink)
# Remove the source connection
connections.remove(source_connection[0])
# List of connections where each endpoint is enabled (sorted by domains, block names)
def by_domain_and_blocks(c):
return c.type, c.source_block.name, c.sink_block.name
rendered = []
for con in sorted(connections, key=by_domain_and_blocks):
template = templates[con.type]
if con.source_port.dtype != 'bus':
code = template.render(
make_port_sig=make_port_sig, source=con.source_port, sink=con.sink_port)
if not self._generate_options.startswith('hb'):
code = 'this->tb->' + code
rendered.append(code)
else:
# Bus ports need to iterate over the underlying connections and then render
# the code for each subconnection
porta = con.source_port
portb = con.sink_port
fg = self._flow_graph
if porta.dtype == 'bus' and portb.dtype == 'bus':
# which bus port is this relative to the bus structure
if len(porta.bus_structure) == len(portb.bus_structure):
for port_num in porta.bus_structure:
hidden_porta = porta.parent.sources[port_num]
hidden_portb = portb.parent.sinks[port_num]
code = template.render(
make_port_sig=make_port_sig, source=hidden_porta, sink=hidden_portb)
if not self._generate_options.startswith('hb'):
code = 'this->tb->' + code
rendered.append(code)
return rendered

View File

@ -0,0 +1,435 @@
% if not generate_options.startswith('hb'):
#!/usr/bin/env python3
% endif
# -*- coding: utf-8 -*-
<%def name="indent(code)">${ '\n '.join(str(code).splitlines()) }</%def>
#
# SPDX-License-Identifier: GPL-3.0
#
##################################################
# GNU Radio Python Flow Graph
# Title: ${title}
% if flow_graph.get_option('author'):
# Author: ${flow_graph.get_option('author')}
% endif
% if flow_graph.get_option('copyright'):
# Copyright: ${flow_graph.get_option('copyright')}
% endif
% if flow_graph.get_option('description'):
# Description: ${flow_graph.get_option('description')}
% endif
# GNU Radio version: ${version}
##################################################
########################################################
##Create Imports
########################################################
% if generate_options in ['qt_gui','hb_qt_gui']:
from PyQt5 import Qt
from gnuradio import qtgui
%endif
% for imp in imports:
##${imp.replace(" # grc-generated hier_block", "")}
${imp}
% endfor
########################################################
##Prepare snippets
########################################################
% 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', 'init_before_blocks']
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 Class
## Write the class declaration for a top or hier block.
## The parameter names are the arguments to __init__.
## Setup the IO signature (hier block only).
########################################################
<%
class_name = flow_graph.get_option('id')
param_str = ', '.join(['self'] + ['%s=%s'%(param.name, param.templates.render('make')) for param in parameters])
%>\
% if generate_options == 'qt_gui':
class ${class_name}(gr.top_block, Qt.QWidget):
def __init__(${param_str}):
gr.top_block.__init__(self, "${title}", catch_exceptions=${catch_exceptions})
Qt.QWidget.__init__(self)
self.setWindowTitle("${title}")
qtgui.util.check_set_qss()
try:
self.setWindowIcon(Qt.QIcon.fromTheme('gnuradio-grc'))
except BaseException as exc:
print(f"Qt GUI: Could not set Icon: {str(exc)}", file=sys.stderr)
self.top_scroll_layout = Qt.QVBoxLayout()
self.setLayout(self.top_scroll_layout)
self.top_scroll = Qt.QScrollArea()
self.top_scroll.setFrameStyle(Qt.QFrame.NoFrame)
self.top_scroll_layout.addWidget(self.top_scroll)
self.top_scroll.setWidgetResizable(True)
self.top_widget = Qt.QWidget()
self.top_scroll.setWidget(self.top_widget)
self.top_layout = Qt.QVBoxLayout(self.top_widget)
self.top_grid_layout = Qt.QGridLayout()
self.top_layout.addLayout(self.top_grid_layout)
self.settings = Qt.QSettings("gnuradio/flowgraphs", "${class_name}")
try:
geometry = self.settings.value("geometry")
if geometry:
self.restoreGeometry(geometry)
except BaseException as exc:
print(f"Qt GUI: Could not restore geometry: {str(exc)}", file=sys.stderr)
% elif generate_options == 'bokeh_gui':
class ${class_name}(gr.top_block):
def __init__(${param_str}):
gr.top_block.__init__(self, "${title}", catch_exceptions=${catch_exceptions})
self.plot_lst = []
self.widget_lst = []
% elif generate_options == 'no_gui':
class ${class_name}(gr.top_block):
def __init__(${param_str}):
gr.top_block.__init__(self, "${title}", catch_exceptions=${catch_exceptions})
% elif generate_options.startswith('hb'):
<% in_sigs = flow_graph.get_hier_block_stream_io('in') %>
<% out_sigs = flow_graph.get_hier_block_stream_io('out') %>
% if generate_options == 'hb_qt_gui':
class ${class_name}(gr.hier_block2, Qt.QWidget):
% else:
class ${class_name}(gr.hier_block2):
% endif
<%def name="make_io_sig(io_sigs)">\
<% size_strs = ['%s*%s'%(io_sig['size'], io_sig['vlen']) for io_sig in io_sigs] %>\
% if len(io_sigs) == 0:
gr.io_signature(0, 0, 0)\
% elif len(io_sigs) == 1:
gr.io_signature(1, 1, ${size_strs[0]})\
% else:
gr.io_signature.makev(${len(io_sigs)}, ${len(io_sigs)}, [${', '.join(size_strs)}])\
% endif
</%def>\
def __init__(${param_str}):
gr.hier_block2.__init__(
self, "${ title }",
${make_io_sig(in_sigs)},
${make_io_sig(out_sigs)},
)
% for pad in flow_graph.get_hier_block_message_io('in'):
self.message_port_register_hier_in("${ pad['label'] }")
% endfor
% for pad in flow_graph.get_hier_block_message_io('out'):
self.message_port_register_hier_out("${ pad['label'] }")
% endfor
% if generate_options == 'hb_qt_gui':
Qt.QWidget.__init__(self)
self.top_layout = Qt.QVBoxLayout()
self.top_grid_layout = Qt.QGridLayout()
self.top_layout.addLayout(self.top_grid_layout)
self.setLayout(self.top_layout)
% endif
% endif
% if flow_graph.get_option('thread_safe_setters'):
self._lock = threading.RLock()
% endif
% if not generate_options.startswith('hb'):
self.flowgraph_started = threading.Event()
% endif
########################################################
##Create Parameters
## Set the parameter to a property of self.
########################################################
% if parameters:
${'##################################################'}
# Parameters
${'##################################################'}
% endif
% for param in parameters:
${indent(param.get_var_make())}
% endfor
########################################################
##Create Variables
########################################################
% if variables:
${'##################################################'}
# Variables
${'##################################################'}
% endif
% for var in variables:
${indent(var.templates.render('var_make'))}
% endfor
% if blocks:
${'##################################################'}
# Blocks
${'##################################################'}
% endif
${'snippets_init_before_blocks(self)' if snippets['init_before_blocks'] else ''}
% for blk, blk_make in blocks:
% if blk_make:
${ indent(blk_make.strip('\n')) }
% endif
% if 'alias' in blk.params and blk.params['alias'].get_evaluated():
self.${blk.name}.set_block_alias("${blk.params['alias'].get_evaluated()}")
% endif
% if 'affinity' in blk.params and blk.params['affinity'].get_evaluated():
self.${blk.name}.set_processor_affinity(${blk.params['affinity'].to_code()})
% endif
% if len(blk.sources) > 0 and 'minoutbuf' in blk.params and int(blk.params['minoutbuf'].get_evaluated()) > 0:
self.${blk.name}.set_min_output_buffer(${blk.params['minoutbuf'].to_code()})
% endif
% if len(blk.sources) > 0 and 'maxoutbuf' in blk.params and int(blk.params['maxoutbuf'].get_evaluated()) > 0:
self.${blk.name}.set_max_output_buffer(${blk.params['maxoutbuf'].to_code()})
% endif
% endfor
% if connections:
${'##################################################'}
# Connections
${'##################################################'}
% for connection in connections:
${ indent(connection.rstrip()) }
% endfor
% endif
########################################################
## QT sink close method reimplementation
########################################################
% if generate_options == 'qt_gui':
def closeEvent(self, event):
self.settings = Qt.QSettings("gnuradio/flowgraphs", "${class_name}")
self.settings.setValue("geometry", self.saveGeometry())
self.stop()
self.wait()
${'snippets_main_after_stop(self)' if snippets['main_after_stop'] else ''}
event.accept()
% if flow_graph.get_option('qt_qss_theme'):
def setStyleSheetFromFile(self, filename):
try:
if not os.path.exists(filename):
filename = os.path.join(
gr.prefix(), "share", "gnuradio", "themes", filename)
with open(filename) as ss:
self.setStyleSheet(ss.read())
except Exception as e:
self.logger.error(f"setting stylesheet: {str(e)}")
% endif
% endif
##
##
##
## Create Callbacks
## Write a set method for this variable that calls the callbacks
########################################################
% for var in parameters + variables:
def get_${ var.name }(self):
return self.${ var.name }
def set_${ var.name }(self, ${ var.name }):
% if flow_graph.get_option('thread_safe_setters'):
with self._lock:
self.${ var.name } = ${ var.name }
% for callback in callbacks[var.name]:
${ indent(callback) }
% endfor
% else:
self.${ var.name } = ${ var.name }
% for callback in callbacks[var.name]:
${ indent(callback) }
% endfor
% endif
% endfor
\
########################################################
##Create Main
## For top block code, generate a main routine.
## Instantiate the top block and run as gui or cli.
########################################################
% if not generate_options.startswith('hb'):
<% params_eq_list = list() %>
% if parameters:
<% arg_parser_args = '' %>\
def argument_parser():
% if flow_graph.get_option('description'):
<%
arg_parser_args = 'description=description'
%>description = ${repr(flow_graph.get_option('description'))}
% endif
parser = ArgumentParser(${arg_parser_args})
% for param in parameters:
<%
switches = ['"--{}"'.format(param.name.replace('_', '-'))]
short_id = param.params['short_id'].get_value()
if short_id:
switches.insert(0, '"-{}"'.format(short_id))
type_ = param.params['type'].get_value()
if type_:
params_eq_list.append('%s=options.%s' % (param.name, param.name))
default = param.templates.render('make')
if type_ == 'eng_float':
default = "eng_notation.num_to_str(float(" + default + "))"
# FIXME:
if type_ == 'string':
type_ = 'str'
%>\
% if type_:
parser.add_argument(
${ ', '.join(switches) }, dest="${param.name}", type=${type_}, default=${ default },
help="Set ${param.params['label'].get_evaluated() or param.name} [default=%(default)r]")
% endif
% endfor
return parser
% endif
def main(top_block_cls=${class_name}, options=None):
% if parameters:
if options is None:
options = argument_parser().parse_args()
% endif
% if flow_graph.get_option('realtime_scheduling'):
if gr.enable_realtime_scheduling() != gr.RT_OK:
gr.logger("realtime").warn("Error: failed to enable real-time scheduling.")
% endif
% if generate_options == 'qt_gui':
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 ''})
tb.flowgraph_started.set()
% 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
tb.show()
def sig_handler(sig=None, frame=None):
tb.stop()
tb.wait()
${'snippets_main_after_stop(tb)' if snippets['main_after_stop'] else ''}
Qt.QApplication.quit()
signal.signal(signal.SIGINT, sig_handler)
signal.signal(signal.SIGTERM, sig_handler)
timer = Qt.QTimer()
timer.start(500)
timer.timeout.connect(lambda: None)
% for m in monitors:
% if m.params['en'].get_value() == 'True':
tb.${m.name}.start()
% endif
% endfor
qapp.exec_()
% elif generate_options == 'bokeh_gui':
# Create Top Block instance
tb = top_block_cls(${ ', '.join(params_eq_list) })
${'snippets_main_after_init(tb)' if snippets['main_after_init'] else ''}
try:
tb.start()
tb.flowgraph_started.set()
${'snippets_main_after_start(tb)' if snippets['main_after_start'] else ''}
bokehgui.utils.run_server(tb, sizing_mode = "${flow_graph.get_option('sizing_mode')}", widget_placement = ${flow_graph.get_option('placement')}, window_size = ${flow_graph.get_option('window_size')})
finally:
tb.logger.info("Exiting the simulation. Stopping Bokeh Server")
tb.stop()
tb.wait()
${'snippets_main_after_stop(tb)' if snippets['main_after_stop'] else ''}
% 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':
tb.${m.name}.stop()
% endif
% 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)
signal.signal(signal.SIGTERM, sig_handler)
% if flow_graph.get_option('run_options') == 'prompt':
tb.start(${ flow_graph.get_option('max_nouts') or '' })
tb.flowgraph_started.set()
${'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()
% endif
% endfor
try:
input('Press Enter to quit: ')
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 ''})
tb.flowgraph_started.set()
${'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()
% endif
% 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()
% endif
% endfor
% endif
if __name__ == '__main__':
main()
% endif

View File

@ -0,0 +1,186 @@
import collections
import os
import codecs
from .top_block import TopBlockGenerator
from .. import Constants
from ..io import yaml
class HierBlockGenerator(TopBlockGenerator):
"""Extends the top block generator to also generate a block YML file"""
def __init__(self, flow_graph, _):
"""
Initialize the hier block generator object.
Args:
flow_graph: the flow graph object
output_dir: the path for written files
"""
platform = flow_graph.parent
output_dir = platform.config.hier_block_lib_dir
if not os.path.exists(output_dir):
os.mkdir(output_dir)
TopBlockGenerator.__init__(self, flow_graph, output_dir)
self._mode = Constants.HIER_BLOCK_FILE_MODE
self.file_path_yml = self.file_path[:-3] + '.block.yml'
def write(self):
"""generate output and write it to files"""
TopBlockGenerator.write(self)
data = yaml.dump(self._build_block_n_from_flow_graph_io())
replace = [
('parameters:', '\nparameters:'),
('inputs:', '\ninputs:'),
('outputs:', '\noutputs:'),
('asserts:', '\nasserts:'),
('templates:', '\ntemplates:'),
('documentation:', '\ndocumentation:'),
('file_format:', '\nfile_format:'),
]
for r in replace:
data = data.replace(*r)
with codecs.open(self.file_path_yml, 'w', encoding='utf-8') as fp:
fp.write(data)
# Windows only supports S_IREAD and S_IWRITE, other flags are ignored
os.chmod(self.file_path_yml, self._mode)
def _build_block_n_from_flow_graph_io(self):
"""
Generate a block YML nested data from the flow graph IO
Returns:
a yml node tree
"""
# Extract info from the flow graph
block_id = self._flow_graph.get_option('id')
parameters = self._flow_graph.get_parameters()
def var_or_value(name):
if name in (p.name for p in parameters):
return "${" + name + " }"
return name
# Build the nested data
data = collections.OrderedDict()
data['id'] = block_id
data['label'] = (
self._flow_graph.get_option('title') or
self._flow_graph.get_option('id').replace('_', ' ').title()
)
data['category'] = self._flow_graph.get_option('category')
# Parameters
data['parameters'] = []
for param_block in parameters:
p = collections.OrderedDict()
p['id'] = param_block.name
p['label'] = param_block.params['label'].get_value() or param_block.name
p['dtype'] = param_block.params['value'].dtype
p['default'] = param_block.params['value'].get_value()
p['hide'] = param_block.params['hide'].get_value()
data['parameters'].append(p)
# Ports
for direction in ('inputs', 'outputs'):
data[direction] = []
for port in get_hier_block_io(self._flow_graph, direction):
p = collections.OrderedDict()
p['label'] = port.parent.params['label'].value
if port.domain != Constants.DEFAULT_DOMAIN:
p['domain'] = port.domain
p['dtype'] = port.dtype
if port.domain != Constants.GR_MESSAGE_DOMAIN:
p['vlen'] = var_or_value(port.vlen)
if port.optional:
p['optional'] = True
data[direction].append(p)
t = data['templates'] = collections.OrderedDict()
t['imports'] = "from {0} import {0} # grc-generated hier_block".format(
self._flow_graph.get_option('id'))
# Make data
if parameters:
t['make'] = '{cls}(\n {kwargs},\n)'.format(
cls=block_id,
kwargs=',\n '.join(
'{key}=${{ {key} }}'.format(key=param.name) for param in parameters
),
)
else:
t['make'] = '{cls}()'.format(cls=block_id)
# Self-connect if there aren't any ports
if not data['inputs'] and not data['outputs']:
t['make'] += '\nself.connect(self.${id})'
# Callback data
t['callbacks'] = [
'set_{key}(${{ {key} }})'.format(key=param_block.name) for param_block in parameters
]
# Documentation
data['documentation'] = "\n".join(field for field in (
self._flow_graph.get_option('author'),
self._flow_graph.get_option('description'),
self.file_path
) if field)
data['grc_source'] = str(self._flow_graph.grc_file_path)
data['file_format'] = 1
return data
class QtHierBlockGenerator(HierBlockGenerator):
def _build_block_n_from_flow_graph_io(self):
n = HierBlockGenerator._build_block_n_from_flow_graph_io(self)
block_n = collections.OrderedDict()
# insert flags after category
for key, value in n.items():
block_n[key] = value
if key == 'category':
block_n['flags'] = 'need_qt_gui'
if not block_n['label'].upper().startswith('QT GUI'):
block_n['label'] = 'QT GUI ' + block_n['label']
gui_hint_param = collections.OrderedDict()
gui_hint_param['id'] = 'gui_hint'
gui_hint_param['label'] = 'GUI Hint'
gui_hint_param['dtype'] = 'gui_hint'
gui_hint_param['hide'] = 'part'
block_n['parameters'].append(gui_hint_param)
block_n['templates']['make'] += (
"\n<% win = 'self.%s'%id %>"
"\n${ gui_hint() % win }"
)
return block_n
def get_hier_block_io(flow_graph, direction, domain=None):
"""
Get a list of io ports for this flow graph.
Returns a list of blocks
"""
pads = flow_graph.get_pad_sources(
) if direction == 'inputs' else flow_graph.get_pad_sinks()
for pad in pads:
for port in (pad.sources if direction == 'inputs' else pad.sinks):
if domain and port.domain != domain:
continue
yield port

View File

@ -0,0 +1,386 @@
import codecs
import operator
import os
import tempfile
import textwrap
from mako.template import Template
from .. import Messages, blocks
from ..Constants import TOP_BLOCK_FILE_MODE
from .FlowGraphProxy import FlowGraphProxy
from ..utils import expr_utils
DATA_DIR = os.path.dirname(__file__)
PYTHON_TEMPLATE = os.path.join(DATA_DIR, 'flow_graph.py.mako')
python_template = Template(filename=PYTHON_TEMPLATE)
class TopBlockGenerator(object):
def __init__(self, flow_graph, output_dir):
"""
Initialize the top block generator object.
Args:
flow_graph: the flow graph object
output_dir: the path for written files
"""
self._flow_graph = FlowGraphProxy(flow_graph)
self._generate_options = self._flow_graph.get_option(
'generate_options')
self._mode = TOP_BLOCK_FILE_MODE
# Handle the case where the directory is read-only
# In this case, use the system's temp directory
if not os.access(output_dir, os.W_OK):
output_dir = tempfile.gettempdir()
filename = self._flow_graph.get_option('id') + '.py'
self.file_path = os.path.join(output_dir, filename)
self.output_dir = output_dir
def _warnings(self):
throttling_blocks = [b for b in self._flow_graph.get_enabled_blocks()
if b.flags.throttle]
if not throttling_blocks and not self._generate_options.startswith('hb'):
Messages.send_warning("This flow graph may not have flow control: "
"no audio or RF hardware blocks found. "
"Add a Misc->Throttle block to your flow "
"graph to avoid CPU congestion.")
if len(throttling_blocks) > 1:
keys = set([b.key for b in throttling_blocks])
if len(keys) > 1 and 'blocks_throttle' in keys:
Messages.send_warning("This flow graph contains a throttle "
"block and another rate limiting block, "
"e.g. a hardware source or sink. "
"This is usually undesired. Consider "
"removing the throttle block.")
deprecated_block_keys = {
b.name for b in self._flow_graph.get_enabled_blocks() if b.flags.deprecated}
for key in deprecated_block_keys:
Messages.send_warning("The block {!r} is deprecated.".format(key))
def write(self):
"""generate output and write it to files"""
self._warnings()
fg = self._flow_graph
self.title = fg.get_option('title') or fg.get_option(
'id').replace('_', ' ').title()
variables = fg.get_variables()
parameters = fg.get_parameters()
monitors = fg.get_monitors()
self.namespace = {
'flow_graph': fg,
'variables': variables,
'parameters': parameters,
'monitors': monitors,
'generate_options': self._generate_options,
}
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.file_path:
os.chmod(filename, self._mode)
def _build_python_code_from_template(self):
"""
Convert the flow graph to python code.
Returns:
a string of python code
"""
output = []
fg = self._flow_graph
platform = fg.parent
title = fg.get_option('title') or fg.get_option(
'id').replace('_', ' ').title()
variables = fg.get_variables()
parameters = fg.get_parameters()
monitors = fg.get_monitors()
for block in fg.iter_enabled_blocks():
if block.key == 'epy_block':
src = block.params['_source_code'].get_value()
elif block.key == 'epy_module':
src = block.params['source_code'].get_value()
else:
continue
file_path = os.path.join(
self.output_dir, block.module_name + ".py")
output.append((file_path, src))
self.namespace = {
'flow_graph': fg,
'variables': variables,
'parameters': parameters,
'monitors': monitors,
'generate_options': self._generate_options,
'version': platform.config.version,
'catch_exceptions': fg.get_option('catch_exceptions')
}
flow_graph_code = python_template.render(
title=title,
imports=self._imports(),
blocks=self._blocks(),
callbacks=self._callbacks(),
connections=self._connections(),
**self.namespace
)
# strip trailing white-space
flow_graph_code = "\n".join(line.rstrip()
for line in flow_graph_code.split("\n"))
output.append((self.file_path, flow_graph_code))
return output
def _imports(self):
fg = self._flow_graph
imports = fg.imports()
seen = set()
output = []
need_path_hack = any(imp.endswith(
"# grc-generated hier_block") for imp in imports)
if need_path_hack:
output.insert(0, textwrap.dedent("""\
import os
import sys
import logging as log
def get_state_directory() -> str:
oldpath = os.path.expanduser("~/.grc_gnuradio")
try:
from gnuradio.gr import paths
newpath = paths.persistent()
if os.path.exists(newpath):
return newpath
if os.path.exists(oldpath):
log.warning(f"Found persistent state path '{newpath}', but file does not exist. " +
f"Old default persistent state path '{oldpath}' exists; using that. " +
"Please consider moving state to new location.")
return oldpath
# Default to the correct path if both are configured.
# neither old, nor new path exist: create new path, return that
os.makedirs(newpath, exist_ok=True)
return newpath
except (ImportError, NameError):
log.warning("Could not retrieve GNU Radio persistent state directory from GNU Radio. " +
"Trying defaults.")
xdgstate = os.getenv("XDG_STATE_HOME", os.path.expanduser("~/.local/state"))
xdgcand = os.path.join(xdgstate, "gnuradio")
if os.path.exists(xdgcand):
return xdgcand
if os.path.exists(oldpath):
log.warning(f"Using legacy state path '{oldpath}'. Please consider moving state " +
f"files to '{xdgcand}'.")
return oldpath
# neither old, nor new path exist: create new path, return that
os.makedirs(xdgcand, exist_ok=True)
return xdgcand
sys.path.append(os.environ.get('GRC_HIER_PATH', get_state_directory()))
"""))
seen.add('import os')
seen.add('import sys')
if fg.get_option('qt_qss_theme'):
imports.append('import os')
imports.append('import sys')
# Used by thread_safe_setters and startup Event
imports.append('import threading')
def is_duplicate(l):
if (l.startswith('import') or l.startswith('from')) and l in seen:
return True
seen.add(line)
return False
for import_ in sorted(imports):
lines = import_.strip().split('\n')
if not lines[0]:
continue
for line in lines:
line = line.rstrip()
if not is_duplicate(line):
output.append(line)
return output
def _blocks(self):
"""
Returns a list of tuples: (block, block_make)
'block' contains a reference to the block object.
'block_make' contains the pre-rendered string for the 'make' part of the
block.
"""
fg = self._flow_graph
parameters = fg.get_parameters()
# List of blocks not including variables and imports and parameters and disabled
def _get_block_sort_text(block):
code = block.templates.render('make').replace(block.name, ' ')
try:
# Newer gui markup w/ qtgui
code += block.params['gui_hint'].get_value()
except KeyError:
# No gui hint
pass
return code
blocks = [
b for b in fg.blocks
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)
blocks_make = []
for block in blocks:
make = block.templates.render('make')
if make:
if not (block.is_variable or block.is_virtual_or_pad):
make = 'self.' + block.name + ' = ' + make
blocks_make.append((block, make))
return blocks_make
def _callbacks(self):
fg = self._flow_graph
variables = fg.get_variables()
parameters = fg.get_parameters()
# List of variable names
var_ids = [var.name for var in parameters + variables]
replace_dict = dict((var_id, 'self.' + var_id) for var_id in var_ids)
callbacks_all = []
for block in fg.iter_enabled_blocks():
callbacks_all.extend(expr_utils.expr_replace(
cb, replace_dict) for cb in block.get_callbacks())
# Map var id to callbacks
def uses_var_id(callback):
used = expr_utils.get_variable_dependencies(callback, [var_id])
# callback might contain var_id itself
return used and (('self.' + var_id in callback) or ('this->' + var_id in callback))
callbacks = {}
for var_id in var_ids:
callbacks[var_id] = [
callback for callback in callbacks_all if uses_var_id(callback)]
return callbacks
def _connections(self):
fg = self._flow_graph
templates = {key: Template(text)
for key, text in fg.parent_platform.connection_templates.items()}
def make_port_sig(port):
# TODO: make sense of this
if port.parent.key in ('pad_source', 'pad_sink'):
block = 'self'
key = fg.get_pad_port_global_key(port)
else:
block = 'self.' + port.parent_block.name
key = port.key
if not key.isdigit():
key = repr(key)
return '({block}, {key})'.format(block=block, key=key)
connections = fg.get_enabled_connections()
# Get the virtual blocks and resolve their connections
connection_factory = fg.parent_platform.Connection
virtual_source_connections = [c for c in connections if isinstance(
c.source_block, blocks.VirtualSource)]
for connection in virtual_source_connections:
sink = connection.sink_port
for source in connection.source_port.resolve_virtual_source():
resolved = connection_factory(
fg.orignal_flowgraph, source, sink)
connections.append(resolved)
virtual_connections = [c for c in connections if (isinstance(
c.source_block, blocks.VirtualSource) or isinstance(c.sink_block, blocks.VirtualSink))]
for connection in virtual_connections:
# Remove the virtual connection
connections.remove(connection)
# Bypassing blocks: Need to find all the enabled connections for the block using
# the *connections* object rather than get_connections(). Create new connections
# that bypass the selected block and remove the existing ones. This allows adjacent
# bypassed blocks to see the newly created connections to downstream blocks,
# allowing them to correctly construct bypass connections.
bypassed_blocks = fg.get_bypassed_blocks()
for block in bypassed_blocks:
# Get the upstream connection (off of the sink ports)
# Use *connections* not get_connections()
source_connection = [
c for c in connections if c.sink_port == block.sinks[0]]
# The source connection should never have more than one element.
assert (len(source_connection) == 1)
# Get the source of the connection.
source_port = source_connection[0].source_port
# Loop through all the downstream connections
for sink in (c for c in connections if c.source_port == block.sources[0]):
if not sink.enabled:
# Ignore disabled connections
continue
connection = connection_factory(
fg.orignal_flowgraph, source_port, sink.sink_port)
connections.append(connection)
# Remove this sink connection
connections.remove(sink)
# Remove the source connection
connections.remove(source_connection[0])
# List of connections where each endpoint is enabled (sorted by domains, block names)
def by_domain_and_blocks(c):
return c.type, c.source_block.name, c.sink_block.name
rendered = []
for con in sorted(connections, key=by_domain_and_blocks):
template = templates[con.type]
if con.source_port.dtype != 'bus':
code = template.render(
make_port_sig=make_port_sig,
source=con.source_port,
sink=con.sink_port,
**con.namespace_templates)
rendered.append(code)
else:
# Bus ports need to iterate over the underlying connections and then render
# the code for each subconnection
porta = con.source_port
portb = con.sink_port
if porta.dtype == 'bus' and portb.dtype == 'bus':
# which bus port is this relative to the bus structure
if len(porta.bus_structure) == len(portb.bus_structure):
for port_num_a, port_num_b in zip(porta.bus_structure, portb.bus_structure):
hidden_porta = porta.parent.sources[port_num_a]
hidden_portb = portb.parent.sinks[port_num_b]
code = template.render(
make_port_sig=make_port_sig,
source=hidden_porta,
sink=hidden_portb,
**con.namespace_templates)
rendered.append(code)
return rendered

5
grc/core/io/__init__.py Normal file
View File

@ -0,0 +1,5 @@
# Copyright 2017 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#

89
grc/core/io/yaml.py Normal file
View File

@ -0,0 +1,89 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from collections import OrderedDict
import yaml
from ..params.param import attributed_str
class GRCDumper(yaml.SafeDumper):
@classmethod
def add(cls, data_type):
def decorator(func):
cls.add_representer(data_type, func)
return func
return decorator
def represent_ordered_mapping(self, data):
value = []
node = yaml.MappingNode(u'tag:yaml.org,2002:map',
value, flow_style=False)
if self.alias_key is not None:
self.represented_objects[self.alias_key] = node
for item_key, item_value in data.items():
node_key = self.represent_data(item_key)
node_value = self.represent_data(item_value)
value.append((node_key, node_value))
return node
def represent_ordered_mapping_flowing(self, data):
node = self.represent_ordered_mapping(data)
node.flow_style = True
return node
def represent_list_flowing(self, data):
node = self.represent_list(data)
node.flow_style = True
return node
def represent_ml_string(self, data):
node = self.represent_str(data)
node.style = '|'
return node
class OrderedDictFlowing(OrderedDict):
pass
class ListFlowing(list):
pass
class MultiLineString(str):
pass
GRCDumper.add_representer(OrderedDict, GRCDumper.represent_ordered_mapping)
GRCDumper.add_representer(
OrderedDictFlowing, GRCDumper.represent_ordered_mapping_flowing)
GRCDumper.add_representer(ListFlowing, GRCDumper.represent_list_flowing)
GRCDumper.add_representer(tuple, GRCDumper.represent_list)
GRCDumper.add_representer(MultiLineString, GRCDumper.represent_ml_string)
GRCDumper.add_representer(yaml.nodes.ScalarNode, lambda r, n: n)
GRCDumper.add_representer(attributed_str, GRCDumper.represent_str)
def dump(data, stream=None, **kwargs):
config = dict(stream=stream, default_flow_style=False,
indent=4, Dumper=GRCDumper)
config.update(kwargs)
return yaml.dump_all([data], **config)
if yaml.__with_libyaml__:
loader = yaml.CSafeLoader
else:
loader = yaml.SafeLoader
def safe_load(stream): return yaml.load(stream, Loader=loader)

View File

@ -0,0 +1,7 @@
# Copyright 2017 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from .param import Param

128
grc/core/params/dtypes.py Normal file
View File

@ -0,0 +1,128 @@
# Copyright 2008-2017 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
import re
import builtins
import keyword
from typing import List, Callable
from .. import blocks
from .. import Constants
# Blacklist certain ids, its not complete, but should help
ID_BLACKLIST = {'self', 'gnuradio'} | set(dir(builtins)) | set(keyword.kwlist)
# Python >= 3.10 has soft keywords in a list, i.e. words that are reserved in
# specific contexts, e.g. `match` isn't generally a keyword, but in `case` it is.
ID_BLACKLIST |= set(getattr(keyword, 'softkwlist', set()))
try:
from gnuradio import gr
ID_BLACKLIST |= {attr for attr in dir(
gr.top_block()) if not attr.startswith('_')}
except (ImportError, AttributeError):
pass
validators = {}
def validates(*dtypes) -> Callable:
"""
Registers a function as validator for the type of element give as strings
"""
def decorator(func):
for dtype in dtypes:
assert dtype in Constants.PARAM_TYPE_NAMES
validators[dtype] = func
return func
return decorator
class ValidateError(Exception):
"""Raised by validate functions"""
@validates('id')
def validate_block_id(param, black_listed_ids: List[str]) -> None:
value = param.value
# Can python use this as a variable?
if not re.match(r'^[a-z|A-Z]\w*$', value):
raise ValidateError('ID "{}" must begin with a letter and may contain letters, numbers, '
'and underscores.'.format(value))
if (value in ID_BLACKLIST or value in black_listed_ids) \
and not getattr(param.parent_block, 'exempt_from_id_validation', False):
# Grant blacklist exemption to epy blocks and modules
raise ValidateError('ID "{}" is blacklisted.'.format(value))
block_names = [
block.name for block in param.parent_flowgraph.iter_enabled_blocks()]
# Id should only appear once, or zero times if block is disabled
if param.key == 'id' and block_names.count(value) > 1:
raise ValidateError('ID "{}" is not unique.'.format(value))
elif value not in block_names:
raise ValidateError('ID "{}" does not exist.'.format(value))
@validates('name')
def validate_name(param, _) -> None:
# Name of a function or other block that will be generated literally not as a string
value = param.value
# Allow blank to pass validation
# Can python use this as a variable?
if not re.match(r'^([a-z|A-Z]\w*)?$', value):
raise ValidateError('ID "{}" must begin with a letter and may contain letters, numbers, '
'and underscores.'.format(value))
@validates('stream_id')
def validate_stream_id(param, _) -> None:
value = param.value
stream_ids = [
block.params['stream_id'].value
for block in param.parent_flowgraph.iter_enabled_blocks()
if isinstance(block, blocks.VirtualSink)
]
# Check that the virtual sink's stream id is unique
if isinstance(param.parent_block, blocks.VirtualSink) and stream_ids.count(value) >= 2:
# Id should only appear once, or zero times if block is disabled
raise ValidateError('Stream ID "{}" is not unique.'.format(value))
# Check that the virtual source's steam id is found
elif isinstance(param.parent_block, blocks.VirtualSource) and value not in stream_ids:
raise ValidateError('Stream ID "{}" is not found.'.format(value))
@validates('complex', 'real', 'float', 'int')
def validate_scalar(param, _) -> None:
valid_types = Constants.PARAM_TYPE_MAP[param.dtype]
if not isinstance(param.get_evaluated(), valid_types):
raise ValidateError('Expression {!r} is invalid for type {!r}.'.format(
param.get_evaluated(), param.dtype))
@validates('complex_vector', 'real_vector', 'float_vector', 'int_vector')
def validate_vector(param, _) -> None:
# todo: check vector types
if param.get_evaluated() is None:
raise ValidateError('Expression {!r} is invalid for type{!r}.'.format(
param.get_evaluated(), param.dtype))
valid_types = Constants.PARAM_TYPE_MAP[param.dtype.split('_', 1)[0]]
if not all(isinstance(item, valid_types) for item in param.get_evaluated()):
raise ValidateError('Expression {!r} is invalid for type {!r}.'.format(
param.get_evaluated(), param.dtype))
@validates('gui_hint')
def validate_gui_hint(param, _) -> None:
try:
# Only parse the param if there are no errors
if len(param.get_error_messages()) > 0:
return
param.parse_gui_hint(param.value)
except Exception as e:
raise ValidateError(str(e))

484
grc/core/params/param.py Normal file
View File

@ -0,0 +1,484 @@
# Copyright 2008-2017 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
import ast
import collections
import textwrap
from typing import Union, List
from .. import Constants
from ..base import Element
from ..utils.descriptors import Evaluated, EvaluatedEnum, setup_names
from . import dtypes
from .template_arg import TemplateArg
attributed_str = type('attributed_str', (str,), {})
@setup_names
class Param(Element):
EvaluationType = Union[None, str, complex, float, int, bool, List[str], List[complex], List[float], List[int]]
is_param = True
name = Evaluated(str, default='no name')
dtype = EvaluatedEnum(Constants.PARAM_TYPE_NAMES, default='raw')
hide = EvaluatedEnum('none all part')
# region init
def __init__(self, parent, id, label='', dtype='raw', default='',
options=None, option_labels=None, option_attributes=None,
category='', hide='none', **_):
"""Make a new param from nested data"""
super(Param, self).__init__(parent)
self.key = id
self.name = 'ID' if id == 'id' else (label.strip() or id.title())
self.category = category or Constants.DEFAULT_PARAM_TAB
self.dtype = dtype
self.value = self.default = str(default)
self.options = self._init_options(options or [], option_labels or [],
option_attributes or {})
self.hide = hide or 'none'
# end of args ########################################################
self._evaluated: Param.EvaluationType = None
self._stringify_flag = False
self._lisitify_flag = False
self.hostage_cells = set()
self._init = False
self.scale = {
'E': 1e18,
'P': 1e15,
'T': 1e12,
'G': 1e9,
'M': 1e6,
'k': 1e3,
'm': 1e-3,
'u': 1e-6,
'n': 1e-9,
'p': 1e-12,
'f': 1e-15,
'a': 1e-18,
}
self.scale_factor = None
self.number = None
def _init_options(self, values, labels, attributes):
"""parse option and option attributes"""
options = collections.OrderedDict()
options.attributes = collections.defaultdict(dict)
padding = [''] * max(len(values), len(labels))
attributes = {key: value + padding for key,
value in attributes.items()}
for i, option in enumerate(values):
# Test against repeated keys
if option in options:
raise KeyError(
'Value "{}" already exists in options'.format(option))
# get label
try:
label = str(labels[i])
except IndexError:
label = str(option)
# Store the option
options[option] = label
options.attributes[option] = {attrib: values[i]
for attrib, values in attributes.items()}
default = next(iter(options)) if options else ''
if not self.value:
self.value = self.default = default
if self.is_enum() and self.value not in options:
self.value = self.default = default # TODO: warn
# raise ValueError('The value {!r} is not in the possible values of {}.'
# ''.format(self.get_value(), ', '.join(self.options)))
return options
# endregion
@property
def template_arg(self):
return TemplateArg(self)
def __str__(self):
return 'Param - {}({})'.format(self.name, self.key)
def __repr__(self):
return '{!r}.param[{}]'.format(self.parent, self.key)
def is_enum(self):
return self.get_raw('dtype') == 'enum'
def get_value(self):
value = self.value
if self.is_enum() and value not in self.options:
value = self.default
self.set_value(value)
return value
def set_value(self, value):
# Must be a string
self.value = str(value)
def set_default(self, value):
if self.default == self.value:
self.set_value(value)
self.default = str(value)
def rewrite(self):
Element.rewrite(self)
del self.name
del self.dtype
del self.hide
self._evaluated = None
try:
self._evaluated = self.evaluate()
except Exception as e:
self.add_error_message(str(e))
rewriter = getattr(dtypes, 'rewrite_' + self.dtype, None)
if rewriter:
rewriter(self)
def validate(self):
"""
Validate the param.
The value must be evaluated and type must a possible type.
"""
Element.validate(self)
if self.dtype not in Constants.PARAM_TYPE_NAMES:
self.add_error_message(
'Type "{}" is not a possible type.'.format(self.dtype))
validator = dtypes.validators.get(self.dtype, None)
if self._init and validator:
try:
validator(self, self.parent_flowgraph.get_imported_names())
except dtypes.ValidateError as e:
self.add_error_message(str(e))
def get_evaluated(self) -> EvaluationType:
return self._evaluated
def _is_float(self, num: str) -> bool:
"""
Check if string can be converted to float.
Returns:
bool type
"""
try:
float(num)
return True
except ValueError:
return False
def evaluate(self) -> EvaluationType:
"""
Evaluate the value.
Returns:
evaluated type
"""
self._init = True
self._lisitify_flag = False
self._stringify_flag = False
dtype = self.dtype
expr = self.get_value()
scale_factor = self.scale_factor
#########################
# ID and Enum types (not evaled)
#########################
if dtype in ('id', 'stream_id', 'name') or self.is_enum():
if self.options.attributes:
expr = attributed_str(expr)
for key, value in self.options.attributes[expr].items():
setattr(expr, key, value)
return expr
#########################
# Numeric Types
#########################
elif dtype in ('raw', 'complex', 'real', 'float', 'int', 'short', 'byte', 'hex', 'bool'):
if expr:
try:
if isinstance(expr, str) and self._is_float(expr[:-1]):
scale_factor = expr[-1:]
if scale_factor in self.scale:
expr = str(float(expr[:-1]) *
self.scale[scale_factor])
value = self.parent_flowgraph.evaluate(expr)
except Exception as e:
raise Exception(
'Value "{}" cannot be evaluated:\n{}'.format(expr, e))
else:
value = None # No parameter value provided
if dtype == 'hex':
value = hex(value)
elif dtype == 'bool':
value = bool(value)
return value
#########################
# Numeric Vector Types
#########################
elif dtype in ('complex_vector', 'real_vector', 'float_vector', 'int_vector'):
if not expr:
return [] # Turn a blank string into an empty list, so it will eval
try:
value = self.parent.parent.evaluate(expr)
except Exception as value:
raise Exception(
'Value "{}" cannot be evaluated:\n{}'.format(expr, value))
if not isinstance(value, Constants.VECTOR_TYPES):
self._lisitify_flag = True
value = [value]
return value
#########################
# String Types
#########################
elif dtype in ('string', 'file_open', 'file_save', 'dir_select', '_multiline', '_multiline_python_external'):
# Do not check if file/directory exists, that is a runtime issue
try:
# 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)
if dtype == '_multiline_python_external':
ast.parse(value) # Raises SyntaxError
return value
#########################
# GUI Position/Hint
#########################
elif dtype == 'gui_hint':
return self.parse_gui_hint(expr) if self.parent_block.state == 'enabled' else ''
#########################
# Import Type
#########################
elif dtype == 'import':
# New namespace
n = dict()
try:
exec(expr, n)
except ImportError:
raise Exception('Import "{}" failed.'.format(expr))
except Exception:
raise Exception('Bad import syntax: "{}".'.format(expr))
return [k for k in list(n.keys()) if str(k) != '__builtins__']
#########################
else:
raise TypeError('Type "{}" not handled'.format(dtype))
def to_code(self):
"""
Convert the value to code.
For string and list types, check the init flag, call evaluate().
This ensures that evaluate() was called to set the xxxify_flags.
Returns:
a string representing the code
"""
self._init = True
value = self.get_value()
# String types
if self.dtype in ('string', 'file_open', 'file_save', 'dir_select', '_multiline', '_multiline_python_external'):
if not self._init:
self.evaluate()
return repr(value) if self._stringify_flag else value
# Vector types
elif self.dtype in ('complex_vector', 'real_vector', 'float_vector', 'int_vector'):
if not self._init:
self.evaluate()
return '[' + value + ']' if self._lisitify_flag else value
else:
if self.dtype in ('int', 'real') and ('+' in value or '-' in value or '*' in value or '/' in value):
value = '(' + value + ')'
return value
def get_opt(self, item):
return self.options.attributes[self.get_value()][item]
##############################################
# GUI Hint
##############################################
def parse_gui_hint(self, expr: str) -> str:
"""
Parse/validate gui hint value.
Args:
expr: gui_hint string from a block's 'gui_hint' param
Returns:
string of python code for positioning GUI elements in pyQT
"""
self.hostage_cells.clear()
# Parsing
if ':' in expr:
tab, pos = expr.split(':')
elif ',' in expr:
tab, pos = '', expr
else:
tab, pos = expr, ''
if '@' in tab:
tab, index = tab.split('@')
else:
index = '0'
index = int(index)
# Validation
def parse_pos():
e = self.parent_flowgraph.evaluate(pos)
if not isinstance(e, (list, tuple)) or len(e) not in (2, 4) or not all(isinstance(ei, int) for ei in e):
self.add_error_message(
'Invalid GUI Hint entered: {e!r} (Must be a list of {{2,4}} non-negative integers).'.format(e=e))
if len(e) == 2:
row, col = e
row_span = col_span = 1
else:
row, col, row_span, col_span = e
if (row < 0) or (col < 0):
self.add_error_message(
'Invalid GUI Hint entered: {e!r} (non-negative rows/cols only).'.format(e=e))
if (row_span < 1) or (col_span < 1):
self.add_error_message(
'Invalid GUI Hint entered: {e!r} (positive row/column span required).'.format(e=e))
return row, col, row_span, col_span
def validate_tab():
tabs = (block for block in self.parent_flowgraph.iter_enabled_blocks()
if block.key == 'qtgui_tab_widget' and block.name == tab)
tab_block = next(iter(tabs), None)
if not tab_block:
self.add_error_message(
'Invalid tab name entered: {tab} (Tab name not found).'.format(tab=tab))
return
tab_index_size = int(tab_block.params['num_tabs'].value)
if index >= tab_index_size:
self.add_error_message(
'Invalid tab index entered: {tab}@{index} (Index out of range).'.format(tab=tab, index=index))
# Collision Detection
def collision_detection(row, col, row_span, col_span):
my_parent = '{tab}@{index}'.format(tab=tab,
index=index) if tab else 'main'
# Calculate hostage cells
for r in range(row, row + row_span):
for c in range(col, col + col_span):
self.hostage_cells.add((my_parent, (r, c)))
for other in self._get_all_params('gui_hint'):
if other is self:
continue
collision = next(
iter(self.hostage_cells & other.hostage_cells), None)
if collision:
self.add_error_message('Block {block!r} is also using parent {parent!r}, cell {cell!r}.'.format(
block=other.parent_block.name, parent=collision[0], cell=collision[1]
))
# Code Generation
if tab:
validate_tab()
if not pos:
layout = '{tab}_layout_{index}'.format(tab=tab, index=index)
else:
layout = '{tab}_grid_layout_{index}'.format(
tab=tab, index=index)
else:
if not pos:
layout = 'top_layout'
else:
layout = 'top_grid_layout'
widget = '%s' # to be fill-out in the mail template
if pos:
row, col, row_span, col_span = parse_pos()
collision_detection(row, col, row_span, col_span)
if self.parent_flowgraph.get_option('output_language') == 'python':
widget_str = textwrap.dedent("""
self.{layout}.addWidget({widget}, {row}, {col}, {row_span}, {col_span})
for r in range({row}, {row_end}):
self.{layout}.setRowStretch(r, 1)
for c in range({col}, {col_end}):
self.{layout}.setColumnStretch(c, 1)
""".strip('\n')).format(
layout=layout, widget=widget,
row=row, row_span=row_span, row_end=row + row_span,
col=col, col_span=col_span, col_end=col + col_span,
)
elif self.parent_flowgraph.get_option('output_language') == 'cpp':
widget_str = textwrap.dedent("""
{layout}->addWidget({widget}, {row}, {col}, {row_span}, {col_span});
for(int r = {row};r < {row_end}; r++)
{layout}->setRowStretch(r, 1);
for(int c = {col}; c <{col_end}; c++)
{layout}->setColumnStretch(c, 1);
""".strip('\n')).format(
layout=layout, widget=widget,
row=row, row_span=row_span, row_end=row + row_span,
col=col, col_span=col_span, col_end=col + col_span,
)
else:
widget_str = ''
else:
if self.parent_flowgraph.get_option('output_language') == 'python':
widget_str = 'self.{layout}.addWidget({widget})'.format(
layout=layout, widget=widget)
elif self.parent_flowgraph.get_option('output_language') == 'cpp':
widget_str = '{layout}->addWidget({widget});'.format(
layout=layout, widget=widget)
else:
widget_str = ''
return widget_str
def _get_all_params(self, dtype, key=None) -> List[Element]:
"""
Get all the params from the flowgraph that have the given type and
optionally a given key
Args:
dtype: the specified type
key: the key to match against
Returns:
a list of params
"""
params = []
for block in self.parent_flowgraph.iter_enabled_blocks():
params.extend(
param for param in block.params.values()
if param.dtype == dtype and (key is None or key == param.name)
)
return params

View File

@ -0,0 +1,37 @@
# Copyright 2008-2017 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
class TemplateArg(str):
"""
A cheetah template argument created from a param.
The str of this class evaluates to the param's to code method.
The use of this class as a dictionary (enum only) will reveal the enum opts.
The __call__ or () method can return the param evaluated to a raw python data type.
"""
def __new__(cls, param):
value = param.to_code()
instance = str.__new__(cls, value)
setattr(instance, '_param', param)
return instance
def __getitem__(self, item):
return str(self._param.get_opt(item)) if self._param.is_enum() else NotImplemented
def __getattr__(self, item):
if not self._param.is_enum():
raise AttributeError()
try:
return str(self._param.get_opt(item))
except KeyError:
raise AttributeError()
def __str__(self):
return str(self._param.to_code())
def __call__(self):
return self._param.get_evaluated()

464
grc/core/platform.py Normal file
View File

@ -0,0 +1,464 @@
# Copyright 2008-2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from codecs import open
from collections import namedtuple
from collections import ChainMap
import os
import logging
from itertools import chain
from typing import Type
from . import (
Messages, Constants,
blocks, params, ports, errors, utils, schema_checker
)
from .blocks import Block
from .Config import Config
from .cache import Cache
from .base import Element
from .io import yaml
from .generator import Generator
from .FlowGraph import FlowGraph
from .Connection import Connection
logger = logging.getLogger(__name__)
class Platform(Element):
def __init__(self, *args, **kwargs):
""" Make a platform for GNU Radio """
Element.__init__(self, parent=None)
self.config = self.Config(*args, **kwargs)
self.block_docstrings = {}
# dummy to be replaced by BlockTreeWindow
self.block_docstrings_loaded_callback = lambda: None
self._docstring_extractor = utils.extract_docs.SubprocessLoader(
callback_query_result=self._save_docstring_extraction_result,
callback_finished=lambda: self.block_docstrings_loaded_callback()
)
self.blocks = self.block_classes
self.domains = {}
self.examples_dict = {}
self.connection_templates = {}
self.cpp_connection_templates = {}
self.connection_params = {}
self._block_categories = {}
self._auto_hier_block_generate_chain = set()
def __str__(self):
return 'Platform - {}'.format(self.config.name)
@staticmethod
def find_file_in_paths(filename, paths, cwd):
"""Checks the provided paths relative to cwd for a certain filename"""
if not os.path.isdir(cwd):
cwd = os.path.dirname(cwd)
if isinstance(paths, str):
paths = (p for p in paths.split(':') if p)
for path in paths:
path = os.path.expanduser(path)
if not os.path.isabs(path):
path = os.path.normpath(os.path.join(cwd, path))
file_path = os.path.join(path, filename)
if os.path.exists(os.path.normpath(file_path)):
return file_path
def load_and_generate_flow_graph(self, file_path, out_dir=None, hier_only=False):
"""Loads a flow graph from file and generates it"""
Messages.set_indent(len(self._auto_hier_block_generate_chain))
Messages.send('>>> Loading: {}\n'.format(file_path))
if file_path in self._auto_hier_block_generate_chain:
Messages.send(' >>> Warning: cyclic hier_block dependency\n')
return None, None
self._auto_hier_block_generate_chain.add(file_path)
try:
flow_graph = self.make_flow_graph()
flow_graph.grc_file_path = file_path
# Other, nested hier_blocks might be auto-loaded here
flow_graph.import_data(self.parse_flow_graph(file_path))
flow_graph.rewrite()
flow_graph.validate()
if not flow_graph.is_valid():
raise Exception('Flowgraph invalid')
if hier_only and not flow_graph.get_option('generate_options').startswith('hb'):
raise Exception('Not a hier block')
except Exception as e:
Messages.send('>>> Load Error: {}: {}\n'.format(file_path, str(e)))
Messages.send_flowgraph_error_report(flow_graph)
return None, None
finally:
self._auto_hier_block_generate_chain.discard(file_path)
Messages.set_indent(len(self._auto_hier_block_generate_chain))
try:
if flow_graph.get_option('generate_options').startswith('hb'):
generator = self.Generator(flow_graph, out_dir)
else:
generator = self.Generator(flow_graph, out_dir or file_path)
Messages.send('>>> Generating: {}\n'.format(generator.file_path))
generator.write()
except Exception as e:
Messages.send(
'>>> Generate Error: {}: {}\n'.format(file_path, str(e)))
return None, None
return flow_graph, generator.file_path
def build_example_library(self, path=None):
self.examples = list(self._iter_files_in_example_path())
def build_library(self, path=None):
"""load the blocks and block tree from the search paths
path: a list of paths/files to search in or load (defaults to config)
"""
self._docstring_extractor.start()
# Reset
self.blocks.clear()
self.domains.clear()
self.connection_templates.clear()
self.cpp_connection_templates.clear()
self._block_categories.clear()
try:
from gnuradio.gr import paths
cache_file = os.path.join(paths.cache(), Constants.GRC_SUBDIR, Constants.CACHE_FILE_NAME)
except ImportError:
cache_file = Constants.FALLBACK_CACHE_FILE
with Cache(cache_file, version=self.config.version) as cache:
for file_path in self._iter_files_in_block_path(path):
if file_path.endswith('.block.yml'):
loader = self.load_block_description
scheme = schema_checker.BLOCK_SCHEME
elif file_path.endswith('.domain.yml'):
loader = self.load_domain_description
scheme = schema_checker.DOMAIN_SCHEME
elif file_path.endswith('.tree.yml'):
loader = self.load_category_tree_description
scheme = None
else:
continue
try:
checker = schema_checker.Validator(scheme)
data = cache.get_or_load(file_path)
passed = checker.run(data)
for msg in checker.messages:
logger.warning('{:<40s} {}'.format(
os.path.basename(file_path), msg))
if not passed:
logger.info(
'YAML schema check failed for: ' + file_path)
loader(data, file_path)
except Exception as error:
logger.exception('Error while loading %s', file_path)
logger.exception(error)
Messages.flowgraph_error = error
Messages.flowgraph_error_file = file_path
continue
for key, block in self.blocks.items():
category = self._block_categories.get(key, block.category)
if not category:
continue
root = category[0]
if root.startswith('[') and root.endswith(']'):
category[0] = root[1:-1]
else:
category.insert(0, Constants.DEFAULT_BLOCK_MODULE_NAME)
block.category = category
self._docstring_extractor.finish()
# self._docstring_extractor.wait()
if 'options' not in self.blocks:
# we didn't find one of the built-in blocks ("options")
# which probably means the GRC blocks path is bad
errstr = (
"Failed to find built-in GRC blocks (specifically, the "
"'options' block). Ensure your GRC block paths are correct "
"and at least one points to your prefix installation:"
)
errstr = "\n".join([errstr] + (path or self.config.block_paths))
raise RuntimeError(errstr)
else:
# might have some cleanup to do on the options block in particular
utils.hide_bokeh_gui_options_if_not_installed(
self.blocks['options'])
def _iter_files_in_block_path(self, path=None, ext='yml'):
"""Iterator for block descriptions and category trees"""
for entry in (path or self.config.block_paths):
if os.path.isfile(entry):
yield entry
elif os.path.isdir(entry):
for dirpath, dirnames, filenames in os.walk(entry):
for filename in sorted(filter(lambda f: f.endswith('.' + ext), filenames)):
yield os.path.join(dirpath, filename)
else:
logger.debug('Ignoring invalid path entry %r', entry)
def _save_docstring_extraction_result(self, block_id, docstrings):
docs = {}
for match, docstring in docstrings.items():
if not docstring or match.endswith('_sptr'):
continue
docs[match] = docstring.replace('\n\n', '\n').strip()
try:
self.blocks[block_id].documentation.update(docs)
except KeyError:
pass # in tests platform might be gone...
##############################################
# Description File Loaders
##############################################
# region loaders
def load_block_description(self, data, file_path):
log = logger.getChild('block_loader')
# don't load future block format versions
file_format = data['file_format']
if file_format < 1 or file_format > Constants.BLOCK_DESCRIPTION_FILE_FORMAT_VERSION:
log.error('Unknown format version %d in %s',
file_format, file_path)
return
block_id = data['id'] = data['id'].rstrip('_')
if block_id in self.block_classes_build_in:
log.warning('Not overwriting build-in block %s with %s',
block_id, file_path)
return
if block_id in self.blocks:
log.warning('Block with id "%s" loaded from\n %s\noverwritten by\n %s',
block_id, self.blocks[block_id].loaded_from, file_path)
try:
block_cls = self.blocks[block_id] = self.new_block_class(**data)
block_cls.loaded_from = file_path
except errors.BlockLoadError as error:
log.error('Unable to load block %s', block_id)
log.exception(error)
return
self._docstring_extractor.query(
block_id, block_cls.templates['imports'], block_cls.templates['make'],
)
def load_domain_description(self, data, file_path):
log = logger.getChild('domain_loader')
domain_id = data['id']
if domain_id in self.domains: # test against repeated keys
log.debug('Domain "{}" already exists. Ignoring: %s', file_path)
return
color = data.get('color', '')
if color.startswith('#'):
try:
tuple(int(color[o:o + 2], 16) / 255.0 for o in range(1, 6, 2))
except ValueError:
log.warning('Cannot parse color code "%s" in %s',
color, file_path)
return
self.domains[domain_id] = self.Domain(
name=data.get('label', domain_id),
multi_in=data.get('multiple_connections_per_input', True),
multi_out=data.get('multiple_connections_per_output', False),
color=color
)
for connection in data.get('templates', []):
try:
source_id, sink_id = connection.get('type', [])
except ValueError:
log.warn('Invalid connection template.')
continue
connection_id = str(source_id), str(sink_id)
self.connection_templates[connection_id] = connection.get(
'connect', '')
self.cpp_connection_templates[connection_id] = connection.get(
'cpp_connect', '')
self.connection_params[connection_id] = connection.get('parameters', {})
def load_category_tree_description(self, data, file_path):
"""Parse category tree file and add it to list"""
log = logger.getChild('tree_loader')
log.debug('Loading %s', file_path)
path = []
def load_category(name, elements):
if not isinstance(name, str):
log.debug('Invalid name %r', name)
return
path.append(name)
for element in utils.to_list(elements):
if isinstance(element, str):
block_id = element
self._block_categories[block_id] = list(path)
elif isinstance(element, dict):
load_category(*next(iter(element.items())))
else:
log.debug('Ignoring some elements of %s', name)
path.pop()
try:
module_name, categories = next(iter(data.items()))
except (AttributeError, StopIteration):
log.warning('no valid data found')
else:
load_category(module_name, categories)
##############################################
# Access
##############################################
def parse_flow_graph(self, filename):
"""
Parse a saved flow graph file.
Ensure that the file exists, and passes the dtd check.
Args:
filename: the flow graph file
Returns:
nested data
@throws exception if the validation fails
"""
filename = filename or self.config.default_flow_graph
is_xml = False
with open(filename, encoding='utf-8') as fp:
is_xml = '<flow_graph>' in fp.read(100)
fp.seek(0)
# todo: try
if not is_xml:
data = yaml.safe_load(fp)
validator = schema_checker.Validator(
schema_checker.FLOW_GRAPH_SCHEME)
validator.run(data)
if is_xml:
Messages.send('>>> Converting from XML\n')
from ..converter.flow_graph import from_xml
data = from_xml(filename)
file_format = data.get('metadata', {}).get('file_format')
if file_format is None:
Messages.send(
'>>> WARNING: Flow graph does not contain a file format version!\n')
elif file_format == 0:
Messages.send(
'>>> WARNING: Flow graph format is version 0 (legacy) and will'
' be converted to version 1 or higher upon saving!\n')
elif file_format > Constants.FLOW_GRAPH_FILE_FORMAT_VERSION:
raise RuntimeError(
f"Flow graph {filename} has unknown flow graph version!")
return data
def save_flow_graph(self, filename, flow_graph):
data = flow_graph.export_data()
try:
data['connections'] = [
yaml.ListFlowing(conn) if isinstance(conn, (list, tuple)) else conn
for conn in data['connections']
]
except KeyError:
pass
try:
for d in chain([data['options']], data['blocks']):
d['states']['coordinate'] = yaml.ListFlowing(
d['states']['coordinate'])
except KeyError:
pass
out = yaml.dump(data, indent=2)
replace = [
('blocks:\n', '\nblocks:\n'),
('connections:\n', '\nconnections:\n'),
('metadata:\n', '\nmetadata:\n'),
]
for r in replace:
out = out.replace(*r)
with open(filename, 'w', encoding='utf-8') as fp:
fp.write(out)
def get_generate_options(self):
for param in self.block_classes['options'].parameters_data:
if param.get('id') == 'generate_options':
break
else:
return []
generate_mode_default = param.get('default')
return [(value, name, value == generate_mode_default)
for value, name in zip(param['options'], param['option_labels'])]
def get_output_language(self):
for param in self.block_classes['options'].parameters_data:
if param.get('id') == 'output_language':
break
else:
return []
output_language_default = param.get('default')
return [(value, name, value == output_language_default)
for value, name in zip(param['options'], param['option_labels'])]
##############################################
# Factories
##############################################
Config = Config
Domain = namedtuple('Domain', 'name multi_in multi_out color')
Generator = Generator
FlowGraph = FlowGraph
Connection = Connection
block_classes_build_in = blocks.build_ins
# separates build-in from loaded blocks)
block_classes = ChainMap({}, block_classes_build_in)
port_classes = {
None: ports.Port, # default
'clone': ports.PortClone, # clone of ports with multiplicity > 1
}
param_classes = {
None: params.Param, # default
}
def make_flow_graph(self, from_filename=None):
fg = self.FlowGraph(parent=self)
if from_filename:
data = self.parse_flow_graph(from_filename)
fg.grc_file_path = from_filename
fg.import_data(data)
return fg
def new_block_class(self, **data) -> Type[Block]:
return blocks.build(**data)
def make_block(self, parent, block_id, **kwargs) -> Block:
cls = self.block_classes[block_id]
return cls(parent, **kwargs)
def make_param(self, parent, **kwargs):
cls = self.param_classes[kwargs.pop('cls_key', None)]
return cls(parent, **kwargs)
def make_port(self, parent, **kwargs):
cls = self.port_classes[kwargs.pop('cls_key', None)]
return cls(parent, **kwargs)

View File

@ -0,0 +1,11 @@
"""
Copyright 2008-2015 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
from .port import Port
from .clone import PortClone

View File

@ -0,0 +1,119 @@
# Copyright 2008-2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from itertools import chain
from .. import blocks
class LoopError(Exception):
pass
def upstream_ports(port):
if port.is_sink:
return _sources_from_virtual_sink_port(port)
else:
return _sources_from_virtual_source_port(port)
def _sources_from_virtual_sink_port(sink_port, _traversed=None):
"""
Resolve the source port that is connected to the given virtual sink port.
Use the get source from virtual source to recursively resolve subsequent ports.
"""
source_ports_per_virtual_connection = (
# there can be multiple ports per virtual connection
_sources_from_virtual_source_port(
c.source_port, _traversed) # type: list
for c in sink_port.connections(enabled=True)
)
# concatenate generated lists of ports
return list(chain(*source_ports_per_virtual_connection))
def _sources_from_virtual_source_port(source_port, _traversed=None):
"""
Recursively resolve source ports over the virtual connections.
Keep track of traversed sources to avoid recursive loops.
"""
_traversed = set(_traversed or []) # a new set!
if source_port in _traversed:
raise LoopError('Loop found when resolving port type')
_traversed.add(source_port)
block = source_port.parent_block
flow_graph = source_port.parent_flowgraph
if not isinstance(block, blocks.VirtualSource):
return [source_port] # nothing to resolve, we're done
stream_id = block.params['stream_id'].value
# currently the validation does not allow multiple virtual sinks and one virtual source
# but in the future it may...
connected_virtual_sink_blocks = (
b for b in flow_graph.iter_enabled_blocks()
if isinstance(b, blocks.VirtualSink) and b.params['stream_id'].value == stream_id
)
source_ports_per_virtual_connection = (
_sources_from_virtual_sink_port(b.sinks[0], _traversed) # type: list
for b in connected_virtual_sink_blocks
)
# concatenate generated lists of ports
return list(chain(*source_ports_per_virtual_connection))
def downstream_ports(port):
if port.is_source:
return _sinks_from_virtual_source_port(port)
else:
return _sinks_from_virtual_sink_port(port)
def _sinks_from_virtual_source_port(source_port, _traversed=None):
"""
Resolve the sink port that is connected to the given virtual source port.
Use the get sink from virtual sink to recursively resolve subsequent ports.
"""
sink_ports_per_virtual_connection = (
# there can be multiple ports per virtual connection
_sinks_from_virtual_sink_port(c.sink_port, _traversed) # type: list
for c in source_port.connections(enabled=True)
)
# concatenate generated lists of ports
return list(chain(*sink_ports_per_virtual_connection))
def _sinks_from_virtual_sink_port(sink_port, _traversed=None):
"""
Recursively resolve sink ports over the virtual connections.
Keep track of traversed sinks to avoid recursive loops.
"""
_traversed = set(_traversed or []) # a new set!
if sink_port in _traversed:
raise LoopError('Loop found when resolving port type')
_traversed.add(sink_port)
block = sink_port.parent_block
flow_graph = sink_port.parent_flowgraph
if not isinstance(block, blocks.VirtualSink):
return [sink_port]
stream_id = block.params['stream_id'].value
connected_virtual_source_blocks = (
b for b in flow_graph.iter_enabled_blocks()
if isinstance(b, blocks.VirtualSource) and b.params['stream_id'].value == stream_id
)
sink_ports_per_virtual_connection = (
_sinks_from_virtual_source_port(b.sources[0], _traversed) # type: list
for b in connected_virtual_source_blocks
)
# concatenate generated lists of ports
return list(chain(*sink_ports_per_virtual_connection))

27
grc/core/ports/clone.py Normal file
View File

@ -0,0 +1,27 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from .port import Port, Element
class PortClone(Port):
def __init__(self, parent, direction, master, name, key):
Element.__init__(self, parent)
self.master_port = master
self.name = name
self.key = key
self.multiplicity = 1
def __getattr__(self, item):
return getattr(self.master_port, item)
def add_clone(self):
raise NotImplementedError()
def remove_clone(self, port):
raise NotImplementedError()

255
grc/core/ports/port.py Normal file
View File

@ -0,0 +1,255 @@
# Copyright 2008-2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from . import _virtual_connections
from .. import Constants
from ..base import Element
from ..utils.descriptors import (
EvaluatedFlag, EvaluatedEnum, EvaluatedPInt,
setup_names, lazy_property
)
@setup_names
class Port(Element):
is_port = True
dtype = EvaluatedEnum(list(Constants.TYPE_TO_SIZEOF.keys()), default='')
vlen = EvaluatedPInt()
multiplicity = EvaluatedPInt()
hidden = EvaluatedFlag()
optional = EvaluatedFlag()
def __init__(self, parent, direction, id, label='', domain=Constants.DEFAULT_DOMAIN, dtype='',
vlen='', multiplicity=1, optional=False, hide=False, bus_struct=None, **_):
"""Make a new port from nested data."""
Element.__init__(self, parent)
self._dir = direction
self.key = id
if not label:
label = id if not id.isdigit() else {'sink': 'in', 'source': 'out'}[
direction]
if dtype == 'bus':
# Look for existing busses to give proper index
busses = [p for p in self.parent.ports() if p._dir ==
self._dir and p.dtype == 'bus']
bus_structure = self.parent.current_bus_structure[self._dir]
bus_index = len(busses)
if len(bus_structure) > bus_index:
number = str(len(busses)) + '#' + \
str(len(bus_structure[bus_index]))
label = dtype + number
else:
raise ValueError(
'Could not initialize bus port due to incompatible bus structure')
self.name = self._base_name = label
self.domain = domain
self.dtype = dtype
self.vlen = vlen
if domain == Constants.GR_MESSAGE_DOMAIN: # ToDo: message port class
self.key = self.name
self.dtype = 'message'
self.multiplicity = multiplicity
self.optional = optional
self.hidden = hide
self.stored_hidden_state = None
self.bus_structure = bus_struct
# end of args ########################################################
self.clones = [] # References to cloned ports (for nports > 1)
def __str__(self):
if self.is_source:
return 'Source - {}({})'.format(self.name, self.key)
if self.is_sink:
return 'Sink - {}({})'.format(self.name, self.key)
def __repr__(self):
return '{!r}.{}[{}]'.format(self.parent, 'sinks' if self.is_sink else 'sources', self.key)
@property
def item_size(self):
return Constants.TYPE_TO_SIZEOF[self.dtype] * self.vlen
@lazy_property
def is_sink(self):
return self._dir == 'sink'
@lazy_property
def is_source(self):
return self._dir == 'source'
@property
def inherit_type(self):
"""always empty for e.g. virtual blocks, may eval to empty for 'Wildcard'"""
return not self.dtype
def validate(self):
del self._error_messages[:]
Element.validate(self)
platform = self.parent_platform
num_connections = len(list(self.connections(enabled=True)))
need_connection = not self.optional and not self.hidden
if need_connection and num_connections == 0:
self.add_error_message('Port is not connected.')
if self.dtype not in Constants.TYPE_TO_SIZEOF.keys():
self.add_error_message(
'Type "{}" is not a possible type.'.format(self.dtype))
try:
domain = platform.domains[self.domain]
if self.is_sink and not domain.multi_in and num_connections > 1:
self.add_error_message('Domain "{}" can have only one upstream block'
''.format(self.domain))
if self.is_source and not domain.multi_out and num_connections > 1:
self.add_error_message('Domain "{}" can have only one downstream block'
''.format(self.domain))
except KeyError:
self.add_error_message(
'Domain key "{}" is not registered.'.format(self.domain))
def rewrite(self):
del self.vlen
del self.multiplicity
del self.hidden
del self.optional
del self.dtype
if self.inherit_type:
self.resolve_empty_type()
Element.rewrite(self)
# Update domain if was deduced from (dynamic) port type
if self.domain == Constants.GR_STREAM_DOMAIN and self.dtype == "message":
self.domain = Constants.GR_MESSAGE_DOMAIN
self.key = self.name
if self.domain == Constants.GR_MESSAGE_DOMAIN and self.dtype != "message":
self.domain = Constants.GR_STREAM_DOMAIN
self.key = '0' # Is rectified in rewrite()
def resolve_virtual_source(self):
"""Only used by Generator after validation is passed"""
return _virtual_connections.upstream_ports(self)
def resolve_empty_type(self):
def find_port(finder):
try:
return next((p for p in finder(self) if not p.inherit_type), None)
except _virtual_connections.LoopError as error:
self.add_error_message(str(error))
except (StopIteration, Exception):
pass
try:
port = find_port(_virtual_connections.upstream_ports) or \
find_port(_virtual_connections.downstream_ports)
# we don't want to override the template
self.set_evaluated('dtype', port.dtype)
# we don't want to override the template
self.set_evaluated('vlen', port.vlen)
self.domain = port.domain
except AttributeError:
pass
def add_clone(self):
"""
Create a clone of this (master) port and store a reference in self._clones.
The new port name (and key for message ports) will have index 1... appended.
If this is the first clone, this (master) port will get a 0 appended to its name (and key)
Returns:
the cloned port
"""
# Add index to master port name if there are no clones yet
if not self.clones:
self.name = self._base_name + '0'
# Also update key for none stream ports
if not self.key.isdigit():
self.key = self.name
name = self._base_name + str(len(self.clones) + 1)
# Dummy value 99999 will be fixed later
key = '99999' if self.key.isdigit() else name
# Clone
port_factory = self.parent_platform.make_port
port = port_factory(self.parent, direction=self._dir,
name=name, key=key,
master=self, cls_key='clone')
self.clones.append(port)
return port
def remove_clone(self, port):
"""
Remove a cloned port (from the list of clones only)
Remove the index 0 of the master port name (and key9 if there are no more clones left
"""
self.clones.remove(port)
# Remove index from master port name if there are no more clones
if not self.clones:
self.name = self._base_name
# Also update key for none stream ports
if not self.key.isdigit():
self.key = self.name
def connections(self, enabled=None):
"""Iterator over all connections to/from this port
enabled: None for all, True for enabled only, False for disabled only
"""
for con in self.parent_flowgraph.connections:
# TODO clean this up - but how to get past this validation
# things don't compare simply with an x in y because
# bus ports are created differently.
port_in_con = False
if self.dtype == 'bus':
if self.is_sink:
if (self.parent.name == con.sink_port.parent.name and
self.name == con.sink_port.name):
port_in_con = True
elif self.is_source:
if (self.parent.name == con.source_port.parent.name and
self.name == con.source_port.name):
port_in_con = True
if port_in_con:
yield con
else:
if self in con and (enabled is None or enabled == con.enabled):
yield con
def get_associated_ports(self):
if not self.dtype == 'bus':
return [self]
else:
if self.is_source:
get_ports = self.parent.sources
bus_structure = self.parent.current_bus_structure['source']
else:
get_ports = self.parent.sinks
bus_structure = self.parent.current_bus_structure['sink']
ports = [i for i in get_ports if not i.dtype == 'bus']
if bus_structure:
busses = [i for i in get_ports if i.dtype == 'bus']
bus_index = busses.index(self)
ports = filter(lambda a: ports.index(
a) in bus_structure[bus_index], ports)
return ports

View File

@ -0,0 +1,5 @@
from .validator import Validator
from .block import BLOCK_SCHEME
from .domain import DOMAIN_SCHEME
from .flow_graph import FLOW_GRAPH_SCHEME

View File

@ -0,0 +1,73 @@
from .utils import Spec, expand
PARAM_SCHEME = expand(
base_key=str, # todo: rename/remove
id=str,
label=str,
category=str,
dtype=str,
default=object,
options=list,
option_labels=list,
option_attributes=Spec(types=dict, required=False,
item_scheme=(str, list)),
hide=str,
)
PORT_SCHEME = expand(
label=str,
domain=str,
id=str,
dtype=str,
vlen=(int, str),
multiplicity=(int, str),
optional=(bool, int, str),
hide=(bool, str),
)
TEMPLATES_SCHEME = expand(
imports=str,
var_make=str,
var_value=str,
make=str,
callbacks=list,
)
CPP_TEMPLATES_SCHEME = expand(
includes=list,
declarations=str,
make=str,
var_make=str,
callbacks=list,
link=list,
packages=list,
translations=dict,
)
BLOCK_SCHEME = expand(
id=Spec(types=str, required=True, item_scheme=None),
label=str,
category=str,
flags=(list, str),
parameters=Spec(types=list, required=False, item_scheme=PARAM_SCHEME),
inputs=Spec(types=list, required=False, item_scheme=PORT_SCHEME),
outputs=Spec(types=list, required=False, item_scheme=PORT_SCHEME),
asserts=(list, str),
value=str,
templates=Spec(types=dict, required=False, item_scheme=TEMPLATES_SCHEME),
cpp_templates=Spec(types=dict, required=False,
item_scheme=CPP_TEMPLATES_SCHEME),
documentation=str,
doc_url=str,
grc_source=str,
file_format=Spec(types=int, required=True, item_scheme=None),
block_wrapper_path=str, # todo: rename/remove
)

View File

@ -0,0 +1,19 @@
from .utils import Spec, expand
from .block import PARAM_SCHEME
DOMAIN_CONNECTION = expand(
type=Spec(types=list, required=True, item_scheme=None),
connect=str,
cpp_connect=str,
parameters=Spec(types=list, required=False, item_scheme=PARAM_SCHEME),
)
DOMAIN_SCHEME = expand(
id=Spec(types=str, required=True, item_scheme=None),
label=str,
color=str,
multiple_connections_per_input=bool,
multiple_connections_per_output=bool,
templates=Spec(types=list, required=False, item_scheme=DOMAIN_CONNECTION)
)

View File

@ -0,0 +1,24 @@
from .utils import Spec, expand
OPTIONS_SCHEME = expand(
parameters=Spec(types=dict, required=False, item_scheme=(str, str)),
states=Spec(types=dict, required=False, item_scheme=(str, str)),
)
BLOCK_SCHEME = expand(
name=str,
id=str,
**OPTIONS_SCHEME
)
FLOW_GRAPH_SCHEME = expand(
options=Spec(types=dict, required=False, item_scheme=OPTIONS_SCHEME),
blocks=Spec(types=dict, required=False, item_scheme=BLOCK_SCHEME),
connections=list,
metadata=Spec(types=dict, required=True, item_scheme=expand(
file_format=Spec(types=int, required=True, item_scheme=None),
grc_version=Spec(types=str, required=False, item_scheme=None),
))
)

View File

@ -0,0 +1,18 @@
from .utils import Spec, expand
MANIFEST_SCHEME = expand(
title=Spec(types=str, required=True, item_scheme=None),
version=str,
brief=str,
website=str,
dependencies=list,
repo=str,
copyright_owner=list,
gr_supported_version=list,
tags=list,
license=str,
description=str,
author=list,
icon=str,
file_format=Spec(types=int, required=True, item_scheme=None),
)

View File

@ -0,0 +1,22 @@
import collections
Spec = collections.namedtuple('Spec', 'types required item_scheme')
def expand(**kwargs):
def expand_spec(spec):
if not isinstance(spec, Spec):
types_ = spec if isinstance(spec, tuple) else (spec,)
spec = Spec(types=types_, required=False, item_scheme=None)
elif not isinstance(spec.types, tuple):
spec = Spec(types=(spec.types,), required=spec.required,
item_scheme=spec.item_scheme)
return spec
return {key: expand_spec(value) for key, value in kwargs.items()}
class Message(collections.namedtuple('Message', 'path type message')):
fmt = '{path}: {type}: {message}'
def __str__(self):
return self.fmt.format(**self._asdict())

View File

@ -0,0 +1,88 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from .utils import Message, Spec
class Validator(object):
def __init__(self, scheme=None):
self._path = []
self.scheme = scheme
self.messages = []
self.passed = False
def run(self, data):
if not self.scheme:
return True
self._reset()
self._path.append('block')
self._check(data, self.scheme)
self._path.pop()
return self.passed
def _reset(self):
del self.messages[:]
del self._path[:]
self.passed = True
def _check(self, data, scheme):
if not data or not isinstance(data, dict):
self._error('Empty data or not a dict')
return
if isinstance(scheme, dict):
self._check_dict(data, scheme)
else:
self._check_var_key_dict(data, *scheme)
def _check_var_key_dict(self, data, key_type, value_scheme):
for key, value in data.items():
if not isinstance(key, key_type):
self._error('Key type {!r} for {!r} not in valid types'.format(
type(value).__name__, key))
if isinstance(value_scheme, Spec):
self._check_dict(value, value_scheme)
elif not isinstance(value, value_scheme):
self._error('Value type {!r} for {!r} not in valid types'.format(
type(value).__name__, key))
def _check_dict(self, data, scheme):
for key, (types_, required, item_scheme) in scheme.items():
try:
value = data[key]
except KeyError:
if required:
self._error('Missing required entry {!r}'.format(key))
continue
self._check_value(value, types_, item_scheme, label=key)
for key in set(data).difference(scheme):
self._warn('Ignoring extra key {!r}'.format(key))
def _check_list(self, data, scheme, label):
for i, item in enumerate(data):
self._path.append('{}[{}]'.format(label, i))
self._check(item, scheme)
self._path.pop()
def _check_value(self, value, types_, item_scheme, label):
if not isinstance(value, types_):
self._error('Value type {!r} for {!r} not in valid types'.format(
type(value).__name__, label))
if item_scheme:
if isinstance(value, list):
self._check_list(value, item_scheme, label)
elif isinstance(value, dict):
self._check(value, item_scheme)
def _error(self, msg):
self.messages.append(Message('.'.join(self._path), 'error', msg))
self.passed = False
def _warn(self, msg):
self.messages.append(Message('.'.join(self._path), 'warn', msg))

View File

@ -0,0 +1,18 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from . import epy_block_io, expr_utils, extract_docs, flow_graph_complexity
from .hide_bokeh_gui_options_if_not_installed import hide_bokeh_gui_options_if_not_installed
def to_list(value):
if not value:
return []
elif isinstance(value, str):
return [value]
else:
return list(value)

View File

@ -0,0 +1,15 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
from ._lazy import lazy_property, nop_write
from .evaluated import (
Evaluated,
EvaluatedEnum,
EvaluatedPInt,
EvaluatedFlag,
setup_names,
)

View File

@ -0,0 +1,63 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
"""
Class method decorators.
"""
import functools
# pylint: disable=too-few-public-methods,invalid-name
class lazy_property:
"""
Class method decorator, similar to @property. This causes a method that is
declared as @lazy_property to be evaluated once, the first time it is called.
Subsequent calls to this property will always return the cached value.
Careful! Not suitable for properties that change at runtime.
Example:
>>> class Foo:
... def __init__(self):
... self.x = 5
...
... @lazy_property
... def xyz(self):
... return complicated_slow_function(self.x)
...
...
>>> f = Foo()
>>> print(f.xyz) # Will give the result, but takes long to compute
>>> print(f.xyz) # Blazing fast!
>>> f.x = 7 # Careful! f.xyz will not be updated any more!
>>> print(f.xyz) # Blazing fast, but returns the same value as before.
"""
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
def __get__(self, instance, owner):
if instance is None:
return self
value = self.func(instance)
setattr(instance, self.func.__name__, value)
return value
# pylint: enable=too-few-public-methods,invalid-name
def nop_write(prop):
"""
Make this a property with a nop setter
Effectively, makes a property read-only, with no error on write.
"""
def nop(self, value):
pass
return prop.setter(nop)

View File

@ -0,0 +1,105 @@
# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
class Evaluated(object):
def __init__(self, expected_type, default, name=None):
self.expected_type = expected_type
self.default = default
self.name = name or 'evaled_property_{}'.format(id(self))
self.eval_function = self.default_eval_func
@property
def name_raw(self):
return '_' + self.name
def default_eval_func(self, instance):
raw = getattr(instance, self.name_raw)
try:
value = instance.parent_block.evaluate(raw)
except Exception as error:
if raw:
instance.add_error_message(f"Failed to eval '{raw}': ({type(error)}) {error}")
return self.default
if not isinstance(value, self.expected_type):
instance.add_error_message("Can not cast evaluated value '{}' to type {}"
"".format(value, self.expected_type))
return self.default
# print(instance, self.name, raw, value)
return value
def __call__(self, func):
self.name = func.__name__
self.eval_function = func
return self
def __get__(self, instance, owner):
if instance is None:
return self
attribs = instance.__dict__
try:
value = attribs[self.name]
except KeyError:
value = attribs[self.name] = self.eval_function(instance)
return value
def __set__(self, instance, value):
attribs = instance.__dict__
value = value or self.default
if isinstance(value, str) and value.startswith('${') and value.endswith('}'):
attribs[self.name_raw] = value[2:-1].strip()
attribs.pop(self.name, None) # reset previous eval result
else:
attribs[self.name] = type(self.default)(value)
def __delete__(self, instance):
attribs = instance.__dict__
if self.name_raw in attribs:
attribs.pop(self.name, None)
class EvaluatedEnum(Evaluated):
def __init__(self, allowed_values, default=None, name=None):
if isinstance(allowed_values, str):
allowed_values = set(allowed_values.split())
self.allowed_values = allowed_values
default = default if default is not None else next(
iter(self.allowed_values))
super(EvaluatedEnum, self).__init__(str, default, name)
def default_eval_func(self, instance):
value = super(EvaluatedEnum, self).default_eval_func(instance)
if value not in self.allowed_values:
instance.add_error_message(
"Value '{}' not in allowed values".format(value))
return self.default
return value
class EvaluatedPInt(Evaluated):
def __init__(self, name=None):
super(EvaluatedPInt, self).__init__(int, 1, name)
def default_eval_func(self, instance):
value = super(EvaluatedPInt, self).default_eval_func(instance)
if value < 1:
# todo: log
return self.default
return value
class EvaluatedFlag(Evaluated):
def __init__(self, name=None):
super(EvaluatedFlag, self).__init__((bool, int), False, name)
def setup_names(cls):
for name, attrib in cls.__dict__.items():
if isinstance(attrib, Evaluated):
attrib.name = name
return cls

View File

@ -0,0 +1,129 @@
import inspect
import collections
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 callbacks')
def _ports(sigs, msgs):
ports = list()
for i, dtype in enumerate(sigs):
port_type = TYPE_MAP.get(dtype.base.name, None)
if not port_type:
raise ValueError("Can't map {0!r} to GRC port type".format(dtype))
vlen = dtype.shape[0] if len(dtype.shape) > 0 else 1
ports.append((str(i), port_type, vlen))
for msg_key in msgs:
if msg_key == 'system':
continue
ports.append((msg_key, 'message', 1))
return ports
def _find_block_class(source_code, cls):
ns = {}
try:
exec(source_code, ns)
except Exception as e:
raise ValueError("Can't interpret source code: " + str(e))
for var in ns.values():
if inspect.isclass(var) and issubclass(var, cls):
return var
raise ValueError('No python block class found in code')
def extract(cls):
try:
from gnuradio import gr
import pmt
except ImportError:
raise EnvironmentError("Can't import GNU Radio")
if not inspect.isclass(cls):
cls = _find_block_class(cls, gr.gateway.gateway_block)
spec = inspect.getfullargspec(cls.__init__)
init_args = spec.args[1:]
defaults = [repr(arg) for arg in (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 __init__ arguments to have 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(init_args, defaults))
def settable(attr):
try:
# check for a property with setter
return callable(getattr(cls, attr).fset)
except AttributeError:
return attr in instance.__dict__ # not dir() - only the instance attribs
callbacks = [attr for attr in dir(
instance) if attr in init_args and settable(attr)]
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, callbacks)
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, param3=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'))
self.param1 = param1
self._param2 = param2
self._param3 = param3
@property
def param2(self):
return self._param2
@property
def param3(self):
return self._param3
@param3.setter
def param3(self, value):
self._param3 = value
def work(self, inputs_items, output_items):
return 10
"""
from pprint import pprint
pprint(dict(extract(blk_code)._asdict()))

View File

@ -0,0 +1,214 @@
"""
Copyright 2008-2011 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
import ast
import string
def expr_replace(expr, replace_dict):
"""
Search for vars in the expression and add the prepend.
Args:
expr: an expression string
replace_dict: a dict of find:replace
Returns:
a new expression with the prepend
"""
expr_splits = _expr_split(expr, var_chars=VAR_CHARS + '.')
for i, es in enumerate(expr_splits):
if es in list(replace_dict.keys()):
expr_splits[i] = replace_dict[es]
return ''.join(expr_splits)
def get_variable_dependencies(expr, vars):
"""
Return a set of variables used in this expression.
Args:
expr: an expression string
vars: a list of variable names
Returns:
a subset of vars used in the expression
"""
expr_toks = _expr_split(expr)
return set(v for v in vars if v in expr_toks)
def sort_objects(objects, get_id, get_expr) -> list:
"""
Sort a list of objects according to their expressions.
Args:
objects: the list of objects to sort
get_id: the function to extract an id from the object
get_expr: the function to extract an expression from the object
Returns:
a list of sorted objects
"""
id2obj = {get_id(obj): obj for obj in objects}
# Map obj id to expression code
id2expr = {get_id(obj): get_expr(obj) for obj in objects}
# Sort according to dependency
sorted_ids = _sort_variables(id2expr)
# Return list of sorted objects
return [id2obj[id] for id in sorted_ids]
def dependencies(expr, names=None):
node = ast.parse(expr, mode='eval')
used_ids = frozenset(
[n.id for n in ast.walk(node) if isinstance(n, ast.Name)])
return used_ids & names if names else used_ids
def sort_objects2(objects, id_getter, expr_getter, check_circular=True):
known_ids = {id_getter(obj) for obj in objects}
def dependent_ids(obj):
deps = dependencies(expr_getter(obj))
return [id_ if id_ in deps else '' for id_ in known_ids]
objects = sorted(objects, key=dependent_ids)
if check_circular: # walk var defines step by step
defined_ids = set() # variables defined so far
for obj in objects:
deps = dependencies(expr_getter(obj), known_ids)
if not defined_ids.issuperset(deps): # can't have an undefined dep
raise RuntimeError(obj, deps, defined_ids)
defined_ids.add(id_getter(obj)) # define this one
return objects
VAR_CHARS = string.ascii_letters + string.digits + '_'
class _graph(object):
"""
Simple graph structure held in a dictionary.
"""
def __init__(self):
self._graph = dict()
def __str__(self):
return str(self._graph)
def add_node(self, node_key):
if node_key in self._graph:
return
self._graph[node_key] = set()
def remove_node(self, node_key):
if node_key not in self._graph:
return
for edges in self._graph.values():
if node_key in edges:
edges.remove(node_key)
self._graph.pop(node_key)
def add_edge(self, src_node_key, dest_node_key):
self._graph[src_node_key].add(dest_node_key)
def remove_edge(self, src_node_key, dest_node_key):
self._graph[src_node_key].remove(dest_node_key)
def get_nodes(self):
return list(self._graph.keys())
def get_edges(self, node_key):
return self._graph[node_key]
def _expr_split(expr, var_chars=VAR_CHARS):
"""
Split up an expression by non alphanumeric characters, including underscore.
Leave strings in-tact.
#TODO ignore escaped quotes, use raw strings.
Args:
expr: an expression string
Returns:
a list of string tokens that form expr
"""
toks = list()
tok = ''
quote = ''
for char in expr:
if quote or char in var_chars:
if char == quote:
quote = ''
tok += char
elif char in ("'", '"'):
toks.append(tok)
tok = char
quote = char
else:
toks.append(tok)
toks.append(char)
tok = ''
toks.append(tok)
return [t for t in toks if t]
def _get_graph(exprs):
"""
Get a graph representing the variable dependencies
Args:
exprs: a mapping of variable name to expression
Returns:
a graph of variable deps
"""
vars = list(exprs.keys())
# Get dependencies for each expression, load into graph
var_graph = _graph()
for var in vars:
var_graph.add_node(var)
for var, expr in exprs.items():
for dep in get_variable_dependencies(expr, vars):
if dep != var:
var_graph.add_edge(dep, var)
return var_graph
def _sort_variables(exprs):
"""
Get a list of variables in order of dependencies.
Args:
exprs: a mapping of variable name to expression
Returns:
a list of variable names
@throws Exception circular dependencies
"""
var_graph = _get_graph(exprs)
sorted_vars = list()
# Determine dependency order
while var_graph.get_nodes():
# Get a list of nodes with no edges
indep_vars = [var for var in var_graph.get_nodes()
if not var_graph.get_edges(var)]
if not indep_vars:
raise Exception('circular dependency caught in sort_variables')
# Add the indep vars to the end of the list
sorted_vars.extend(sorted(indep_vars))
# Remove each edge-less node from the graph
for var in indep_vars:
var_graph.remove_node(var)
return reversed(sorted_vars)

View File

@ -0,0 +1,317 @@
"""
Copyright 2008-2015 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
import sys
import re
import subprocess
import threading
import json
import random
import itertools
import queue
###############################################################################
# The docstring extraction
###############################################################################
def docstring_guess_from_key(key):
"""
Extract the documentation from the python __doc__ strings
By guessing module and constructor names from key
Args:
key: the block key
Returns:
a dict (block_name --> doc string)
"""
doc_strings = dict()
in_tree = [key.partition('_')[::2] + (
lambda package: getattr(__import__('gnuradio.' + package), package),
)]
key_parts = key.split('_')
oot = [
('_'.join(key_parts[:i]), '_'.join(key_parts[i:]), __import__)
for i in range(1, len(key_parts))
]
for module_name, init_name, importer in itertools.chain(in_tree, oot):
if not module_name or not init_name:
continue
try:
module = importer(module_name)
break
except ImportError:
continue
else:
return doc_strings
pattern = re.compile(get_block_regex(init_name))
for match in filter(pattern.match, dir(module)):
try:
doc_strings[match] = getattr(module, match).__doc__
except AttributeError:
continue
return doc_strings
def get_block_regex(block_name):
"""
Helper function to create regular expression for a given block name
Assumes variable type blocks ends with '_x', '_xx', '_xxx', '_vxx',
or "_xx_ts"
"""
regex = "^" + block_name + r"\w*$"
variable_suffixes = ["_x", "_xx", "_xxx", "_vxx", "_xx_ts"]
for suffix in variable_suffixes:
if block_name.endswith(suffix):
base_name, _, _ = block_name.rpartition(suffix)
var_suffix = suffix.replace("x", r"\w")
regex = "^" + base_name + var_suffix + r"\w*$"
break
return regex
def docstring_from_make(key, imports, make):
"""
Extract the documentation from the python __doc__ strings
By importing it and checking a truncated make
Args:
key: the block key
imports: a list of import statements (string) to execute
make: block constructor template
Returns:
a list of tuples (block_name, doc string)
"""
try:
blk_cls = make.partition('(')[0].strip()
if '$' in blk_cls:
raise ValueError('Not an identifier')
ns = dict()
exec(imports.strip(), ns)
blk = eval(blk_cls, ns)
doc_strings = {key: blk.__doc__}
except (ImportError, AttributeError, SyntaxError, ValueError):
doc_strings = docstring_guess_from_key(key)
return doc_strings
###############################################################################
# Manage docstring extraction in separate process
###############################################################################
class SubprocessLoader(object):
"""
Start and manage docstring extraction process
Manages subprocess and handles RPC.
"""
BOOTSTRAP = "import runpy; runpy.run_path({!r}, run_name='__worker__')"
AUTH_CODE = random.random() # sort out unwanted output of worker process
RESTART = 5 # number of worker restarts before giving up
DONE = object() # sentinel value to signal end-of-queue
def __init__(self, callback_query_result, callback_finished=None):
self.callback_query_result = callback_query_result
self.callback_finished = callback_finished or (lambda: None)
self._queue = queue.Queue()
self._thread = None
self._worker = None
self._shutdown = threading.Event()
self._last_cmd = None
def start(self):
""" Start the worker process handler thread """
if self._thread is not None:
return
self._shutdown.clear()
thread = self._thread = threading.Thread(target=self.run_worker)
thread.daemon = True
thread.start()
def run_worker(self):
""" Read docstring back from worker stdout and execute callback. """
for _ in range(self.RESTART):
if self._shutdown.is_set():
break
try:
self._worker = subprocess.Popen(
args=(sys.executable, '-uc',
self.BOOTSTRAP.format(__file__)),
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
self._handle_worker()
except (OSError, IOError):
msg = "Warning: restarting the docstring loader"
cmd, args = self._last_cmd
if cmd == 'query':
msg += " (crashed while loading {0!r})".format(args[0])
print(msg, file=sys.stderr)
continue # restart
else:
break # normal termination, return
finally:
if self._worker:
self._worker.stdin.close()
self._worker.stdout.close()
self._worker.stderr.close()
self._worker.terminate()
self._worker.wait()
else:
print("Warning: docstring loader crashed too often", file=sys.stderr)
self._thread = None
self._worker = None
self.callback_finished()
def _handle_worker(self):
""" Send commands and responses back from worker. """
assert '1' == self._worker.stdout.read(1).decode('utf-8')
for cmd, args in iter(self._queue.get, self.DONE):
self._last_cmd = cmd, args
self._send(cmd, args)
cmd, args = self._receive()
self._handle_response(cmd, args)
def _send(self, cmd, args):
""" Send a command to worker's stdin """
fd = self._worker.stdin
query = json.dumps((self.AUTH_CODE, cmd, args))
fd.write(query.encode('utf-8'))
fd.write(b'\n')
fd.flush()
def _receive(self):
""" Receive response from worker's stdout """
for line in iter(self._worker.stdout.readline, ''):
try:
key, cmd, args = json.loads(line.decode('utf-8'))
if key != self.AUTH_CODE:
raise ValueError('Got wrong auth code')
return cmd, args
except ValueError:
if self._worker.poll():
raise IOError("Worker died")
else:
continue # ignore invalid output from worker
else:
raise IOError("Can't read worker response")
def _handle_response(self, cmd, args):
""" Handle response from worker, call the callback """
if cmd == 'result':
key, docs = args
self.callback_query_result(key, docs)
elif cmd == 'error':
print(args)
else:
print("Unknown response:", cmd, args, file=sys.stderr)
def query(self, key, imports=None, make=None):
""" Request docstring extraction for a certain key """
if self._thread is None:
self.start()
if imports and make:
self._queue.put(('query', (key, imports, make)))
else:
self._queue.put(('query_key_only', (key,)))
def finish(self):
""" Signal end of requests """
self._queue.put(self.DONE)
def wait(self):
""" Wait for the handler thread to die """
if self._thread:
self._thread.join()
def terminate(self):
""" Terminate the worker and wait """
self._shutdown.set()
try:
self._worker.terminate()
self.wait()
except (OSError, AttributeError):
pass
###############################################################################
# Main worker entry point
###############################################################################
def worker_main():
"""
Main entry point for the docstring extraction process.
Manages RPC with main process through stdin/stdout.
Runs a docstring extraction for each key it read on stdin.
"""
def send(code, cmd, args):
json.dump((code, cmd, args), sys.stdout)
sys.stdout.write('\n')
# fluh out to get new commands from the queue into stdin
sys.stdout.flush()
sys.stdout.write('1')
# flush out to signal the main process we are ready for new commands
sys.stdout.flush()
for line in iter(sys.stdin.readline, ''):
code, cmd, args = json.loads(line)
try:
if cmd == 'query':
key, imports, make = args
send(code, 'result', (key, docstring_from_make(key, imports, make)))
elif cmd == 'query_key_only':
key, = args
send(code, 'result', (key, docstring_guess_from_key(key)))
elif cmd == 'exit':
break
except Exception as e:
send(code, 'error', repr(e))
if __name__ == '__worker__':
worker_main()
elif __name__ == '__main__':
def callback(key, docs):
print(key)
for match, doc in docs.items():
print('-->', match)
print(str(doc).strip())
print()
print()
r = SubprocessLoader(callback)
# r.query('analog_feedforward_agc_cc')
# r.query('uhd_source')
r.query('expr_utils_graph')
r.query('blocks_add_cc')
r.query('blocks_add_cc', ['import gnuradio.blocks'],
'gnuradio.blocks.add_cc(')
# r.query('analog_feedforward_agc_cc')
# r.query('uhd_source')
# r.query('uhd_source')
# r.query('analog_feedforward_agc_cc')
r.finish()
# r.terminate()
r.wait()

View File

@ -0,0 +1,57 @@
def calculate(flowgraph):
""" Determines the complexity of a flowgraph """
try:
dbal = 0.0
for block in flowgraph.blocks:
if block.key == "options":
continue
# Determine the base value for this block
sinks = sum(1.0 for port in block.sinks if not port.optional)
sources = sum(1.0 for port in block.sources if not port.optional)
base = max(min(sinks, sources), 1)
# Determine the port multiplier
block_connections = 0.0
for port in block.sources:
block_connections += sum(1.0 for c in port.connections())
source_multi = max(block_connections / max(sources, 1.0), 1.0)
# Port ratio multiplier
multi = 1.0
if min(sinks, sources) > 0:
multi = float(sinks / sources)
multi = float(1 / multi) if multi > 1 else multi
dbal += base * multi * source_multi
blocks = float(len(flowgraph.blocks) - 1)
connections = float(len(flowgraph.connections))
variables = float(len(flowgraph.get_variables()))
enabled = float(len(flowgraph.get_enabled_blocks()))
enabled_connections = float(len(flowgraph.get_enabled_connections()))
disabled_connections = connections - enabled_connections
# Disabled multiplier
if enabled > 0:
disabled_multi = 1 / \
(max(1 - ((blocks - enabled) / max(blocks, 1)), 0.05))
else:
disabled_multi = 1
# Connection multiplier (How many connections )
if (connections - disabled_connections) > 0:
conn_multi = 1 / \
(max(1 - (disabled_connections / max(connections, 1)), 0.05))
else:
conn_multi = 1
final = round(max((dbal - 1) * disabled_multi *
conn_multi * connections, 0.0) / 1000000, 6)
return final
except Exception:
return "<Error>"

View File

@ -0,0 +1,16 @@
# Copyright 2008-2017 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
def hide_bokeh_gui_options_if_not_installed(options_blk):
try:
import bokehgui
except ImportError:
for param in options_blk.parameters_data:
if param['id'] == 'generate_options':
ind = param['options'].index('bokeh_gui')
del param['options'][ind]
del param['option_labels'][ind]

12
grc/grc.conf.in Normal file
View File

@ -0,0 +1,12 @@
# This file contains system wide configuration data for GNU Radio.
# You may override any setting on a per-user basis by editing
# ~/.config/gnuradio/grc.conf
[grc]
global_blocks_path = @blocksdir@
local_blocks_path =
examples_path = @examplesdir@
default_flow_graph =
xterm_executable = @GRC_XTERM_EXE@
canvas_font_size = 8
canvas_default_size = 1280, 1024

742
grc/gui/Actions.py Normal file
View File

@ -0,0 +1,742 @@
"""
Copyright 2007-2011 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
import logging
from gi.repository import Gtk, Gdk, Gio, GLib, GObject
from . import Utils
log = logging.getLogger(__name__)
def filter_from_dict(vars):
return filter(lambda x: isinstance(x[1], Action), vars.items())
class Namespace(object):
def __init__(self):
self._actions = {}
def add(self, action):
key = action.props.name
self._actions[key] = action
def connect(self, name, handler):
#log.debug("Connecting action <{}> to handler <{}>".format(name, handler.__name__))
self._actions[name].connect('activate', handler)
def register(self,
name,
parameter=None,
handler=None,
label=None,
tooltip=None,
icon_name=None,
keypresses=None,
preference_name=None,
default=None):
# Check types
if not isinstance(name, str):
raise TypeError("Cannot register function: 'name' must be a str")
if parameter and not isinstance(parameter, str):
raise TypeError(
"Cannot register function: 'parameter' must be a str")
if handler and not callable(handler):
raise TypeError(
"Cannot register function: 'handler' must be callable")
# Check if the name has a prefix.
prefix = None
if name.startswith("app.") or name.startswith("win."):
# Set a prefix for later and remove it
prefix = name[0:3]
name = name[4:]
if handler:
log.debug(
"Register action [{}, prefix={}, param={}, handler={}]".format(
name, prefix, parameter, handler.__name__))
else:
log.debug(
"Register action [{}, prefix={}, param={}, handler=None]".
format(name, prefix, parameter))
action = Action(name,
parameter,
label=label,
tooltip=tooltip,
icon_name=icon_name,
keypresses=keypresses,
prefix=prefix,
preference_name=preference_name,
default=default)
if handler:
action.connect('activate', handler)
key = name
if prefix:
key = "{}.{}".format(prefix, name)
if prefix == "app":
pass
# self.app.add_action(action)
elif prefix == "win":
pass
# self.win.add_action(action)
#log.debug("Registering action as '{}'".format(key))
self._actions[key] = action
return action
# If the actions namespace is called, trigger an action
def __call__(self, name):
# Try to parse the action string.
valid, action_name, target_value = Action.parse_detailed_name(name)
if not valid:
raise Exception("Invalid action string: '{}'".format(name))
if action_name not in self._actions:
raise Exception(
"Action '{}' is not registered!".format(action_name))
if target_value:
self._actions[action_name].activate(target_value)
else:
self._actions[action_name].activate()
def __getitem__(self, key):
return self._actions[key]
def __iter__(self):
return self._actions.itervalues()
def __repr__(self):
return str(self)
def get_actions(self):
return self._actions
def __str__(self):
s = "{Actions:"
for key in self._actions:
s += " {},".format(key)
s = s.rstrip(",") + "}"
return s
class Action(Gio.SimpleAction):
# Change these to normal python properties.
# prefs_name
def __init__(self,
name,
parameter=None,
label=None,
tooltip=None,
icon_name=None,
keypresses=None,
prefix=None,
preference_name=None,
default=None):
self.name = name
self.label = label
self.tooltip = tooltip
self.icon_name = icon_name
if keypresses:
self.keypresses = [
kp.replace("<Ctrl>", Utils.get_modifier_key(True))
for kp in keypresses
]
else:
self.keypresses = None
self.prefix = prefix
self.preference_name = preference_name
self.default = default
# Don't worry about checking types here, since it's done in register()
# Save the parameter type to use for converting in __call__
self.type = None
variant = None
state = None
if parameter:
variant = GLib.VariantType.new(parameter)
if preference_name:
state = GLib.Variant.new_boolean(True)
Gio.SimpleAction.__init__(self,
name=name,
parameter_type=variant,
state=state)
def enable(self):
self.props.enabled = True
def disable(self):
self.props.enabled = False
def set_enabled(self, state):
if not isinstance(state, bool):
raise TypeError("State must be True/False.")
self.props.enabled = state
def __str__(self):
return self.props.name
def __repr__(self):
return str(self)
def get_active(self):
if self.props.state:
return self.props.state.get_boolean()
return False
def set_active(self, state):
if not isinstance(state, bool):
raise TypeError("State must be True/False.")
self.change_state(GLib.Variant.new_boolean(state))
# Allows actions to be directly called.
def __call__(self, parameter=None):
if self.type and parameter:
# Try to convert it to the correct type.
try:
param = GLib.Variant(self.type, parameter)
self.activate(param)
except TypeError:
raise TypeError(
"Invalid parameter type for action '{}'. Expected: '{}'".
format(self.get_name(), self.type))
else:
self.activate()
def load_from_preferences(self, *args):
log.debug("load_from_preferences({})".format(args))
if self.preference_name is not None:
config = Gtk.Application.get_default().config
self.set_active(
config.entry(self.preference_name, default=bool(self.default)))
def save_to_preferences(self, *args):
log.debug("save_to_preferences({})".format(args))
if self.preference_name is not None:
config = Gtk.Application.get_default().config
config.entry(self.preference_name, value=self.get_active())
actions = Namespace()
def get_actions():
return actions.get_actions()
def connect(action, handler=None):
return actions.connect(action, handler=handler)
########################################################################
# Old Actions
########################################################################
PAGE_CHANGE = actions.register("win.page_change")
EXTERNAL_UPDATE = actions.register("app.external_update")
VARIABLE_EDITOR_UPDATE = actions.register("app.variable_editor_update")
FLOW_GRAPH_NEW = actions.register(
"app.flowgraph.new",
label='_New',
tooltip='Create a new flow graph',
icon_name='document-new',
keypresses=["<Ctrl>n"],
)
FLOW_GRAPH_NEW_TYPE = actions.register(
"app.flowgraph.new_type",
parameter="s",
)
FLOW_GRAPH_OPEN = actions.register(
"app.flowgraph.open",
label='_Open',
tooltip='Open an existing flow graph',
icon_name='document-open',
keypresses=["<Ctrl>o"],
)
FLOW_GRAPH_OPEN_RECENT = actions.register(
"app.flowgraph.open_recent",
label='Open _Recent',
tooltip='Open a recently used flow graph',
icon_name='document-open-recent',
parameter="s",
)
FLOW_GRAPH_CLEAR_RECENT = actions.register("app.flowgraph.clear_recent")
FLOW_GRAPH_SAVE = actions.register(
"app.flowgraph.save",
label='_Save',
tooltip='Save the current flow graph',
icon_name='document-save',
keypresses=["<Ctrl>s"],
)
FLOW_GRAPH_SAVE_AS = actions.register(
"app.flowgraph.save_as",
label='Save _As',
tooltip='Save the current flow graph as...',
icon_name='document-save-as',
keypresses=["<Ctrl><Shift>s"],
)
FLOW_GRAPH_SAVE_COPY = actions.register(
"app.flowgraph.save_copy",
label='Save Copy',
tooltip='Save a copy of current flow graph',
)
FLOW_GRAPH_DUPLICATE = actions.register(
"app.flowgraph.duplicate",
label='_Duplicate',
tooltip='Create a duplicate of current flow graph',
# stock_id=Gtk.STOCK_COPY,
keypresses=["<Ctrl><Shift>d"],
)
FLOW_GRAPH_CLOSE = actions.register(
"app.flowgraph.close",
label='_Close',
tooltip='Close the current flow graph',
icon_name='window-close',
keypresses=["<Ctrl>w"],
)
APPLICATION_INITIALIZE = actions.register("app.initialize")
APPLICATION_QUIT = actions.register(
"app.quit",
label='_Quit',
tooltip='Quit program',
icon_name='application-exit',
keypresses=["<Ctrl>q"],
)
FLOW_GRAPH_UNDO = actions.register(
"win.undo",
label='_Undo',
tooltip='Undo a change to the flow graph',
icon_name='edit-undo',
keypresses=["<Ctrl>z"],
)
FLOW_GRAPH_REDO = actions.register(
"win.redo",
label='_Redo',
tooltip='Redo a change to the flow graph',
icon_name='edit-redo',
keypresses=["<Ctrl>y"],
)
NOTHING_SELECT = actions.register("win.unselect")
SELECT_ALL = actions.register(
"win.select_all",
label='Select _All',
tooltip='Select all blocks and connections in the flow graph',
icon_name='edit-select-all',
keypresses=["<Ctrl>a"],
)
ELEMENT_SELECT = actions.register("win.select")
ELEMENT_CREATE = actions.register("win.add")
ELEMENT_DELETE = actions.register(
"win.delete",
label='_Delete',
tooltip='Delete the selected blocks',
icon_name='edit-delete',
keypresses=["Delete"],
)
BLOCK_MOVE = actions.register("win.block_move")
BLOCK_ROTATE_CCW = actions.register(
"win.block_rotate_ccw",
label='Rotate Counterclockwise',
tooltip='Rotate the selected blocks 90 degrees to the left',
icon_name='object-rotate-left',
keypresses=["Left"],
)
BLOCK_ROTATE_CW = actions.register(
"win.block_rotate",
label='Rotate Clockwise',
tooltip='Rotate the selected blocks 90 degrees to the right',
icon_name='object-rotate-right',
keypresses=["Right"],
)
BLOCK_VALIGN_TOP = actions.register(
"win.block_align_top",
label='Vertical Align Top',
tooltip='Align tops of selected blocks',
keypresses=["<Shift>t"],
)
BLOCK_VALIGN_MIDDLE = actions.register(
"win.block_align_middle",
label='Vertical Align Middle',
tooltip='Align centers of selected blocks vertically',
keypresses=["<Shift>m"],
)
BLOCK_VALIGN_BOTTOM = actions.register(
"win.block_align_bottom",
label='Vertical Align Bottom',
tooltip='Align bottoms of selected blocks',
keypresses=["<Shift>b"],
)
BLOCK_HALIGN_LEFT = actions.register(
"win.block_align_left",
label='Horizontal Align Left',
tooltip='Align left edges of blocks selected blocks',
keypresses=["<Shift>l"],
)
BLOCK_HALIGN_CENTER = actions.register(
"win.block_align_center",
label='Horizontal Align Center',
tooltip='Align centers of selected blocks horizontally',
keypresses=["<Shift>c"],
)
BLOCK_HALIGN_RIGHT = actions.register(
"win.block_align_right",
label='Horizontal Align Right',
tooltip='Align right edges of selected blocks',
keypresses=["<Shift>r"],
)
BLOCK_ALIGNMENTS = [
BLOCK_VALIGN_TOP,
BLOCK_VALIGN_MIDDLE,
BLOCK_VALIGN_BOTTOM,
None,
BLOCK_HALIGN_LEFT,
BLOCK_HALIGN_CENTER,
BLOCK_HALIGN_RIGHT,
]
BLOCK_PARAM_MODIFY = actions.register(
"win.block_modify",
label='_Properties',
tooltip='Modify params for the selected block',
icon_name='document-properties',
keypresses=["Return"],
)
BLOCK_ENABLE = actions.register(
"win.block_enable",
label='E_nable',
tooltip='Enable the selected blocks',
icon_name='network-wired',
keypresses=["e"],
)
BLOCK_DISABLE = actions.register(
"win.block_disable",
label='D_isable',
tooltip='Disable the selected blocks',
icon_name='network-wired-disconnected',
keypresses=["d"],
)
BLOCK_BYPASS = actions.register(
"win.block_bypass",
label='_Bypass',
tooltip='Bypass the selected block',
icon_name='media-seek-forward',
keypresses=["b"],
)
ZOOM_IN = actions.register("win.zoom_in",
label='Zoom In',
tooltip='Increase the canvas zoom level',
keypresses=["<Ctrl>plus",
"<Ctrl>equal", "<Ctrl>KP_Add"],
)
ZOOM_OUT = actions.register("win.zoom_out",
label='Zoom Out',
tooltip='Decrease the canvas zoom level',
keypresses=["<Ctrl>minus", "<Ctrl>KP_Subtract"],
)
ZOOM_RESET = actions.register("win.zoom_reset",
label='Reset Zoom',
tooltip='Reset the canvas zoom level',
keypresses=["<Ctrl>0", "<Ctrl>KP_0"],
)
TOGGLE_SNAP_TO_GRID = actions.register("win.snap_to_grid",
label='_Snap to grid',
tooltip='Snap blocks to a grid for an easier connection alignment',
preference_name='snap_to_grid',
default=True,
)
TOGGLE_HIDE_DISABLED_BLOCKS = actions.register(
"win.hide_disabled",
label='Hide _Disabled Blocks',
tooltip='Toggle visibility of disabled blocks and connections',
icon_name='image-missing',
keypresses=["<Ctrl>d"],
preference_name='hide_disabled',
default=False,
)
TOGGLE_HIDE_VARIABLES = actions.register(
"win.hide_variables",
label='Hide Variables',
tooltip='Hide all variable blocks',
preference_name='hide_variables',
default=False,
)
TOGGLE_SHOW_PARAMETER_EXPRESSION = actions.register(
"win.show_param_expression",
label='Show parameter expressions in block',
tooltip='Display the expression that defines a parameter inside the block',
preference_name='show_param_expression',
default=False,
)
TOGGLE_SHOW_PARAMETER_EVALUATION = actions.register(
"win.show_param_expression_value",
label='Show parameter value in block',
tooltip='Display the evaluated value of a parameter expressions inside the block',
preference_name='show_param_expression_value',
default=True,
)
TOGGLE_SHOW_BLOCK_IDS = actions.register(
"win.show_block_ids",
label='Show All Block IDs',
tooltip='Show all the block IDs',
preference_name='show_block_ids',
default=False,
)
TOGGLE_SHOW_FIELD_COLORS = actions.register(
"win.show_field_colors",
label='Show properties fields colors',
tooltip='Use colors to indicate the type of each field in the block properties',
preference_name='show_field_colors',
default=False,
)
TOGGLE_FLOW_GRAPH_VAR_EDITOR = actions.register(
"win.toggle_variable_editor",
label='Show _Variable Editor',
tooltip='Show the variable editor. Modify variables and imports in this flow graph',
icon_name='accessories-text-editor',
default=True,
keypresses=["<Ctrl>e"],
preference_name='variable_editor_visable',
)
TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR = actions.register(
"win.toggle_variable_editor_sidebar",
label='Move the Variable Editor to the Sidebar',
tooltip='Move the variable editor to the sidebar',
default=False,
preference_name='variable_editor_sidebar',
)
TOGGLE_AUTO_HIDE_PORT_LABELS = actions.register(
"win.auto_hide_port_labels",
label='Auto-Hide _Port Labels',
tooltip='Automatically hide port labels',
preference_name='auto_hide_port_labels',
default=False,
)
TOGGLE_SHOW_BLOCK_COMMENTS = actions.register(
"win.show_block_comments",
label='Show Block Comments',
tooltip="Show comment beneath each block",
preference_name='show_block_comments',
default=True,
)
TOGGLE_SHOW_CODE_PREVIEW_TAB = actions.register(
"win.toggle_code_preview",
label='Generated Code Preview',
tooltip="Show a preview of the code generated for each Block in its "
"Properties Dialog",
preference_name='show_generated_code_tab',
default=False,
)
TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY = actions.register(
"win.show_flowgraph_complexity",
label='Show Flowgraph Complexity',
tooltip="How many Balints is the flowgraph...",
preference_name='show_flowgraph_complexity',
default=False,
)
BLOCK_CREATE_HIER = actions.register(
"win.block_create_hier",
label='C_reate Hier',
tooltip='Create hier block from selected blocks',
icon_name='document-new',
keypresses=["c"],
)
BLOCK_CUT = actions.register(
"win.block_cut",
label='Cu_t',
tooltip='Cut',
icon_name='edit-cut',
keypresses=["<Ctrl>x"],
)
BLOCK_COPY = actions.register(
"win.block_copy",
label='_Copy',
tooltip='Copy',
icon_name='edit-copy',
keypresses=["<Ctrl>c"],
)
BLOCK_PASTE = actions.register(
"win.block_paste",
label='_Paste',
tooltip='Paste',
icon_name='edit-paste',
keypresses=["<Ctrl>v"],
)
ERRORS_WINDOW_DISPLAY = actions.register(
"app.errors",
label='Flowgraph _Errors',
tooltip='View flow graph errors',
icon_name='dialog-error',
)
TOGGLE_CONSOLE_WINDOW = actions.register(
"win.toggle_console_window",
label='Show _Console Panel',
tooltip='Toggle visibility of the console',
keypresses=["<Ctrl>r"],
preference_name='console_window_visible',
default=True)
# TODO: Might be able to convert this to a Gio.PropertyAction eventually.
# actions would need to be defined in the correct class and not globally
TOGGLE_BLOCKS_WINDOW = actions.register(
"win.toggle_blocks_window",
label='Show _Block Tree Panel',
tooltip='Toggle visibility of the block tree widget',
keypresses=["<Ctrl>b"],
preference_name='blocks_window_visible',
default=True)
TOGGLE_SCROLL_LOCK = actions.register(
"win.console.scroll_lock",
label='Console Scroll _Lock',
tooltip='Toggle scroll lock for the console window',
preference_name='scroll_lock',
keypresses=["Scroll_Lock"],
default=False,
)
ABOUT_WINDOW_DISPLAY = actions.register(
"app.about",
label='_About',
tooltip='About this program',
icon_name='help-about',
)
GET_INVOLVED_WINDOW_DISPLAY = actions.register(
"app.get",
label='_Get Involved',
tooltip='Get involved in the community - instructions',
icon_name='help-faq',
)
HELP_WINDOW_DISPLAY = actions.register(
"app.help",
label='_Help',
tooltip='Usage tips',
icon_name='help-contents',
keypresses=["F1"],
)
TYPES_WINDOW_DISPLAY = actions.register(
"app.types",
label='_Types',
tooltip='Types color mapping',
icon_name='dialog-information',
)
KEYBOARD_SHORTCUTS_WINDOW_DISPLAY = actions.register(
"app.keys",
label='_Keys',
tooltip='Keyboard - Shortcuts',
icon_name='dialog-information',
keypresses=["<Ctrl>K"],
)
FLOW_GRAPH_GEN = actions.register(
"app.flowgraph.generate",
label='_Generate',
tooltip='Generate the flow graph',
icon_name='insert-object',
keypresses=["F5"],
)
FLOW_GRAPH_EXEC = actions.register(
"app.flowgraph.execute",
label='_Execute',
tooltip='Execute the flow graph',
icon_name='media-playback-start',
keypresses=["F6"],
)
FLOW_GRAPH_KILL = actions.register(
"app.flowgraph.kill",
label='_Kill',
tooltip='Kill the flow graph',
icon_name='media-playback-stop',
keypresses=["F7"],
)
FLOW_GRAPH_SCREEN_CAPTURE = actions.register(
"app.flowgraph.screen_capture",
label='Screen Ca_pture',
tooltip='Create a screen capture of the flow graph',
icon_name='printer',
keypresses=["<Ctrl>p"],
)
PORT_CONTROLLER_DEC = actions.register(
"win.port_controller_dec",
keypresses=["KP_Subtract", "minus"],
)
PORT_CONTROLLER_INC = actions.register(
"win.port_controller_inc",
keypresses=["KP_Add", "plus"],
)
BLOCK_INC_TYPE = actions.register(
"win.block_inc_type",
keypresses=["Down"],
)
BLOCK_DEC_TYPE = actions.register(
"win.block_dec_type",
keypresses=["Up"],
)
RELOAD_BLOCKS = actions.register("app.reload_blocks",
label='Reload _Blocks',
tooltip='Reload Blocks',
icon_name='view-refresh')
FIND_BLOCKS = actions.register(
"win.find_blocks",
label='_Find Blocks',
tooltip='Search for a block by name (and key)',
icon_name='edit-find',
keypresses=["<Ctrl>f", "slash"],
)
CLEAR_CONSOLE = actions.register(
"win.console.clear",
label='_Clear Console',
tooltip='Clear Console',
icon_name='edit-clear',
keypresses=["<Ctrl>l"],
)
SAVE_CONSOLE = actions.register(
"win.console.save",
label='_Save Console',
tooltip='Save Console',
icon_name='edit-save',
keypresses=["<Ctrl><Shift>p"],
)
OPEN_HIER = actions.register(
"win.open_hier",
label='Open H_ier',
tooltip='Open the source of the selected hierarchical block',
icon_name='go-jump',
)
BUSSIFY_SOURCES = actions.register(
"win.bussify_sources",
label='Toggle So_urce Bus',
tooltip='Gang source ports into a single bus port',
icon_name='go-jump',
)
BUSSIFY_SINKS = actions.register(
"win.bussify_sinks",
label='Toggle S_ink Bus',
tooltip='Gang sink ports into a single bus port',
icon_name='go-jump',
)
XML_PARSER_ERRORS_DISPLAY = actions.register(
"app.xml_errors",
label='_Parser Errors',
tooltip='View errors that occurred while parsing XML files',
icon_name='dialog-error',
)
FLOW_GRAPH_OPEN_QSS_THEME = actions.register(
"app.open_qss_theme",
label='Set Default QT GUI _Theme',
tooltip='Set a default QT Style Sheet file to use for QT GUI',
icon_name='document-open',
)
TOOLS_RUN_FDESIGN = actions.register(
"app.filter_design",
label='Filter Design Tool',
tooltip='Execute gr_filter_design',
icon_name='media-playback-start',
)
POST_HANDLER = actions.register("app.post_handler")
READY = actions.register("app.ready")

901
grc/gui/Application.py Normal file
View File

@ -0,0 +1,901 @@
"""
Copyright 2007-2011 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
import logging
import os
import subprocess
from gi.repository import Gtk, Gio, GLib, GObject
from getpass import getuser
from . import Constants, Dialogs, Actions, Executor, FileDialogs, Utils, Bars
from .MainWindow import MainWindow
# from .ParserErrorsDialog import ParserErrorsDialog
from .PropsDialog import PropsDialog
from ..core import Messages
from ..core.Connection import Connection
from ..core.blocks import Block
log = logging.getLogger(__name__)
class Application(Gtk.Application):
"""
The action handler will setup all the major window components,
and handle button presses and flow graph operations from the GUI.
"""
def __init__(self, file_paths, platform):
Gtk.Application.__init__(self)
"""
Application constructor.
Create the main window, setup the message handler, import the preferences,
and connect all of the action handlers. Finally, enter the gtk main loop and block.
Args:
file_paths: a list of flow graph file passed from command line
platform: platform module
"""
self.clipboard = None
self.dialog = None
# Setup the main window
self.platform = platform
self.config = platform.config
log.debug("Application()")
# Connect all actions to _handle_action
for x in Actions.get_actions():
Actions.connect(x, handler=self._handle_action)
Actions.actions[x].enable()
if x.startswith("app."):
self.add_action(Actions.actions[x])
# Setup the shortcut keys
# These are the globally defined shortcuts
keypress = Actions.actions[x].keypresses
if keypress:
self.set_accels_for_action(x, keypress)
# Initialize
self.init_file_paths = [os.path.abspath(
file_path) for file_path in file_paths]
self.init = False
def do_startup(self):
Gtk.Application.do_startup(self)
log.debug("Application.do_startup()")
def do_activate(self):
Gtk.Application.do_activate(self)
log.debug("Application.do_activate()")
self.main_window = MainWindow(self, self.platform)
self.main_window.connect('delete-event', self._quit)
self.get_focus_flag = self.main_window.get_focus_flag
# setup the messages
Messages.register_messenger(self.main_window.add_console_line)
Messages.send_init(self.platform)
log.debug("Calling Actions.APPLICATION_INITIALIZE")
Actions.APPLICATION_INITIALIZE()
def _quit(self, window, event):
"""
Handle the delete event from the main window.
Generated by pressing X to close, alt+f4, or right click+close.
This method in turns calls the state handler to quit.
Returns:
true
"""
Actions.APPLICATION_QUIT()
return True
def _handle_action(self, action, *args):
log.debug("_handle_action({0}, {1})".format(action, args))
main = self.main_window
page = main.current_page
flow_graph = page.flow_graph if page else None
def flow_graph_update(fg=flow_graph):
main.vars.update_gui(fg.blocks)
fg.update()
##################################################
# Initialize/Quit
##################################################
if action == Actions.APPLICATION_INITIALIZE:
log.debug("APPLICATION_INITIALIZE")
file_path_to_show = self.config.file_open()
for file_path in (self.init_file_paths or self.config.get_open_files()):
if os.path.exists(file_path):
main.new_page(
file_path, show=file_path_to_show == file_path)
if not main.current_page:
main.new_page() # ensure that at least a blank page exists
main.btwin.search_entry.hide()
"""
Only disable certain actions on startup. Each of these actions are
conditionally enabled in _handle_action, so disable them first.
- FLOW_GRAPH_UNDO/REDO are set in gui/StateCache.py
- XML_PARSER_ERRORS_DISPLAY is set in RELOAD_BLOCKS
TODO: These 4 should probably be included, but they are not currently
enabled anywhere else:
- PORT_CONTROLLER_DEC, PORT_CONTROLLER_INC
- BLOCK_INC_TYPE, BLOCK_DEC_TYPE
TODO: These should be handled better. They are set in
update_exec_stop(), but not anywhere else
- FLOW_GRAPH_GEN, FLOW_GRAPH_EXEC, FLOW_GRAPH_KILL
"""
for action in (
Actions.ERRORS_WINDOW_DISPLAY,
Actions.ELEMENT_DELETE,
Actions.BLOCK_PARAM_MODIFY,
Actions.BLOCK_ROTATE_CCW,
Actions.BLOCK_ROTATE_CW,
Actions.BLOCK_VALIGN_TOP,
Actions.BLOCK_VALIGN_MIDDLE,
Actions.BLOCK_VALIGN_BOTTOM,
Actions.BLOCK_HALIGN_LEFT,
Actions.BLOCK_HALIGN_CENTER,
Actions.BLOCK_HALIGN_RIGHT,
Actions.BLOCK_CUT,
Actions.BLOCK_COPY,
Actions.BLOCK_PASTE,
Actions.BLOCK_ENABLE,
Actions.BLOCK_DISABLE,
Actions.BLOCK_BYPASS,
Actions.BLOCK_CREATE_HIER,
Actions.OPEN_HIER,
Actions.BUSSIFY_SOURCES,
Actions.BUSSIFY_SINKS,
Actions.FLOW_GRAPH_SAVE,
Actions.FLOW_GRAPH_UNDO,
Actions.FLOW_GRAPH_REDO,
Actions.XML_PARSER_ERRORS_DISPLAY
):
action.disable()
# Load preferences
for action in (
Actions.TOGGLE_BLOCKS_WINDOW,
Actions.TOGGLE_CONSOLE_WINDOW,
Actions.TOGGLE_HIDE_DISABLED_BLOCKS,
Actions.TOGGLE_SCROLL_LOCK,
Actions.TOGGLE_AUTO_HIDE_PORT_LABELS,
Actions.TOGGLE_SNAP_TO_GRID,
Actions.TOGGLE_SHOW_BLOCK_COMMENTS,
Actions.TOGGLE_SHOW_CODE_PREVIEW_TAB,
Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY,
Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR,
Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR,
Actions.TOGGLE_HIDE_VARIABLES,
Actions.TOGGLE_SHOW_PARAMETER_EXPRESSION,
Actions.TOGGLE_SHOW_PARAMETER_EVALUATION,
Actions.TOGGLE_SHOW_BLOCK_IDS,
Actions.TOGGLE_SHOW_FIELD_COLORS,
):
action.set_enabled(True)
if hasattr(action, 'load_from_preferences'):
action.load_from_preferences()
# Hide the panels *IF* it's saved in preferences
main.update_panel_visibility(
main.BLOCKS, Actions.TOGGLE_BLOCKS_WINDOW.get_active())
main.update_panel_visibility(
main.CONSOLE, Actions.TOGGLE_CONSOLE_WINDOW.get_active())
main.update_panel_visibility(
main.VARIABLES, Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR.get_active())
# Force an update on the current page to match loaded preferences.
# In the future, change the __init__ order to load preferences first
page = main.current_page
if page:
page.flow_graph.update()
self.init = True
elif action == Actions.APPLICATION_QUIT:
if main.close_pages():
while Gtk.main_level():
Gtk.main_quit()
exit(0)
##################################################
# Selections
##################################################
elif action == Actions.ELEMENT_SELECT:
pass # do nothing, update routines below
elif action == Actions.NOTHING_SELECT:
flow_graph.unselect()
elif action == Actions.SELECT_ALL:
if main.btwin.search_entry.has_focus():
main.btwin.search_entry.select_region(0, -1)
else:
flow_graph.select_all()
##################################################
# Enable/Disable
##################################################
elif action in (Actions.BLOCK_ENABLE, Actions.BLOCK_DISABLE, Actions.BLOCK_BYPASS):
changed = flow_graph.change_state_selected(new_state={
Actions.BLOCK_ENABLE: 'enabled',
Actions.BLOCK_DISABLE: 'disabled',
Actions.BLOCK_BYPASS: 'bypassed',
}[action])
if changed:
flow_graph_update()
page.flow_graph.update()
page.state_cache.save_new_state(flow_graph.export_data())
page.saved = False
##################################################
# Cut/Copy/Paste
##################################################
elif action == Actions.BLOCK_CUT:
Actions.BLOCK_COPY()
Actions.ELEMENT_DELETE()
elif action == Actions.BLOCK_COPY:
self.clipboard = flow_graph.copy_to_clipboard()
elif action == Actions.BLOCK_PASTE:
if self.clipboard:
flow_graph.paste_from_clipboard(self.clipboard)
flow_graph_update()
page.state_cache.save_new_state(flow_graph.export_data())
page.saved = False
##################################################
# Create hier block
##################################################
elif action == Actions.BLOCK_CREATE_HIER:
selected_blocks = []
pads = []
params = set()
for block in flow_graph.selected_blocks():
selected_blocks.append(block)
# Check for string variables within the blocks
for param in block.params.values():
for variable in flow_graph.get_variables():
# If a block parameter exists that is a variable, create a parameter for it
if param.get_value() == variable.name:
params.add(param.get_value())
for flow_param in flow_graph.get_parameters():
# If a block parameter exists that is a parameter, create a parameter for it
if param.get_value() == flow_param.name:
params.add(param.get_value())
x_min = min(block.coordinate[0] for block in selected_blocks)
y_min = min(block.coordinate[1] for block in selected_blocks)
for connection in flow_graph.connections:
# Get id of connected blocks
source = connection.source_block
sink = connection.sink_block
if source not in selected_blocks and sink in selected_blocks:
# Create Pad Source
pads.append({
'key': connection.sink_port.key,
'coord': source.coordinate,
# Ignore the options block
'block_index': selected_blocks.index(sink) + 1,
'direction': 'source'
})
elif sink not in selected_blocks and source in selected_blocks:
# Create Pad Sink
pads.append({
'key': connection.source_port.key,
'coord': sink.coordinate,
# Ignore the options block
'block_index': selected_blocks.index(source) + 1,
'direction': 'sink'
})
# Copy the selected blocks and paste them into a new page
# then move the flowgraph to a reasonable position
Actions.BLOCK_COPY()
main.new_page()
flow_graph = main.current_page.flow_graph
Actions.BLOCK_PASTE()
coords = (x_min, y_min)
flow_graph.move_selected(coords)
# Set flow graph to heir block type
top_block = flow_graph.get_block(Constants.DEFAULT_FLOW_GRAPH_ID)
top_block.params['generate_options'].set_value('hb')
# this needs to be a unique name
top_block.params['id'].set_value('new_hier')
# Remove the default samp_rate variable block that is created
remove_me = flow_graph.get_block("samp_rate")
flow_graph.remove_element(remove_me)
# Add the param blocks along the top of the window
x_pos = 150
for param in params:
param_id = flow_graph.add_new_block('parameter', (x_pos, 10))
param_block = flow_graph.get_block(param_id)
param_block.params['id'].set_value(param)
x_pos = x_pos + 100
for pad in pads:
# add the pad sources and sinks within the new hier block
if pad['direction'] == 'sink':
# add new pad_sink block to the canvas
pad_id = flow_graph.add_new_block('pad_sink', pad['coord'])
# setup the references to the sink and source
pad_block = flow_graph.get_block(pad_id)
pad_sink = pad_block.sinks[0]
source_block = flow_graph.get_block(
flow_graph.blocks[pad['block_index']].name)
source = source_block.get_source(pad['key'])
# ensure the port types match
if pad_sink.dtype != source.dtype:
if pad_sink.dtype == 'complex' and source.dtype == 'fc32':
pass
else:
pad_block.params['type'].value = source.dtype
pad_sink.dtype = source.dtype
# connect the pad to the proper sinks
new_connection = flow_graph.connect(source, pad_sink)
elif pad['direction'] == 'source':
pad_id = flow_graph.add_new_block(
'pad_source', pad['coord'])
# setup the references to the sink and source
pad_block = flow_graph.get_block(pad_id)
pad_source = pad_block.sources[0]
sink_block = flow_graph.get_block(
flow_graph.blocks[pad['block_index']].name)
sink = sink_block.get_sink(pad['key'])
# ensure the port types match
if pad_source.dtype != sink.dtype:
if pad_source.dtype == 'complex' and sink.dtype == 'fc32':
pass
else:
pad_block.params['type'].value = sink.dtype
pad_source.dtype = sink.dtype
# connect the pad to the proper sinks
new_connection = flow_graph.connect(pad_source, sink)
flow_graph_update(flow_graph)
##################################################
# Move/Rotate/Delete/Create
##################################################
elif action == Actions.BLOCK_MOVE:
page.state_cache.save_new_state(flow_graph.export_data())
page.saved = False
elif action in Actions.BLOCK_ALIGNMENTS:
if flow_graph.align_selected(action):
page.state_cache.save_new_state(flow_graph.export_data())
page.saved = False
elif action == Actions.BLOCK_ROTATE_CCW:
if flow_graph.rotate_selected(90):
flow_graph_update()
page.state_cache.save_new_state(flow_graph.export_data())
page.saved = False
elif action == Actions.BLOCK_ROTATE_CW:
if flow_graph.rotate_selected(-90):
flow_graph_update()
page.state_cache.save_new_state(flow_graph.export_data())
page.saved = False
elif action == Actions.ELEMENT_DELETE:
if flow_graph.remove_selected():
flow_graph_update()
page.state_cache.save_new_state(flow_graph.export_data())
Actions.NOTHING_SELECT()
page.saved = False
elif action == Actions.ELEMENT_CREATE:
flow_graph_update()
page.state_cache.save_new_state(flow_graph.export_data())
Actions.NOTHING_SELECT()
page.saved = False
elif action == Actions.BLOCK_INC_TYPE:
if flow_graph.type_controller_modify_selected(1):
flow_graph_update()
page.state_cache.save_new_state(flow_graph.export_data())
page.saved = False
elif action == Actions.BLOCK_DEC_TYPE:
if flow_graph.type_controller_modify_selected(-1):
flow_graph_update()
page.state_cache.save_new_state(flow_graph.export_data())
page.saved = False
elif action == Actions.PORT_CONTROLLER_INC:
if flow_graph.port_controller_modify_selected(1):
flow_graph_update()
page.state_cache.save_new_state(flow_graph.export_data())
page.saved = False
elif action == Actions.PORT_CONTROLLER_DEC:
if flow_graph.port_controller_modify_selected(-1):
flow_graph_update()
page.state_cache.save_new_state(flow_graph.export_data())
page.saved = False
##################################################
# Window stuff
##################################################
elif action == Actions.ABOUT_WINDOW_DISPLAY:
Dialogs.show_about(main, self.platform.config)
elif action == Actions.HELP_WINDOW_DISPLAY:
Dialogs.show_help(main)
elif action == Actions.GET_INVOLVED_WINDOW_DISPLAY:
Dialogs.show_get_involved(main)
elif action == Actions.TYPES_WINDOW_DISPLAY:
Dialogs.show_types(main)
elif action == Actions.KEYBOARD_SHORTCUTS_WINDOW_DISPLAY:
Dialogs.show_keyboard_shortcuts(main)
elif action == Actions.ERRORS_WINDOW_DISPLAY:
Dialogs.ErrorsDialog(main, flow_graph).run_and_destroy()
elif action == Actions.TOGGLE_CONSOLE_WINDOW:
action.set_active(not action.get_active())
main.update_panel_visibility(main.CONSOLE, action.get_active())
action.save_to_preferences()
elif action == Actions.TOGGLE_BLOCKS_WINDOW:
# This would be better matched to a Gio.PropertyAction, but to do
# this, actions would have to be defined in the window not globally
action.set_active(not action.get_active())
main.update_panel_visibility(main.BLOCKS, action.get_active())
action.save_to_preferences()
elif action == Actions.TOGGLE_SCROLL_LOCK:
action.set_active(not action.get_active())
active = action.get_active()
main.console.text_display.scroll_lock = active
if active:
main.console.text_display.scroll_to_end()
action.save_to_preferences()
elif action == Actions.CLEAR_CONSOLE:
main.console.text_display.clear()
elif action == Actions.SAVE_CONSOLE:
file_path = FileDialogs.SaveConsole(main, page.file_path).run()
if file_path is not None:
main.console.text_display.save(file_path)
elif action == Actions.TOGGLE_HIDE_DISABLED_BLOCKS:
action.set_active(not action.get_active())
flow_graph_update()
action.save_to_preferences()
page.state_cache.save_new_state(flow_graph.export_data())
Actions.NOTHING_SELECT()
elif action == Actions.TOGGLE_AUTO_HIDE_PORT_LABELS:
action.set_active(not action.get_active())
action.save_to_preferences()
for page in main.get_pages():
page.flow_graph.create_shapes()
elif action in (Actions.TOGGLE_SNAP_TO_GRID,
Actions.TOGGLE_SHOW_BLOCK_COMMENTS,
Actions.TOGGLE_SHOW_CODE_PREVIEW_TAB):
action.set_active(not action.get_active())
action.save_to_preferences()
elif action == Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY:
action.set_active(not action.get_active())
action.save_to_preferences()
for page in main.get_pages():
flow_graph_update(page.flow_graph)
elif action == Actions.TOGGLE_SHOW_PARAMETER_EXPRESSION:
action.set_active(not action.get_active())
action.save_to_preferences()
flow_graph_update()
elif action == Actions.TOGGLE_SHOW_PARAMETER_EVALUATION:
action.set_active(not action.get_active())
action.save_to_preferences()
flow_graph_update()
elif action == Actions.TOGGLE_HIDE_VARIABLES:
action.set_active(not action.get_active())
active = action.get_active()
# Either way, triggering this should simply trigger the variable editor
# to be visible.
varedit = Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR
if active:
log.debug(
"Variables are hidden. Forcing the variable panel to be visible.")
varedit.disable()
else:
varedit.enable()
# Just force it to show.
varedit.set_active(True)
main.update_panel_visibility(main.VARIABLES)
Actions.NOTHING_SELECT()
action.save_to_preferences()
varedit.save_to_preferences()
flow_graph_update()
elif action == Actions.TOGGLE_SHOW_BLOCK_IDS:
action.set_active(not action.get_active())
active = action.get_active()
Actions.NOTHING_SELECT()
action.save_to_preferences()
flow_graph_update()
elif action == Actions.TOGGLE_SHOW_FIELD_COLORS:
action.set_active(not action.get_active())
action.save_to_preferences()
elif action == Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR:
# TODO: There may be issues at startup since these aren't triggered
# the same was as Gtk.Actions when loading preferences.
action.set_active(not action.get_active())
# Just assume this was triggered because it was enabled.
main.update_panel_visibility(main.VARIABLES, action.get_active())
action.save_to_preferences()
# Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR.set_enabled(action.get_active())
action.save_to_preferences()
elif action == Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR:
action.set_active(not action.get_active())
if self.init:
Dialogs.MessageDialogWrapper(
main, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE,
markup="Moving the variable editor requires a restart of GRC."
).run_and_destroy()
action.save_to_preferences()
elif action == Actions.ZOOM_IN:
page.drawing_area.zoom_in()
elif action == Actions.ZOOM_OUT:
page.drawing_area.zoom_out()
elif action == Actions.ZOOM_RESET:
page.drawing_area.reset_zoom()
##################################################
# Param Modifications
##################################################
elif action == Actions.BLOCK_PARAM_MODIFY:
selected_block = args[0] if args[0] else flow_graph.selected_block
selected_conn = args[0] if args[0] else flow_graph.selected_connection
if selected_block and isinstance(selected_block, Block):
self.dialog = PropsDialog(self.main_window, selected_block)
response = Gtk.ResponseType.APPLY
while response == Gtk.ResponseType.APPLY: # rerun the dialog if Apply was hit
response = self.dialog.run()
if response in (Gtk.ResponseType.APPLY, Gtk.ResponseType.ACCEPT):
page.state_cache.save_new_state(
flow_graph.export_data())
# Following line forces a complete update of io ports
flow_graph_update()
page.saved = False
if response in (Gtk.ResponseType.REJECT, Gtk.ResponseType.ACCEPT):
n = page.state_cache.get_current_state()
flow_graph.import_data(n)
flow_graph_update()
if response == Gtk.ResponseType.APPLY:
# null action, that updates the main window
Actions.ELEMENT_SELECT()
self.dialog.destroy()
self.dialog = None
elif selected_conn and isinstance(selected_conn, Connection):
self.dialog = PropsDialog(self.main_window, selected_conn)
response = Gtk.ResponseType.APPLY
while response == Gtk.ResponseType.APPLY: # rerun the dialog if Apply was hit
response = self.dialog.run()
if response in (Gtk.ResponseType.APPLY, Gtk.ResponseType.ACCEPT):
page.state_cache.save_new_state(
flow_graph.export_data())
# Following line forces a complete update of io ports
flow_graph_update()
page.saved = False
if response in (Gtk.ResponseType.REJECT, Gtk.ResponseType.ACCEPT):
curr_state = page.state_cache.get_current_state()
flow_graph.import_data(curr_state)
flow_graph_update()
if response == Gtk.ResponseType.APPLY:
# null action, that updates the main window
Actions.ELEMENT_SELECT()
self.dialog.destroy()
self.dialog = None
elif action == Actions.EXTERNAL_UPDATE:
page.state_cache.save_new_state(flow_graph.export_data())
flow_graph_update()
if self.dialog is not None:
self.dialog.update_gui(force=True)
page.saved = False
elif action == Actions.VARIABLE_EDITOR_UPDATE:
page.state_cache.save_new_state(flow_graph.export_data())
flow_graph_update()
page.saved = False
##################################################
# View Parser Errors
##################################################
elif action == Actions.XML_PARSER_ERRORS_DISPLAY:
pass
##################################################
# Undo/Redo
##################################################
elif action == Actions.FLOW_GRAPH_UNDO:
n = page.state_cache.get_prev_state()
if n:
flow_graph.unselect()
flow_graph.import_data(n)
flow_graph_update()
page.saved = False
elif action == Actions.FLOW_GRAPH_REDO:
n = page.state_cache.get_next_state()
if n:
flow_graph.unselect()
flow_graph.import_data(n)
flow_graph_update()
page.saved = False
##################################################
# New/Open/Save/Close
##################################################
elif action == Actions.FLOW_GRAPH_NEW:
main.new_page()
args = (GLib.Variant('s', 'qt_gui'),)
flow_graph = main.current_page.flow_graph
flow_graph.options_block.params['generate_options'].set_value(args[0].get_string())
flow_graph.options_block.params['author'].set_value(getuser())
flow_graph_update(flow_graph)
elif action == Actions.FLOW_GRAPH_NEW_TYPE:
main.new_page()
if args:
flow_graph = main.current_page.flow_graph
flow_graph.options_block.params['generate_options'].set_value(args[0].get_string())
flow_graph_update(flow_graph)
elif action == Actions.FLOW_GRAPH_OPEN:
file_paths = args[0] if args[0] else FileDialogs.OpenFlowGraph(
main, page.file_path).run()
if file_paths: # Open a new page for each file, show only the first
for i, file_path in enumerate(file_paths):
main.new_page(file_path, show=(i == 0))
self.config.add_recent_file(file_path)
main.tool_bar.refresh_submenus()
main.menu.refresh_submenus()
elif action == Actions.FLOW_GRAPH_OPEN_QSS_THEME:
file_paths = FileDialogs.OpenQSS(main, self.platform.config.install_prefix +
'/share/gnuradio/themes/').run()
if file_paths:
self.platform.config.default_qss_theme = file_paths[0]
elif action == Actions.FLOW_GRAPH_CLOSE:
main.close_page()
elif action == Actions.FLOW_GRAPH_OPEN_RECENT:
file_path = args[0].get_string()
main.new_page(file_path, show=True)
self.config.add_recent_file(file_path)
main.tool_bar.refresh_submenus()
main.menu.refresh_submenus()
elif action == Actions.FLOW_GRAPH_SAVE:
# read-only or undefined file path, do save-as
if page.get_read_only() or not page.file_path:
Actions.FLOW_GRAPH_SAVE_AS()
# otherwise try to save
else:
try:
self.platform.save_flow_graph(page.file_path, flow_graph)
flow_graph.grc_file_path = page.file_path
page.saved = True
except IOError:
Messages.send_fail_save(page.file_path)
page.saved = False
elif action == Actions.FLOW_GRAPH_SAVE_AS:
file_path = FileDialogs.SaveFlowGraph(main, page.file_path).run()
if file_path is not None:
if flow_graph.options_block.params['id'].get_value() == Constants.DEFAULT_FLOW_GRAPH_ID:
file_name = os.path.basename(file_path).replace(".grc", "")
flow_graph.options_block.params['id'].set_value(file_name)
flow_graph_update(flow_graph)
page.file_path = os.path.abspath(file_path)
try:
self.platform.save_flow_graph(page.file_path, flow_graph)
flow_graph.grc_file_path = page.file_path
page.saved = True
except IOError:
Messages.send_fail_save(page.file_path)
page.saved = False
self.config.add_recent_file(file_path)
main.tool_bar.refresh_submenus()
main.menu.refresh_submenus()
elif action == Actions.FLOW_GRAPH_SAVE_COPY:
try:
if not page.file_path:
# Make sure the current flowgraph has been saved
Actions.FLOW_GRAPH_SAVE_AS()
else:
dup_file_path = page.file_path
# Assuming .grc extension at the end of file_path
dup_file_name = '.'.join(
dup_file_path.split('.')[:-1]) + "_copy"
dup_file_path_temp = dup_file_name + Constants.FILE_EXTENSION
count = 1
while os.path.exists(dup_file_path_temp):
dup_file_path_temp = '{}({}){}'.format(
dup_file_name, count, Constants.FILE_EXTENSION)
count += 1
dup_file_path_user = FileDialogs.SaveFlowGraph(
main, dup_file_path_temp).run()
if dup_file_path_user is not None:
self.platform.save_flow_graph(
dup_file_path_user, flow_graph)
Messages.send('Saved Copy to: "' +
dup_file_path_user + '"\n')
except IOError:
Messages.send_fail_save(
"Can not create a copy of the flowgraph\n")
elif action == Actions.FLOW_GRAPH_DUPLICATE:
previous = flow_graph
# Create a new page
main.new_page()
page = main.current_page
new_flow_graph = page.flow_graph
# Import the old data and mark the current as not saved
new_flow_graph.import_data(previous.export_data())
flow_graph_update(new_flow_graph)
page.state_cache.save_new_state(new_flow_graph.export_data())
page.saved = False
elif action == Actions.FLOW_GRAPH_SCREEN_CAPTURE:
file_path, background_transparent = FileDialogs.SaveScreenShot(
main, page.file_path).run()
if file_path is not None:
try:
Utils.make_screenshot(
flow_graph, file_path, background_transparent)
except ValueError:
Messages.send('Failed to generate screen shot\n')
##################################################
# Gen/Exec/Stop
##################################################
elif action == Actions.FLOW_GRAPH_GEN:
self.generator = None
if not page.process:
if not page.saved or not page.file_path:
Actions.FLOW_GRAPH_SAVE() # only save if file path missing or not saved
if page.saved and page.file_path:
generator = page.get_generator()
try:
Messages.send_start_gen(generator.file_path)
generator.write()
self.generator = generator
except Exception as e:
Messages.send_fail_gen(e)
elif action == Actions.FLOW_GRAPH_EXEC:
if not page.process:
Actions.FLOW_GRAPH_GEN()
if self.generator:
xterm = self.platform.config.xterm_executable
if self.config.xterm_missing() != xterm:
if not os.path.exists(xterm):
Dialogs.show_missing_xterm(main, xterm)
self.config.xterm_missing(xterm)
if page.saved and page.file_path:
# Save config before execution
self.config.save()
Executor.ExecFlowGraphThread(
flow_graph_page=page,
xterm_executable=xterm,
callback=self.update_exec_stop
)
elif action == Actions.FLOW_GRAPH_KILL:
if page.process:
try:
page.process.terminate()
except OSError:
print("could not terminate process: %d" % page.process.pid)
elif action == Actions.PAGE_CHANGE: # pass and run the global actions
flow_graph_update()
elif action == Actions.RELOAD_BLOCKS:
self.platform.build_library()
main.btwin.repopulate()
# todo: implement parser error dialog for YAML
# Force a redraw of the graph, by getting the current state and re-importing it
main.update_pages()
elif action == Actions.FIND_BLOCKS:
main.update_panel_visibility(main.BLOCKS, True)
main.btwin.search_entry.show()
main.btwin.search_entry.grab_focus()
elif action == Actions.OPEN_HIER:
for b in flow_graph.selected_blocks():
grc_source = b.extra_data.get('grc_source', '')
if grc_source:
main.new_page(grc_source, show=True)
elif action == Actions.BUSSIFY_SOURCES:
for b in flow_graph.selected_blocks():
b.bussify('source')
flow_graph._old_selected_port = None
flow_graph._new_selected_port = None
Actions.ELEMENT_CREATE()
elif action == Actions.BUSSIFY_SINKS:
for b in flow_graph.selected_blocks():
b.bussify('sink')
flow_graph._old_selected_port = None
flow_graph._new_selected_port = None
Actions.ELEMENT_CREATE()
elif action == Actions.TOOLS_RUN_FDESIGN:
subprocess.Popen('gr_filter_design',
shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
else:
log.warning('!!! Action "%s" not handled !!!' % action)
##################################################
# Global Actions for all States
##################################################
page = main.current_page # page and flow graph might have changed
flow_graph = page.flow_graph if page else None
selected_blocks = list(flow_graph.selected_blocks())
selected_block = selected_blocks[0] if selected_blocks else None
# See if a connection has modifiable parameters or grey out the entry
# in the menu
selected_connections = list(flow_graph.selected_connections())
selected_connection = selected_connections[0] \
if len(selected_connections) == 1 \
else None
selected_conn_has_params = selected_connection and bool(len(selected_connection.params))
# update general buttons
Actions.ERRORS_WINDOW_DISPLAY.set_enabled(not flow_graph.is_valid())
Actions.ELEMENT_DELETE.set_enabled(bool(flow_graph.selected_elements))
Actions.BLOCK_PARAM_MODIFY.set_enabled(bool(selected_block) or bool(selected_conn_has_params))
Actions.BLOCK_ROTATE_CCW.set_enabled(bool(selected_blocks))
Actions.BLOCK_ROTATE_CW.set_enabled(bool(selected_blocks))
# update alignment options
for act in Actions.BLOCK_ALIGNMENTS:
if act:
act.set_enabled(len(selected_blocks) > 1)
# update cut/copy/paste
Actions.BLOCK_CUT.set_enabled(bool(selected_blocks))
Actions.BLOCK_COPY.set_enabled(bool(selected_blocks))
Actions.BLOCK_PASTE.set_enabled(bool(self.clipboard))
# update enable/disable/bypass
can_enable = any(block.state != 'enabled'
for block in selected_blocks)
can_disable = any(block.state != 'disabled'
for block in selected_blocks)
can_bypass_all = (
all(block.can_bypass() for block in selected_blocks) and
any(not block.get_bypassed() for block in selected_blocks)
)
Actions.BLOCK_ENABLE.set_enabled(can_enable)
Actions.BLOCK_DISABLE.set_enabled(can_disable)
Actions.BLOCK_BYPASS.set_enabled(can_bypass_all)
Actions.BLOCK_CREATE_HIER.set_enabled(bool(selected_blocks))
Actions.OPEN_HIER.set_enabled(bool(selected_blocks))
Actions.BUSSIFY_SOURCES.set_enabled(any(block.sources for block in selected_blocks))
Actions.BUSSIFY_SINKS.set_enabled(any(block.sinks for block in selected_blocks))
Actions.RELOAD_BLOCKS.enable()
Actions.FIND_BLOCKS.enable()
self.update_exec_stop()
Actions.FLOW_GRAPH_SAVE.set_enabled(not page.saved)
main.update()
flow_graph.update_selected()
page.drawing_area.queue_draw()
return True # Action was handled
def update_exec_stop(self):
"""
Update the exec and stop buttons.
Lock and unlock the mutex for race conditions with exec flow graph threads.
"""
page = self.main_window.current_page
sensitive = page.flow_graph.is_valid() and not page.process
Actions.FLOW_GRAPH_GEN.set_enabled(sensitive)
Actions.FLOW_GRAPH_EXEC.set_enabled(sensitive)
Actions.FLOW_GRAPH_KILL.set_enabled(page.process is not None)

330
grc/gui/Bars.py Normal file
View File

@ -0,0 +1,330 @@
"""
Copyright 2007, 2008, 2009, 2015, 2016 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
import logging
from gi.repository import Gtk, GObject, Gio, GLib
from . import Actions
log = logging.getLogger(__name__)
'''
# Menu/Toolbar Lists:
#
# Sub items can be 1 of 3 types
# - List Creates a section within the current menu
# - Tuple Creates a submenu using a string or action as the parent. The child
# can be another menu list or an identifier used to call a helper function.
# - Action Appends a new menu item to the current menu
#
LIST_NAME = [
[Action1, Action2], # New section
(Action3, [Action4, Action5]), # Submenu with action as parent
("Label", [Action6, Action7]), # Submenu with string as parent
("Label2", "helper") # Submenu with helper function. Calls 'create_helper()'
]
'''
# The list of actions for the toolbar.
TOOLBAR_LIST = [
[(Actions.FLOW_GRAPH_NEW, 'flow_graph_new_type'),
(Actions.FLOW_GRAPH_OPEN, 'flow_graph_recent'),
Actions.FLOW_GRAPH_SAVE, Actions.FLOW_GRAPH_CLOSE],
[Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR, Actions.FLOW_GRAPH_SCREEN_CAPTURE],
[Actions.BLOCK_CUT, Actions.BLOCK_COPY,
Actions.BLOCK_PASTE, Actions.ELEMENT_DELETE],
[Actions.FLOW_GRAPH_UNDO, Actions.FLOW_GRAPH_REDO],
[Actions.ERRORS_WINDOW_DISPLAY, Actions.FLOW_GRAPH_GEN,
Actions.FLOW_GRAPH_EXEC, Actions.FLOW_GRAPH_KILL],
[Actions.BLOCK_ROTATE_CCW, Actions.BLOCK_ROTATE_CW],
[Actions.BLOCK_ENABLE, Actions.BLOCK_DISABLE,
Actions.BLOCK_BYPASS, Actions.TOGGLE_HIDE_DISABLED_BLOCKS],
[Actions.FIND_BLOCKS, Actions.RELOAD_BLOCKS, Actions.OPEN_HIER]
]
# The list of actions and categories for the menu bar.
MENU_BAR_LIST = [
('_File', [
[(Actions.FLOW_GRAPH_NEW, 'flow_graph_new_type'), Actions.FLOW_GRAPH_DUPLICATE,
Actions.FLOW_GRAPH_OPEN, (Actions.FLOW_GRAPH_OPEN_RECENT, 'flow_graph_recent')],
[Actions.FLOW_GRAPH_SAVE, Actions.FLOW_GRAPH_SAVE_AS,
Actions.FLOW_GRAPH_SAVE_COPY],
[Actions.FLOW_GRAPH_SCREEN_CAPTURE],
[Actions.FLOW_GRAPH_CLOSE, Actions.APPLICATION_QUIT]
]),
('_Edit', [
[Actions.FLOW_GRAPH_UNDO, Actions.FLOW_GRAPH_REDO],
[Actions.BLOCK_CUT, Actions.BLOCK_COPY, Actions.BLOCK_PASTE,
Actions.ELEMENT_DELETE, Actions.SELECT_ALL],
[Actions.BLOCK_ROTATE_CCW, Actions.BLOCK_ROTATE_CW,
('_Align', Actions.BLOCK_ALIGNMENTS)],
[Actions.BLOCK_ENABLE, Actions.BLOCK_DISABLE, Actions.BLOCK_BYPASS],
[Actions.BLOCK_PARAM_MODIFY]
]),
('_View', [
[Actions.TOGGLE_BLOCKS_WINDOW],
[Actions.TOGGLE_CONSOLE_WINDOW, Actions.TOGGLE_SCROLL_LOCK,
Actions.SAVE_CONSOLE, Actions.CLEAR_CONSOLE],
[Actions.TOGGLE_HIDE_VARIABLES, Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR, Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR,
Actions.TOGGLE_SHOW_PARAMETER_EXPRESSION, Actions.TOGGLE_SHOW_PARAMETER_EVALUATION],
[Actions.TOGGLE_HIDE_DISABLED_BLOCKS, Actions.TOGGLE_AUTO_HIDE_PORT_LABELS,
Actions.TOGGLE_SNAP_TO_GRID, Actions.TOGGLE_SHOW_BLOCK_COMMENTS, Actions.TOGGLE_SHOW_BLOCK_IDS,
Actions.TOGGLE_SHOW_FIELD_COLORS, ],
[Actions.TOGGLE_SHOW_CODE_PREVIEW_TAB],
[Actions.ZOOM_IN],
[Actions.ZOOM_OUT],
[Actions.ZOOM_RESET],
[Actions.ERRORS_WINDOW_DISPLAY, Actions.FIND_BLOCKS],
]),
('_Run', [
Actions.FLOW_GRAPH_GEN, Actions.FLOW_GRAPH_EXEC, Actions.FLOW_GRAPH_KILL
]),
('_Tools', [
[Actions.TOOLS_RUN_FDESIGN, Actions.FLOW_GRAPH_OPEN_QSS_THEME],
[Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY]
]),
('_Help', [
[Actions.HELP_WINDOW_DISPLAY, Actions.TYPES_WINDOW_DISPLAY,
Actions.KEYBOARD_SHORTCUTS_WINDOW_DISPLAY, Actions.XML_PARSER_ERRORS_DISPLAY],
[Actions.GET_INVOLVED_WINDOW_DISPLAY, Actions.ABOUT_WINDOW_DISPLAY]
])]
# The list of actions for the context menu.
CONTEXT_MENU_LIST = [
[Actions.BLOCK_CUT, Actions.BLOCK_COPY,
Actions.BLOCK_PASTE, Actions.ELEMENT_DELETE],
[Actions.BLOCK_ROTATE_CCW, Actions.BLOCK_ROTATE_CW,
Actions.BLOCK_ENABLE, Actions.BLOCK_DISABLE, Actions.BLOCK_BYPASS],
[("_More", [
[Actions.BLOCK_CREATE_HIER, Actions.OPEN_HIER],
[Actions.BUSSIFY_SOURCES, Actions.BUSSIFY_SINKS]
])],
[Actions.BLOCK_PARAM_MODIFY],
]
class SubMenuHelper(object):
''' Generates custom submenus for the main menu or toolbar. '''
def __init__(self):
self.submenus = {}
def build_submenu(self, name, parent_obj, obj_idx, obj, set_func):
# Get the correct helper function
create_func = getattr(self, "create_{}".format(name))
# Save the helper functions for rebuilding the menu later
self.submenus[name] = (create_func, parent_obj, obj_idx, obj, set_func)
# Actually build the menu
set_func(obj, create_func())
def create_flow_graph_new_type(self):
""" Different flowgraph types """
menu = Gio.Menu()
platform = Gtk.Application.get_default().platform
generate_modes = platform.get_generate_options()
for key, name, default in generate_modes:
target = "app.flowgraph.new_type::{}".format(key)
menu.append(name, target)
return menu
def create_flow_graph_recent(self):
""" Recent flow graphs """
config = Gtk.Application.get_default().config
recent_files = config.get_recent_files()
menu = Gio.Menu()
if len(recent_files) > 0:
files = Gio.Menu()
for i, file_name in enumerate(recent_files):
target = "app.flowgraph.open_recent::{}".format(file_name)
files.append(file_name.replace("_", "__"), target)
menu.append_section(None, files)
#clear = Gio.Menu()
#clear.append("Clear recent files", "app.flowgraph.clear_recent")
#menu.append_section(None, clear)
else:
# Show an empty menu
menuitem = Gio.MenuItem.new("No items found", "app.none")
menu.append_item(menuitem)
return menu
class MenuHelper(SubMenuHelper):
"""
Recursively builds a menu from a given list of actions.
Args:
- actions: List of actions to build the menu
- menu: Current menu being built
Notes:
- Tuple: Create a new submenu from the parent (1st) and child (2nd) elements
- Action: Append to current menu
- List: Start a new section
"""
def __init__(self):
SubMenuHelper.__init__(self)
def build_menu(self, actions, menu):
for idx, item in enumerate(actions):
log.debug("build_menu idx, action: %s, %s", idx, item)
if isinstance(item, tuple):
# Create a new submenu
parent, child = (item[0], item[1])
# Create the parent
label, target = (parent, None)
if isinstance(parent, Actions.Action):
label = parent.label
target = "{}.{}".format(parent.prefix, parent.name)
menuitem = Gio.MenuItem.new(label, None)
if hasattr(parent, "icon_name"):
menuitem.set_icon(
Gio.Icon.new_for_string(parent.icon_name))
# Create the new submenu
if isinstance(child, list):
submenu = Gio.Menu()
self.build_menu(child, submenu)
menuitem.set_submenu(submenu)
elif isinstance(child, str):
# Child is the name of the submenu to create
def set_func(obj, menu):
obj.set_submenu(menu)
self.build_submenu(child, menu, idx, menuitem, set_func)
menu.append_item(menuitem)
elif isinstance(item, list):
# Create a new section
section = Gio.Menu()
self.build_menu(item, section)
menu.append_section(None, section)
elif isinstance(item, Actions.Action):
# Append a new menuitem
target = "{}.{}".format(item.prefix, item.name)
menuitem = Gio.MenuItem.new(item.label, target)
if item.icon_name:
menuitem.set_icon(Gio.Icon.new_for_string(item.icon_name))
menu.append_item(menuitem)
def refresh_submenus(self):
for name in self.submenus:
create_func, parent_obj, obj_idx, obj, set_func = self.submenus[name]
set_func(obj, create_func())
parent_obj.remove(obj_idx)
parent_obj.insert_item(obj_idx, obj)
class ToolbarHelper(SubMenuHelper):
"""
Builds a toolbar from a given list of actions.
Args:
- actions: List of actions to build the menu
- item: Current menu being built
Notes:
- Tuple: Create a new submenu from the parent (1st) and child (2nd) elements
- Action: Append to current menu
- List: Start a new section
"""
def __init__(self):
SubMenuHelper.__init__(self)
def build_toolbar(self, actions, current):
for idx, item in enumerate(actions):
if isinstance(item, list):
# Toolbar's don't have sections like menus, so call this function
# recursively with the "section" and just append a separator.
self.build_toolbar(item, self)
current.insert(Gtk.SeparatorToolItem.new(), -1)
elif isinstance(item, tuple):
parent, child = (item[0], item[1])
# Create an item with a submenu
# Generate the submenu and add to the item.
# Add the item to the toolbar
button = Gtk.MenuToolButton.new()
# The tuple should be made up of an Action and something.
button.set_label(parent.label)
button.set_tooltip_text(parent.tooltip)
button.set_icon_name(parent.icon_name)
target = "{}.{}".format(parent.prefix, parent.name)
button.set_action_name(target)
def set_func(obj, menu):
obj.set_menu(Gtk.Menu.new_from_model(menu))
self.build_submenu(child, current, idx, button, set_func)
current.insert(button, -1)
elif isinstance(item, Actions.Action):
button = Gtk.ToolButton.new()
button.set_label(item.label)
button.set_tooltip_text(item.tooltip)
button.set_icon_name(item.icon_name)
target = "{}.{}".format(item.prefix, item.name)
button.set_action_name(target)
current.insert(button, -1)
def refresh_submenus(self):
for name in self.submenus:
create_func, parent_obj, _, obj, set_func = self.submenus[name]
set_func(obj, create_func())
class Menu(Gio.Menu, MenuHelper):
""" Main Menu """
def __init__(self):
GObject.GObject.__init__(self)
MenuHelper.__init__(self)
log.debug("Building the main menu")
self.build_menu(MENU_BAR_LIST, self)
class ContextMenu(Gio.Menu, MenuHelper):
""" Context menu for the drawing area """
def __init__(self):
GObject.GObject.__init__(self)
log.debug("Building the context menu")
self.build_menu(CONTEXT_MENU_LIST, self)
class Toolbar(Gtk.Toolbar, ToolbarHelper):
""" The gtk toolbar with actions added from the toolbar list. """
def __init__(self):
"""
Parse the list of action names in the toolbar list.
Look up the action for each name in the action list and add it to the
toolbar.
"""
GObject.GObject.__init__(self)
ToolbarHelper.__init__(self)
self.set_style(Gtk.ToolbarStyle.ICONS)
# self.get_style_context().add_class(Gtk.STYLE_CLASS_PRIMARY_TOOLBAR)
# SubMenuCreator.__init__(self)
self.build_toolbar(TOOLBAR_LIST, self)

300
grc/gui/BlockTreeWindow.py Normal file
View File

@ -0,0 +1,300 @@
"""
Copyright 2007, 2008, 2009, 2016 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
from gi.repository import Gtk, Gdk, GObject
from . import Actions, Utils, Constants
NAME_INDEX, KEY_INDEX, DOC_INDEX = range(3)
def _format_doc(doc):
docs = []
if doc.get(''):
docs += doc.get('').splitlines() + ['']
for block_name, docstring in doc.items():
docs.append('--- {0} ---'.format(block_name))
docs += docstring.splitlines()
docs.append('')
out = ''
for n, line in enumerate(docs[:-1]):
if n:
out += '\n'
out += Utils.encode(line)
if n > 10 or len(out) > 500:
out += '\n...'
break
return out or 'undocumented'
def _format_cat_tooltip(category):
tooltip = '{}: {}'.format('Category' if len(
category) > 1 else 'Module', category[-1])
if category == ('Core',):
tooltip += '\n\nThis subtree is meant for blocks included with GNU Radio (in-tree).'
elif category == (Constants.DEFAULT_BLOCK_MODULE_NAME,):
tooltip += '\n\n' + Constants.DEFAULT_BLOCK_MODULE_TOOLTIP
return tooltip
class BlockTreeWindow(Gtk.VBox):
"""The block selection panel."""
__gsignals__ = {
'create_new_block': (GObject.SignalFlags.RUN_FIRST, None, (str,))
}
def __init__(self, platform):
"""
BlockTreeWindow constructor.
Create a tree view of the possible blocks in the platform.
The tree view nodes will be category names, the leaves will be block names.
A mouse double click or button press action will trigger the add block event.
Args:
platform: the particular platform will all block prototypes
"""
Gtk.VBox.__init__(self)
self.platform = platform
# search entry
self.search_entry = Gtk.Entry()
try:
self.search_entry.set_icon_from_icon_name(
Gtk.EntryIconPosition.PRIMARY, 'edit-find')
self.search_entry.set_icon_activatable(
Gtk.EntryIconPosition.PRIMARY, False)
self.search_entry.set_icon_from_icon_name(
Gtk.EntryIconPosition.SECONDARY, 'window-close')
self.search_entry.connect('icon-release', self._handle_icon_event)
except AttributeError:
pass # no icon for old pygtk
self.search_entry.connect('changed', self._update_search_tree)
self.search_entry.connect(
'key-press-event', self._handle_search_key_press)
self.pack_start(self.search_entry, False, False, 0)
# make the tree model for holding blocks and a temporary one for search results
self.treestore = Gtk.TreeStore(
GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING)
self.treestore_search = Gtk.TreeStore(
GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING)
self.treeview = Gtk.TreeView(model=self.treestore)
self.treeview.set_enable_search(False) # disable pop up search box
self.treeview.set_search_column(-1) # really disable search
self.treeview.set_headers_visible(False)
self.treeview.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
self.treeview.connect('button-press-event',
self._handle_mouse_button_press)
self.treeview.connect('key-press-event', self._handle_search_key_press)
self.treeview.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn('Blocks', renderer, text=NAME_INDEX)
self.treeview.append_column(column)
self.treeview.set_tooltip_column(DOC_INDEX)
# setup sort order
column.set_sort_column_id(0)
self.treestore.set_sort_column_id(0, Gtk.SortType.ASCENDING)
# setup drag and drop
self.treeview.enable_model_drag_source(
Gdk.ModifierType.BUTTON1_MASK, Constants.DND_TARGETS, Gdk.DragAction.COPY)
self.treeview.connect('drag-data-get', self._handle_drag_get_data)
# make the scrolled window to hold the tree view
scrolled_window = Gtk.ScrolledWindow()
scrolled_window.set_policy(
Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scrolled_window.add(self.treeview)
scrolled_window.set_size_request(
Constants.DEFAULT_BLOCKS_WINDOW_WIDTH, -1)
self.pack_start(scrolled_window, True, True, 0)
# map categories to iters, automatic mapping for root
self._categories = {tuple(): None}
self._categories_search = {tuple(): None}
self.platform.block_docstrings_loaded_callback = self.update_docs
self.repopulate()
def clear(self):
self.treestore.clear()
self._categories = {(): None}
def repopulate(self):
self.clear()
for block in self.platform.blocks.values():
if block.category:
self.add_block(block)
self.expand_module_in_tree()
def expand_module_in_tree(self, module_name='Core'):
self.treeview.collapse_all()
core_module_iter = self._categories.get((module_name,))
if core_module_iter:
self.treeview.expand_row(
self.treestore.get_path(core_module_iter), False)
############################################################
# Block Tree Methods
############################################################
def add_block(self, block, treestore=None, categories=None):
"""
Add a block with category to this selection window.
Add only the category when block is None.
Args:
block: the block object or None
"""
treestore = treestore or self.treestore
categories = categories or self._categories
# tuple is hashable, remove empty cats
category = tuple(filter(str, block.category))
# add category and all sub categories
for level, parent_cat_name in enumerate(category, 1):
parent_category = category[:level]
if parent_category not in categories:
iter_ = treestore.insert_before(
categories[parent_category[:-1]], None)
treestore.set_value(iter_, NAME_INDEX, parent_cat_name)
treestore.set_value(iter_, KEY_INDEX, '')
treestore.set_value(
iter_, DOC_INDEX, _format_cat_tooltip(parent_category))
categories[parent_category] = iter_
# add block
iter_ = treestore.insert_before(categories[category], None)
treestore.set_value(iter_, KEY_INDEX, block.key)
treestore.set_value(iter_, NAME_INDEX, block.label)
treestore.set_value(iter_, DOC_INDEX, _format_doc(block.documentation))
def update_docs(self):
"""Update the documentation column of every block"""
def update_doc(model, _, iter_):
key = model.get_value(iter_, KEY_INDEX)
if not key:
return # category node, no doc string
block = self.platform.blocks[key]
model.set_value(iter_, DOC_INDEX, _format_doc(block.documentation))
self.treestore.foreach(update_doc)
self.treestore_search.foreach(update_doc)
############################################################
# Helper Methods
############################################################
def _get_selected_block_key(self):
"""
Get the currently selected block key.
Returns:
the key of the selected block or a empty string
"""
selection = self.treeview.get_selection()
treestore, iter = selection.get_selected()
return iter and treestore.get_value(iter, KEY_INDEX) or ''
def _expand_category(self):
treestore, iter = self.treeview.get_selection().get_selected()
if iter and treestore.iter_has_child(iter):
path = treestore.get_path(iter)
self.treeview.expand_to_path(path)
############################################################
# Event Handlers
############################################################
def _handle_icon_event(self, widget, icon, event):
if icon == Gtk.EntryIconPosition.PRIMARY:
pass
elif icon == Gtk.EntryIconPosition.SECONDARY:
widget.set_text('')
self.search_entry.hide()
def _update_search_tree(self, widget):
key = widget.get_text().lower()
if not key:
self.treeview.set_model(self.treestore)
self.expand_module_in_tree()
else:
matching_blocks = [b for b in list(self.platform.blocks.values())
if key in b.key.lower() or key in b.label.lower()]
self.treestore_search.clear()
self._categories_search = {tuple(): None}
for block in matching_blocks:
self.add_block(block, self.treestore_search,
self._categories_search)
self.treeview.set_model(self.treestore_search)
self.treeview.expand_all()
def _handle_search_key_press(self, widget, event):
"""Handle Return and Escape key events in search entry and treeview"""
if event.keyval == Gdk.KEY_Return:
# add block on enter
if widget == self.search_entry:
# Get the first block in the search tree and add it
selected = self.treestore_search.get_iter_first()
while self.treestore_search.iter_children(selected):
selected = self.treestore_search.iter_children(selected)
if selected is not None:
key = self.treestore_search.get_value(selected, KEY_INDEX)
if key:
self.emit('create_new_block', key)
elif widget == self.treeview:
key = self._get_selected_block_key()
if key:
self.emit('create_new_block', key)
else:
self._expand_category()
else:
return False # propagate event
elif event.keyval == Gdk.KEY_Escape:
# reset the search
self.search_entry.set_text('')
self.search_entry.hide()
elif (event.get_state() & Gdk.ModifierType.CONTROL_MASK and event.keyval == Gdk.KEY_f) \
or event.keyval == Gdk.KEY_slash:
# propagation doesn't work although treeview search is disabled =(
# manually trigger action...
Actions.FIND_BLOCKS.activate()
elif event.get_state() & Gdk.ModifierType.CONTROL_MASK and event.keyval == Gdk.KEY_b:
# ugly...
Actions.TOGGLE_BLOCKS_WINDOW.activate()
else:
return False # propagate event
return True
def _handle_drag_get_data(self, widget, drag_context, selection_data, info, time):
"""
Handle a drag and drop by setting the key to the selection object.
This will call the destination handler for drag and drop.
Only call set when the key is valid to ignore DND from categories.
"""
key = self._get_selected_block_key()
if key:
selection_data.set_text(key, len(key))
def _handle_mouse_button_press(self, widget, event):
"""
Handle the mouse button press.
If a left double click is detected, call add selected block.
"""
if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS:
key = self._get_selected_block_key()
if key:
self.emit('create_new_block', key)

186
grc/gui/Config.py Normal file
View File

@ -0,0 +1,186 @@
"""
Copyright 2016 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
import sys
import os
import configparser
from ..main import get_config_file_path
from ..core.Config import Config as CoreConfig
from . import Constants
HEADER = """\
# This contains only GUI settings for GRC and is not meant for users to edit.
#
# GRC settings not accessible through the GUI are in config.conf under
# section [grc].
"""
class Config(CoreConfig):
name = 'GNU Radio Companion'
gui_prefs_file = os.environ.get('GRC_PREFS_PATH', get_config_file_path())
def __init__(self, install_prefix, *args, **kwargs):
CoreConfig.__init__(self, *args, **kwargs)
self.install_prefix = install_prefix
Constants.update_font_size(self.font_size)
self.parser = configparser.ConfigParser()
for section in ['main', 'files_open', 'files_recent']:
try:
self.parser.add_section(section)
except Exception as e:
print(e)
try:
self.parser.read(self.gui_prefs_file)
except Exception as err:
print(err, file=sys.stderr)
def save(self):
try:
with open(self.gui_prefs_file, 'w') as fp:
fp.write(HEADER)
self.parser.write(fp)
except Exception as err:
print(err, file=sys.stderr)
def entry(self, key, value=None, default=None):
if value is not None:
self.parser.set('main', key, str(value))
result = value
else:
_type = type(default) if default is not None else str
getter = {
bool: self.parser.getboolean,
int: self.parser.getint,
}.get(_type, self.parser.get)
try:
result = getter('main', key)
except (AttributeError, configparser.Error):
result = _type() if default is None else default
return result
@property
def editor(self):
return self._gr_prefs.get_string('grc', 'editor', '')
@editor.setter
def editor(self, value):
self._gr_prefs.set_string('grc', 'editor', value)
self._gr_prefs.save()
@property
def xterm_executable(self):
return self._gr_prefs.get_string('grc', 'xterm_executable', 'xterm')
@property
def wiki_block_docs_url_prefix(self):
return self._gr_prefs.get_string('grc-docs', 'wiki_block_docs_url_prefix', '')
@property
def font_size(self):
try: # ugly, but matches current code style
font_size = self._gr_prefs.get_long('grc', 'canvas_font_size',
Constants.DEFAULT_FONT_SIZE)
if font_size <= 0:
raise ValueError
except (ValueError, TypeError):
font_size = Constants.DEFAULT_FONT_SIZE
print("Error: invalid 'canvas_font_size' setting.", file=sys.stderr)
return font_size
@property
def default_qss_theme(self):
return self._gr_prefs.get_string('qtgui', 'qss', '')
@default_qss_theme.setter
def default_qss_theme(self, value):
self._gr_prefs.set_string("qtgui", "qss", value)
self._gr_prefs.save()
##### Originally from Preferences.py #####
def main_window_size(self, size=None):
if size is None:
size = [None, None]
w = self.entry('main_window_width', size[0], default=800)
h = self.entry('main_window_height', size[1], default=600)
return w, h
def file_open(self, filename=None):
return self.entry('file_open', filename, default='')
def set_file_list(self, key, files):
self.parser.remove_section(key) # clear section
self.parser.add_section(key)
for i, filename in enumerate(files):
self.parser.set(key, '%s_%d' % (key, i), filename)
def get_file_list(self, key):
try:
files = [value for name, value in self.parser.items(key)
if name.startswith('%s_' % key)]
except (AttributeError, configparser.Error):
files = []
return files
def get_open_files(self):
return self.get_file_list('files_open')
def set_open_files(self, files):
return self.set_file_list('files_open', files)
def get_recent_files(self):
""" Gets recent files, removes any that do not exist and re-saves it """
files = list(
filter(os.path.exists, self.get_file_list('files_recent')))
self.set_recent_files(files)
return files
def set_recent_files(self, files):
return self.set_file_list('files_recent', files)
def add_recent_file(self, file_name):
# double check file_name
if os.path.exists(file_name):
recent_files = self.get_recent_files()
if file_name in recent_files:
recent_files.remove(file_name) # Attempt removal
recent_files.insert(0, file_name) # Insert at start
self.set_recent_files(recent_files[:10]) # Keep up to 10 files
def console_window_position(self, pos=None):
return self.entry('console_window_position', pos, default=-1) or 1
def blocks_window_position(self, pos=None):
return self.entry('blocks_window_position', pos, default=-1) or 1
def variable_editor_position(self, pos=None, sidebar=False):
# Figure out default
if sidebar:
_, h = self.main_window_size()
return self.entry('variable_editor_sidebar_position', pos, default=int(h * 0.7))
else:
return self.entry('variable_editor_position', pos, default=int(self.blocks_window_position() * 0.5))
def variable_editor_sidebar(self, pos=None):
return self.entry('variable_editor_sidebar', pos, default=False)
def variable_editor_confirm_delete(self, pos=None):
return self.entry('variable_editor_confirm_delete', pos, default=True)
def xterm_missing(self, cmd=None):
return self.entry('xterm_missing', cmd, default='INVALID_XTERM_SETTING')
def screen_shot_background_transparent(self, transparent=None):
return self.entry('screen_shot_background_transparent', transparent, default=False)

43
grc/gui/Console.py Normal file
View File

@ -0,0 +1,43 @@
"""
Copyright 2008, 2009, 2011 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
from ..core import Messages
from .Dialogs import TextDisplay, MessageDialogWrapper
from .Constants import DEFAULT_CONSOLE_WINDOW_WIDTH
from gi.repository import Gtk, Gdk, GObject
import os
import logging
import gi
gi.require_version('Gtk', '3.0')
log = logging.getLogger(__name__)
class Console(Gtk.ScrolledWindow):
def __init__(self):
Gtk.ScrolledWindow.__init__(self)
log.debug("console()")
self.app = Gtk.Application.get_default()
self.text_display = TextDisplay()
self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
self.add(self.text_display)
self.set_size_request(-1, DEFAULT_CONSOLE_WINDOW_WIDTH)
def add_line(self, line):
"""
Place line at the end of the text buffer, then scroll its window all the way down.
Args:
line: the new text
"""
self.text_display.insert(line)

123
grc/gui/Constants.py Normal file
View File

@ -0,0 +1,123 @@
"""
Copyright 2008, 2009 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
from gi.repository import Gtk, Gdk
from ..core.Constants import *
# default path for the open/save dialogs
DEFAULT_FILE_PATH = os.getcwd() if os.name != 'nt' else os.path.expanduser("~/Documents")
FILE_EXTENSION = '.grc'
# name for new/unsaved flow graphs
NEW_FLOGRAPH_TITLE = 'untitled'
# main window constraints
MIN_WINDOW_WIDTH = 600
MIN_WINDOW_HEIGHT = 400
# dialog constraints
MIN_DIALOG_WIDTH = 600
MIN_DIALOG_HEIGHT = 500
# default sizes
DEFAULT_BLOCKS_WINDOW_WIDTH = 100
DEFAULT_CONSOLE_WINDOW_WIDTH = 100
FONT_SIZE = DEFAULT_FONT_SIZE = 8
FONT_FAMILY = "Sans"
BLOCK_FONT = PORT_FONT = "Sans 8"
PARAM_FONT = "Sans 7.5"
# size of the state saving cache in the flow graph (undo/redo functionality)
STATE_CACHE_SIZE = 42
# Shared targets for drag and drop of blocks
DND_TARGETS = [Gtk.TargetEntry.new('STRING', Gtk.TargetFlags.SAME_APP, 0),
Gtk.TargetEntry.new('UTF8_STRING', Gtk.TargetFlags.SAME_APP, 1)]
# label constraint dimensions
LABEL_SEPARATION = 3
BLOCK_LABEL_PADDING = 7
PORT_LABEL_PADDING = 2
# canvas grid size
CANVAS_GRID_SIZE = 8
# port constraint dimensions
PORT_BORDER_SEPARATION = 8
PORT_SPACING = 2 * PORT_BORDER_SEPARATION
PORT_SEPARATION = 32
PORT_MIN_WIDTH = 20
PORT_LABEL_HIDDEN_WIDTH = 10
PORT_EXTRA_BUS_HEIGHT = 40
# minimal length of connector
CONNECTOR_EXTENSION_MINIMAL = 11
# increment length for connector
CONNECTOR_EXTENSION_INCREMENT = 11
# connection arrow dimensions
CONNECTOR_ARROW_BASE = 10
CONNECTOR_ARROW_HEIGHT = 13
# possible rotations in degrees
POSSIBLE_ROTATIONS = (0, 90, 180, 270)
# How close the mouse can get to the edge of the visible window before scrolling is invoked.
SCROLL_PROXIMITY_SENSITIVITY = 50
# When the window has to be scrolled, move it this distance in the required direction.
SCROLL_DISTANCE = 15
# How close the mouse click can be to a line and register a connection select.
LINE_SELECT_SENSITIVITY = 5
DEFAULT_BLOCK_MODULE_TOOLTIP = """\
This subtree holds all blocks (from OOT modules) that specify no module name. \
The module name is the root category enclosed in square brackets.
Please consider contacting OOT module maintainer for any block in here \
and kindly ask to update their GRC Block Descriptions or Block Tree to include a module name."""
# _SCREEN = Gdk.Screen.get_default()
# _SCREEN_RESOLUTION = _SCREEN.get_resolution() if _SCREEN else -1
# DPI_SCALING = _SCREEN_RESOLUTION / 96.0 if _SCREEN_RESOLUTION > 0 else 1.0
# todo: figure out the GTK3 way (maybe cairo does this for us
DPI_SCALING = 1.0
# Gtk-themes classified as dark
GTK_DARK_THEMES = [
'Adwaita-dark',
'HighContrastInverse',
]
GTK_SETTINGS_INI_PATH = '~/.config/gtk-3.0/settings.ini'
GTK_INI_PREFER_DARK_KEY = 'gtk-application-prefer-dark-theme'
GTK_INI_THEME_NAME_KEY = 'gtk-theme-name'
def update_font_size(font_size):
global PORT_SEPARATION, BLOCK_FONT, PORT_FONT, PARAM_FONT, FONT_SIZE
FONT_SIZE = font_size
BLOCK_FONT = "%s %f" % (FONT_FAMILY, font_size)
PORT_FONT = BLOCK_FONT
PARAM_FONT = "%s %f" % (FONT_FAMILY, font_size - 0.5)
PORT_SEPARATION = PORT_SPACING + 2 * \
PORT_LABEL_PADDING + int(1.5 * font_size)
PORT_SEPARATION += - \
PORT_SEPARATION % (2 * CANVAS_GRID_SIZE) # even multiple
update_font_size(DEFAULT_FONT_SIZE)

432
grc/gui/Dialogs.py Normal file
View File

@ -0,0 +1,432 @@
# Copyright 2008, 2009, 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
import sys
import textwrap
from shutil import which as find_executable
from gi.repository import Gtk, GLib, Gdk, Gio
from . import Utils, Actions, Constants
from ..core import Messages
class SimpleTextDisplay(Gtk.TextView):
"""
A non user-editable gtk text view.
"""
def __init__(self, text=''):
"""
TextDisplay constructor.
Args:
text: the text to display (string)
"""
Gtk.TextView.__init__(self)
self.set_text = self.get_buffer().set_text
self.set_text(text)
self.set_editable(False)
self.set_cursor_visible(False)
self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
class TextDisplay(SimpleTextDisplay):
"""
A non user-editable scrollable text view with popup menu.
"""
def __init__(self, text=''):
"""
TextDisplay constructor.
Args:
text: the text to display (string)
"""
SimpleTextDisplay.__init__(self, text)
self.scroll_lock = True
self.connect("populate-popup", self.populate_popup)
def insert(self, line):
"""
Append text after handling backspaces and auto-scroll.
Args:
line: the text to append (string)
"""
line = self._consume_backspaces(line)
self.get_buffer().insert(self.get_buffer().get_end_iter(), line)
self.scroll_to_end()
def _consume_backspaces(self, line):
"""
Removes text from the buffer if line starts with '\b'
Args:
line: a string which may contain backspaces
Returns:
The string that remains from 'line' with leading '\b's removed.
"""
if not line:
return
# for each \b delete one char from the buffer
back_count = 0
start_iter = self.get_buffer().get_end_iter()
while len(line) > back_count and line[back_count] == '\b':
# stop at the beginning of a line
if not start_iter.starts_line():
start_iter.backward_char()
back_count += 1
# remove chars from buffer
self.get_buffer().delete(start_iter, self.get_buffer().get_end_iter())
return line[back_count:]
def scroll_to_end(self):
""" Update view's scroll position. """
if self.scroll_lock:
buf = self.get_buffer()
mark = buf.get_insert()
buf.move_mark(mark, buf.get_end_iter())
self.scroll_mark_onscreen(mark)
def clear(self):
""" Clear all text from buffer. """
buf = self.get_buffer()
buf.delete(buf.get_start_iter(), buf.get_end_iter())
def save(self, file_path):
"""
Save context of buffer to the given file.
Args:
file_path: location to save buffer contents
"""
with open(file_path, 'w') as logfile:
buf = self.get_buffer()
logfile.write(buf.get_text(buf.get_start_iter(),
buf.get_end_iter(), True))
# Action functions are set by the Application's init function
def clear_cb(self, menu_item, web_view):
""" Callback function to clear the text buffer """
Actions.CLEAR_CONSOLE()
def scroll_back_cb(self, menu_item, web_view):
""" Callback function to toggle scroll lock """
Actions.TOGGLE_SCROLL_LOCK()
def save_cb(self, menu_item, web_view):
""" Callback function to save the buffer """
Actions.SAVE_CONSOLE()
def populate_popup(self, view, menu):
"""Create a popup menu for the scroll lock and clear functions"""
menu.append(Gtk.SeparatorMenuItem())
lock = Gtk.CheckMenuItem(label="Scroll Lock")
menu.append(lock)
lock.set_active(self.scroll_lock)
lock.connect('activate', self.scroll_back_cb, view)
save = Gtk.ImageMenuItem(label="Save Console")
menu.append(save)
save.connect('activate', self.save_cb, view)
clear = Gtk.ImageMenuItem(label="Clear Console")
menu.append(clear)
clear.connect('activate', self.clear_cb, view)
menu.show_all()
return False
class MessageDialogWrapper(Gtk.MessageDialog):
""" Run a message dialog. """
def __init__(self, parent, message_type, buttons, title=None, markup=None,
default_response=None, extra_buttons=None):
"""
Create a modal message dialog.
Args:
message_type: the type of message may be one of:
Gtk.MessageType.INFO
Gtk.MessageType.WARNING
Gtk.MessageType.QUESTION or Gtk.MessageType.ERROR
buttons: the predefined set of buttons to use:
Gtk.ButtonsType.NONE
Gtk.ButtonsType.OK
Gtk.ButtonsType.CLOSE
Gtk.ButtonsType.CANCEL
Gtk.ButtonsType.YES_NO
Gtk.ButtonsType.OK_CANCEL
title: the title of the window (string)
markup: the message text with pango markup
default_response: if set, determines which button is highlighted by default
extra_buttons: a tuple containing pairs of values:
each value is the button's text and the button's return value
"""
Gtk.MessageDialog.__init__(
self, transient_for=parent, modal=True, destroy_with_parent=True,
message_type=message_type, buttons=buttons
)
self.set_keep_above(True)
if title:
self.set_title(title)
if markup:
self.set_markup(markup)
if extra_buttons:
self.add_buttons(*extra_buttons)
if default_response:
self.set_default_response(default_response)
def run_and_destroy(self):
response = self.run()
self.hide()
return response
class ErrorsDialog(Gtk.Dialog):
""" Display flowgraph errors. """
def __init__(self, parent, flowgraph):
"""Create a listview of errors"""
Gtk.Dialog.__init__(
self,
title='Errors and Warnings',
transient_for=parent,
modal=True,
destroy_with_parent=True,
)
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
self.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)
self.set_size_request(750, Constants.MIN_DIALOG_HEIGHT)
self.set_border_width(10)
self.store = Gtk.ListStore(str, str, str)
self.update(flowgraph)
self.treeview = Gtk.TreeView(model=self.store)
self.treeview.connect("button_press_event", self.mouse_click)
for i, column_title in enumerate(["Block", "Aspect", "Message"]):
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
column.set_sort_column_id(i) # liststore id matches treeview id
column.set_resizable(True)
self.treeview.append_column(column)
self.scrollable = Gtk.ScrolledWindow()
self.scrollable.set_vexpand(True)
self.scrollable.add(self.treeview)
self.vbox.pack_start(self.scrollable, True, True, 0)
self.show_all()
def update(self, flowgraph):
self.store.clear()
for element, message in flowgraph.iter_error_messages():
if element.is_block:
src, aspect = element.name, ''
elif element.is_connection:
src = element.source_block.name
aspect = "Connection to '{}'".format(element.sink_block.name)
elif element.is_port:
src = element.parent_block.name
aspect = "{} '{}'".format(
'Sink' if element.is_sink else 'Source', element.name)
elif element.is_param:
src = element.parent_block.name
aspect = "Param '{}'".format(element.name)
else:
src = aspect = ''
self.store.append([src, aspect, message])
def run_and_destroy(self):
response = self.run()
self.hide()
return response
def mouse_click(self, _, event):
""" Handle mouse click, so user can copy the error message """
if event.button == 3:
path_info = self.treeview.get_path_at_pos(event.x, event.y)
if path_info is not None:
path, col, _, _ = path_info
self.treeview.grab_focus()
self.treeview.set_cursor(path, col, 0)
selection = self.treeview.get_selection()
(model, iterator) = selection.get_selected()
self.clipboard.set_text(model[iterator][2], -1)
print(model[iterator][2])
def show_about(parent, config):
ad = Gtk.AboutDialog(transient_for=parent)
ad.set_program_name(config.name)
ad.set_name('')
ad.set_license(config.license)
py_version = sys.version.split()[0]
ad.set_version("{} (Python {})".format(config.version, py_version))
try:
ad.set_logo(Gtk.IconTheme().load_icon('gnuradio-grc', 64, 0))
except GLib.Error:
Messages.send("Failed to set window logo\n")
# ad.set_comments("")
ad.set_copyright(config.license.splitlines()[0])
ad.set_website(config.website)
ad.connect("response", lambda action, param: action.hide())
ad.show()
def show_help(parent):
""" Display basic usage tips. """
markup = textwrap.dedent("""\
<b>Usage Tips</b>
\n\
<u>Add block</u>: drag and drop or double click a block in the block
selection window.
<u>Rotate block</u>: Select a block, press left/right on the keyboard.
<u>Change type</u>: Select a block, press up/down on the keyboard.
<u>Edit parameters</u>: double click on a block in the flow graph.
<u>Make connection</u>: click on the source port of one block, then
click on the sink port of another block.
<u>Remove connection</u>: select the connection and press delete, or
drag the connection.
\n\
*Press Ctrl+K or see menu for Keyboard - Shortcuts
\
""")
markup = markup.replace("Ctrl", Utils.get_modifier_key())
MessageDialogWrapper(
parent, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, title='Help', markup=markup
).run_and_destroy()
def show_keyboard_shortcuts(parent):
""" Display keyboard shortcut-keys. """
markup = textwrap.dedent("""\
<b>Keyboard Shortcuts</b>
\n\
<u>Ctrl+N</u>: Create a new flowgraph.
<u>Ctrl+O</u>: Open an existing flowgraph.
<u>Ctrl+S</u>: Save the current flowgraph or save as for new.
<u>Ctrl+W</u>: Close the current flowgraph.
<u>Ctrl+Z</u>: Undo a change to the flowgraph.
<u>Ctrl+Y</u>: Redo a change to the flowgraph.
<u>Ctrl+A</u>: Selects all blocks and connections.
<u>Ctrl+P</u>: Screen Capture of the Flowgraph.
<u>Ctrl+Shift+P</u>: Save the console output to file.
<u>Ctrl+L</u>: Clear the console.
<u>Ctrl+E</u>: Show variable editor.
<u>Ctrl+F</u>: Search for a block by name.
<u>Ctrl+Q</u>: Quit.
<u>F1</u> : Help menu.
<u>F5</u> : Generate the Flowgraph.
<u>F6</u> : Execute the Flowgraph.
<u>F7</u> : Kill the Flowgraph.
<u>Ctrl+Shift+S</u>: Save as the current flowgraph.
<u>Ctrl+Shift+D</u>: Create a duplicate of current flow graph.
<u>Ctrl+X/C/V</u>: Edit-cut/copy/paste.
<u>Ctrl+D/B/R</u>: Toggle visibility of disabled blocks or
connections/block tree widget/console.
<u>Shift+T/M/B/L/C/R</u>: Vertical Align Top/Middle/Bottom and
Horizontal Align Left/Center/Right respectively of the
selected block.
<u>Ctrl+0</u>: Reset the zoom level
<u>Ctrl++/-</u>: Zoom in and out
\
""")
markup = markup.replace("Ctrl", Utils.get_modifier_key())
MessageDialogWrapper(
parent, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, title='Keyboard - Shortcuts', markup=markup
).run_and_destroy()
def show_get_involved(parent):
"""Get Involved Instructions"""
markup = textwrap.dedent("""\
<b>Welcome to GNU Radio Community!</b>
\n\
For more details on contributing to GNU Radio and getting engaged with our great community visit <a href="https://wiki.gnuradio.org/index.php/HowToGetInvolved">here</a>.
\n\
You can also join our <a href="https://chat.gnuradio.org/">Matrix chat server</a>, IRC Channel (#gnuradio) or contact through our <a href="https://lists.gnu.org/mailman/listinfo/discuss-gnuradio">mailing list (discuss-gnuradio)</a>.
\
""")
MessageDialogWrapper(
parent, Gtk.MessageType.QUESTION, Gtk.ButtonsType.CLOSE, title='Get - Involved', markup=markup
).run_and_destroy()
def show_types(parent):
""" Display information about standard data types. """
colors = [(name, color)
for name, key, sizeof, color in Constants.CORE_TYPES]
max_len = 10 + max(len(name) for name, code in colors)
message = '\n'.join(
'<span background="{color}"><tt>{name}</tt></span>'
''.format(color=color, name=Utils.encode(name).center(max_len))
for name, color in colors
)
MessageDialogWrapper(
parent, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, title='Types - Color Mapping', markup=message
).run_and_destroy()
def show_missing_xterm(parent, xterm):
markup = textwrap.dedent("""\
The xterm executable {0!r} is missing.
You can change this setting in your gnuradio.conf, in section [grc], 'xterm_executable'.
\n\
(This message is shown only once)\
""").format(xterm)
MessageDialogWrapper(
parent, message_type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.OK,
title='Warning: missing xterm executable', markup=markup
).run_and_destroy()
def choose_editor(parent, config):
"""
Give the option to either choose an editor or use the default.
"""
content_type = Gio.content_type_from_mime_type("text/x-python")
if content_type == "*":
# fallback to plain text on Windows if no useful x-python association
content_type = Gio.content_type_from_mime_type("text/plain")
dialog = Gtk.AppChooserDialog.new_for_content_type(
parent,
Gtk.DialogFlags.MODAL,
content_type,
)
dialog.set_heading("Choose an editor below")
widget = dialog.get_widget()
widget.set_default_text("Choose an editor")
widget.set_show_default(True)
widget.set_show_recommended(True)
widget.set_show_fallback(True)
editor = None
response = dialog.run()
if response == Gtk.ResponseType.OK:
appinfo = dialog.get_app_info()
editor = config.editor = appinfo.get_executable()
dialog.destroy()
return editor

Some files were not shown because too many files have changed in this diff Show More