From 6a96720df4f1400ae9843a0e5dffd161d046957f Mon Sep 17 00:00:00 2001
From: Martin Braun <martin.braun@ettus.com>
Date: Fri, 15 Nov 2024 09:58:00 +0100
Subject: [PATCH 35/41] uhd: Add GRC workflow for building RFNoC image core
 bitfiles

This adds a new output language and build options to GRC and allows
building RFNoC FPGA bitfile straight out of GNU Radio.

Signed-off-by: Martin Braun <martin.braun@ettus.com>
---
 .../grc/rfnoc_images/x310_image_core.grc      |  45 ++++---
 gr-uhd/grc/CMakeLists.txt                     |   2 +
 gr-uhd/grc/rfnoc_image_builder.workflow.yml   |  74 +++++++++++
 gr-uhd/python/uhd/CMakeLists.txt              |   4 +
 gr-uhd/python/uhd/grc_workflows.py            | 122 ++++++++++++++++++
 grc/gui_qt/components/executor.py             |   1 -
 6 files changed, 226 insertions(+), 22 deletions(-)
 create mode 100644 gr-uhd/grc/rfnoc_image_builder.workflow.yml
 create mode 100644 gr-uhd/python/uhd/grc_workflows.py

diff --git a/gr-uhd/examples/grc/rfnoc_images/x310_image_core.grc b/gr-uhd/examples/grc/rfnoc_images/x310_image_core.grc
index d1951a5f13..3c9f36780f 100644
--- a/gr-uhd/examples/grc/rfnoc_images/x310_image_core.grc
+++ b/gr-uhd/examples/grc/rfnoc_images/x310_image_core.grc
@@ -1,34 +1,37 @@
 options:
   parameters:
+    alias: ''
     author: Martin Braun <martin.braun@ettus.com>
+    build_dir: ''
+    build_ip_dir: ''
+    build_output_dir: ''
     catch_exceptions: 'True'
-    category: '[GRC Hier Blocks]'
-    cmake_opt: ''
+    clean_ip: 'False'
     comment: ''
     copyright: 2024 Ettus Research
     description: This is a graphical version of the X310 image core files.
-    gen_cmake: 'On'
+    fpga_dir: /home/mbr0wn/prefix/master/src/uhd/fpga
     gen_linking: dynamic
-    generate_options: qt_gui
+    generate_options: no_gui
+    generator_class_name: RfnocImageGenerator
+    generator_module: gnuradio.uhd.grc_workflows
     hier_block_src_path: '.:'
     id: x310_image_core
-    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
+    ignore_warnings: 'False'
+    include_date: 'False'
+    include_dirs: ''
+    include_hash: 'False'
+    jobs: '1'
+    output_language: rfnoc_bitfile
+    reuse: 'False'
     thread_safe_setters: ''
     title: Not titled yet
-    window_size: (1000,1000)
+    vivado_path: ''
   states:
     bus_sink: false
     bus_source: false
     bus_structure: null
-    coordinate: [8, 8]
+    coordinate: [9.25, 9.25]
     rotation: 0
     state: enabled
 
@@ -117,7 +120,7 @@ blocks:
       setting. UHD will pick the first SEP (which
 
       is this one) and assign it a control port.'
-    ctrl: 'Auto'
+    ctrl: Auto
     data: 'True'
     maxoutbuf: '0'
     minoutbuf: '0'
@@ -144,7 +147,7 @@ blocks:
       SEP from UHD, and therefore the
 
       buffers would be a waste of resources.'
-    ctrl: 'Auto'
+    ctrl: Auto
     data: 'True'
     maxoutbuf: '0'
     minoutbuf: '0'
@@ -163,7 +166,7 @@ blocks:
     alias: ep2
     buff_size_bytes: '524288'
     comment: ''
-    ctrl: 'Auto'
+    ctrl: Auto
     data: 'True'
     maxoutbuf: '0'
     minoutbuf: '0'
@@ -182,7 +185,7 @@ blocks:
     alias: ep3
     buff_size_bytes: '0'
     comment: ''
-    ctrl: 'Auto'
+    ctrl: Auto
     data: 'True'
     maxoutbuf: '0'
     minoutbuf: '0'
@@ -201,7 +204,7 @@ blocks:
     alias: ep4
     buff_size_bytes: '32768'
     comment: ''
-    ctrl: 'Auto'
+    ctrl: Auto
     data: 'True'
     maxoutbuf: '0'
     minoutbuf: '0'
@@ -243,7 +246,7 @@ blocks:
       of the radio block, we can reduce the
 
       buffer size of these SEPs.'
-    ctrl: 'Auto'
+    ctrl: Auto
     data: 'True'
     maxoutbuf: '0'
     minoutbuf: '0'
diff --git a/gr-uhd/grc/CMakeLists.txt b/gr-uhd/grc/CMakeLists.txt
index 23340c5e03..84841519da 100644
--- a/gr-uhd/grc/CMakeLists.txt
+++ b/gr-uhd/grc/CMakeLists.txt
@@ -82,6 +82,8 @@ if(ENABLE_UHD_RFNOC)
         uhd_fpga_io_radio.domain.yml
         uhd_fpga_io_time_keeper.domain.yml
         uhd_fpga_io_xport.domain.yml
+        # GRC workflow
+        rfnoc_image_builder.workflow.yml
         DESTINATION ${GRC_BLOCKS_DIR})
     if(PYQT5_FOUND)
         install (FILES uhd_msgpushbutton.block.yml DESTINATION ${GRC_BLOCKS_DIR})
diff --git a/gr-uhd/grc/rfnoc_image_builder.workflow.yml b/gr-uhd/grc/rfnoc_image_builder.workflow.yml
new file mode 100644
index 0000000000..501cd3805e
--- /dev/null
+++ b/gr-uhd/grc/rfnoc_image_builder.workflow.yml
@@ -0,0 +1,74 @@
+id: rfnoc_image_builder_workflow
+description: "RFNoC Image Builder Workflow"
+output_language: rfnoc_bitfile
+output_language_label: RFNoC Bitfile
+generator_module: gnuradio.uhd.grc_workflows
+generator_class: RfnocImageGenerator
+generate_options: no_gui
+generate_options_label: No GUI
+parameters:
+-   id: fpga_dir
+    label: FPGA Source Directory
+    dtype: string
+    default: ''
+    hide: 'part'
+-   id: build_dir
+    label: Build Artifact Directory
+    dtype: string
+    default: ''
+    hide: 'part'
+-   id: build_output_dir
+    label: Build Output Directory
+    dtype: string
+    default: ''
+    hide: 'part'
+-   id: build_ip_dir
+    label: IP Build Artifact Directory
+    dtype: string
+    default: ''
+    hide: 'part'
+-   id: include_dirs
+    label: RFNOC OOT Directories
+    dtype: string
+    default: ''
+    hide: 'part'
+-   id: reuse
+    label: Reuse Generated Files
+    dtype: bool
+    default: 'False'
+    hide: 'part'
+-   id: clean_ip
+    label: Clean IP
+    dtype: bool
+    default: 'False'
+    hide: 'part'
+    category: 'Advanced'
+-   id: ignore_warnings
+    label: Ignore Warnings
+    dtype: bool
+    default: 'False'
+    hide: 'part'
+    category: 'Advanced'
+-   id: vivado_path
+    label: Vivado Path
+    dtype: string
+    default: ''
+    hide: 'part'
+    category: 'Advanced'
+-   id: include_hash
+    label: Include Hash
+    dtype: bool
+    default: 'False'
+    hide: 'part'
+    category: 'Advanced'
+-   id: include_date
+    label: Include Date
+    dtype: bool
+    default: 'False'
+    hide: 'part'
+    category: 'Advanced'
+-   id: jobs
+    label: Parallel Build Jobs
+    dtype: int
+    default: '1'
+    hide: 'part'
diff --git a/gr-uhd/python/uhd/CMakeLists.txt b/gr-uhd/python/uhd/CMakeLists.txt
index 4d9092eb62..f6e05f2c8f 100644
--- a/gr-uhd/python/uhd/CMakeLists.txt
+++ b/gr-uhd/python/uhd/CMakeLists.txt
@@ -16,6 +16,10 @@ if(PYQT5_FOUND)
     gr_python_install(FILES replaymsgpushbutton.py DESTINATION ${GR_PYTHON_DIR}/gnuradio/uhd)
 endif(PYQT5_FOUND)
 
+if(ENABLE_UHD_RFNOC)
+    gr_python_install(FILES grc_workflows.py DESTINATION ${GR_PYTHON_DIR}/gnuradio/uhd)
+endif(ENABLE_UHD_RFNOC)
+
 ########################################################################
 # Handle the unit tests
 ########################################################################
diff --git a/gr-uhd/python/uhd/grc_workflows.py b/gr-uhd/python/uhd/grc_workflows.py
new file mode 100644
index 0000000000..cfc374c8fc
--- /dev/null
+++ b/gr-uhd/python/uhd/grc_workflows.py
@@ -0,0 +1,122 @@
+"""Workflow for generating RFNoC bitfiles from GRC.
+
+Copyright 2024 Ettus Research, an NI Brand
+
+This file is part of GNU Radio
+
+SPDX-License-Identifier: GPL-3.0-or-later
+"""
+
+import logging
+import os
+import subprocess
+import tempfile
+from pathlib import Path
+
+from gnuradio.grc.core import Messages
+from gnuradio.grc.core.generator.FlowGraphProxy import FlowGraphProxy
+from gnuradio.grc.workflows import GeneratorBase
+from gnuradio.grc.workflows.common import add_xterm_to_run_command
+
+
+def _get_output_path(fg, output_dir):
+    """Generate the output path for the image core artefacts.
+
+    - If the user has defined a directory, check it's writable and use it.
+    - Otherwise, use the output directory.
+    - If the output directory is not writable, use the system temp directory.
+    """
+    image_core_name = fg.get_option("id")
+    if fg.get_option("build_dir"):
+        if not os.access(fg.get_option("build_dir"), os.W_OK):
+            raise ValueError(
+                f"Build directory {fg.get_option('build_dir')} is not writable")
+        return os.normpath(os.path.abspath(fg.get_option('build_dir')))
+    build_dir_name = "build-" + image_core_name
+    if not os.access(output_dir, os.W_OK):
+        return os.path.join(tempfile.gettempdir(), build_dir_name)
+    return os.path.join(output_dir, build_dir_name)
+
+
+def generate_build_command(input_file, build_dir, flow_graph):
+    """Generate the build command for the RFNoC image."""
+    args = ['rfnoc_image_builder', ]
+    if flow_graph.get_option('fpga_dir'):
+        args.extend(['--fpga-dir', flow_graph.get_option('fpga_dir')])
+    build_dir = os.path.normpath(os.path.abspath(build_dir))
+    build_output_dir = flow_graph.get_option('build_output') \
+        if flow_graph.get_option('build_output') \
+        else str(Path(input_file).parent)
+    build_ip_dir = flow_graph.get_option('build_ip') \
+        if flow_graph.get_option('build_ip') \
+        else os.path.join(build_dir, 'build-ip')
+    include_dirs = [d.strip() for d in flow_graph.get_option('include_dirs').split(os.pathsep) if d.strip()]
+    for include_dir in include_dirs:
+        args.extend(['-I', include_dir])
+    args.extend([
+        "--build-dir", build_dir,
+        "--build-output-dir", build_output_dir,
+        "--build-ip-dir", build_ip_dir])
+    if flow_graph.get_option('jobs'):
+        args.extend(['--jobs', str(flow_graph.get_option('jobs'))])
+    else:
+        args.extend(['--jobs', str(os.cpu_count)])
+    if not flow_graph.get_option('include_hash'):
+        args.append('--no-hash')
+    if not flow_graph.get_option('include_date'):
+        args.append('--no-date')
+    if flow_graph.get_option('vivado_path'):
+        args.extend(['--vivado-path', flow_graph.get_option('vivado_path')])
+    if flow_graph.get_option('ignore_warnings'):
+        args.append('--ignore-warnings')
+    if flow_graph.get_option('reuse'):
+        args.append('--reuse')
+    args.extend(['--grc-config', input_file])
+    return args
+
+
+class RfnocImageGenerator(GeneratorBase):
+    """Generator class for RFNoC bitfiles from GRC."""
+
+    def __init__(self, flow_graph, output_dir):
+        """Initialize the RfnocImageGenerator object.
+
+        Args:
+            flow_graph: The flow graph to generate the RFNoC image for.
+            output_dir: The directory to output the generated image to.
+        """
+        self.flow_graph = FlowGraphProxy(flow_graph)
+        # file_path is the build artifact dir. We call it file_path because GRC
+        # expects this attribute and uses it to generate a message.
+        self.file_path = _get_output_path(flow_graph, output_dir)
+        self.input_file = flow_graph.grc_file_path
+        self.log = logging.getLogger(self.__class__.__name__)
+        self.xterm = self.flow_graph.parent_platform.config.xterm_executable
+
+    def write(self, called_from_exec=False):
+        """Generate the RFNoC image."""
+        if called_from_exec:
+            self.log.debug("Skipping implied image core generation")
+            return
+        image_builder_cmd = generate_build_command(
+            self.input_file, self.file_path, self.flow_graph) + ['--generate-only']
+        self.log.debug("Launching image builder: %s", image_builder_cmd)
+        proc_info = subprocess.run(
+            image_builder_cmd,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            universal_newlines=True,
+        )
+        # FIXME error handling
+        if proc_info.stdout:
+            Messages.send_verbose_exec(proc_info.stdout)
+
+    def get_exec_args(self):
+        """Return a process object that executes the RFNoC image generation."""
+        image_builder_cmd = generate_build_command(
+            self.input_file, self.file_path, self.flow_graph)
+        image_builder_cmd = add_xterm_to_run_command(image_builder_cmd, self.xterm)
+        return dict(
+            args=image_builder_cmd,
+            shell=False,
+        )
diff --git a/grc/gui_qt/components/executor.py b/grc/gui_qt/components/executor.py
index 1cb608bd8c..42d6d010b6 100644
--- a/grc/gui_qt/components/executor.py
+++ b/grc/gui_qt/components/executor.py
@@ -13,7 +13,6 @@ from pathlib import Path
 from gnuradio import gr
 
 from ...core import Messages
-from ...core.utils.system import get_cmake_nproc
 
 
 class ExecFlowGraphThread(threading.Thread):
-- 
2.47.3

