uhd/host/lib/usrp/common/mpm_client.cpp.mako
Abhishek Verma 05443d301d host/mpm: make gRPC robust to idle teardowns
The main RPC channel can sit idle for a long time (e.g. an interactive
Python session) while only the claim-loop channel sees traffic. Without
keepalive, gRPC self-closes the idle transport (~30 min) and the next
RPC fails re-establishing within its per-call deadline.

With this change the client now pings every 30 s to keep the channel
warm, and the server is configured to accept that cadence instead of
replying GOAWAY "too_many_pings".
2026-08-23 02:23:02 +02:00

707 lines
34 KiB
Mako

//
// Auto-generated C++ client implementation
// Generated from protobuf definition
//
// Copyright 2025 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// clang-format off
#include "mpm_client.hpp"
#include <uhd/exception.hpp>
#include <uhd/utils/log.hpp>
#include <stdexcept>
#include <array>
#include <atomic>
#include <chrono>
#include <optional>
#include <mutex>
#include <thread>
#include <grpcpp/grpcpp.h>
#include "mpm_server.pb.h"
#include "mpm_server.grpc.pb.h"
// GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL is not declared by older gRPC releases
// (e.g. the version shipped with Ubuntu 20.04), but the underlying channel-arg
// key string is stable across versions.
#ifndef GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL
#define GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL "grpc.use_local_subchannel_pool"
#endif
// Keepalive channel-arg keys. These string keys are stable
// across gRPC versions; provide fallback definitions for older releases that
// do not export the macros.
#ifndef GRPC_ARG_KEEPALIVE_TIME_MS
#define GRPC_ARG_KEEPALIVE_TIME_MS "grpc.keepalive_time_ms"
#endif
#ifndef GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS
#define GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS "grpc.keepalive_permit_without_calls"
#endif
#ifndef GRPC_ARG_HTTP2_MAX_PINGS_WITHOUT_DATA
#define GRPC_ARG_HTTP2_MAX_PINGS_WITHOUT_DATA "grpc.http2.max_pings_without_data"
#endif
using uhd::rpc_exception;
<%
def build_param_log_strings(parameters):
"""Helper function to build parameter logging strings"""
param_strs = []
for param in parameters:
parts = param.strip().split()
param_name = parts[-1]
param_type = ' '.join(parts[:-1])
# Show only a 4-char prefix for correlation in large setups; never log the full token
if param_name == 'token':
param_strs.append(f'"token=" << {param_name}.substr(0, 4) << "****"')
continue
# Handle complex types that can't be streamed
if 'std::vector<std::map' in param_type:
param_strs.append(f'"{param_name}=[vector<map> size=" << {param_name}.size() << "]"')
elif 'std::vector<std::vector' in param_type:
param_strs.append(f'"{param_name}=[vector<vector> size=" << {param_name}.size() << "]"')
elif 'std::vector<std::string>' in param_type:
param_strs.append(f'"{param_name}=[vector<string> size=" << {param_name}.size() << "]"')
elif 'std::vector<int' in param_type:
param_strs.append(f'"{param_name}=[vector<int> size=" << {param_name}.size() << "]"')
elif 'std::vector<double>' in param_type or 'std::vector<float>' in param_type:
param_strs.append(f'"{param_name}=[vector<double> size=" << {param_name}.size() << "]"')
elif 'std::map<std::string, std::vector' in param_type:
param_strs.append(f'"{param_name}=[map<string,bytes> size=" << {param_name}.size() << "]"')
elif 'std::map<std::string, std::string>' in param_type:
param_strs.append(f'"{param_name}=[map size=" << {param_name}.size() << "]"')
else:
param_strs.append(f'"{param_name}=" << {param_name}')
return param_strs
%>
#include <uhdlib/usrp/common/mpmd_timeouts.hpp>
constexpr int MAX_GRPC_MESSAGE_SIZE = 128 * 1024 * 1024; // 128 MB in bytes
class rpc_client_impl : public uhd::rpc_client, public std::enable_shared_from_this<rpc_client_impl>
{
public:
class dboard_iface_impl;
private:
static constexpr size_t MAX_DBOARDS = 2; // 0, 1 (two daughterboards)
std::unique_ptr<${package_name}::${service_name}::Stub> stub_;
std::string _token;
std::chrono::milliseconds _timeout;
std::optional<std::chrono::milliseconds> _scoped_timeout;
mutable std::recursive_mutex _rpc_call_mutex;
std::array<std::unique_ptr<dboard_iface_impl>, MAX_DBOARDS> dboard_instances_;
// Diagnostics for tracking gRPC channel/transport lifecycle. Each client
// gets a unique id so its create/RPC/destroy events can be correlated in
// the logs (e.g. to see whether tearing down a stale session's channel
// drops the active session's connection).
static std::atomic<uint64_t> _client_id_counter;
uint64_t _client_id;
std::string _server_address;
std::shared_ptr<grpc::Channel> _channel;
// Human-readable name for a gRPC connectivity state.
static const char* connectivity_state_str(grpc_connectivity_state s)
{
switch (s) {
case GRPC_CHANNEL_IDLE: return "IDLE";
case GRPC_CHANNEL_CONNECTING: return "CONNECTING";
case GRPC_CHANNEL_READY: return "READY";
case GRPC_CHANNEL_TRANSIENT_FAILURE: return "TRANSIENT_FAILURE";
case GRPC_CHANNEL_SHUTDOWN: return "SHUTDOWN";
default: return "UNKNOWN";
}
}
// RAII guard that applies a scoped timeout while serializing concurrent RPCs.
class timeout_scope_impl : public uhd::rpc_client::timeout_scope
{
public:
timeout_scope_impl(rpc_client_impl& parent, std::chrono::milliseconds timeout)
: _parent(parent), _lock(parent._rpc_call_mutex)
{
_parent._scoped_timeout = timeout;
}
~timeout_scope_impl() override
{
_parent._scoped_timeout.reset();
}
private:
rpc_client_impl& _parent;
std::unique_lock<std::recursive_mutex> _lock;
};
// Helper method to convert StringMap to std::map
std::map<std::string, std::string> stringmap_to_stdmap(const ${package_name}::StringMap& sm) {
std::map<std::string, std::string> result;
for (const auto& pair : sm.data()) {
result[pair.first] = pair.second;
}
return result;
}
// Helper method to convert BytesMap to eeprom_map_t
std::map<std::string, std::vector<uint8_t>> bytesmap_to_eeprom_map(const ${package_name}::BytesMap& bm) {
std::map<std::string, std::vector<uint8_t>> result;
for (const auto& pair : bm.data()) {
result[pair.first] = std::vector<uint8_t>(pair.second.begin(), pair.second.end());
}
return result;
}
// Helper method to convert SensorValueMap to std::map
std::map<std::string, std::string> sensorvaluemap_to_stdmap(const ${package_name}::SensorValueMap& svm) {
std::map<std::string, std::string> result;
for (const auto& pair : svm.data()) {
result[pair.first] = pair.second;
}
return result;
}
// Convert std::map -> StringMap
void stdmap_to_stringmap(const std::map<std::string, std::string>& stdmap, ${package_name}::StringMap* sm) {
auto* data = sm->mutable_data();
for (const auto& pair : stdmap) {
(*data)[pair.first] = pair.second;
}
}
// Convert eeprom_map_t -> BytesMap
void eeprom_map_to_bytesmap(const std::map<std::string, std::vector<uint8_t>>& eeprom_map, ${package_name}::BytesMap* bm) {
auto* data = bm->mutable_data();
for (const auto& pair : eeprom_map) {
(*data)[pair.first] = std::string(pair.second.begin(), pair.second.end());
}
}
// Convert vector of maps to repeated StringMap
void vector_to_repeated_stringmap(const std::vector<std::map<std::string, std::string>>& vec,
google::protobuf::RepeatedPtrField<${package_name}::StringMap>* repeated) {
for (const auto& map : vec) {
${package_name}::StringMap* sm = repeated->Add();
stdmap_to_stringmap(map, sm);
}
}
// Convert repeated StringMap to vector of maps
std::vector<std::map<std::string, std::string>> repeated_stringmap_to_vector(
const google::protobuf::RepeatedPtrField<${package_name}::StringMap>& repeated) {
std::vector<std::map<std::string, std::string>> result;
for (const auto& sm : repeated) {
result.push_back(stringmap_to_stdmap(sm));
}
return result;
}
// Convert MethodInfo to std::map (avoid leaking protobuf types)
std::map<std::string, std::string> methodinfo_to_stdmap(const ${package_name}::MethodInfo& mi) {
std::map<std::string, std::string> result;
result["method_name"] = mi.method_name();
result["docstring"] = mi.docstring();
result["requires_claim"] = mi.requires_claim() ? "true" : "false";
return result;
}
public:
rpc_client_impl(const std::string& server_name, uint16_t port, uint64_t timeout_ms)
: _timeout(std::chrono::milliseconds(timeout_ms))
, _client_id(_client_id_counter++)
{
std::string server_address = server_name + ":" + std::to_string(port);
_server_address = server_address;
// Set channel arguments with MAX_GRPC_MESSAGE_SIZE limit (in bytes).
grpc::ChannelArguments args;
args.SetInt(GRPC_ARG_MAX_RECEIVE_MESSAGE_LENGTH, MAX_GRPC_MESSAGE_SIZE);
args.SetInt(GRPC_ARG_MAX_SEND_MESSAGE_LENGTH, MAX_GRPC_MESSAGE_SIZE);
// Give every channel its own subchannel (and thus its own HTTP/2
// transport) instead of sharing gRPC's global subchannel pool. All
// clients here target the same MPM address with identical channel
// args, so with the default global pool they would collapse onto a
// single shared TCP/HTTP2 connection. When a stale session's channel
// is destroyed (which, with the phase-coherence tests, happens tens of
// seconds after that session was unclaimed), tearing down the shared
// transport surfaces on the *active* session as
// "<RPC> RPC failed: Socket closed" even though the MPM is healthy.
// A local subchannel pool isolates each channel so destroying an old
// session cannot drop the active session's connection.
args.SetInt(GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL, 1);
// Keep idle channels alive. The main RPC channel can sit idle for a
// long time (e.g. an interactive Python session left open) while only
// the separate claimer channel sees traffic. Without keepalive, gRPC
// sends nothing on an idle channel, so it self-closes the transport
// (GRPC_ARG_CLIENT_IDLE_TIMEOUT_MS). The next RPC then has to re-
// establish the connection within its short per-call deadline and may
// reports "Deadline Exceeded". Sending HTTP/2 pings every 60 s keeps
// the flow warm. The server must permit this ping cadence otherwise
// it responds with GOAWAY "too_many_pings".
args.SetInt(GRPC_ARG_KEEPALIVE_TIME_MS, 60000);
// gRPC pings keepalive even if the channel is idle (no active RPC calls)
args.SetInt(GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS, 1);
// By default gRPC stops sending keepalive pings after 2 pings without
// any data frame (GRPC_ARG_HTTP2_MAX_PINGS_WITHOUT_DATA default = 2),
// which would silently defeat keepalive on an idle channel.
// Set to 0 (unlimited) so pings continue for the lifetime of an idle
// connection.
args.SetInt(GRPC_ARG_HTTP2_MAX_PINGS_WITHOUT_DATA, 0);
auto channel = grpc::CreateCustomChannel(server_address, grpc::InsecureChannelCredentials(), args);
_channel = channel;
stub_ = ${package_name}::${service_name}::NewStub(channel);
// Eagerly establish the TCP connection so subsequent RPC calls don't
// also pay the connection-setup cost within their per-call deadline.
// WaitForConnected() returns false on timeout; we let the first RPC
// call report the failure naturally in that case.
channel->WaitForConnected(std::chrono::system_clock::now() + _timeout);
UHD_LOGGER_TRACE("MPM_CLIENT")
<< "rpc_client #" << _client_id << " CREATED -> " << _server_address
<< " state=" << connectivity_state_str(_channel->GetState(false));
// Pre-allocate all daughterboard instances
for (size_t i = 0; i < MAX_DBOARDS; ++i) {
dboard_instances_[i] = std::make_unique<dboard_iface_impl>(this, i);
}
}
~rpc_client_impl() override
{
// This destruction is the event of interest: with the phase-coherence
// tests, a stale session's client is destroyed tens of seconds after
// it was unclaimed. Log it (with the last-known channel state) so it
// can be correlated with any "Socket closed" seen on the active session.
UHD_LOGGER_TRACE("MPM_CLIENT")
<< "rpc_client #" << _client_id << " DESTROYED -> " << _server_address
<< " state="
<< (_channel ? connectivity_state_str(_channel->GetState(false)) : "n/a");
}
void set_token(const std::string& token) override {
_token = token;
}
// Set the default timeout used for RPCs without a scoped override.
void set_timeout(uint64_t timeout_ms) override {
std::lock_guard<std::recursive_mutex> rpc_call_lock(_rpc_call_mutex);
_timeout = std::chrono::milliseconds(timeout_ms);
}
// Return an RAII guard that applies a timeout override for its lifetime.
uhd::rpc_client::timeout_scope_uptr set_scope_timeout(uint64_t timeout_ms) override {
return std::make_unique<timeout_scope_impl>(
*this, std::chrono::milliseconds(timeout_ms));
}
uhd::rpc_client::sptr get_raw_rpc_client() override {
return shared_from_this();
}
uint64_t get_client_id() const override {
return _client_id;
}
private:
// Helper method to get the appropriate deadline for a call. Scoped
// timeout has precedence while the returned timeout_scope is alive.
std::chrono::system_clock::time_point get_deadline_for_call(
std::chrono::milliseconds default_timeout) {
if (_scoped_timeout.has_value()) {
return std::chrono::system_clock::now() + _scoped_timeout.value();
}
return std::chrono::system_clock::now() + default_timeout;
}
public:
// Main domain method implementations
% for method in main_methods:
${method['return_type']} ${method['name']}(${', '.join(method['parameters'])}) override {
<% param_strs = build_param_log_strings(method['parameters']) %>
% if param_strs:
UHD_LOG_TRACE("RPC", ">>> ${method['camel_name']}(" << ${'<< ", " << '.join(param_strs)} << ")");
% else:
UHD_LOG_TRACE("RPC", ">>> ${method['camel_name']}()");
% endif
${package_name}::${method['request_type']} request;
${package_name}::${method['response_type']} response;
grpc::ClientContext context;
std::lock_guard<std::recursive_mutex> rpc_call_lock(_rpc_call_mutex);
// Set timeout
% if method.get('timeout'):
% if method['timeout'].isdigit():
context.set_deadline(get_deadline_for_call(std::chrono::milliseconds(${method['timeout']})));
% else:
context.set_deadline(get_deadline_for_call(std::chrono::milliseconds(${method['timeout']})));
% endif
% else:
context.set_deadline(get_deadline_for_call(_timeout));
% endif
% if method['requires_token']:
request.set_token(_token); // Set token
% endif
// Set parameters
% for param in method['parameters']:
<%
param_parts = param.strip().split()
param_name = param_parts[-1]
param_type = ' '.join(param_parts[:-1])
%>
% if 'std::vector<std::map<std::string, std::string>>' in param_type:
vector_to_repeated_stringmap(${param_name}, request.mutable_${param_name}());
% elif 'std::vector<std::vector<uint8_t>>' in param_type or 'std::vector<std::vector<unsigned char>>' in param_type:
// Convert vector of byte vectors to repeated bytes field
for (const auto& data : ${param_name}) {
request.add_${param_name}(data.data(), data.size());
}
% elif 'std::vector<std::string>' in param_type:
// Convert vector of strings to repeated string field
for (const auto& item : ${param_name}) {
request.add_${param_name}(item);
}
% elif 'std::vector<int' in param_type:
// Convert vector of integers to repeated int field
for (const auto& item : ${param_name}) {
request.add_${param_name}(item);
}
% elif 'std::vector<double>' in param_type or 'std::vector<float>' in param_type:
// Convert vector of doubles/floats to repeated field
for (const auto& item : ${param_name}) {
request.add_${param_name}(item);
}
% elif 'std::map<std::string, std::vector<uint8_t>>' in param_type:
eeprom_map_to_bytesmap(${param_name}, request.mutable_${param_name}());
% elif 'std::map<std::string, std::string>' in param_type:
stdmap_to_stringmap(${param_name}, request.mutable_${param_name}());
% else:
request.set_${param_name}(${param_name});
% endif
% endfor
grpc::Status status = stub_->${method['camel_name']}(&context, request, &response);
if (status.ok()) {
// Return response
% if method['return_type'] == 'void':
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} [void]");
return;
% elif method['response_field'] is None:
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} [none]");
return;
% elif method['return_type'] == 'double' or method['return_type'] == 'float':
auto result = response.${method['response_field']['name']}();
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} = " << result);
return result;
% elif method['return_type'] == 'uint16_t':
// Cast uint32 to uint16_t for get_proto_ver
auto result = static_cast<uint16_t>(response.${method['response_field']['name']}());
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} = " << result);
return result;
% elif method['return_type'] == 'std::string':
auto result = response.${method['response_field']['name']}();
% if method['response_field']['name'] == 'token':
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} with token = " << result.substr(0, 4) << "****");
% else:
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} = \"" << result << "\"");
% endif
return result;
% elif method['return_type'] == 'std::map<std::string, std::vector<uint8_t>>':
auto result = bytesmap_to_eeprom_map(response.${method['response_field']['name']}());
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} = map<string,bytes>[" << result.size() << " entries]");
return result;
% elif method['return_type'] == 'std::map<std::string, std::string>':
% if 'map<' in method['response_field']['type']:
std::map<std::string, std::string> result;
for (const auto& pair : response.${method['response_field']['name']}()) {
result[pair.first] = pair.second;
}
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} = map[" << result.size() << " entries]");
return result;
% elif method['response_field']['type'] == 'SensorValueMap':
auto result = sensorvaluemap_to_stdmap(response.${method['response_field']['name']}());
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} = sensor_map[" << result.size() << " entries]");
return result;
% else:
auto result = stringmap_to_stdmap(response.${method['response_field']['name']}());
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} = map[" << result.size() << " entries]");
return result;
% endif
% elif method['return_type'] == 'std::vector<std::map<std::string, std::string>>':
% if 'MethodInfo' in method['response_field']['type']:
// Convert protobuf MethodInfo to standard map to avoid leaking protobuf types
std::vector<std::map<std::string, std::string>> result;
for (const auto& item : response.${method['response_field']['name']}()) {
result.push_back(methodinfo_to_stdmap(item));
}
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} = vector<map>[" << result.size() << " items]");
return result;
% else:
auto result = repeated_stringmap_to_vector(response.${method['response_field']['name']}());
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} = vector<map>[" << result.size() << " items]");
return result;
% endif
% elif method['return_type'].startswith('std::pair<'):
// Handle pair return type
<%
f0 = method['pair_field_names'][0]
f1 = method['pair_field_names'][1]
inner = method['return_type'][len('std::pair<'):-1]
depth, split = 0, 0
for _i, _c in enumerate(inner):
if _c == '<': depth += 1
elif _c == '>': depth -= 1
elif _c == ',' and depth == 0: split = _i; break
f0_type = inner[:split].strip()
f1_type = inner[split+1:].strip()
%>
${f0_type} ${f0} = response.${f0}();
% if f1_type.startswith('std::vector<'):
${f1_type} ${f1}(response.${f1}().begin(), response.${f1}().end());
% else:
${f1_type} ${f1} = response.${f1}();
% endif
auto result = std::make_pair(${f0}, ${f1});
% if f1_type.startswith('std::vector<'):
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} = pair(${f0}=" << result.first << ", ${f1}=[" << result.second.size() << " items])");
% else:
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} = pair(${f0}=" << result.first << ", ${f1}=" << result.second << ")");
% endif
return result;
% elif method['return_type'].startswith('std::vector<'):
% if 'MethodInfo' in method['response_field']['type']:
// Convert protobuf MethodInfo to standard map to avoid leaking protobuf types
std::vector<std::map<std::string, std::string>> result;
for (const auto& item : response.${method['response_field']['name']}()) {
result.push_back(methodinfo_to_stdmap(item));
}
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} = vector<map>[" << result.size() << " items]");
return result;
% else:
// Convert protobuf repeated field to std::vector
${method['return_type']} result;
for (const auto& item : response.${method['response_field']['name']}()) {
result.push_back(item);
}
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} = vector[" << result.size() << " items]");
return result;
% endif
% else:
auto result = response.${method['response_field']['name']}();
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']} = " << result);
return result;
% endif
} else if (status.error_code() == grpc::StatusCode::PERMISSION_DENIED) {
UHD_LOG_TRACE("RPC", "!!! ${method['camel_name']} FAILED [" << status.error_code() << "]: Authentication error");
throw rpc_exception("Authentication failed: " + status.error_message());
} else {
UHD_LOG_TRACE("RPC", "!!! ${method['camel_name']} FAILED [" << status.error_code() << "]: " << status.error_message());
UHD_LOGGER_TRACE("MPM_CLIENT")
<< "rpc_client #" << _client_id << " ${method['camel_name']} FAILED ["
<< status.error_code() << "]: " << status.error_message()
<< " | channel state="
<< (_channel ? connectivity_state_str(_channel->GetState(false)) : "n/a")
<< " -> " << _server_address;
throw rpc_exception("${method['camel_name']} RPC failed: " + status.error_message());
}
}
% endfor
// Daughterboard implementation
class dboard_iface_impl : public uhd::rpc_client::dboard_iface
{
private:
rpc_client_impl* parent_;
size_t db_idx_;
public:
dboard_iface_impl(rpc_client_impl* parent, size_t db_idx)
: parent_(parent), db_idx_(db_idx) {}
% for method in dboard_methods:
${method['return_type']} ${method['name']}(${', '.join(method['parameters'])}) override {
<% param_strs = build_param_log_strings(method['parameters']) %>
% if param_strs:
UHD_LOG_TRACE("RPC", ">>> ${method['camel_name']}(db_idx=" << db_idx_ << ", " << ${'<< ", " << '.join(param_strs)} << ")");
% else:
UHD_LOG_TRACE("RPC", ">>> ${method['camel_name']}(db_idx=" << db_idx_ << ")");
% endif
${package_name}::${method['request_type']} request;
${package_name}::${method['response_type']} response;
grpc::ClientContext context;
std::lock_guard<std::recursive_mutex> rpc_call_lock(parent_->_rpc_call_mutex);
// Set timeout
% if method.get('timeout'):
% if method['timeout'].isdigit():
context.set_deadline(parent_->get_deadline_for_call(std::chrono::milliseconds(${method['timeout']})));
% else:
context.set_deadline(parent_->get_deadline_for_call(std::chrono::milliseconds(${method['timeout']})));
% endif
% else:
context.set_deadline(parent_->get_deadline_for_call(parent_->_timeout));
% endif
% if method['requires_token']:
request.set_token(parent_->_token); // Set token
% endif
request.set_db_idx(static_cast<uint32_t>(db_idx_)); // Set db_idx
// Set other parameters
% for param in method['parameters']:
<%
param_parts = param.strip().split()
param_name = param_parts[-1]
param_type = ' '.join(param_parts[:-1])
%>
% if 'std::vector<std::map<std::string, std::string>>' in param_type:
parent_->vector_to_repeated_stringmap(${param_name}, request.mutable_${param_name}());
% elif 'std::vector<int' in param_type:
// Convert vector of integers to repeated int field
for (const auto& item : ${param_name}) {
request.add_${param_name}(item);
}
% elif 'std::vector<double>' in param_type or 'std::vector<float>' in param_type:
// Convert vector of doubles/floats to repeated field
for (const auto& item : ${param_name}) {
request.add_${param_name}(item);
}
% elif 'std::map<std::string, std::vector<uint8_t>>' in param_type:
parent_->eeprom_map_to_bytesmap(${param_name}, request.mutable_${param_name}());
% elif 'std::map<std::string, std::string>' in param_type:
parent_->stdmap_to_stringmap(${param_name}, request.mutable_${param_name}());
% else:
request.set_${param_name}(${param_name});
% endif
% endfor
grpc::Status status = parent_->stub_->${method['camel_name']}(&context, request, &response);
if (status.ok()) {
// Return response
% if method['return_type'] == 'void':
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']}[db=" << db_idx_ << "] [void]");
return;
% elif method['response_field'] is None:
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']}[db=" << db_idx_ << "] [none]");
return;
% elif method['return_type'] == 'double' or method['return_type'] == 'float':
auto result = response.${method['response_field']['name']}();
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']}[db=" << db_idx_ << "] = " << result);
return result;
% elif method['return_type'] == 'std::string':
auto result = response.${method['response_field']['name']}();
% if method['response_field']['name'] == 'token':
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']}[db=" << db_idx_ << "] with token = " << result.substr(0, 4) << "****");
% else:
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']}[db=" << db_idx_ << "] = \"" << result << "\"");
% endif
return result;
% elif method['return_type'] == 'std::map<std::string, std::vector<uint8_t>>':
auto result = parent_->bytesmap_to_eeprom_map(response.${method['response_field']['name']}());
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']}[db=" << db_idx_ << "] = map<string,bytes>[" << result.size() << " entries]");
return result;
% elif method['return_type'] == 'std::map<std::string, std::string>':
% if 'map<' in method['response_field']['type']:
std::map<std::string, std::string> result;
for (const auto& pair : response.${method['response_field']['name']}()) {
result[pair.first] = pair.second;
}
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']}[db=" << db_idx_ << "] = map[" << result.size() << " entries]");
return result;
% elif method['response_field']['type'] == 'SensorValueMap':
auto result = parent_->sensorvaluemap_to_stdmap(response.${method['response_field']['name']}());
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']}[db=" << db_idx_ << "] = sensor_map[" << result.size() << " entries]");
return result;
% else:
auto result = parent_->stringmap_to_stdmap(response.${method['response_field']['name']}());
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']}[db=" << db_idx_ << "] = map[" << result.size() << " entries]");
return result;
% endif
% elif method['return_type'] == 'std::vector<std::map<std::string, std::string>>':
auto result = parent_->repeated_stringmap_to_vector(response.${method['response_field']['name']}());
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']}[db=" << db_idx_ << "] = vector<map>[" << result.size() << " items]");
return result;
% elif method['return_type'].startswith('std::pair<'):
// Handle pair return type
<%
f0 = method['pair_field_names'][0]
f1 = method['pair_field_names'][1]
inner = method['return_type'][len('std::pair<'):-1]
depth, split = 0, 0
for _i, _c in enumerate(inner):
if _c == '<': depth += 1
elif _c == '>': depth -= 1
elif _c == ',' and depth == 0: split = _i; break
f0_type = inner[:split].strip()
f1_type = inner[split+1:].strip()
%>
${f0_type} ${f0} = response.${f0}();
% if f1_type.startswith('std::vector<'):
${f1_type} ${f1}(response.${f1}().begin(), response.${f1}().end());
% else:
${f1_type} ${f1} = response.${f1}();
% endif
auto result = std::make_pair(${f0}, ${f1});
% if f1_type.startswith('std::vector<'):
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']}[db=" << db_idx_ << "] = pair(${f0}=" << result.first << ", ${f1}=[" << result.second.size() << " items])");
% else:
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']}[db=" << db_idx_ << "] = pair(${f0}=" << result.first << ", ${f1}=" << result.second << ")");
% endif
return result;
% elif method['return_type'].startswith('std::vector<'):
// Convert protobuf repeated field to std::vector
${method['return_type']} result;
for (const auto& item : response.${method['response_field']['name']}()) {
result.push_back(item);
}
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']}[db=" << db_idx_ << "] = vector[" << result.size() << " items]");
return result;
% else:
auto result = response.${method['response_field']['name']}();
UHD_LOG_TRACE("RPC", "<<< ${method['camel_name']}[db=" << db_idx_ << "] = " << result);
return result;
% endif
} else if (status.error_code() == grpc::StatusCode::PERMISSION_DENIED) {
UHD_LOG_TRACE("RPC", "!!! ${method['camel_name']}[db=" << db_idx_ << "] FAILED [" << status.error_code() << "]: Authentication error");
throw rpc_exception("Authentication failed: " + status.error_message());
} else {
UHD_LOG_TRACE("RPC", "!!! ${method['camel_name']}[db=" << db_idx_ << "] FAILED [" << status.error_code() << "]: " << status.error_message());
throw rpc_exception("${method['camel_name']} RPC failed: " + status.error_message());
}
}
% endfor
};
uhd::rpc_client::dboard_iface& get_dboard(size_t db_idx) override {
// Validate db_idx - supports 2 daughterboards (0, 1)
if (db_idx >= MAX_DBOARDS) {
throw rpc_exception("Invalid daughterboard index: " + std::to_string(db_idx) +
". Valid range is 0-" + std::to_string(MAX_DBOARDS-1));
}
return *dboard_instances_[db_idx];
}
// Make helper methods accessible to dboard implementation
friend class dboard_iface_impl;
};
std::atomic<uint64_t> rpc_client_impl::_client_id_counter{0};
// Factory implementation
uhd::rpc_client::sptr uhd::rpc_client::make(const std::string& server_name, uint16_t port, uint64_t timeout_ms) {
return std::make_shared<rpc_client_impl>(server_name, port, timeout_ms);
}
// clang-format on