From 6f892f797a3e0ffd1042a0137cf683ff47e39403 Mon Sep 17 00:00:00 2001 From: Martin Braun Date: Fri, 26 Jan 2024 16:57:20 +0100 Subject: examples: Add Python support to rfnoc-example - Generate Pybind11 bindings for the C++ code - Generate a Python module (rfnoc_example) that can be imported and used - Add an example to show how the gain block affects dBFS readings --- host/cmake/Modules/UHDPython.cmake | 2 + host/examples/rfnoc-example/CMakeLists.txt | 50 ++- .../rfnoc-example/cmake/Modules/UHDPython.cmake | 370 +++++++++++++++++++++ .../examples/rx_gain_estimate_power.py | 105 ++++++ .../lib/gain_block_control_python.hpp | 23 ++ host/examples/rfnoc-example/python/CMakeLists.txt | 84 +++++ .../rfnoc-example/python/pyrfnoc-example.cpp | 38 +++ .../rfnoc-example/python/rfnoc_example/__init__.py | 14 + host/examples/rfnoc-example/python/setup.py.in | 34 ++ 9 files changed, 710 insertions(+), 10 deletions(-) create mode 100644 host/examples/rfnoc-example/cmake/Modules/UHDPython.cmake create mode 100755 host/examples/rfnoc-example/examples/rx_gain_estimate_power.py create mode 100644 host/examples/rfnoc-example/lib/gain_block_control_python.hpp create mode 100644 host/examples/rfnoc-example/python/CMakeLists.txt create mode 100644 host/examples/rfnoc-example/python/pyrfnoc-example.cpp create mode 100644 host/examples/rfnoc-example/python/rfnoc_example/__init__.py create mode 100755 host/examples/rfnoc-example/python/setup.py.in diff --git a/host/cmake/Modules/UHDPython.cmake b/host/cmake/Modules/UHDPython.cmake index e53e63824..4fdacc243 100644 --- a/host/cmake/Modules/UHDPython.cmake +++ b/host/cmake/Modules/UHDPython.cmake @@ -5,6 +5,8 @@ # # SPDX-License-Identifier: GPL-3.0-or-later # +# Note: When modifying this file, check if the changes also need to go into +# host/examples/rfnoc-example/cmake/Modules/UHDPython.cmake. if (POLICY CMP0094) # See https://cmake.org/cmake/help/v3.15/policy/CMP0094.html diff --git a/host/examples/rfnoc-example/CMakeLists.txt b/host/examples/rfnoc-example/CMakeLists.txt index 738b5ae8e..2c7e238ec 100644 --- a/host/examples/rfnoc-example/CMakeLists.txt +++ b/host/examples/rfnoc-example/CMakeLists.txt @@ -8,20 +8,22 @@ cmake_minimum_required(VERSION 3.8) project(rfnoc-example CXX C) #make sure our local CMake Modules path comes first -#list(INSERT CMAKE_MODULE_PATH 0 ${CMAKE_SOURCE_DIR}/cmake/Modules) +list(INSERT CMAKE_MODULE_PATH 0 ${CMAKE_SOURCE_DIR}/cmake/Modules) -#install to PyBOMBS target prefix if defined -#if(DEFINED ENV{PYBOMBS_PREFIX}) -# set(CMAKE_INSTALL_PREFIX $ENV{PYBOMBS_PREFIX}) -# message(STATUS "PyBOMBS installed GNU Radio. Setting CMAKE_INSTALL_PREFIX to $ENV{PYBOMBS_PREFIX}") -#endif() +# Set the version information here +set(VERSION_MAJOR 4) +set(VERSION_API 9) +set(VERSION_ABI 0) ######################################################################## # Setup install directories ######################################################################## -set(RFNOC_DATA_DIR share CACHE PATH "Base location for data") -set(RFNOC_PKG_DATA_DIR ${RFNOC_DATA_DIR}/uhd/rfnoc/ CACHE PATH "Path to install RFNoC package data") -set(PROJECT_DATA_DIR ${RFNOC_PKG_DATA_DIR}/example/ CACHE PATH "Path for this project's package data") +set(RFNOC_DATA_DIR share + CACHE PATH "Base location for data") +set(RFNOC_PKG_DATA_DIR ${RFNOC_DATA_DIR}/uhd/rfnoc/ + CACHE PATH "Path to install RFNoC package data") +set(PROJECT_DATA_DIR ${RFNOC_PKG_DATA_DIR}/example/ + CACHE PATH "Path for this project's package data") if(NOT DEFINED LIB_SUFFIX AND REDHAT AND CMAKE_SYSTEM_PROCESSOR MATCHES "64$") set(LIB_SUFFIX 64) @@ -49,9 +51,10 @@ endif() ########################################################################### # Find UHD ########################################################################### -find_package(UHD) +find_package(UHD 4.6) if(UHD_FOUND) message(STATUS "Found UHD:") + message(STATUS " * Version: ${UHD_VERSION}") include_directories(${UHD_INCLUDE_DIRS}) message(STATUS " * INCLUDES = ${UHD_INCLUDE_DIRS}") link_directories(${UHD_LIBRARIES}) @@ -67,6 +70,30 @@ else() message(WARNING "UHD not found. Cannot build block controllers.") endif() +########################################################################### +# Find Python and uhd Python module +########################################################################### +include(UHDPython) + +PYTHON_CHECK_MODULE( + "UHD Python API" + "uhd" + "uhd.__version__ == '${UHD_VERSION}'" + HAVE_PYTHON_MODULE_UHD +) + +if(pybind11_FOUND AND HAVE_PYTHON_MODULE_UHD) + set(ENABLE_PYTHON_API TRUE + CACHE BOOL "Enable Python API") +else() + set(ENABLE_PYTHON_API FALSE + CACHE BOOL "Enable Python API") +endif() + +if(ENABLE_PYTHON_API) + message(STATUS "Enabling Python API for this module.") +endif() + ########################################################################### # Find FPGA ########################################################################### @@ -179,3 +206,6 @@ if(UHD_FOUND) add_subdirectory(lib) add_subdirectory(apps) endif() +if(ENABLE_PYTHON_API) + add_subdirectory(python) +endif() diff --git a/host/examples/rfnoc-example/cmake/Modules/UHDPython.cmake b/host/examples/rfnoc-example/cmake/Modules/UHDPython.cmake new file mode 100644 index 000000000..4fdacc243 --- /dev/null +++ b/host/examples/rfnoc-example/cmake/Modules/UHDPython.cmake @@ -0,0 +1,370 @@ +# +# Copyright 2010-2011 Ettus Research LLC +# Copyright 2018 Ettus Research, a National Instruments Company +# Copyright 2019 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Note: When modifying this file, check if the changes also need to go into +# host/examples/rfnoc-example/cmake/Modules/UHDPython.cmake. + +if (POLICY CMP0094) + # See https://cmake.org/cmake/help/v3.15/policy/CMP0094.html + # set Python3_FIND_STRATEGY to LOCATION - this ensures that Python from + # sysroot is used first when cross-compiling + # note: policy CMP0094 is available starting with CMake 3.15 + cmake_policy(SET CMP0094 NEW) +endif() + +if(NOT DEFINED INCLUDED_UHD_PYTHON_CMAKE) +set(INCLUDED_UHD_PYTHON_CMAKE TRUE) + +######################################################################## +# Setup Python Part 0: Pybind11 +# +# We do this first so it doesn't interfere with the other steps. In +# particular, searching for pybind11 will mess with PYTHON_VERSION. +######################################################################## +find_package(pybind11 ${PYBIND11_MIN_VERSION} QUIET) + +######################################################################## +# Setup Python Part 1: Find the interpreters +######################################################################## +message(STATUS "") +message(STATUS "Configuring the Python interpreter...") +#this allows the user to override PYTHON_EXECUTABLE +if(PYTHON_EXECUTABLE) + set(PYTHONINTERP_FOUND TRUE) +endif(PYTHON_EXECUTABLE) + +if(NOT PYTHONINTERP_FOUND) + find_package(Python3 ${PYTHON_MIN_VERSION} QUIET) + if(Python3_Interpreter_FOUND) + set(PYTHON_VERSION ${Python3_VERSION}) + set(PYTHON_EXECUTABLE ${Python3_EXECUTABLE}) + set(PYTHONINTERP_FOUND TRUE) + endif(Python3_Interpreter_FOUND) +endif(NOT PYTHONINTERP_FOUND) + +if(NOT PYTHONINTERP_FOUND) + find_package(PythonInterp ${PYTHON_MIN_VERSION} QUIET) + if(PYTHONINTERP_FOUND) + set(PYTHON_VERSION ${PYTHON_VERSION_STRING}) + endif(PYTHONINTERP_FOUND) +endif(NOT PYTHONINTERP_FOUND) + +# If that fails, try using the build-in find program routine. +if(NOT PYTHONINTERP_FOUND) + message(STATUS "Attempting to find Python without CMake...") + find_program(PYTHON_EXECUTABLE NAMES python3 python3.6 python3.7 python3.8 python3.9) + if(PYTHON_EXECUTABLE) + set(PYTHONINTERP_FOUND TRUE) + endif(PYTHON_EXECUTABLE) +endif(NOT PYTHONINTERP_FOUND) + +if(NOT PYTHON_VERSION) + message(STATUS "Manually determining build Python version...") + execute_process(COMMAND ${PYTHON_EXECUTABLE} -c " +import sys +print('{}.{}.{}'.format( + sys.version_info.major, + sys.version_info.minor, + sys.version_info.micro))" + OUTPUT_VARIABLE PYTHON_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE) +endif(NOT PYTHON_VERSION) + +# If we still haven't found a Python interpreter, then we're done. +if(NOT PYTHONINTERP_FOUND) + message(FATAL_ERROR "Error: Python interpreter required by the build system.") +endif(NOT PYTHONINTERP_FOUND) +if(NOT PYTHON_EXECUTABLE) + message(FATAL_ERROR "Error: Python interpreter required by the build system.") +endif(NOT PYTHON_EXECUTABLE) + +#make the path to the executable appear in the cmake gui +set(PYTHON_EXECUTABLE ${PYTHON_EXECUTABLE} CACHE FILEPATH + "python buildtime interpreter") + +message(STATUS "Python interpreter: ${PYTHON_EXECUTABLE} Version: ${PYTHON_VERSION}") +message(STATUS "Override with: -DPYTHON_EXECUTABLE=") + +#this allows the user to override RUNTIME_PYTHON_EXECUTABLE +if(NOT RUNTIME_PYTHON_EXECUTABLE) + if(CMAKE_CROSSCOMPILING) + message(STATUS "Cross compiling, setting python runtime to /usr/bin/python3") + message(STATUS "and interpreter to min. required version ${PYTHON_MIN_VERSION}") + message(STATUS "If this is not what you want, please set RUNTIME_PYTHON_EXECUTABLE") + message(STATUS "and RUNTIME_PYTHON_VERSION manually") + set(RUNTIME_PYTHON_EXECUTABLE "/usr/bin/python3") + set(RUNTIME_PYTHON_VERSION ${PYTHON_MIN_VERSION}) + set(EXACT_ARGUMENT "") + else(CMAKE_CROSSCOMPILING) + #default to the buildtime interpreter + set(RUNTIME_PYTHON_EXECUTABLE ${PYTHON_EXECUTABLE}) + set(RUNTIME_PYTHON_VERSION ${PYTHON_VERSION}) + set(EXACT_ARGUMENT "EXACT") + endif(CMAKE_CROSSCOMPILING) +else(NOT RUNTIME_PYTHON_EXECUTABLE) + set(EXACT_ARGUMENT "EXACT") +endif(NOT RUNTIME_PYTHON_EXECUTABLE) + +if(NOT RUNTIME_PYTHON_VERSION) + message(STATUS "Manually determining runtime Python version...") + execute_process(COMMAND ${PYTHON_EXECUTABLE} -c " +from __future__ import print_function +import sys +print('{}.{}.{}'.format( + sys.version_info.major, + sys.version_info.minor, + sys.version_info.micro))" + OUTPUT_VARIABLE RUNTIME_PYTHON_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE) +endif(NOT RUNTIME_PYTHON_VERSION) + +#make the path to the executable appear in the cmake gui +set(RUNTIME_PYTHON_EXECUTABLE ${RUNTIME_PYTHON_EXECUTABLE} CACHE FILEPATH + "python runtime interpreter") + +message(STATUS "Python runtime interpreter: ${RUNTIME_PYTHON_EXECUTABLE} Version: ${RUNTIME_PYTHON_VERSION}") +message(STATUS "Override with: -DRUNTIME_PYTHON_EXECUTABLE=") + +############################################################################### +# Determine if a Python module is installed, or, more generally, determine +# if some condition that Python can report through a Boolean expression is +# met. This macro allows one or more modules to be imported and a Python +# Boolean expression to be evaluated. +# +# - desc: +# Description of what's being checked (for user feedback) +# - module: +# The module(s) to be passed to the `import` command +# - bool_expr: +# A Python expression to be evaluated that returns True or False based on +# the presence or absence of the module (or in the general case, the +# condition being checked) +# - have_ver: +# The variable name to be set to TRUE if the Python expression returns True, +# or FALSE otherwise +macro(PYTHON_CHECK_MODULE desc module bool_expr have_var) + message(STATUS "") + message(STATUS "Python checking for ${desc}") + execute_process( + COMMAND ${PYTHON_EXECUTABLE} -c " +######################################### +try: + import ${module} +except: + exit(1) +try: + assert ${bool_expr} +except: + exit(2) +exit(0) +#########################################" + RESULT_VARIABLE python_result + ) + if(python_result EQUAL 0) + message(STATUS "Python checking for ${desc} - found") + set(${have_var} TRUE) + elseif(python_result EQUAL 1) + message(STATUS "Python checking for ${desc} - \"import ${module}\" failed (is it installed?)") + set(${have_var} FALSE) + elseif(python_result EQUAL 2) + message(STATUS "Python checking for ${desc} - \"assert ${bool_expr}\" failed") + set(${have_var} FALSE) + else() + message(STATUS "Python checking for ${desc} - unknown error") + set(${have_var} FALSE) + endif() +endmacro(PYTHON_CHECK_MODULE) + + +############################################################################### +# Determine if a Python module is installed and if it meets a minimum required +# version. +# +# - desc: +# Description of what's being checked (for user feedback) +# - module: +# The module to be `import`ed +# - module_version_expr: +# A Python expression to be evaluated that returns the module version string +# (usually "module_name.__version__", but may be tailored for non-conformant +# modules, or other custom use cases) +# - min_module_version: +# The minimum version required of the module as a canonical Python version +# string ("major.minor.micro") as defined in PEP 440 +# - have_ver: +# The variable name to be set to TRUE if the module is present and meets +# the minimum version requirement or FALSE otherwise +macro(PYTHON_CHECK_MODULE_VERSION desc module module_version_expr min_module_version have_var) + message(STATUS "") + message(STATUS "Python checking for ${desc}") + execute_process( + COMMAND ${PYTHON_EXECUTABLE} -c " +######################################### +try: + import ${module} +except: + exit(1) +try: + version = ${module_version_expr} + print(version) +except: + exit(2) +exit(0) +#########################################" + RESULT_VARIABLE python_result + OUTPUT_VARIABLE version_output + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(python_result EQUAL 0) + if(${version_output} VERSION_GREATER_EQUAL ${min_module_version}) + message(STATUS "Python checking for ${desc} - ${version_output} satisfies minimum required version ${min_module_version}") + set(${have_var} TRUE) + else() + message(STATUS "Python checking for ${desc} - ${version_output} does not satisfy minimum required version ${min_module_version}") + set(${have_var} FALSE) + endif() + elseif(python_result EQUAL 1) + message(STATUS "Python checking for ${desc} - \"import ${module}\" failed (is it installed?)") + set(${have_var} FALSE) + elseif(python_result EQUAL 2) + message(STATUS "Python checking for ${desc} - evaluation of \"${module_version_expr}\" failed") + set(${have_var} FALSE) + else() + message(STATUS "Python checking for ${desc} - unknown error") + set(${have_var} FALSE) + endif() +endmacro(PYTHON_CHECK_MODULE_VERSION) + + +############################################################################### +# Install a Python module into a system/prefix/virtualenv location. +# +# - LIBTARGET: Is there a library target included in this module (e.g., pyuhd)? +# If so, state its name here. It will update RPATH on Linux/Unix +# systems. +# - MODULE: Name of module (e.g., 'uhd') +macro(PYTHON_INSTALL_MODULE) + cmake_parse_arguments( + _py_install_mod + "" "LIBTARGET;MODULE" "" + ${ARGN} + ) + + # Check if we're in a virtual environment -- the rules are a bit different + # there. + PYTHON_CHECK_MODULE( + "virtual environment" + "sys" + "sys.prefix != sys.base_prefix" + HAVE_PYTHON_VIRTUALENV + ) + + if(HAVE_PYTHON_VIRTUALENV) + message( + STATUS + "Python virtual environment detected -- Ignoring UHD_PYTHON_DIR.") + # In virtualenvs, let setuptools do its thing + install(CODE "message(\"Installing ${_py_install_mod_MODULE} Python module into venv via pip.\")") + install(CODE + "execute_process(COMMAND pip3 install . --force-reinstall WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})") + else() + # Otherwise, use sysconfig to determine the correct relative path for Python + # packages, and install to our prefix + if(NOT DEFINED UHD_PYTHON_DIR) + execute_process(COMMAND ${PYTHON_EXECUTABLE} -c + # Avoid the posix_local install scheme + "import os,sysconfig;\ + install_scheme = 'posix_user';\ + platlib = sysconfig.get_path('platlib', scheme=install_scheme);\ + prefix = sysconfig.get_config_var('prefix');\ + print(os.path.relpath(platlib, prefix));" + OUTPUT_VARIABLE UHD_PYTHON_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + endif(NOT DEFINED UHD_PYTHON_DIR) + file(TO_CMAKE_PATH ${UHD_PYTHON_DIR} UHD_PYTHON_DIR) + + message( + STATUS + "Installing '${_py_install_mod_MODULE}' Python module to: " + "${CMAKE_INSTALL_PREFIX}/${UHD_PYTHON_DIR}") + # We use sysconfig (above) to figure out the destination path, and then + # we simply copy this module recursively into its final destination. + install(DIRECTORY + ${CMAKE_CURRENT_BINARY_DIR}/${_py_install_mod_MODULE} + DESTINATION ${UHD_PYTHON_DIR} + COMPONENT pythonapi + ) + # On Linux/Unix systems, we must properly install the library file. + # install(DIRECTORY) will treat the .so file like any other file, which + # means it won't update its RPATH, and thus the RPATH would be stuck to the + # build directory. + if(UNIX AND _py_install_mod_LIBTARGET) + install(TARGETS ${_py_install_mod_LIBTARGET} + DESTINATION ${UHD_PYTHON_DIR}/${_py_install_mod_MODULE} + ) + endif() + endif(HAVE_PYTHON_VIRTUALENV) +endmacro(PYTHON_INSTALL_MODULE) + +############################################################################### +# Part 2: Python Libraries +############################################################################### +# The libraries must match the RUNTIME_PYTHON_EXECUTABLE's version. +# - Figure out version +# - See if Python3_LIBRARIES is already set (or Python2_LIBRARIES) +if(NOT PYTHON_LIBRARIES OR NOT PYTHON_INCLUDE_DIRS) + message(STATUS "Finding Python Libraries...") + find_package(PythonLibs ${RUNTIME_PYTHON_VERSION} ${EXACT_ARGUMENT} QUIET) + if(NOT RUNTIME_PYTHON_VERSION VERSION_LESS 3) + if(NOT PYTHON_LIBRARIES OR NOT PYTHON_INCLUDE_DIRS) + find_package(Python3 ${RUNTIME_PYTHON_VERSION} + ${EXACT_ARGUMENT} + QUIET + COMPONENTS Interpreter Development) + if(Python3_Development_FOUND) + set(PYTHON_LIBRARIES ${Python3_LIBRARIES}) + set(PYTHON_INCLUDE_DIRS ${Python3_INCLUDE_DIRS}) + endif(Python3_Development_FOUND) + endif(NOT PYTHON_LIBRARIES OR NOT PYTHON_INCLUDE_DIRS) + else(NOT RUNTIME_PYTHON_VERSION VERSION_LESS 3) + if(NOT PYTHON_LIBRARIES OR NOT PYTHON_INCLUDE_DIRS) + find_package(Python2 ${RUNTIME_PYTHON_VERSION} + ${EXACT_ARGUMENT} + QUIET + COMPONENTS Interpreter Development) + if(Python2_Development_FOUND) + set(PYTHON_LIBRARIES ${Python2_LIBRARIES}) + set(PYTHON_INCLUDE_DIRS ${Python2_INCLUDE_DIRS}) + endif(Python2_Development_FOUND) + endif(NOT PYTHON_LIBRARIES OR NOT PYTHON_INCLUDE_DIRS) + endif(NOT RUNTIME_PYTHON_VERSION VERSION_LESS 3) + if(NOT PYTHON_LIBRARIES OR NOT PYTHON_INCLUDE_DIRS) + message(STATUS "Could not find Python Libraries.") + endif(NOT PYTHON_LIBRARIES OR NOT PYTHON_INCLUDE_DIRS) +endif(NOT PYTHON_LIBRARIES OR NOT PYTHON_INCLUDE_DIRS) + +if(PYTHON_LIBRARIES AND PYTHON_INCLUDE_DIRS) + set(HAVE_PYTHON_LIBS TRUE) + message(STATUS "Python Libraries: ${PYTHON_LIBRARIES}") + message(STATUS "Python include directories: ${PYTHON_INCLUDE_DIRS}") +else(PYTHON_LIBRARIES AND PYTHON_INCLUDE_DIRS) + set(HAVE_PYTHON_LIBS FALSE) +endif(PYTHON_LIBRARIES AND PYTHON_INCLUDE_DIRS) + +if(NOT PYTHON_LIBRARY) + set(PYTHON_LIBRARIES ${PYTHON_LIBRARIES} CACHE FILEPATH + "Python libraries") + mark_as_advanced(PYTHON_LIBRARIES) +endif(NOT PYTHON_LIBRARY) +if(NOT PYTHON_INCLUDE_DIR) + set(PYTHON_INCLUDE_DIRS ${PYTHON_INCLUDE_DIRS} CACHE FILEPATH + "Python include dirs") + mark_as_advanced(PYTHON_INCLUDE_DIRS) +endif(NOT PYTHON_INCLUDE_DIR) + +endif(NOT DEFINED INCLUDED_UHD_PYTHON_CMAKE) diff --git a/host/examples/rfnoc-example/examples/rx_gain_estimate_power.py b/host/examples/rfnoc-example/examples/rx_gain_estimate_power.py new file mode 100755 index 000000000..27018e756 --- /dev/null +++ b/host/examples/rfnoc-example/examples/rx_gain_estimate_power.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# +# Copyright 2024 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +Test the gain block with the RFNoC Python API +""" + + +import argparse +import sys +import uhd +import rfnoc_example + +def parse_args(): + """Parse the command line arguments""" + parser = argparse.ArgumentParser() + parser.add_argument("-a", "--args", default="", + help="USRP Device Args") + parser.add_argument("--gain-block", "-G", type=str, default="0/Gain#0", + help="Gain block to use. Defaults to \"0/Gain#0\".") + parser.add_argument("--radio-block", "-R", type=str, default="0/Radio#0", + help="Radio block to use. Defaults to \"0/Radio#0\".") + parser.add_argument("-f", "--freq", type=float, required=True, + help="Center Frequency") + parser.add_argument("-g", "--gain", type=float, required=True, + help="Analog gain") + parser.add_argument("-d", "--digital-gain", type=int, required=True, + help="Digital gain") + parser.add_argument("-c", "--channel", type=int, default=0, + help="Radio block channel index") + parser.add_argument("-t", "--antenna", + help="USRP RX Antenna") + parser.add_argument("-r", "--rate", default=1e6, type=float, + help="Sampling Rate") + parser.add_argument("-b", "--bandwidth", type=float, + help="Analog filter bandwidth (if supported)") + parser.add_argument("-n", "--samps-per-est", type=float, default=1e6, + help="Samples per estimate.") + return parser.parse_args() + + +def main(): + """Go go go!""" + args = parse_args() + # Create graph and block references + graph = uhd.rfnoc.RfnocGraph(args.args) + gain_block_base = graph.get_block(args.gain_block) + gain_block = rfnoc_example.GainBlockControl(gain_block_base) + radio_block = uhd.rfnoc.RadioControl(graph.get_block(args.radio_block)) + radio_chan = args.channel + assert radio_chan < radio_block.get_num_output_ports() + rx_streamer = graph.create_rx_streamer(1, uhd.usrp.StreamArgs("fc32", "sc16")) + # Set up graph + blocks_in_graph = uhd.rfnoc.connect_through_blocks( + graph, + radio_block.get_unique_id(), radio_chan, + gain_block_base.get_unique_id(), 0) + ddc_block_id, ddc_port = next(( + (x.dst_blockid, x.dst_port) + for x in blocks_in_graph + if uhd.rfnoc.BlockID(x.dst_blockid).get_block_name() == 'DDC' + ), (None, None)) + graph.connect( + gain_block_base.get_unique_id(), 0, + rx_streamer, 0) + graph.commit() + # Apply settings + radio_block.set_rx_frequency(args.freq, radio_chan) + print( + f"Requested RX frequency: {args.freq/1e9:.3f} GHz, " + f"actual RX frequency: {radio_block.get_rx_frequency(radio_chan)/1e9:.3f} GHz") + radio_block.set_rx_antenna(args.antenna, radio_chan) + print( + f"Requested RX antenna: {args.antenna}, " + f"actual RX antenna: {radio_block.get_rx_antenna(radio_chan)}") + radio_block.set_rx_gain(args.gain, radio_chan) + print( + f"Requested analog RX gain: {args.gain:.1f} dB, " + f"actual analog RX gain: {radio_block.get_rx_gain(radio_chan):.1f} dB") + gain_block.set_gain_value(args.digital_gain) + print( + f"Requested digital RX gain factor: x{args.digital_gain}, " + f"actual digital RX gain factor: x{gain_block.get_gain_value()}") + if ddc_block_id: + ddc_block = uhd.rfnoc.DdcBlockControl(graph.get_block(ddc_block_id)) + ddc_block.set_output_rate(args.rate, ddc_port) + rate = ddc_block.get_output_rate(ddc_port) + else: + radio_block.set_rate(args.rate) + rate = radio_block.get_rate() + print( + f"Requested RX rate: {args.rate/1e6} Msps, " + f"actual RX rate: {rate} Msps") + # Now do a power reading + print(f"Estimating RX power from {args.samps_per_est/rate} s worth of signal...") + power_dbfs = uhd.dsp.signals.get_usrp_power( + rx_streamer, num_samps=int(args.samps_per_est)) + print(f"Received power: {power_dbfs:+6.2f} dBFS") + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/host/examples/rfnoc-example/lib/gain_block_control_python.hpp b/host/examples/rfnoc-example/lib/gain_block_control_python.hpp new file mode 100644 index 000000000..c6079b5a5 --- /dev/null +++ b/host/examples/rfnoc-example/lib/gain_block_control_python.hpp @@ -0,0 +1,23 @@ +// +// Copyright 2024 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#pragma once + +#include +#include + +using namespace rfnoc::example; + +void export_gain_block_control(py::module& m) +{ + py::class_(m, "gain_block_control") + .def(py::init( + &uhd::rfnoc::block_controller_factory::make_from)) + .def("set_gain_value", &gain_block_control::set_gain_value, py::arg("gain")) + .def("get_gain_value", &gain_block_control::get_gain_value) + + ; +} diff --git a/host/examples/rfnoc-example/python/CMakeLists.txt b/host/examples/rfnoc-example/python/CMakeLists.txt new file mode 100644 index 000000000..3290611f4 --- /dev/null +++ b/host/examples/rfnoc-example/python/CMakeLists.txt @@ -0,0 +1,84 @@ +# +# Copyright 2024 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +############################################################################### +# Build Python wrapper module for C++ -> Python bindings +############################################################################### +# Global Python API constants +set(PYMODULE_NAME rfnoc_example) +set(SETUP_PY_IN "${CMAKE_CURRENT_SOURCE_DIR}/setup.py.in") +set(SETUP_PY "${CMAKE_CURRENT_BINARY_DIR}/setup.py") +set(TIMESTAMP_FILE "${CMAKE_CURRENT_BINARY_DIR}/build/timestamp") +# convert binary directory to native format to use in SETUP_PY file. +file(TO_NATIVE_PATH ${CMAKE_CURRENT_BINARY_DIR} NATIVE_CURRENT_BINARY_DIR) +configure_file(${SETUP_PY_IN} ${SETUP_PY}) + + +############################################################################### +# Build Python wrapper module for C++ -> Python bindings +############################################################################### +pybind11_add_module(${PYMODULE_NAME}_python + MODULE + pyrfnoc-example.cpp +) +target_include_directories(${PYMODULE_NAME}_python + PUBLIC + ${CMAKE_SOURCE_DIR}/lib + ${CMAKE_SOURCE_DIR}/include +) +target_link_libraries( + ${PYMODULE_NAME}_python + PRIVATE + pybind11::pybind11 + uhd +) + +# Copy pybind bindings library to the staging directory (it will get copied to +# its final destination further down) +add_custom_command(TARGET ${PYMODULE_NAME}_python + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_CURRENT_BINARY_DIR}/${PYMODULE_NAME}/$) + + +# List of Python files that are part of the module but don't get +# generated during build time. +# Note: When adding Python files into the module, they don't get added to the +# dependency list until CMake is re-run. +file(GLOB_RECURSE PYMODULE_FILE + ${CMAKE_CURRENT_SOURCE_DIR}/${PYMODULE_NAME}/*.py +) + +# If we're not in a virtual environment, then we need to figure out where to +# install the Python module. +if(NOT DEFINED UHD_PYTHON_DIR) + execute_process(COMMAND ${PYTHON_EXECUTABLE} -c + # Avoid the posix_local install scheme + "import os,sysconfig;\ + install_scheme = 'posix_prefix';\ + platlib = sysconfig.get_path('platlib', scheme=install_scheme);\ + prefix = sysconfig.get_config_var('prefix');\ + print(os.path.relpath(platlib, prefix));" + OUTPUT_VARIABLE UHD_PYTHON_DIR OUTPUT_STRIP_TRAILING_WHITESPACE + ) +endif(NOT DEFINED UHD_PYTHON_DIR) + +# This copies the contents of the Python module into the build directory. We will +# use that as a staging ground for installing the final module to the system. +# We make sure that we always have an up-to-date copy in here. +add_custom_command(OUTPUT ${TIMESTAMP_FILE} + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/${PYMODULE_NAME} ${CMAKE_CURRENT_BINARY_DIR}/${PYMODULE_NAME} + COMMAND ${PYTHON_EXECUTABLE} ${SETUP_PY} -q build + COMMAND ${CMAKE_COMMAND} -E touch ${TIMESTAMP_FILE} + DEPENDS ${PYMODULE_FILE}) + +add_custom_target(pymodule_library + ALL DEPENDS ${TIMESTAMP_FILE} ${PYMODULE_NAME}_python) + +# Now install the Python module from the build directory to the final destination. +PYTHON_INSTALL_MODULE( + MODULE "${PYMODULE_NAME}" + LIBTARGET "${PYMODULE_NAME}_python" +) diff --git a/host/examples/rfnoc-example/python/pyrfnoc-example.cpp b/host/examples/rfnoc-example/python/pyrfnoc-example.cpp new file mode 100644 index 000000000..9dc69cb2c --- /dev/null +++ b/host/examples/rfnoc-example/python/pyrfnoc-example.cpp @@ -0,0 +1,38 @@ +// +// Copyright 2024 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +// NOTE: Most of these includes, as well as the numpy support, are not required +// for rfnoc-example, but are commonly required +#include +#include +#include + +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION +#include + +namespace py = pybind11; + +#include "gain_block_control_python.hpp" + +// We need this hack because import_array() returns NULL +// for newer Python versions. +// This function is also necessary because it ensures access to the C API +// and removes a warning. +void* init_numpy() +{ + import_array(); + return NULL; +} + +PYBIND11_MODULE(rfnoc_example_python, m) +{ + // Initialize the numpy C API + // (otherwise we will see segmentation faults) + init_numpy(); + + // uhd::rfnoc::python::export_noc_block_base(m); + export_gain_block_control(m); +} diff --git a/host/examples/rfnoc-example/python/rfnoc_example/__init__.py b/host/examples/rfnoc-example/python/rfnoc_example/__init__.py new file mode 100644 index 000000000..b85ef6f9d --- /dev/null +++ b/host/examples/rfnoc-example/python/rfnoc_example/__init__.py @@ -0,0 +1,14 @@ +# +# Copyright 2024 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +rfnoc-example: Example module for Python support +""" + +# Import all bindings from C++ +from . import rfnoc_example_python as lib + +# In UHD, we use CamelCase for names in Python, so we'll do the same here +GainBlockControl = lib.gain_block_control diff --git a/host/examples/rfnoc-example/python/setup.py.in b/host/examples/rfnoc-example/python/setup.py.in new file mode 100755 index 000000000..7989a71de --- /dev/null +++ b/host/examples/rfnoc-example/python/setup.py.in @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# +# Copyright 2024 Ettus Research, a National Instruments Company +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +"""Setup file for rfnoc-example module""" + +from setuptools import setup, find_packages + +packages = find_packages() + +print("Including packages in rfnoc-example:", packages) + +setup(name='rfnoc_example', + version='${VERSION_MAJOR}.${VERSION_API}.${VERSION_ABI}', + description='rfnoc-example: An example module for RFNoC OOT Python support', + classifiers=[ + 'Development Status :: 4 - Beta', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'Programming Language :: C++', + 'Programming Language :: Python', + 'Topic :: System :: Hardware :: Hardware Drivers', + ], + keywords='SDR UHD USRP', + author='Ettus Research', + author_email='packages@ettus.com', + url='https://www.ettus.com/', + license='GPLv3', + package_dir={'': r'${NATIVE_CURRENT_BINARY_DIR}'}, + package_data={'uhd': ['*.so']}, + zip_safe=False, + packages=packages, + install_requires=[]) -- cgit v1.2.3-59-g8ed1b