From 9f96402e02e34ceb27edb15adcc791d8d87971fa Mon Sep 17 00:00:00 2001 From: Yoel Bassin Date: Sat, 26 Apr 2025 04:18:02 +0300 Subject: [PATCH] main - feat: Make some initial MCP server work --- README.md | 20 +- fm_radio_receiver.grc | 265 ++++++++++++++++++++++ fm_radio_receiver_with_signal_meter.grc | 288 ++++++++++++++++++++++++ fm_receiver.grc | 126 +++++++++++ grc/main.py | 70 +++++- grc/mcp/__init__.py | 0 grc/mcp/mcp.py | 247 ++++++++++++++++++++ 7 files changed, 1014 insertions(+), 2 deletions(-) create mode 100644 fm_radio_receiver.grc create mode 100644 fm_radio_receiver_with_signal_meter.grc create mode 100644 fm_receiver.grc create mode 100644 grc/mcp/__init__.py create mode 100644 grc/mcp/mcp.py diff --git a/README.md b/README.md index 610ec05..09c54c9 100644 --- a/README.md +++ b/README.md @@ -21,4 +21,22 @@ python -m grc ``` ## Current Status -Currently, the project is only a port of GNURadio Companion (v3.10.12.0) \ No newline at end of file +Currently, the project is only a port of GNURadio Companion (v3.10.12.0) + + +## Wanted API +1. Resources +- List available blocks +- List block params + +- List current blocks +- List current connections + +2. Tools +- Add block +- Remove block +- Set block params +- Add connection +- Remove connection + +-[ ] use pydantic diff --git a/fm_radio_receiver.grc b/fm_radio_receiver.grc new file mode 100644 index 0000000..77df89d --- /dev/null +++ b/fm_radio_receiver.grc @@ -0,0 +1,265 @@ +options: + parameters: + author: GNU Radio + catch_exceptions: 'True' + category: '[GRC Hier Blocks]' + cmake_opt: '' + comment: '' + copyright: '' + description: RTL-SDR FM Radio Receiver with frequency and volume controls + gen_cmake: 'On' + gen_linking: dynamic + generate_options: qt_gui + hier_block_src_path: '.:' + id: default + max_nouts: '0' + output_language: python + placement: (0,0) + qt_qss_theme: '' + realtime_scheduling: '' + run: 'True' + run_command: '{python} -u {filename}' + run_options: prompt + sizing_mode: fixed + thread_safe_setters: '' + title: FM Radio Receiver + window_size: 1280, 1024 + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [8, 8] + rotation: 0 + state: enabled + +blocks: +- name: samp_rate + id: variable + parameters: + comment: '' + value: '2400000' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [200, 12] + rotation: 0 + state: enabled +- name: variable_qtgui_range_0 + id: variable_qtgui_range + parameters: + comment: '' + gui_hint: '' + label: Frequency + min_len: '200' + orient: QtCore.Qt.Horizontal + rangeType: float + start: '87500000' + step: '100000' + stop: '108000000' + value: '95500000' + widget: counter_slider + states: + bus_sink: false + bus_source: false + bus_structure: null + state: enabled +- name: variable_qtgui_range_1 + id: variable_qtgui_range + parameters: + comment: '' + gui_hint: '' + label: Volume + min_len: '200' + orient: QtCore.Qt.Horizontal + rangeType: float + start: '0' + step: '0.1' + stop: '2' + value: '1' + widget: slider + states: + bus_sink: false + bus_source: false + bus_structure: null + state: enabled +- name: analog_wfm_rcv_0 + id: analog_wfm_rcv + parameters: + affinity: '' + alias: '' + audio_decimation: '10' + comment: '' + maxoutbuf: '0' + minoutbuf: '0' + quad_rate: '480000' + states: + bus_sink: false + bus_source: false + bus_structure: null + state: enabled +- name: audio_sink_0 + id: audio_sink + parameters: + affinity: '' + alias: '' + comment: '' + device_name: '' + num_inputs: '1' + ok_to_block: 'True' + samp_rate: '48000' + states: + bus_sink: false + bus_source: false + bus_structure: null + state: enabled +- name: blocks_multiply_const_vxx_0 + id: blocks_multiply_const_vxx + parameters: + affinity: '' + alias: '' + comment: '' + const: variable_qtgui_range_1 + maxoutbuf: '0' + minoutbuf: '0' + type: float + vlen: '1' + states: + bus_sink: false + bus_source: false + bus_structure: null + state: enabled +- name: low_pass_filter_0 + id: low_pass_filter + parameters: + affinity: '' + alias: '' + beta: '6.76' + comment: '' + cutoff_freq: '100000' + decim: '5' + gain: '1' + interp: '1' + maxoutbuf: '0' + minoutbuf: '0' + samp_rate: '2400000' + type: fir_filter_ccf + width: '30000' + win: window.WIN_HAMMING + states: + bus_sink: false + bus_source: false + bus_structure: null + state: enabled +- name: qtgui_freq_sink_x_0 + id: qtgui_freq_sink_x + parameters: + affinity: '' + alias: '' + alpha1: '1.0' + alpha10: '1.0' + alpha2: '1.0' + alpha3: '1.0' + alpha4: '1.0' + alpha5: '1.0' + alpha6: '1.0' + alpha7: '1.0' + alpha8: '1.0' + alpha9: '1.0' + autoscale: 'False' + average: '1.0' + axislabels: 'True' + bw: '2400000' + color1: '"blue"' + color10: '"dark blue"' + color2: '"red"' + color3: '"green"' + color4: '"black"' + color5: '"cyan"' + color6: '"magenta"' + color7: '"yellow"' + color8: '"dark red"' + color9: '"dark green"' + comment: '' + ctrlpanel: 'False' + fc: variable_qtgui_range_0 + fftsize: '1024' + freqhalf: 'True' + grid: 'False' + gui_hint: '' + label: Relative Gain + label1: '' + label10: '''''' + label2: '''''' + label3: '''''' + label4: '''''' + label5: '''''' + label6: '''''' + label7: '''''' + label8: '''''' + label9: '''''' + legend: 'True' + maxoutbuf: '0' + minoutbuf: '0' + name: RF Spectrum + nconnections: '1' + norm_window: 'False' + showports: 'False' + tr_chan: '0' + tr_level: '0.0' + tr_mode: qtgui.TRIG_MODE_FREE + tr_tag: '""' + type: complex + units: dB + update_time: '0.10' + width1: '1' + width10: '1' + width2: '1' + width3: '1' + width4: '1' + width5: '1' + width6: '1' + width7: '1' + width8: '1' + width9: '1' + wintype: window.WIN_BLACKMAN_hARRIS + ymax: '10' + ymin: '-140' + states: + bus_sink: false + bus_source: false + bus_structure: null + state: enabled +- name: soapy_rtlsdr_source_0 + id: soapy_rtlsdr_source + parameters: + affinity: '' + agc: 'True' + alias: '' + bias: 'False' + bufflen: '16384' + center_freq: variable_qtgui_range_0 + comment: '' + dev_args: '' + freq_correction: '0' + gain: '20' + maxoutbuf: '0' + minoutbuf: '0' + samp_rate: '2400000' + type: fc32 + states: + bus_sink: false + bus_source: false + bus_structure: null + state: enabled + +connections: +- [analog_wfm_rcv_0, '0', blocks_multiply_const_vxx_0, '0'] +- [blocks_multiply_const_vxx_0, '0', audio_sink_0, '0'] +- [low_pass_filter_0, '0', analog_wfm_rcv_0, '0'] +- [soapy_rtlsdr_source_0, '0', low_pass_filter_0, '0'] +- [soapy_rtlsdr_source_0, '0', qtgui_freq_sink_x_0, '0'] + +metadata: + file_format: 1 + grc_version: 3.10.12.0 diff --git a/fm_radio_receiver_with_signal_meter.grc b/fm_radio_receiver_with_signal_meter.grc new file mode 100644 index 0000000..75d2ca4 --- /dev/null +++ b/fm_radio_receiver_with_signal_meter.grc @@ -0,0 +1,288 @@ +options: + parameters: + author: '' + catch_exceptions: 'True' + category: '[GRC Hier Blocks]' + cmake_opt: '' + comment: '' + copyright: '' + description: '' + gen_cmake: 'On' + gen_linking: dynamic + generate_options: qt_gui + hier_block_src_path: '.:' + id: default + max_nouts: '0' + output_language: python + placement: (0,0) + qt_qss_theme: '' + realtime_scheduling: '' + run: 'True' + run_command: '{python} -u {filename}' + run_options: prompt + sizing_mode: fixed + thread_safe_setters: '' + title: Not titled yet + window_size: (1000,1000) + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [296, 4.0] + rotation: 0 + state: enabled + +blocks: +- name: freq + id: variable_qtgui_range + parameters: + comment: '' + gui_hint: '' + label: Frequency + min_len: '200' + orient: QtCore.Qt.Horizontal + rangeType: float + start: '88000000' + step: '100000' + stop: '108000000' + value: '95700000' + widget: counter_slider + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [48, 44.0] + rotation: 0 + state: enabled +- name: rf_gain + id: variable_qtgui_range + parameters: + comment: '' + gui_hint: '' + label: RF Gain + min_len: '200' + orient: QtCore.Qt.Horizontal + rangeType: float + start: '0' + step: '1' + stop: '47' + value: '20' + widget: counter_slider + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [568, 116.0] + rotation: 0 + state: enabled +- name: samp_rate + id: variable + parameters: + comment: '' + value: '32000' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [200, 12] + rotation: 0 + state: enabled +- name: variable_qtgui_range_0 + id: variable_qtgui_range + parameters: + comment: '' + gui_hint: '' + label: '' + min_len: '200' + orient: QtCore.Qt.Horizontal + rangeType: float + start: '0' + step: '1' + stop: '100' + value: '50' + widget: counter_slider + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [184, 104.0] + rotation: 0 + state: enabled +- name: volume + id: variable + parameters: + comment: '' + value: '1' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [384, 128.0] + rotation: 0 + state: enabled +- name: analog_wfm_rcv_1 + id: analog_wfm_rcv + parameters: + affinity: '' + alias: '' + audio_decimation: '8' + comment: '' + maxoutbuf: '0' + minoutbuf: '0' + quad_rate: '2048000' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [400, 240.0] + rotation: 0 + state: enabled +- name: audio_sink_0 + id: audio_sink + parameters: + affinity: '' + alias: '' + comment: '' + device_name: '' + num_inputs: '1' + ok_to_block: 'True' + samp_rate: samp_rate + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [848, 204.0] + rotation: 0 + state: enabled +- name: blocks_complex_to_mag_squared_0 + id: blocks_complex_to_mag_squared + parameters: + affinity: '' + alias: '' + comment: '' + maxoutbuf: '0' + minoutbuf: '0' + vlen: '1' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [456, 484.0] + rotation: 0 + state: enabled +- name: blocks_multiply_const_vxx_0 + id: blocks_multiply_const_vxx + parameters: + affinity: '' + alias: '' + comment: '' + const: volume + maxoutbuf: '0' + minoutbuf: '0' + type: float + vlen: '1' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [696, 220.0] + rotation: 0 + state: enabled +- name: qtgui_number_sink_0 + id: qtgui_number_sink + parameters: + affinity: '' + alias: '' + autoscale: 'False' + avg: '0' + color1: ("black", "black") + color10: ("black", "black") + color2: ("black", "black") + color3: ("black", "black") + color4: ("black", "black") + color5: ("black", "black") + color6: ("black", "black") + color7: ("black", "black") + color8: ("black", "black") + color9: ("black", "black") + comment: '' + factor1: '1' + factor10: '1' + factor2: '1' + factor3: '1' + factor4: '1' + factor5: '1' + factor6: '1' + factor7: '1' + factor8: '1' + factor9: '1' + graph_type: qtgui.NUM_GRAPH_HORIZ + gui_hint: '' + label1: '' + label10: '' + label2: '' + label3: '' + label4: '' + label5: '' + label6: '' + label7: '' + label8: '' + label9: '' + max: '1' + min: '-1' + name: '""' + nconnections: '1' + type: float + unit1: '' + unit10: '' + unit2: '' + unit3: '' + unit4: '' + unit5: '' + unit6: '' + unit7: '' + unit8: '' + unit9: '' + update_time: '0.10' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [856, 320.0] + rotation: 0 + state: enabled +- name: soapy_rtlsdr_source_1 + id: soapy_rtlsdr_source + parameters: + affinity: '' + agc: 'False' + alias: '' + bias: 'False' + bufflen: '16384' + center_freq: freq + comment: '' + dev_args: '' + freq_correction: '0' + gain: rf_gain + maxoutbuf: '0' + minoutbuf: '0' + samp_rate: '2048000' + type: fc32 + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [88, 352.0] + rotation: 0 + state: enabled + +connections: +- [analog_wfm_rcv_1, '0', blocks_multiply_const_vxx_0, '0'] +- [blocks_complex_to_mag_squared_0, '0', qtgui_number_sink_0, '0'] +- [blocks_multiply_const_vxx_0, '0', audio_sink_0, '0'] +- [soapy_rtlsdr_source_1, '0', analog_wfm_rcv_1, '0'] +- [soapy_rtlsdr_source_1, '0', blocks_complex_to_mag_squared_0, '0'] + +metadata: + file_format: 1 + grc_version: 3.10.12.0 diff --git a/fm_receiver.grc b/fm_receiver.grc new file mode 100644 index 0000000..cd482ea --- /dev/null +++ b/fm_receiver.grc @@ -0,0 +1,126 @@ +options: + parameters: + author: '' + catch_exceptions: 'True' + category: '[GRC Hier Blocks]' + cmake_opt: '' + comment: '' + copyright: '' + description: '' + gen_cmake: 'On' + gen_linking: dynamic + generate_options: qt_gui + hier_block_src_path: '.:' + id: default + max_nouts: '0' + output_language: python + placement: (0,0) + qt_qss_theme: '' + realtime_scheduling: '' + run: 'True' + run_command: '{python} -u {filename}' + run_options: prompt + sizing_mode: fixed + thread_safe_setters: '' + title: Not titled yet + window_size: (1000,1000) + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [8, 8] + rotation: 0 + state: enabled + +blocks: +- name: samp_rate + id: variable + parameters: + comment: '' + value: '32000' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [200, 12] + rotation: 0 + state: enabled +- name: analog_wfm_rcv_0 + id: analog_wfm_rcv + parameters: + affinity: '' + alias: '' + audio_decimation: '10' + comment: '' + maxoutbuf: '0' + minoutbuf: '0' + quad_rate: '480000' + states: + bus_sink: false + bus_source: false + bus_structure: null + state: enabled +- name: audio_sink_0 + id: audio_sink + parameters: + affinity: '' + alias: '' + comment: '' + device_name: '' + num_inputs: '1' + ok_to_block: 'True' + samp_rate: '44100' + states: + bus_sink: false + bus_source: false + bus_structure: null + state: enabled +- name: rational_resampler_xxx_0 + id: rational_resampler_xxx + parameters: + affinity: '' + alias: '' + comment: '' + decim: '480' + fbw: '0' + interp: '441' + maxoutbuf: '0' + minoutbuf: '0' + taps: '[]' + type: fff + states: + bus_sink: false + bus_source: false + bus_structure: null + state: enabled +- name: soapy_rtlsdr_source_0 + id: soapy_rtlsdr_source + parameters: + affinity: '' + agc: 'False' + alias: '' + bias: 'False' + bufflen: '16384' + center_freq: '95500000' + comment: '' + dev_args: '' + freq_correction: '0' + gain: '20' + maxoutbuf: '0' + minoutbuf: '0' + samp_rate: '2457600' + type: fc32 + states: + bus_sink: false + bus_source: false + bus_structure: null + state: enabled + +connections: +- [analog_wfm_rcv_0, '0', rational_resampler_xxx_0, '0'] +- [rational_resampler_xxx_0, '0', audio_sink_0, '0'] +- [soapy_rtlsdr_source_0, '0', analog_wfm_rcv_0, '0'] + +metadata: + file_format: 1 + grc_version: 3.10.12.0 diff --git a/grc/main.py b/grc/main.py index 32fb6a3..a326547 100755 --- a/grc/main.py +++ b/grc/main.py @@ -69,6 +69,71 @@ log = logging.getLogger('grc') # The default console logging should be WARNING log.setLevel(logging.DEBUG) +def run_api(args, log): + + from .core.platform import Platform + from .core.FlowGraph import FlowGraph + + platform = Platform( + version=gr.version(), + version_parts=(gr.major_version(), gr.api_version(), gr.minor_version()), + prefs=gr.prefs(), + # install_prefix=gr.prefix() + ) + platform.build_library() + + + from .mcp.mcp import CoreMiddleware + mcp = CoreMiddleware(platform, "") + + from fastmcp import FastMCP + + app = FastMCP("Demo 🚀") + + app.tool()(mcp.list_all_blocks) + app.tool()(mcp.list_block_input_data) + app.tool()(mcp.list_block_output_data) + app.tool()(mcp.list_block_parameters_data) + + app.tool()(mcp.get_placed_blocks) + app.tool()(mcp.get_placed_connections) + app.tool()(mcp.get_placed_block_params) + + app.tool()(mcp.validate_block) + app.tool()(mcp.validate_connection) + app.tool()(mcp.validate_flowgraph) + app.tool()(mcp.get_all_errors) + + app.tool()(mcp.add_block) + app.tool()(mcp.remove_block) + app.tool()(mcp.connect_blocks) + app.tool()(mcp.update_block_params) + + app.tool()(mcp.save_flowgraph) + + + app.run(transport='sse') + + + # print(mcp.flowgraph_blocks()) + # print(mcp.flowgraph_connections()) + # initial_state = platform.parse_flow_graph("") + # fg = FlowGraph(platform) + # fg.import_data(initial_state) + # source = fg.new_block('analog_const_source_x') + # source.params['id'].set_value('test1') + # sink = fg.new_block('virtual_sink') + # sink.params['id'].set_value('test2') + # conn = fg.connect(source.sources[0], sink.sinks[0]) + # platform.save_flow_graph('test.grc', fg) + + # mcp.add_block('analog_const_source_x') + # mcp.add_block('virtual_sink') + # mcp.connect_blocks('analog_const_source_x_0', 0, 'virtual_sink_0', 0) + + + + def run_gtk(args, log): ''' Runs the GTK version of GNU Radio Companion ''' @@ -182,7 +247,6 @@ def run_qt(args, log): install_prefix=gr.prefix() ) model.build_library() - # Launch GRC app = grc.Application(settings, model, args.flow_graphs) sys.exit(app.run()) @@ -285,6 +349,8 @@ def main(): help="GNU Radio Companion (QT)") gui_group_exclusive.add_argument("--gtk", dest='framework', action='store_const', const='gtk', help="GNU Radio Companion (GTK)") + gui_group_exclusive.add_argument("--api", dest='framework', action='store_const', const='api', + help="GNU Radio Companion (API)") # Default options if not already set with add_argument() args = parser.parse_args() @@ -316,6 +382,8 @@ def main(): run_qt(args, log) elif args.framework == 'gtk': run_gtk(args, log) + elif args.framework == 'api': + run_api(args, log) else: # args.framework == None if grc_version_from_config == 'grc_qt': run_qt(args, log) diff --git a/grc/mcp/__init__.py b/grc/mcp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/grc/mcp/mcp.py b/grc/mcp/mcp.py new file mode 100644 index 0000000..9b81eb6 --- /dev/null +++ b/grc/mcp/mcp.py @@ -0,0 +1,247 @@ +from itertools import count +import re +from typing import Any, Dict, List, Optional, Set + +from grc.core.FlowGraph import FlowGraph +from grc.core.blocks.block import Block +from grc.core.params.param import Param +from grc.core.Connection import Connection +from grc.core.platform import Platform +from grc.core.ports.port import Port + + +class CoreMiddleware: + def __init__(self, platform: Platform, filepath: Optional[str] = ""): + + self._platform = platform + + initial_state = platform.parse_flow_graph(filepath) + self._flowgraph = FlowGraph(platform) + self._flowgraph.import_data(initial_state) + + ############################################## + # Static Data + ############################################## + + def list_all_blocks(self) -> List[Dict[str, Any]]: + blocks_data = [] + for block in self._platform.blocks.values(): + blocks_data.append( + { + "key": block.key, + "label": block.label, + } + ) + return blocks_data + + def list_block_parameters_data(self, block_key: str) -> List[str]: + block = self._platform.blocks[block_key] + return block.parameters_data + + def list_block_output_data(self, block_key: str) -> List[Dict[str, Any]]: + block = self._platform.blocks[block_key] + return block.outputs_data + + def list_block_input_data(self, block_key: str) -> List[Dict[str, Any]]: + block = self._platform.blocks[block_key] + return block.inputs_data + + ############################################## + # Flowgraph Data + ############################################## + + def get_placed_blocks(self) -> List[Dict[str, Any]]: + blocks_data = [] + for block in self._flowgraph.blocks: + blocks_data.append( + { + "key": block.key, + "name": block.params["id"].get_value(), + } + ) + return blocks_data + + def get_placed_block_params(self, block_name: str) -> Dict[str, Any]: + block = self._flowgraph.get_block(block_name) + return {param.key: param.get_value() for param in block.params.values()} + + def get_placed_connections(self) -> Set[Dict[str, Any]]: + connections_data = [] + for connection in self._flowgraph.connections: + connection: Connection = connection + source_port: Port = connection.source_port + sink_port: Port = connection.sink_port + connections_data.append( + { + "src_block": source_port.parent.key, + "src_port": source_port.key, + "dst_block": sink_port.parent.key, + "dst_port": sink_port.key, + } + ) + return connections_data + + ############################################## + # Flowgraph Operations + ############################################## + + def add_block(self, block_key: str, name: Optional[str] = None) -> Block: + name = name or self._get_unique_id(block_key) + block = self._flowgraph.new_block(block_key) + block.params["id"].set_value(name) + return "success" + + def remove_block(self, block_name: str) -> None: + block = self._flowgraph.get_block(block_name) + self._flowgraph.remove_element(block) + return "success" + + def connect_blocks( + self, + src_block_name: str, + src_port_index: int, + dst_block_name: str, + dst_port_index: int, + ) -> Connection: + src_block = self._flowgraph.get_block(src_block_name) + dst_block = self._flowgraph.get_block(dst_block_name) + self._flowgraph.connect( + src_block.sources[src_port_index], dst_block.sinks[dst_port_index] + ) + return "success" + + def disconnect_blocks( + self, + src_block_name: str, + src_port_index: int, + dst_block_name: str, + dst_port_index: int, + ) -> None: + src_block = self._flowgraph.get_block(src_block_name) + dst_block = self._flowgraph.get_block(dst_block_name) + self._flowgraph.disconnect( + src_block.sources[src_port_index], dst_block.sinks[dst_port_index] + ) + return "success" + + def update_block_params(self, block_name: str, params: Dict[str, Any]) -> None: + block = self._flowgraph.get_block(block_name) + for param_name, param_value in params.items(): + block.params[param_name].set_value(param_value) + return "success" + + ############################################## + # Flowgraph Validation + ############################################## + + def validate_block(self, block_name: str) -> bool: + self._flowgraph.rewrite() + block = self._flowgraph.get_block(block_name) + block.validate() + return block.is_valid() + + def validate_connection( + self, + src_block_name: str, + src_port_index: int, + dst_block_name: str, + dst_port_index: int, + ) -> bool: + self._flowgraph.rewrite() + connections: Set[Connection] = self._flowgraph.connections + for connection in connections: + if ( + connection.source_port.parent.key == src_block_name + and connection.sink_port.parent.key == dst_block_name + and connection.source_port.key == src_port_index + and connection.sink_port.key == dst_port_index + ): + connection.validate() + return connection.is_valid() + raise ValueError("Connection not found") + + def validate_flowgraph(self) -> bool: + self._flowgraph.rewrite() + self._flowgraph.validate() + return self._flowgraph.is_valid() + + def get_all_errors(self) -> List[Dict[str, Any]]: + self._flowgraph.rewrite() + self._flowgraph.validate() + errors = [] + for elem, msg in self._flowgraph.iter_error_messages(): + msg = re.sub("[^A-Za-z0-9]+", " ", msg).strip() + if isinstance(elem, Connection): + connection: Connection = elem + source_port: Port = elem.source_port + sink_port: Port = elem.sink_port + errors.append( + { + "type": "connection", + "key": { + "src_block": source_port.parent.key, + "src_port": source_port.key, + "dst_block": sink_port.parent.key, + "dst_port": sink_port.key, + }, + "message": msg, + } + ) + + if isinstance(elem, Param): + errors.append( + { + "type": "param", + "key": f"{elem.parent.params["id"].get_value()}:{elem.key}", + "message": msg, + } + ) + + if isinstance(elem, Port): + errors.append( + { + "type": "port", + "key": f"{elem.parent.params["id"].get_value()}:{elem.key}", + "message": msg, + } + ) + + if isinstance(elem, Block): + errors.append( + { + "type": "block", + "key": elem.params["id"].get_value(), + "message": msg, + } + ) + + return errors + + ############################################## + # Misc + ############################################## + + def save_flowgraph(self, filepath: str) -> None: + self._platform.save_flow_graph(filepath, self._flowgraph) + return "success" + + ############################################## + # Helper Functions + ############################################## + + def _get_unique_id(self, base_id=""): + """ + Get a unique id starting with the base id. + + Args: + base_id: the id starts with this and appends a count + + Returns: + a unique id + """ + block_ids = set(b.name for b in self._flowgraph.blocks) + for index in count(): + block_id = "{}_{}".format(base_id, index) + if block_id not in block_ids: + break + return block_id