main - feat: copy GRC from latest stable GNURadio
This commit is contained in:
commit
1c3acd7922
4
grc/00-grc-docs.conf.in
Normal file
4
grc/00-grc-docs.conf.in
Normal 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
194
grc/CMakeLists.txt
Normal 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
0
grc/__init__.py
Normal file
9
grc/__main__.py
Normal file
9
grc/__main__.py
Normal 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
1
grc/blocks/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
variable_struct.xml
|
||||||
48
grc/blocks/CMakeLists.txt
Normal file
48
grc/blocks/CMakeLists.txt
Normal 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
24
grc/blocks/grc.tree.yml
Normal 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
|
||||||
21
grc/blocks/import.block.yml
Normal file
21
grc/blocks/import.block.yml
Normal 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
|
||||||
58
grc/blocks/json_config.block.yml
Normal file
58
grc/blocks/json_config.block.yml
Normal 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
|
||||||
12
grc/blocks/message.domain.yml
Normal file
12
grc/blocks/message.domain.yml
Normal 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
10
grc/blocks/note.block.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
id: note
|
||||||
|
label: Note
|
||||||
|
flags: [ python, cpp ]
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
- id: note
|
||||||
|
label: Note
|
||||||
|
dtype: string
|
||||||
|
|
||||||
|
file_format: 1
|
||||||
179
grc/blocks/options.block.yml
Normal file
179
grc/blocks/options.block.yml
Normal 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
|
||||||
56
grc/blocks/pad_sink.block.yml
Normal file
56
grc/blocks/pad_sink.block.yml
Normal 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
|
||||||
56
grc/blocks/pad_source.block.yml
Normal file
56
grc/blocks/pad_source.block.yml
Normal 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
|
||||||
60
grc/blocks/parameter.block.yml
Normal file
60
grc/blocks/parameter.block.yml
Normal 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
|
||||||
46
grc/blocks/snippet.block.yml
Normal file
46
grc/blocks/snippet.block.yml
Normal 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
|
||||||
11
grc/blocks/stream.domain.yml
Normal file
11
grc/blocks/stream.domain.yml
Normal 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) })
|
||||||
25
grc/blocks/variable.block.yml
Normal file
25
grc/blocks/variable.block.yml
Normal 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
|
||||||
59
grc/blocks/variable_config.block.yml
Normal file
59
grc/blocks/variable_config.block.yml
Normal 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
|
||||||
66
grc/blocks/variable_function_probe.block.yml
Normal file
66
grc/blocks/variable_function_probe.block.yml
Normal 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
|
||||||
100
grc/blocks/variable_struct.block.yml.py
Normal file
100
grc/blocks/variable_struct.block.yml.py
Normal 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())
|
||||||
58
grc/blocks/yaml_config.block.yml
Normal file
58
grc/blocks/yaml_config.block.yml
Normal 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
65
grc/compiler.py
Executable 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)
|
||||||
8
grc/converter/__init__.py
Normal file
8
grc/converter/__init__.py
Normal 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
|
||||||
8
grc/converter/__main__.py
Normal file
8
grc/converter/__main__.py
Normal 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
58
grc/converter/block.dtd
Normal 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
219
grc/converter/block.py
Normal 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)
|
||||||
15
grc/converter/block_tree.dtd
Normal file
15
grc/converter/block_tree.dtd
Normal 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)>
|
||||||
44
grc/converter/block_tree.py
Normal file
44
grc/converter/block_tree.py
Normal 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}
|
||||||
269
grc/converter/cheetah_converter.py
Normal file
269
grc/converter/cheetah_converter.py
Normal 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
|
||||||
27
grc/converter/flow_graph.dtd
Normal file
27
grc/converter/flow_graph.dtd
Normal 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
121
grc/converter/flow_graph.py
Normal 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
160
grc/converter/main.py
Normal 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
73
grc/converter/xml.py
Normal 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
79
grc/core/Config.py
Normal 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
182
grc/core/Connection.py
Normal 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
147
grc/core/Constants.py
Normal 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
578
grc/core/FlowGraph.py
Normal 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
147
grc/core/Messages.py
Normal 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
0
grc/core/__init__.py
Normal file
153
grc/core/base.py
Normal file
153
grc/core/base.py
Normal 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
|
||||||
27
grc/core/blocks/__init__.py
Normal file
27
grc/core/blocks/__init__.py
Normal 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
170
grc/core/blocks/_build.py
Normal 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
36
grc/core/blocks/_flags.py
Normal 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)
|
||||||
79
grc/core/blocks/_templates.py
Normal file
79
grc/core/blocks/_templates.py
Normal 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
789
grc/core/blocks/block.py
Normal 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
47
grc/core/blocks/dummy.py
Normal 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
|
||||||
248
grc/core/blocks/embedded_python.py
Normal file
248
grc/core/blocks/embedded_python.py
Normal 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)
|
||||||
62
grc/core/blocks/virtual.py
Normal file
62
grc/core/blocks/virtual.py
Normal 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
106
grc/core/cache.py
Normal 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()
|
||||||
32
grc/core/default_flow_graph.grc
Normal file
32
grc/core/default_flow_graph.grc
Normal 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
17
grc/core/errors.py
Normal 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
27
grc/core/flow_graph.dtd
Normal 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)>
|
||||||
191
grc/core/generator/FlowGraphProxy.py
Normal file
191
grc/core/generator/FlowGraphProxy.py
Normal 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
|
||||||
51
grc/core/generator/Generator.py
Normal file
51
grc/core/generator/Generator.py
Normal 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)
|
||||||
7
grc/core/generator/__init__.py
Normal file
7
grc/core/generator/__init__.py
Normal 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
|
||||||
217
grc/core/generator/cpp_hier_block.py
Normal file
217
grc/core/generator/cpp_hier_block.py
Normal 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
|
||||||
72
grc/core/generator/cpp_templates/CMakeLists.txt.mako
Normal file
72
grc/core/generator/cpp_templates/CMakeLists.txt.mako
Normal 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
|
||||||
|
)
|
||||||
|
|
||||||
189
grc/core/generator/cpp_templates/flow_graph.cpp.mako
Normal file
189
grc/core/generator/cpp_templates/flow_graph.cpp.mako
Normal 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
|
||||||
193
grc/core/generator/cpp_templates/flow_graph.hpp.mako
Normal file
193
grc/core/generator/cpp_templates/flow_graph.hpp.mako
Normal 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
|
||||||
|
|
||||||
499
grc/core/generator/cpp_top_block.py
Normal file
499
grc/core/generator/cpp_top_block.py
Normal 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
|
||||||
435
grc/core/generator/flow_graph.py.mako
Normal file
435
grc/core/generator/flow_graph.py.mako
Normal 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
|
||||||
186
grc/core/generator/hier_block.py
Normal file
186
grc/core/generator/hier_block.py
Normal 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
|
||||||
386
grc/core/generator/top_block.py
Normal file
386
grc/core/generator/top_block.py
Normal 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
5
grc/core/io/__init__.py
Normal 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
89
grc/core/io/yaml.py
Normal 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)
|
||||||
7
grc/core/params/__init__.py
Normal file
7
grc/core/params/__init__.py
Normal 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
128
grc/core/params/dtypes.py
Normal 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
484
grc/core/params/param.py
Normal 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
|
||||||
37
grc/core/params/template_arg.py
Normal file
37
grc/core/params/template_arg.py
Normal 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
464
grc/core/platform.py
Normal 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)
|
||||||
11
grc/core/ports/__init__.py
Normal file
11
grc/core/ports/__init__.py
Normal 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
|
||||||
119
grc/core/ports/_virtual_connections.py
Normal file
119
grc/core/ports/_virtual_connections.py
Normal 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
27
grc/core/ports/clone.py
Normal 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
255
grc/core/ports/port.py
Normal 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
|
||||||
5
grc/core/schema_checker/__init__.py
Normal file
5
grc/core/schema_checker/__init__.py
Normal 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
|
||||||
73
grc/core/schema_checker/block.py
Normal file
73
grc/core/schema_checker/block.py
Normal 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
|
||||||
|
)
|
||||||
19
grc/core/schema_checker/domain.py
Normal file
19
grc/core/schema_checker/domain.py
Normal 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)
|
||||||
|
)
|
||||||
24
grc/core/schema_checker/flow_graph.py
Normal file
24
grc/core/schema_checker/flow_graph.py
Normal 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),
|
||||||
|
))
|
||||||
|
|
||||||
|
)
|
||||||
18
grc/core/schema_checker/manifest.py
Normal file
18
grc/core/schema_checker/manifest.py
Normal 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),
|
||||||
|
)
|
||||||
22
grc/core/schema_checker/utils.py
Normal file
22
grc/core/schema_checker/utils.py
Normal 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())
|
||||||
88
grc/core/schema_checker/validator.py
Normal file
88
grc/core/schema_checker/validator.py
Normal 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))
|
||||||
18
grc/core/utils/__init__.py
Normal file
18
grc/core/utils/__init__.py
Normal 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)
|
||||||
15
grc/core/utils/descriptors/__init__.py
Normal file
15
grc/core/utils/descriptors/__init__.py
Normal 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,
|
||||||
|
)
|
||||||
63
grc/core/utils/descriptors/_lazy.py
Normal file
63
grc/core/utils/descriptors/_lazy.py
Normal 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)
|
||||||
105
grc/core/utils/descriptors/evaluated.py
Normal file
105
grc/core/utils/descriptors/evaluated.py
Normal 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
|
||||||
129
grc/core/utils/epy_block_io.py
Normal file
129
grc/core/utils/epy_block_io.py
Normal 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()))
|
||||||
214
grc/core/utils/expr_utils.py
Normal file
214
grc/core/utils/expr_utils.py
Normal 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)
|
||||||
317
grc/core/utils/extract_docs.py
Normal file
317
grc/core/utils/extract_docs.py
Normal 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()
|
||||||
57
grc/core/utils/flow_graph_complexity.py
Normal file
57
grc/core/utils/flow_graph_complexity.py
Normal 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>"
|
||||||
16
grc/core/utils/hide_bokeh_gui_options_if_not_installed.py
Normal file
16
grc/core/utils/hide_bokeh_gui_options_if_not_installed.py
Normal 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
12
grc/grc.conf.in
Normal 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
742
grc/gui/Actions.py
Normal 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
901
grc/gui/Application.py
Normal 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
330
grc/gui/Bars.py
Normal 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
300
grc/gui/BlockTreeWindow.py
Normal 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
186
grc/gui/Config.py
Normal 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
43
grc/gui/Console.py
Normal 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
123
grc/gui/Constants.py
Normal 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
432
grc/gui/Dialogs.py
Normal 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
Loading…
x
Reference in New Issue
Block a user