From ee803b014f85dc0e1f0e1c5f96860adead2360ae Mon Sep 17 00:00:00 2001 From: Tiago Lourinho Date: Wed, 26 Aug 2026 16:24:32 +0200 Subject: [PATCH] added create dynamic to candevice --- CMakeLists.txt | 1 + src/include/CanDevice.h | 4 + src/include/dynamicParsing.h | 93 +++++++++++++++ src/include/utils.h | 39 +++++++ src/main/CanDevice.cpp | 95 +++++++++++++++- src/main/dynamicParsing.cpp | 196 ++++++++++++++++++++++++++++++++ test/cpp/CanDevice_test.cpp | 212 +++++++++++++++++++++++++++++++++++ 7 files changed, 639 insertions(+), 1 deletion(-) create mode 100644 src/include/dynamicParsing.h create mode 100644 src/include/utils.h create mode 100644 src/main/dynamicParsing.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 045c05cc..ae5dfa29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,7 @@ set(SOURCES src/main/CanDeviceConfiguration.cpp src/main/CanDiagnostics.cpp src/main/CanDerivedStats.cpp + src/main/dynamicParsing.cpp ) include(cmake/env.cmake) diff --git a/src/include/CanDevice.h b/src/include/CanDevice.h index b9fe2cfc..89145937 100644 --- a/src/include/CanDevice.h +++ b/src/include/CanDevice.h @@ -71,6 +71,10 @@ struct CanDevice { static std::unique_ptr create( std::string_view vendor, const CanDeviceArguments& configuration); + static std::unique_ptr create_dynamic( + const std::map& parameters, + const std::function& receiver); + protected: /** * @brief Constructor for the CanDevice class. diff --git a/src/include/dynamicParsing.h b/src/include/dynamicParsing.h new file mode 100644 index 00000000..2ec7702a --- /dev/null +++ b/src/include/dynamicParsing.h @@ -0,0 +1,93 @@ +#ifndef SRC_INCLUDE_DYNAMICPARSING_H_ +#define SRC_INCLUDE_DYNAMICPARSING_H_ + +#include +#include +#include +#include +#include + +#include "CanDeviceConfiguration.h" + +/** + * @namespace dynamic_parsing + * @brief Namespace containing the helpers used to build a + * CanDeviceConfiguration out of string parameters. + */ +namespace dynamic_parsing { + +/** + * @brief Parser of a single configuration parameter. + * + * A parser validates the raw string value, assigns it to the corresponding + * entry of the given configuration and returns std::nullopt. If the value + * cannot be converted to the type of the parameter, the configuration is left + * untouched and the reason of the failure is returned instead. + */ +using ParameterParser = std::function( + const std::string&, CanDeviceConfiguration&)>; + +/** + * @brief Configuration parameters accepted by a vendor. + * + */ +struct VendorParameters { + /** + * @brief Parameters that must be provided. + */ + const std::set required; + + /** + * @brief Parameters that may be provided. + */ + const std::set optional; +}; + +/** + * @brief Parameters accepted by each vendor supported by + * CanDevice::create_dynamic. + */ +extern const std::map vendor_parameters; + +/** + * @brief Parser of each configuration parameter of CanDeviceConfiguration, + * indexed by parameter name. + */ +extern const std::map parameter_parsers; + +/** + * @brief Applies simplication rules that depend on more than one + * parameter, making it more user friendly. + * + * @param config The configuration to normalize, modified in place. + */ +void normalize_configuration(const std::string& vendor, + CanDeviceConfiguration& config) noexcept; + +/** + * @brief Logs and reports an invalid set of dynamic parameters. + * + * @param reason The problem found while validating the parameters. + * @throws std::invalid_argument always. + */ +[[noreturn]] void throw_invalid_parameters(const std::string& reason); + +/** + * @brief Lists the vendors supported by CanDevice::create_dynamic. + * + * @return The sorted, comma separated list of supported vendor names. + */ +std::string supported_vendors() noexcept; + +/** + * @brief Lists the parameters accepted by the given vendor. + * + * @param vendor The name of a supported vendor. + * @return The sorted, comma separated list of accepted parameter names, or an + * empty string if the vendor is not supported. + */ +std::string accepted_parameters(const std::string& vendor) noexcept; + +} // namespace dynamic_parsing + +#endif // SRC_INCLUDE_DYNAMICPARSING_H_ diff --git a/src/include/utils.h b/src/include/utils.h new file mode 100644 index 00000000..d31daf70 --- /dev/null +++ b/src/include/utils.h @@ -0,0 +1,39 @@ +#ifndef SRC_INCLUDE_UTILS_H_ +#define SRC_INCLUDE_UTILS_H_ + +#include +#include + +/** + * @namespace utils + * @brief Namespace containing generic helpers used across the CAN Module. + */ +namespace utils { + +/** + * @brief Joins the elements of a container of strings. + * + * @param values The strings to join, in order. + * @param separator The separator inserted between two consecutive elements. + * @return The joined string. + */ +template +std::string join(const Container& values, + const std::string& separator) noexcept { + std::ostringstream oss; + bool first = true; + + for (const auto& value : values) { + if (!first) { + oss << separator; + } + oss << value; + first = false; + } + + return oss.str(); +} + +} // namespace utils + +#endif // SRC_INCLUDE_UTILS_H_ diff --git a/src/main/CanDevice.cpp b/src/main/CanDevice.cpp index 204b70ea..6c932cb8 100644 --- a/src/main/CanDevice.cpp +++ b/src/main/CanDevice.cpp @@ -1,14 +1,17 @@ #include "CanDevice.h" #include +#include +#include #include -#include +#include #include #include #include "CanLogIt.h" #include "CanVendorAnagate.h" #include "CanVendorLoopback.h" +#include "dynamicParsing.h" #ifndef _WIN32 #include "CanVendorSocketCan.h" @@ -174,6 +177,96 @@ std::unique_ptr CanDevice::create( return nullptr; } +/** + * @brief Creates a CAN device instance from a map of string parameters. + * + * The parameters are validated and converted into a CanDeviceConfiguration, + * which is then forwarded to CanDevice::create. + * + * @param parameters A map holding the "vendor" key and the configuration + * parameters accepted by that vendor, listed in + * dynamic_parsing::vendor_parameters. Unsigned integers must be written as + * decimal numbers without sign and booleans as "true" or "false". + * @param receiver A function, lambda or functor called whenever a CAN frame is + * received. + * @return std::unique_ptr A unique pointer to the created CAN device + * object. + * @throws std::invalid_argument on the first missing, unexpected or invalid + * parameter found, or if the vendor is not recognized. + */ +std::unique_ptr CanDevice::create_dynamic( + const std::map& parameters, + const std::function& receiver) { + LOG(Log::INF, CanLogIt::h()) << "Creating CAN device with dynamic parameters"; + + // Assert vendor was passed + const auto vendor_entry = parameters.find("vendor"); + if (vendor_entry == parameters.end()) { + dynamic_parsing::throw_invalid_parameters( + "missing required parameter 'vendor', supported " + "vendors: " + + dynamic_parsing::supported_vendors()); + } + + // Assert vendor is supported + const std::string& vendor = vendor_entry->second; + const auto vendor_spec = dynamic_parsing::vendor_parameters.find(vendor); + if (vendor_spec == dynamic_parsing::vendor_parameters.end()) { + dynamic_parsing::throw_invalid_parameters( + "unsupported vendor '" + vendor + + "', supported vendors: " + dynamic_parsing::supported_vendors()); + } + + // Assert required parameters were given + for (const std::string& required : vendor_spec->second.required) { + if (parameters.count(required) == 0) { + dynamic_parsing::throw_invalid_parameters("missing required parameter '" + + required + "' for vendor '" + + vendor + "'"); + } + } + + // Build the config with the given parameters + CanDeviceConfiguration config{}; + for (const auto& [key, value] : parameters) { + // Passed directly as an additional argument + if (key == "vendor") { + continue; + } + + // Assert no unrecognized parameters for this vendor were given + if (vendor_spec->second.required.count(key) == 0 && + vendor_spec->second.optional.count(key) == 0) { + dynamic_parsing::throw_invalid_parameters( + "parameter '" + key + "' is not accepted by vendor '" + vendor + + "', accepted parameters: " + + dynamic_parsing::accepted_parameters(vendor)); + } + + // Check for parsing errors + const std::optional error = + dynamic_parsing::parameter_parsers.at(key)(value, config); + if (error.has_value()) { + dynamic_parsing::throw_invalid_parameters("invalid value '" + value + + "' for parameter '" + key + + "': " + error.value()); + } + } + + dynamic_parsing::normalize_configuration(vendor, config); + + auto device = create(vendor, CanDeviceArguments{config, receiver}); + + // CanDevice:create returns nullptr only if vendor is not recognized + // (shouldn't happen as vendor was already verified before) + if (device == nullptr) { + dynamic_parsing::throw_invalid_parameters("the vendor '" + vendor + + "' is not recognized"); + } + + return device; +} + std::ostream& operator<<(std::ostream& os, CanReturnCode code) { switch (code) { case CanReturnCode::success: diff --git a/src/main/dynamicParsing.cpp b/src/main/dynamicParsing.cpp new file mode 100644 index 00000000..051d0c82 --- /dev/null +++ b/src/main/dynamicParsing.cpp @@ -0,0 +1,196 @@ +#include "dynamicParsing.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../include/utils.h" +#include "CanDeviceConfiguration.h" +#include "CanLogIt.h" + +/** + * @brief Parses a boolean configuration parameter. + * + * @param value The raw value to parse, either "true" or "false". + * @param target The configuration entry to assign on success. + * @return std::nullopt on success, or the reason why the value is invalid. + */ +std::optional parse_bool(const std::string& value, + std::optional& target) noexcept { + if (value != "true" && value != "false") { + return "expected 'true' or 'false'"; + } + + target = value == "true"; + + return std::nullopt; +} + +/** + * @brief Parses an unsigned 32 bits integer configuration parameter. + * + * @param value The raw value to parse, a decimal number without sign. + * @param target The configuration entry to assign on success. + * @return std::nullopt on success, or the reason why the value is invalid. + */ +std::optional parse_uint32( + const std::string& value, std::optional& target) noexcept { + const bool is_decimal = + !value.empty() && + std::all_of(value.begin(), value.end(), [](unsigned char character) { + return std::isdigit(character) != 0; + }); + + if (!is_decimal) { + return "expected an unsigned integer"; + } + + try { + const uint64_t parsed = std::stoull(value); + + if (parsed > std::numeric_limits::max()) { + return "value is outside the range of an unsigned 32 bits integer " + "(0..4294967295)"; + } + + target = static_cast(parsed); + } catch (const std::out_of_range&) { + return "value is outside the range of an unsigned 32 bits integer " + "(0..4294967295)"; + } + + return std::nullopt; +} + +namespace dynamic_parsing { + +const std::map vendor_parameters = { + {"socketcan", {{"bus_name"}, {"bitrate", "timeout", "vcan"}}}, + {"socketcan_systec", {{"bus_name"}, {"bitrate", "timeout", "vcan"}}}, + {"anagate", + {{"bus_number", "host"}, + {"bitrate", "enable_termination", "high_speed", "sent_acknowledgement", + "timeout"}}}, +}; + +const std::map parameter_parsers = { + {"bus_name", + [](const std::string& value, + CanDeviceConfiguration& config) -> std::optional { + config.bus_name = value; + return std::nullopt; + }}, + {"bus_number", + [](const std::string& value, CanDeviceConfiguration& config) { + return parse_uint32(value, config.bus_number); + }}, + {"host", + [](const std::string& value, + CanDeviceConfiguration& config) -> std::optional { + config.host = value; + return std::nullopt; + }}, + {"bitrate", + [](const std::string& value, CanDeviceConfiguration& config) { + return parse_uint32(value, config.bitrate); + }}, + {"enable_termination", + [](const std::string& value, CanDeviceConfiguration& config) { + return parse_bool(value, config.enable_termination); + }}, + {"high_speed", + [](const std::string& value, CanDeviceConfiguration& config) { + return parse_bool(value, config.high_speed); + }}, + {"timeout", + [](const std::string& value, CanDeviceConfiguration& config) { + return parse_uint32(value, config.timeout); + }}, + {"vcan", + [](const std::string& value, CanDeviceConfiguration& config) { + return parse_bool(value, config.vcan); + }}, + {"sent_acknowledgement", + [](const std::string& value, CanDeviceConfiguration& config) { + return parse_uint32(value, config.sent_acknowledgement); + }}, +}; + +void normalize_configuration(const std::string& vendor, + CanDeviceConfiguration& config) noexcept { + if (vendor == "socketcan") { + // Cover the case where the user forgot to set vcan=true + // but had bus_name=vcan0 for example + if (config.bus_name.value_or("").find("vcan") != std::string::npos && + !config.vcan.value_or(false)) { + LOG(Log::INF, CanLogIt::h()) + << "Bus " << config.bus_name.value() + << " is a virtual CAN bus, setting vcan to true"; + config.vcan = true; + } + + // If the user supplied a bitrate but the bus is virtual, + // ignore it instead of crashing + if (config.vcan.value_or(false) && config.bitrate.has_value()) { + LOG(Log::WRN, CanLogIt::h()) + << "Ignoring the bitrate of a virtual CAN bus"; + config.bitrate.reset(); + } + } +} + +/** + * @brief Logs and reports an invalid set of dynamic parameters. + * + * @param reason The problem found while validating the parameters. + * @throws std::invalid_argument always. + */ +[[noreturn]] void throw_invalid_parameters(const std::string& reason) { + const std::string message = "Invalid CAN device parameters: " + reason; + LOG(Log::ERR, CanLogIt::h()) << message; + throw std::invalid_argument(message); +} + +/** + * @brief Lists the vendors supported by CanDevice::create_dynamic. + * + * @return The sorted, comma separated list of supported vendor names. + */ +std::string supported_vendors() noexcept { + std::vector names; + + for (const auto& entry : vendor_parameters) { + names.push_back(entry.first); + } + + return utils::join(names, ", "); +} + +/** + * @brief Lists the parameters accepted by the given vendor. + * + * @param vendor The name of a supported vendor. + * @return The sorted, comma separated list of accepted parameter names, or an + * empty string if the vendor is not supported. + */ +std::string accepted_parameters(const std::string& vendor) noexcept { + const auto entry = vendor_parameters.find(vendor); + + if (entry == vendor_parameters.end()) { + return ""; + } + + std::set names = entry->second.required; + names.insert(entry->second.optional.begin(), entry->second.optional.end()); + + return utils::join(names, ", "); +} + +} // namespace dynamic_parsing diff --git a/test/cpp/CanDevice_test.cpp b/test/cpp/CanDevice_test.cpp index 0273a2e8..4289abd7 100644 --- a/test/cpp/CanDevice_test.cpp +++ b/test/cpp/CanDevice_test.cpp @@ -3,7 +3,11 @@ #include #include +#include +#include +#include #include // NOLINT +#include #include // Test fixture for CanFrame @@ -52,3 +56,211 @@ TEST_F(CanDeviceTest, LoopbackDeviceMessageTransmission) { inFrames[i].is_remote_request()); } } + +TEST_F(CanDeviceTest, CreateDynamicBuildsAnagateConfiguration) { + auto device = CanDevice::create_dynamic( + { + {"vendor", "anagate"}, + {"host", "127.0.0.1"}, + {"bus_number", "2"}, + {"bitrate", "125000"}, + {"enable_termination", "true"}, + {"high_speed", "false"}, + {"timeout", "6000"}, + {"sent_acknowledgement", "1"}, + }, + [](const CanFrame&) {}); + + ASSERT_NE(device, nullptr); + ASSERT_EQ(device->vendor_name(), "anagate"); + + const CanDeviceConfiguration& config = device->args().config; + ASSERT_EQ(config.host.value(), "127.0.0.1"); + ASSERT_EQ(config.bus_number.value(), 2); + ASSERT_EQ(config.bitrate.value(), 125000); + ASSERT_TRUE(config.enable_termination.value()); + ASSERT_FALSE(config.high_speed.value()); + ASSERT_EQ(config.timeout.value(), 6000); + ASSERT_EQ(config.sent_acknowledgement.value(), 1); + ASSERT_FALSE(config.bus_name.has_value()); + ASSERT_FALSE(config.vcan.has_value()); +} + +TEST_F(CanDeviceTest, CreateDynamicBuildsSocketCanConfiguration) { + for (const std::string vendor : {"socketcan", "socketcan_systec"}) { + auto device = CanDevice::create_dynamic( + { + {"vendor", vendor}, + {"bus_name", "can0"}, + {"bitrate", "125000"}, + {"timeout", "100"}, + {"vcan", "false"}, + }, + [](const CanFrame&) {}); + + ASSERT_NE(device, nullptr); + ASSERT_EQ(device->vendor_name(), vendor); + + const CanDeviceConfiguration& config = device->args().config; + ASSERT_EQ(config.bus_name.value(), "can0"); + ASSERT_EQ(config.bitrate.value(), 125000); + ASSERT_EQ(config.timeout.value(), 100); + ASSERT_FALSE(config.vcan.value()); + ASSERT_FALSE(config.host.has_value()); + ASSERT_FALSE(config.bus_number.has_value()); + ASSERT_FALSE(config.enable_termination.has_value()); + ASSERT_FALSE(config.high_speed.has_value()); + ASSERT_FALSE(config.sent_acknowledgement.has_value()); + } +} + +TEST_F(CanDeviceTest, CreateDynamicForwardsTheReceiver) { + std::vector frames; + + auto device = CanDevice::create_dynamic( + {{"vendor", "anagate"}, {"host", "127.0.0.1"}, {"bus_number", "0"}}, + [&frames](const CanFrame& frame) { frames.push_back(frame); }); + + ASSERT_NE(device, nullptr); + ASSERT_NE(device->args().receiver, nullptr); + + device->args().receiver(CanFrame{1}); + + ASSERT_EQ(frames.size(), 1); +} + +TEST_F(CanDeviceTest, CreateDynamicDetectsVirtualBusFromItsName) { + auto device = CanDevice::create_dynamic( + {{"vendor", "socketcan"}, {"bus_name", "vcan0"}}, [](const CanFrame&) {}); + + ASSERT_NE(device, nullptr); + ASSERT_TRUE(device->args().config.vcan.value()); +} + +TEST_F(CanDeviceTest, CreateDynamicIgnoresTheBitrateOfAVirtualBus) { + const std::vector> virtual_buses = { + {{"vendor", "socketcan"}, {"bus_name", "vcan0"}, {"bitrate", "125000"}}, + {{"vendor", "socketcan"}, + {"bus_name", "can0"}, + {"vcan", "true"}, + {"bitrate", "125000"}}, + }; + + for (const auto& parameters : virtual_buses) { + auto device = CanDevice::create_dynamic(parameters, [](const CanFrame&) {}); + + ASSERT_NE(device, nullptr); + ASSERT_TRUE(device->args().config.vcan.value()); + ASSERT_FALSE(device->args().config.bitrate.has_value()); + } +} + +TEST_F(CanDeviceTest, CreateDynamicKeepsTheBitrateOfARealBus) { + auto device = CanDevice::create_dynamic( + {{"vendor", "socketcan"}, {"bus_name", "can0"}, {"bitrate", "125000"}}, + [](const CanFrame&) {}); + + ASSERT_NE(device, nullptr); + ASSERT_EQ(device->args().config.bitrate.value(), 125000); + ASSERT_FALSE(device->args().config.vcan.has_value()); +} + +TEST_F(CanDeviceTest, CreateDynamicRejectsUnknownParameters) { + ASSERT_THROW(CanDevice::create_dynamic({{"vendor", "anagate"}, + {"host", "127.0.0.1"}, + {"bus_number", "0"}, + {"buss_name", "can0"}}, + [](const CanFrame&) {}), + std::invalid_argument); +} + +TEST_F(CanDeviceTest, CreateDynamicRejectsInvalidVendor) { + ASSERT_THROW( + CanDevice::create_dynamic({{"vendor", "invalid"}, {"bus_name", "can0"}}, + [](const CanFrame&) {}), + std::invalid_argument); + + ASSERT_THROW( + CanDevice::create_dynamic({{"bus_name", "can0"}}, [](const CanFrame&) {}), + std::invalid_argument); +} + +TEST_F(CanDeviceTest, CreateDynamicRejectsInvalidValues) { + const std::vector> invalid_values = { + {"bus_number", "-1"}, + {"bus_number", "+1"}, + {"bus_number", "1.5"}, + {"bus_number", "12a"}, + {"bus_number", "0x2"}, + {"bus_number", ""}, + {"bitrate", "4294967296"}, + {"bitrate", "99999999999999999999"}, + {"enable_termination", "maybe"}, + {"high_speed", "never"}, + {"timeout", " 6000 "}, + {"sent_acknowledgement", "abc"}, + }; + + for (const auto& [key, value] : invalid_values) { + std::map parameters = { + {"vendor", "anagate"}, {"host", "127.0.0.1"}, {"bus_number", "0"}}; + parameters[key] = value; + + ASSERT_THROW(CanDevice::create_dynamic(parameters, [](const CanFrame&) {}), + std::invalid_argument) + << "Expected '" << value << "' to be invalid for '" << key << "'"; + } +} + +TEST_F(CanDeviceTest, CreateDynamicRejectsMissingRequiredParameters) { + ASSERT_THROW( + CanDevice::create_dynamic({{"vendor", "anagate"}, {"host", "127.0.0.1"}}, + [](const CanFrame&) {}), + std::invalid_argument); + + ASSERT_THROW( + CanDevice::create_dynamic({{"vendor", "anagate"}, {"bus_number", "0"}}, + [](const CanFrame&) {}), + std::invalid_argument); + + ASSERT_THROW(CanDevice::create_dynamic( + {{"vendor", "socketcan"}, {"bitrate", "125000"}}, + [](const CanFrame&) {}), + std::invalid_argument); +} + +TEST_F(CanDeviceTest, CreateDynamicRejectsParametersOfAnotherVendor) { + const std::vector> anagate_only = { + {"host", "127.0.0.1"}, {"bus_number", "0"}, + {"enable_termination", "true"}, {"high_speed", "true"}, + {"sent_acknowledgement", "1"}, + }; + + for (const std::string vendor : {"socketcan", "socketcan_systec"}) { + for (const auto& [key, value] : anagate_only) { + std::map parameters = {{"vendor", vendor}, + {"bus_name", "can0"}}; + parameters[key] = value; + + ASSERT_THROW( + CanDevice::create_dynamic(parameters, [](const CanFrame&) {}), + std::invalid_argument) + << "Expected '" << key << "' to be rejected by '" << vendor << "'"; + } + } + + const std::vector> socketcan_only = { + {"bus_name", "can0"}, + {"vcan", "true"}, + }; + + for (const auto& [key, value] : socketcan_only) { + std::map parameters = { + {"vendor", "anagate"}, {"host", "127.0.0.1"}, {"bus_number", "0"}}; + parameters[key] = value; + + ASSERT_THROW(CanDevice::create_dynamic(parameters, [](const CanFrame&) {}), + std::invalid_argument) + << "Expected '" << key << "' to be rejected by 'anagate'"; + } +}