python: Make discoverable features available

This makes all discoverable features available and creates a pattern for
any future discoverable features.
This commit is contained in:
Martin Anderseck 2026-07-24 16:29:42 +02:00 committed by mkoop-ni
parent 05443d301d
commit 538a2e3fb3
7 changed files with 251 additions and 0 deletions

View File

@ -0,0 +1,40 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
#ifndef INCLUDED_UHD_FEATURES_PYTHON_HPP
#define INCLUDED_UHD_FEATURES_PYTHON_HPP
#include <uhd/features/discoverable_feature.hpp>
#include <uhd/features/spi_getter_iface.hpp>
void export_discoverable_feature(py::module& m)
{
using discoverable_feature = uhd::features::discoverable_feature;
using feature_id_t = discoverable_feature::feature_id_t;
py::enum_<feature_id_t>(m, "discoverable_feature_id")
.value("RESERVED0", feature_id_t::RESERVED0)
.value("RESERVED1", feature_id_t::RESERVED1)
.value("FPGA_LOAD_NOTIFICATION", feature_id_t::FPGA_LOAD_NOTIFICATION)
.value("ADC_SELF_CALIBRATION", feature_id_t::ADC_SELF_CALIBRATION)
.value("REF_CLK_CALIBRATION", feature_id_t::REF_CLK_CALIBRATION)
.value("TRIG_IO_MODE", feature_id_t::TRIG_IO_MODE)
.value("GPIO_POWER", feature_id_t::GPIO_POWER)
.value("SPI_GETTER_IFACE", feature_id_t::SPI_GETTER_IFACE)
.value("INTERNAL_SYNC", feature_id_t::INTERNAL_SYNC)
.value("GPS", feature_id_t::GPS)
.value("TX_COMPLEX_GAIN", feature_id_t::TX_COMPLEX_GAIN)
.value("RX_COMPLEX_GAIN", feature_id_t::RX_COMPLEX_GAIN);
py::class_<uhd::features::spi_periph_config_t>(m, "spi_periph_config")
.def(py::init<>())
.def_readwrite("periph_cs", &uhd::features::spi_periph_config_t::periph_cs)
.def_readwrite("periph_sdi", &uhd::features::spi_periph_config_t::periph_sdi)
.def_readwrite("periph_sdo", &uhd::features::spi_periph_config_t::periph_sdo)
.def_readwrite("periph_clk", &uhd::features::spi_periph_config_t::periph_clk);
}
#endif // INCLUDED_UHD_FEATURES_PYTHON_HPP

View File

@ -6,15 +6,86 @@
#pragma once
#include <uhd/features/adc_self_calibration_iface.hpp>
#include <uhd/features/complex_gain_iface.hpp>
#include <uhd/features/discoverable_feature.hpp>
#include <uhd/features/internal_sync_iface.hpp>
#include <uhd/features/spi_getter_iface.hpp>
#include <uhd/rfnoc/block_controller_factory_python.hpp>
#include <uhd/rfnoc/radio_control.hpp>
#include <pybind11/stl.h>
using namespace uhd::rfnoc;
namespace {
using feature_id_t = uhd::features::discoverable_feature::feature_id_t;
bool radio_has_feature(radio_control& self, const feature_id_t feature_id)
{
switch (feature_id) {
case uhd::features::discoverable_feature::INTERNAL_SYNC:
return self.has_feature<uhd::features::internal_sync_iface>();
case uhd::features::discoverable_feature::TX_COMPLEX_GAIN:
return self.has_feature<uhd::features::tx_complex_gain_iface>();
case uhd::features::discoverable_feature::RX_COMPLEX_GAIN:
return self.has_feature<uhd::features::rx_complex_gain_iface>();
case uhd::features::discoverable_feature::SPI_GETTER_IFACE:
return self.has_feature<uhd::features::spi_getter_iface>();
case uhd::features::discoverable_feature::ADC_SELF_CALIBRATION:
return self.has_feature<uhd::features::adc_self_calibration_iface>();
default:
return false;
}
}
py::object radio_get_feature_obj(radio_control& self, const feature_id_t feature_id)
{
switch (feature_id) {
case uhd::features::discoverable_feature::INTERNAL_SYNC:
if (!self.has_feature<uhd::features::internal_sync_iface>()) {
return py::none();
}
return py::cast(&self.get_feature<uhd::features::internal_sync_iface>(),
py::return_value_policy::reference_internal,
py::cast(&self));
case uhd::features::discoverable_feature::TX_COMPLEX_GAIN:
if (!self.has_feature<uhd::features::tx_complex_gain_iface>()) {
return py::none();
}
return py::cast(&self.get_feature<uhd::features::tx_complex_gain_iface>(),
py::return_value_policy::reference_internal,
py::cast(&self));
case uhd::features::discoverable_feature::RX_COMPLEX_GAIN:
if (!self.has_feature<uhd::features::rx_complex_gain_iface>()) {
return py::none();
}
return py::cast(&self.get_feature<uhd::features::rx_complex_gain_iface>(),
py::return_value_policy::reference_internal,
py::cast(&self));
case uhd::features::discoverable_feature::SPI_GETTER_IFACE:
if (!self.has_feature<uhd::features::spi_getter_iface>()) {
return py::none();
}
return py::cast(&self.get_feature<uhd::features::spi_getter_iface>(),
py::return_value_policy::reference_internal,
py::cast(&self));
case uhd::features::discoverable_feature::ADC_SELF_CALIBRATION:
if (!self.has_feature<uhd::features::adc_self_calibration_iface>()) {
return py::none();
}
return py::cast(
&self.get_feature<uhd::features::adc_self_calibration_iface>(),
py::return_value_policy::reference_internal,
py::cast(&self));
default:
throw uhd::key_error(
"Feature ID is not currently bound in the Python API for radio_control.");
}
}
} // namespace
void export_radio_control(py::module& m)
{
// Re-import ALL_CHANS here to avoid linker errors
@ -37,9 +108,24 @@ void export_radio_control(py::module& m)
py::arg("chan"),
py::arg("time") = py::none())
.def("get_gain_coeff", &uhd::features::rx_complex_gain_iface::get_gain_coeff);
py::class_<uhd::features::adc_self_calibration_iface>(m, "adc_self_calibration")
.def("run",
static_cast<void (uhd::features::adc_self_calibration_iface::*)(
const size_t)>(&uhd::features::adc_self_calibration_iface::run),
py::arg("chan"))
.def("run",
static_cast<void (uhd::features::adc_self_calibration_iface::*)(
const size_t, const uhd::device_addr_t)>(
&uhd::features::adc_self_calibration_iface::run),
py::arg("chan"),
py::arg("params"));
py::class_<radio_control, noc_block_base, radio_control::sptr>(m, "radio_control")
.def(py::init(&block_controller_factory<radio_control>::make_from))
.def("enumerate_features",
[](radio_control& self) { return self.enumerate_features(); })
.def("has_feature", &radio_has_feature, py::arg("feature_id"))
.def("get_feature", &radio_get_feature_obj, py::arg("feature_id"))
.def("set_rate", &radio_control::set_rate)
.def("get_rate", &radio_control::get_rate)
.def("get_rate_range", &radio_control::get_rate_range)

View File

@ -12,6 +12,8 @@
#include <uhd/features/gpio_power_iface.hpp>
#include <uhd/features/gps_iface.hpp>
#include <uhd/features/ref_clk_calibration_iface.hpp>
#include <uhd/features/spi_getter_iface.hpp>
#include <uhd/features/trig_io_mode_iface.hpp>
#include <uhd/rfnoc/block_id.hpp>
#include <uhd/rfnoc/filter_node.hpp>
#include <uhd/rfnoc/graph_edge.hpp>
@ -22,7 +24,9 @@
#include <uhd/rfnoc_graph.hpp>
#include <uhd/transport/adapter_id.hpp>
#include <uhd/types/device_addr.hpp>
#include <uhd/types/trig_io_mode.hpp>
#include <uhd/utils/graph_utils.hpp>
#include <uhdlib/features/fpga_load_notification_iface.hpp>
#include <pybind11/operators.h>
#include <pybind11/stl.h>
#include <memory>
@ -33,6 +37,72 @@ using namespace uhd::rfnoc;
namespace { // anon
using feature_id_t = uhd::features::discoverable_feature::feature_id_t;
bool mb_has_feature(uhd::rfnoc::mb_controller& self, const feature_id_t feature_id)
{
switch (feature_id) {
case uhd::features::discoverable_feature::FPGA_LOAD_NOTIFICATION:
return self.has_feature<uhd::features::fpga_load_notification_iface>();
case uhd::features::discoverable_feature::GPIO_POWER:
return self.has_feature<uhd::features::gpio_power_iface>();
case uhd::features::discoverable_feature::GPS:
return self.has_feature<uhd::features::gps_iface>();
case uhd::features::discoverable_feature::REF_CLK_CALIBRATION:
return self.has_feature<uhd::features::ref_clk_calibration_iface>();
case uhd::features::discoverable_feature::TRIG_IO_MODE:
return self.has_feature<uhd::features::trig_io_mode_iface>();
default:
return false;
}
}
py::object mb_get_feature_obj(
uhd::rfnoc::mb_controller& self, const feature_id_t feature_id)
{
switch (feature_id) {
case uhd::features::discoverable_feature::FPGA_LOAD_NOTIFICATION:
if (!self.has_feature<uhd::features::fpga_load_notification_iface>()) {
return py::none();
}
return py::cast(
&self.get_feature<uhd::features::fpga_load_notification_iface>(),
py::return_value_policy::reference_internal,
py::cast(&self));
case uhd::features::discoverable_feature::GPIO_POWER:
if (!self.has_feature<uhd::features::gpio_power_iface>()) {
return py::none();
}
return py::cast(&self.get_feature<uhd::features::gpio_power_iface>(),
py::return_value_policy::reference_internal,
py::cast(&self));
case uhd::features::discoverable_feature::GPS:
if (!self.has_feature<uhd::features::gps_iface>()) {
return py::none();
}
return py::cast(&self.get_feature<uhd::features::gps_iface>(),
py::return_value_policy::reference_internal,
py::cast(&self));
case uhd::features::discoverable_feature::REF_CLK_CALIBRATION:
if (!self.has_feature<uhd::features::ref_clk_calibration_iface>()) {
return py::none();
}
return py::cast(&self.get_feature<uhd::features::ref_clk_calibration_iface>(),
py::return_value_policy::reference_internal,
py::cast(&self));
case uhd::features::discoverable_feature::TRIG_IO_MODE:
if (!self.has_feature<uhd::features::trig_io_mode_iface>()) {
return py::none();
}
return py::cast(&self.get_feature<uhd::features::trig_io_mode_iface>(),
py::return_value_policy::reference_internal,
py::cast(&self));
default:
throw uhd::key_error(
"Feature ID is not currently bound in the Python API for mb_controller.");
}
}
using timekeeper = mb_controller::timekeeper;
// Make a "trampoline" class to help Pybind11
@ -72,6 +142,36 @@ void export_rfnoc(py::module& m)
{
#define RIS_FIELD(name) .def_readwrite(#name, &register_iface_stats::name)
py::class_<uhd::features::discoverable_feature,
uhd::features::discoverable_feature::sptr>(m, "discoverable_feature")
.def("get_feature_name", &uhd::features::discoverable_feature::get_feature_name);
py::class_<uhd::spi_iface, uhd::spi_iface::sptr>(m, "spi_iface")
.def("transact_spi",
&uhd::spi_iface::transact_spi,
py::arg("which_slave"),
py::arg("config"),
py::arg("data"),
py::arg("num_bits"),
py::arg("readback"))
.def("read_spi",
&uhd::spi_iface::read_spi,
py::arg("which_slave"),
py::arg("config"),
py::arg("data"),
py::arg("num_bits"))
.def("write_spi",
&uhd::spi_iface::write_spi,
py::arg("which_slave"),
py::arg("config"),
py::arg("data"),
py::arg("num_bits"));
py::class_<uhd::features::spi_getter_iface,
uhd::features::spi_getter_iface::sptr,
uhd::features::discoverable_feature>(m, "spi_getter")
.def("get_spi_ref", &uhd::features::spi_getter_iface::get_spi_ref);
py::class_<register_iface_stats>(m, "register_iface_stats")
// clang-format off
RIS_FIELD(ctrl_packets_sent)
@ -289,6 +389,17 @@ void export_rfnoc(py::module& m)
.def("store_ref_clk_tuning_word",
&uhd::features::ref_clk_calibration_iface::store_ref_clk_tuning_word);
py::enum_<uhd::trig_io_mode_t>(m, "trig_io_mode_t")
.value("PPS_OUTPUT", uhd::trig_io_mode_t::PPS_OUTPUT)
.value("INPUT", uhd::trig_io_mode_t::INPUT)
.value("OFF", uhd::trig_io_mode_t::OFF);
py::class_<uhd::features::fpga_load_notification_iface>(m, "fpga_load_notification")
.def("onload", &uhd::features::fpga_load_notification_iface::onload);
py::class_<uhd::features::trig_io_mode_iface>(m, "trig_io_mode")
.def("set_trig_io_mode", &uhd::features::trig_io_mode_iface::set_trig_io_mode);
py::class_<detail::filter_node>(m, "filter_node")
.def("get_rx_filter_names", &detail::filter_node::get_rx_filter_names)
.def("get_rx_filter", &detail::filter_node::get_rx_filter)
@ -300,6 +411,10 @@ void export_rfnoc(py::module& m)
py::class_<mb_controller, mb_controller::sptr>(m, "mb_controller")
.def("get_num_timekeepers", &mb_controller::get_num_timekeepers)
.def("get_timekeeper", &mb_controller::get_timekeeper)
.def("enumerate_features",
[](mb_controller& self) { return self.enumerate_features(); })
.def("has_feature", &mb_has_feature, py::arg("feature_id"))
.def("get_feature", &mb_get_feature_obj, py::arg("feature_id"))
.def("init", &mb_controller::init)
.def("get_mboard_name", &mb_controller::get_mboard_name)
.def("set_time_source", &mb_controller::set_time_source)

View File

@ -20,7 +20,9 @@ void export_spi_config(py::module& m)
.value("EDGE_FALL", spi_edge_t::EDGE_FALL);
py::class_<spi_config_t>(m, "spi_config")
.def(py::init<>())
.def(py::init<spi_edge_t>())
.def(py::init<spi_edge_t, size_t>(), py::arg("edge"), py::arg("divider"))
// Properties
.def_readwrite("mosi_edge", &spi_config_t::mosi_edge)

View File

@ -16,6 +16,7 @@ namespace py = pybind11;
#include "cal/cal_python.hpp"
#include "device_python.hpp"
#include "features/features_python.hpp"
#include "include/uhdlib/usrp/common/max287x_python.hpp"
#include "property_tree_python.hpp"
#include "rfnoc/ddc_block_control_python.hpp"
@ -87,6 +88,7 @@ PYBIND11_MODULE(libpyuhd, m)
export_types(types_module);
export_time_spec(types_module);
export_spi_config(types_module);
export_discoverable_feature(types_module);
export_metadata(types_module);
export_sensors(types_module);
export_tune(types_module);

View File

@ -34,5 +34,9 @@ SwitchboardBlockControl = lib.rfnoc.switchboard_block_control
VectorIirBlockControl = lib.rfnoc.vector_iir_block_control
WindowBlockControl = lib.rfnoc.window_block_control
# Dynamic discoverable-feature API exports (CamelCase only)
SpiIface = lib.rfnoc.spi_iface
SpiGetter = lib.rfnoc.spi_getter
connect_through_blocks = lib.rfnoc.connect_through_blocks
get_block_chain = lib.rfnoc.get_block_chain

View File

@ -33,3 +33,5 @@ TuneResult = lib.types.tune_result
separate_device_addr = lib.types.separate_device_addr
combine_device_addrs = lib.types.combine_device_addrs
Direction = lib.types.direction_t
DiscoverableFeatureID = lib.types.discoverable_feature_id
SPIPeriphConfig = lib.types.spi_periph_config