From 978f51cf5751c16b599302ad5d81cde3abb2b01c Mon Sep 17 00:00:00 2001 From: Odysseus Date: Mon, 11 May 2026 14:56:34 -0700 Subject: [PATCH] Implement netremote type for rfattenuator tool --- src/linux/rfattenuator/cli/CMakeLists.txt | 1 + src/linux/rfattenuator/cli/Main.cxx | 294 ++++++++++++++++++++-- 2 files changed, 278 insertions(+), 17 deletions(-) diff --git a/src/linux/rfattenuator/cli/CMakeLists.txt b/src/linux/rfattenuator/cli/CMakeLists.txt index 34d00714..aa623416 100644 --- a/src/linux/rfattenuator/cli/CMakeLists.txt +++ b/src/linux/rfattenuator/cli/CMakeLists.txt @@ -8,6 +8,7 @@ target_sources(${PROJECT_NAME}-rfattenuator-cli-linux target_link_libraries(${PROJECT_NAME}-rfattenuator-cli-linux PRIVATE ${PROJECT_NAME}-rfattenuator-linux + ${PROJECT_NAME}-protocol ) set_target_properties(${PROJECT_NAME}-rfattenuator-cli-linux diff --git a/src/linux/rfattenuator/cli/Main.cxx b/src/linux/rfattenuator/cli/Main.cxx index 6d37a73d..055ec090 100644 --- a/src/linux/rfattenuator/cli/Main.cxx +++ b/src/linux/rfattenuator/cli/Main.cxx @@ -4,18 +4,172 @@ #include #include #include +#include #include #include #include #include #include +#include +#include +#include +#include +#include +#include #include #include // #include "RfAttenuatorSoftwareSimulated.hxx" namespace detail { +struct RfAttenuatorNetRemoteException : public RfAttenuatorException +{ + explicit RfAttenuatorNetRemoteException(std::string message) : + Message(std::move(message)) + { + } + + const char* + what() const noexcept override + { + return std::data(Message); + } + + std::string Message; +}; + +struct RfAttenuatorNetRemoteController : public IRfAttenuatorController +{ + explicit RfAttenuatorNetRemoteController(std::string address) : + m_address(std::move(address)), + m_channel(grpc::CreateChannel(m_address, grpc::InsecureChannelCredentials())), + m_client(Microsoft::Net::Remote::Service::NetRemoteRfAttenuator::NewStub(m_channel)) + { + if (m_channel == nullptr || m_client == nullptr) { + throw RfAttenuatorNetRemoteException(std::format("failed to create netremote client for '{}'", m_address)); + } + } + + void + Reset() override + { + const google::protobuf::Empty request{ }; + Microsoft::Net::Remote::RfAttenuator::ResetResult response{ }; + grpc::ClientContext clientContext{ }; + + auto status = m_client->Reset(&clientContext, request, &response); + ThrowIfRpcFailed(status, "Reset"); + ThrowIfOperationFailed(response.status(), "Reset"); + } + + RfAttenuatorProperties + GetProperties() override + { + const google::protobuf::Empty request{ }; + Microsoft::Net::Remote::RfAttenuator::GetPropertiesResult response{ }; + grpc::ClientContext clientContext{ }; + + auto status = m_client->GetProperties(&clientContext, request, &response); + ThrowIfRpcFailed(status, "GetProperties"); + ThrowIfOperationFailed(response.status(), "GetProperties"); + + std::vector channels{ }; + channels.reserve(static_cast(response.channels_size())); + for (const auto channel : response.channels()) { + channels.emplace_back(channel); + } + + return RfAttenuatorProperties{ + .Channels = std::move(channels), + .AttenuationRangeDbmMin = response.attenuationrangedbmmin(), + .AttenuationRangeDbmMax = response.attenuationrangedbmmax(), + .AttenuationStepDbmMin = response.attenuationstepdbmmin(), + .AttenuationStepDbmMax = response.attenuationstepdbmmax(), + .AttenuationAccuracyDbmMin = response.attenuationaccuracydbmmin(), + .AttenuationAccuracyDbmMax = response.attenuationaccuracydbmmax(), + .FrequencyBandwidthMHzMin = response.frequencybandwidthmhzmin(), + .FrequencyBandwidthMHzMax = response.frequencybandwidthmhzmax(), + .SupportsSweep = response.supportssweep(), + .Identification = response.identification(), + }; + } + + double + GetAttenuationForChannel(uint32_t channel) override + { + Microsoft::Net::Remote::RfAttenuator::GetAttenuationRequest request{ }; + request.set_channel(channel); + + Microsoft::Net::Remote::RfAttenuator::GetAttenuationResult response{ }; + grpc::ClientContext clientContext{ }; + + auto status = m_client->GetAttenuationForChannel(&clientContext, request, &response); + ThrowIfRpcFailed(status, "GetAttenuationForChannel"); + ThrowIfOperationFailed(response.status(), "GetAttenuationForChannel"); + + return response.attenuationdbm(); + } + + bool + SetAttenuationForChannel(uint32_t channel, double attenuation) override + { + Microsoft::Net::Remote::RfAttenuator::SetAttenuationRequest request{ }; + request.set_channel(channel); + request.set_attenuationdbm(attenuation); + + Microsoft::Net::Remote::RfAttenuator::SetAttenuationResult response{ }; + grpc::ClientContext clientContext{ }; + + auto status = m_client->SetAttenuationForChannel(&clientContext, request, &response); + ThrowIfRpcFailed(status, "SetAttenuationForChannel"); + ThrowIfOperationFailed(response.status(), "SetAttenuationForChannel"); + + return true; + } + + static void + ThrowIfRpcFailed(const grpc::Status& status, std::string_view operation) + { + if (!status.ok()) { + throw RfAttenuatorNetRemoteException( + std::format("{} RPC failed ({}: {})", operation, static_cast(status.error_code()), status.error_message())); + } + } + + static void + ThrowIfOperationFailed(const Microsoft::Net::Remote::RfAttenuator::RfAttenuatorOperationStatus& status, std::string_view operation) + { + if (status.code() != Microsoft::Net::Remote::RfAttenuator::RfAttenuatorOperationStatusCode::RfAttenuatorOperationStatusCodeSucceeded) { + throw RfAttenuatorNetRemoteException(std::format( + "{} operation failed (code={}, message='{}')", + operation, + static_cast(status.code()), + status.has_message() ? status.message() : "")); + } + } + +private: + std::string m_address{ }; + std::shared_ptr m_channel{ }; + std::unique_ptr m_client{ }; +}; + +bool +TryParsePort(const std::string& portString, uint16_t& port) +{ + std::istringstream ss{ portString }; + uint32_t parsedPort{ 0 }; + ss >> parsedPort; + + if (!ss || !ss.eof() || parsedPort > std::numeric_limits::max()) { + return false; + } + + port = static_cast(parsedPort); + return true; +} + void DisplayAttenuatorProperties(const RfAttenuatorProperties& properties, std::ostream& out) { @@ -160,19 +314,23 @@ ShowUsage(const char* programPath) const auto programName = std::filesystem::path(programPath).filename().string(); // clang-format off - std::cout << programName << " [attenuator-type-args] [validation-mode]" << std::endl + std::cout << programName << " [attenuator-type-args] [command]" << std::endl << std::endl << " Attenuator Types: " << std::endl << " 'software'" << std::endl << " 'socket'" << std::endl + << " 'netremote'" << std::endl << std::endl << " Attenuator Type Arguments:" << std::endl << " 'software': none" << std::endl << " 'socket': AFW83 " << std::endl + << " 'netremote': " << std::endl << std::endl - << " Validation Modes: " << std::endl - << " 'basic' (default)" << std::endl - << " 'extended'" << std::endl + << " Commands (optional): " << std::endl + << " 'get': get - Get attenuation for a channel" << std::endl + << " 'set': set - Set attenuation for a channel" << std::endl + << " 'basic': Run basic validation (default if no command specified)" << std::endl + << " 'extended': Run extended validation" << std::endl << std::endl; // clang-format on } @@ -184,8 +342,11 @@ main(int argc, char* argv[]) static const auto MinExpectedArguments = 1; static const auto MinExpectedArgumentsSoftware = 1; static const auto MinExpectedArgumentsSocket = 4; - static const auto ValidationModeArgBasic = "basic"; - static const auto ValidationModeArgExtended = "extended"; + static const auto MinExpectedArgumentsNetRemote = 3; + static const auto CommandArgBasic = "basic"; + static const auto CommandArgExtended = "extended"; + static const auto CommandArgGet = "get"; + static const auto CommandArgSet = "set"; if (argc < (MinExpectedArguments + 1)) { std::cerr << std::format("error: {} arguments are expected, {} specified", MinExpectedArguments, argc - 1) << std::endl; @@ -197,7 +358,6 @@ main(int argc, char* argv[]) int argIndex{ 1 }; int numArgsExpected{ 0 }; - std::string validationMode{ ValidationModeArgBasic }; std::string attenuatorType{ argv[argIndex++] }; if (attenuatorType == "software") { @@ -213,10 +373,6 @@ main(int argc, char* argv[]) return -1; } - if (argc > numArgsExpected + 1) { - validationMode = argv[argIndex++]; - } - attenuator = RfAttenuatorFactory::CreateSimulatedSoftwareAttenuator(); } else if (attenuatorType == "socket") { numArgsExpected = MinExpectedArgumentsSocket; @@ -235,13 +391,14 @@ main(int argc, char* argv[]) const std::string attenuatorName{ argv[argIndex++] }; const std::string ipAddress{ argv[argIndex++] }; const std::string portString{ argv[argIndex++] }; - if (argc > numArgsExpected + 1) { - validationMode = argv[argIndex++]; - } - std::istringstream ss{ portString }; uint16_t port{ 0 }; - ss >> port; + if (!detail::TryParsePort(portString, port)) { + std::cerr << std::format("invalid socket attenuator port '{}'", portString) << std::endl; + detail::ShowUsage(argv[0]); + return -1; + } + if (attenuatorName == "AFW83") { attenuator = RfAttenuatorFactory::CreateSocketAfw83Attenuator(std::move(ipAddress), port); } @@ -250,6 +407,39 @@ main(int argc, char* argv[]) detail::ShowUsage(argv[0]); return -1; } + } else if (attenuatorType == "netremote") { + numArgsExpected = MinExpectedArgumentsNetRemote; + + if (argc < numArgsExpected + 1) { + std::cerr << std::format( + "error: {} arguments are expected for netremote attenuators, {} specified", + numArgsExpected, + argc - 1) + << std::endl; + detail::ShowUsage(argv[0]); + return -1; + } + + const std::string ipAddress{ argv[argIndex++] }; + const std::string portString{ argv[argIndex++] }; + + uint16_t port{ 0 }; + if (!detail::TryParsePort(portString, port)) { + std::cerr << std::format("invalid netremote server port '{}'", portString) << std::endl; + detail::ShowUsage(argv[0]); + return -1; + } + + const auto address = std::format("{}:{}", ipAddress, port); + std::cout << std::format("Creating netremote attenuator client @ {} ... ", address); + + try { + attenuator = std::make_unique(address); + std::cout << "succeeded" << std::endl; + } catch (const std::exception& e) { + std::cout << std::format("failed ({})", e.what()) << std::endl; + return -1; + } } else { std::cerr << "attenuator type not specified, exiting" << std::endl; detail::ShowUsage(argv[0]); @@ -261,7 +451,77 @@ main(int argc, char* argv[]) return -1; } - const std::function validateSanity = (validationMode == ValidationModeArgExtended) + const std::string command{ (argc > argIndex) ? argv[argIndex++] : CommandArgBasic }; + + if (command == CommandArgGet) { + // get + if (argc != argIndex + 1) { + std::cerr << "error: 'get' command requires exactly one argument: " << std::endl; + detail::ShowUsage(argv[0]); + return -1; + } + + uint32_t channel{ 0 }; + try { + channel = static_cast(std::stoul(argv[argIndex])); + } catch (const std::exception& e) { + std::cerr << std::format("error: invalid channel '{}': {}", argv[argIndex], e.what()) << std::endl; + return -1; + } + + try { + double attenuation = attenuator->GetAttenuationForChannel(channel); + std::cout << std::format("Attenuation for channel {}: {} dBm", channel, attenuation) << std::endl; + return 0; + } catch (const std::exception& e) { + std::cerr << std::format("error: failed to get attenuation for channel {}: {}", channel, e.what()) << std::endl; + return -1; + } + } + + if (command == CommandArgSet) { + // set + if (argc != argIndex + 2) { + std::cerr << "error: 'set' command requires exactly two arguments: " << std::endl; + detail::ShowUsage(argv[0]); + return -1; + } + + uint32_t channel{ 0 }; + double attenuation{ 0.0 }; + + try { + channel = static_cast(std::stoul(argv[argIndex])); + attenuation = std::stod(argv[argIndex + 1]); + } catch (const std::exception& e) { + std::cerr << std::format("error: invalid arguments: {}", e.what()) << std::endl; + return -1; + } + + try { + std::cout << std::format("Setting attenuation for channel {} to {} dBm: ", channel, attenuation); + bool succeeded = attenuator->SetAttenuationForChannel(channel, attenuation); + std::cout << ((succeeded) ? "succeeded" : "failed") << std::endl; + return succeeded ? 0 : -1; + } catch (const std::exception& e) { + std::cerr << std::format("error: failed to set attenuation: {}", e.what()) << std::endl; + return -1; + } + } + + if (command != CommandArgBasic && command != CommandArgExtended) { + std::cerr << std::format("error: unknown command '{}'", command) << std::endl; + detail::ShowUsage(argv[0]); + return -1; + } + + if (argc != argIndex) { + std::cerr << std::format("error: unexpected extra arguments provided for command '{}'", command) << std::endl; + detail::ShowUsage(argv[0]); + return -1; + } + + const std::function validateSanity = (command == CommandArgExtended) ? detail::ValidateAttenuatorSanityExtended : detail::ValidateAttenuatorSanityBasic;