diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 90396bbf..67a5a031 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -91,6 +91,7 @@ if (ENABLE_ESI_PARSER) find_package(tinyxml2 CONFIG REQUIRED) list(APPEND KICKCAT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/ESI/Parser.cc) list(APPEND KICKCAT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/ESI/SIIBuilder.cc) + list(APPEND KICKCAT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/xml.cc) list(APPEND OS_LIBRARIES tinyxml2::tinyxml2) endif() diff --git a/lib/include/kickcat/CoE/OD.h b/lib/include/kickcat/CoE/OD.h index e3457c61..27a6e6b7 100644 --- a/lib/include/kickcat/CoE/OD.h +++ b/lib/include/kickcat/CoE/OD.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -108,6 +110,9 @@ namespace kickcat::CoE }; char const* toString(enum DataType type); + // ETG.1020 base data type label as ESI and ENI files write it (e.g. "UINT", "BOOL"). + std::optional dataTypeFromLabel(std::string_view label); + // Wire properties of a basic CoE data type, for code that interprets raw entry // bytes (the name is already available via toString). size 0 = string. struct DataTypeInfo @@ -257,8 +262,8 @@ namespace kickcat::CoE struct Object // ETG1000.5 6.1.4.2.1 Formal model { - uint16_t index; - ObjectCode code; + uint16_t index = 0; + ObjectCode code = ObjectCode::NIL; std::string name; std::vector entries; }; diff --git a/lib/include/kickcat/CoE/protocol.h b/lib/include/kickcat/CoE/protocol.h index 2d6ca654..bcdd4908 100644 --- a/lib/include/kickcat/CoE/protocol.h +++ b/lib/include/kickcat/CoE/protocol.h @@ -2,9 +2,21 @@ #define KICKCAT_COE_PROTOCOL_H #include "kickcat/CoE/OD.h" +#include "kickcat/protocol.h" namespace kickcat::CoE { + // SDO access a configuration file (ESI or ENI) asks for during ESM transitions. + struct InitCmd + { + Transitions transitions = 0; + uint16_t index = 0; + uint8_t subindex = 0; + std::vector data; + bool complete_access = false; + std::string comment; + }; + constexpr uint16_t SM_COM_TYPE = 0x1C00; // each sub-entry described SM[x] com type (mailbox in/out, PDO in/out, not used) constexpr uint16_t SM_CHANNEL = 0x1C10; // each entry is associated with the mapped PDOs (if in used) @@ -16,6 +28,7 @@ namespace kickcat::CoE uint16_t index; uint8_t subindex; uint8_t bitlen; + DataType type = DataType::UNKNOWN; // not part of the mapping word }; constexpr uint32_t toMappingWord(PdoMappingEntry entry) diff --git a/lib/include/kickcat/ESI/Device.h b/lib/include/kickcat/ESI/Device.h index 39db6992..4601cd49 100644 --- a/lib/include/kickcat/ESI/Device.h +++ b/lib/include/kickcat/ESI/Device.h @@ -9,6 +9,7 @@ #include #include "kickcat/CoE/OD.h" +#include "kickcat/CoE/protocol.h" #include "kickcat/protocol.h" namespace kickcat::ESI @@ -45,22 +46,6 @@ namespace kickcat::ESI bool op_only = false; }; - // ETG.2000 InitCmd Transition: ESM transitions during which the InitCmd applies. - namespace transition - { - enum Type : uint8_t - { - IP = 0, // Init -> PreOp - PS = 1, // PreOp -> SafeOp - SO = 2, // SafeOp -> Op - SP = 3, // SafeOp -> PreOp - OP = 4, // Op -> PreOp - OS = 5, // Op -> SafeOp - }; - char const* toString(Type const& t); - void fromString(std::string_view text, Type& out); - } - struct Mailbox { bool data_link_layer = false; @@ -68,16 +53,10 @@ namespace kickcat::ESI struct CoE { - struct InitCmd + struct InitCmd : kickcat::CoE::InitCmd { - std::vector transitions; - uint16_t index = 0; - uint8_t subindex = 0; - std::vector data; - bool adapt_automatically = false; - bool complete_access = false; - bool overwritten_by_module = false; - std::string comment; + bool adapt_automatically = false; + bool overwritten_by_module = false; }; bool sdo_info = false; @@ -97,7 +76,7 @@ namespace kickcat::ESI { struct InitCmd { - std::vector transitions; + Transitions transitions = 0; int32_t type = 0; std::vector data; std::string comment; @@ -115,7 +94,7 @@ namespace kickcat::ESI { struct InitCmd { - std::vector transitions; + Transitions transitions = 0; int32_t idn = 0; int32_t channel = 0; std::vector data; @@ -131,7 +110,7 @@ namespace kickcat::ESI { struct InitCmd { - std::vector transitions; + Transitions transitions = 0; std::vector data; std::string comment; }; diff --git a/lib/include/kickcat/ESI/Parser.h b/lib/include/kickcat/ESI/Parser.h index 61664e33..4822d9c1 100644 --- a/lib/include/kickcat/ESI/Parser.h +++ b/lib/include/kickcat/ESI/Parser.h @@ -4,7 +4,6 @@ #include #include #include -#include #include #include "kickcat/CoE/OD.h" @@ -39,13 +38,7 @@ namespace kickcat::ESI static CoE::Object buildMappingObject (Pdo const& pdo, bool is_rx); static CoE::Object buildAssignmentObject(std::vector const& pdos, uint16_t index, bool is_rx); - // Map an ESI basic-type label ("BOOL", "UINT", ...) to its CoE::DataType, - // whose value is the ETG SII data-type code. nullopt for unknown labels. - static std::optional coeTypeFromLabel(std::string const& label); - private: - static std::optional readHexDecAttr(tinyxml2::XMLElement* node, char const* name); - void openFile (std::string const& file); void openString(std::string const& xml); void resolveTopLevel(); @@ -77,18 +70,16 @@ namespace kickcat::ESI // for objects that aren't already declared in /. void synthesizePdoMappingObjects(Device& device); - std::vector loadHexBinary(tinyxml2::XMLElement* node); - - void loadDefaultData(tinyxml2::XMLNode* node, CoE::Object& obj, CoE::Entry& entry); - uint16_t loadAccess(tinyxml2::XMLNode* node); + void loadDefaultData(tinyxml2::XMLElement* node, CoE::Object& obj, CoE::Entry& entry); + uint16_t loadAccess(tinyxml2::XMLElement* node); static constexpr int MAX_TYPE_DEPTH = 16; - std::tuple parseType(tinyxml2::XMLNode* node); + std::tuple parseType(tinyxml2::XMLElement* node); CoE::DataType resolveType (std::string const& type_name, int depth = 0); - tinyxml2::XMLNode* findNodeType(tinyxml2::XMLNode* node, std::string const& where); + tinyxml2::XMLElement* findNodeType(tinyxml2::XMLElement* node); - CoE::Object createObject(tinyxml2::XMLNode* node); + CoE::Object createObject(tinyxml2::XMLElement* node, uint16_t index); tinyxml2::XMLDocument doc_; tinyxml2::XMLElement* root_ = nullptr; @@ -99,8 +90,6 @@ namespace kickcat::ESI std::string vendor_name_; std::string profile_no_; - - static const std::unordered_map BASIC_TYPES; }; } diff --git a/lib/include/kickcat/protocol.h b/lib/include/kickcat/protocol.h index 6b8dd2f9..cb0556c8 100644 --- a/lib/include/kickcat/protocol.h +++ b/lib/include/kickcat/protocol.h @@ -140,6 +140,34 @@ namespace kickcat char const* toString(State state); char const* toShortString(State state); + // ESM transitions as ESI (ETG.2000) and ENI (ETG.2100) InitCmds name them. + namespace transition + { + enum Type : uint16_t + { + NONE = 0, + IP = 1 << 0, // Init -> PreOp + PS = 1 << 1, // PreOp -> SafeOp + PI = 1 << 2, // PreOp -> Init + SP = 1 << 3, // SafeOp -> PreOp + SO = 1 << 4, // SafeOp -> Op + SI = 1 << 5, // SafeOp -> Init + OS = 1 << 6, // Op -> SafeOp + OP = 1 << 7, // Op -> PreOp + OI = 1 << 8, // Op -> Init + IB = 1 << 9, // Init -> Bootstrap + BI = 1 << 10, // Bootstrap -> Init + II = 1 << 11, // Init -> Init + PP = 1 << 12, // PreOp -> PreOp + SS = 1 << 13, // SafeOp -> SafeOp + PO = 1 << 14, // PreOp -> Op (ENI XSD only) + }; + char const* toString(Type t); + Type fromString(std::string_view text); // throws std::invalid_argument + Type between(State from, State to); // NONE if no transition matches + } + using Transitions = uint16_t; // OR of transition::Type + char const* ALStatus_to_string(int32_t code); enum StatusCode : uint16_t diff --git a/lib/include/kickcat/utils/xml.h b/lib/include/kickcat/utils/xml.h new file mode 100644 index 00000000..d17b93b9 --- /dev/null +++ b/lib/include/kickcat/utils/xml.h @@ -0,0 +1,146 @@ +#ifndef KICKCAT_UTILS_XML_H +#define KICKCAT_UTILS_XML_H + +#include +#include +#include +#include +#include +#include +#include + +#include + +// Reading ESI (ETG.2000) and ENI (ETG.2100) documents. +// Errors throw std::invalid_argument naming the element they come from; withContext() lets a caller +// add what it was reading (the document kind, an object...). +namespace kickcat::xml +{ + /// Element names from the root, with the position among same-name siblings when there are several, + /// e.g. "EtherCATConfig/Config/Slave[1]/InitCmds/InitCmd". + std::string pathOf(tinyxml2::XMLElement const* elem); + + /// Throw " in ". + [[noreturn]] void fail(tinyxml2::XMLElement const* where, std::string const& what); + + /// Run parse(), prefixing the message of the std::invalid_argument it may throw with ": ". + template + auto withContext(std::string const& context, F&& parse) -> decltype(parse()) + { + try + { + return parse(); + } + catch (std::invalid_argument const& e) + { + throw std::invalid_argument(context + ": " + e.what()); + } + } + + /// First child called name; throws when there is none. + tinyxml2::XMLElement* require(tinyxml2::XMLElement* parent, char const* name); + + /// Every text and CDATA child, so that a comment inside a value does not cut it, without the + /// surrounding XML whitespace. + std::string textOf(tinyxml2::XMLElement const* elem); + + /// Text of the first child called name; empty when there is none. + std::string optionalText(tinyxml2::XMLElement const* parent, char const* name); + + /// Text of the first child called name; throws when there is none or when it is empty. + std::string requireText(tinyxml2::XMLElement* parent, char const* name); + + /// Attribute without the surrounding XML whitespace; nullopt when absent. + std::optional attributeOf(tinyxml2::XMLElement const* elem, char const* name); + + enum Reading : uint8_t + { + VALUE, // the number must fit the type + BIT_PATTERN, // also its two's complement: xs:int identifiers write 0xFFFFFFFF as -1, positions -1 as 65535 + }; + + /// HexDecValue read from text found in elem. + int64_t parseInteger(tinyxml2::XMLElement const* elem, std::string const& text); + + template + T narrow(tinyxml2::XMLElement const* elem, int64_t value, Reading reading = VALUE) + { + static_assert(sizeof(T) < sizeof(int64_t), "narrow() needs a type narrower than int64_t"); + using U = std::make_unsigned_t; + int64_t lo = static_cast(std::numeric_limits::min()); + int64_t hi = static_cast(std::numeric_limits::max()); + if (reading == BIT_PATTERN) + { + lo = static_cast(std::numeric_limits>::min()); + hi = static_cast(std::numeric_limits::max()); + } + if ((value < lo) or (value > hi)) + { + fail(elem, "value " + std::to_string(value) + " out of range [" + std::to_string(lo) + ", " + std::to_string(hi) + "]"); + } + return static_cast(static_cast(value)); + } + + template + T numberOf(tinyxml2::XMLElement const* elem, Reading reading = VALUE) + { + return narrow(elem, parseInteger(elem, textOf(elem)), reading); + } + + template + T requireNumber(tinyxml2::XMLElement* parent, char const* name, Reading reading = VALUE) + { + return numberOf(require(parent, name), reading); + } + + template + std::optional optionalNumber(tinyxml2::XMLElement const* parent, char const* name, Reading reading = VALUE) + { + tinyxml2::XMLElement const* child = parent->FirstChildElement(name); + if (child == nullptr) + { + return std::nullopt; + } + return numberOf(child, reading); + } + + template + std::optional numberAttribute(tinyxml2::XMLElement const* elem, char const* name, Reading reading = VALUE) + { + std::optional raw = attributeOf(elem, name); + if (not raw) + { + return std::nullopt; + } + return withContext(std::string{"@"} + name, [&]() + { + return narrow(elem, parseInteger(elem, *raw), reading); + }); + } + + /// xs:boolean read from text found in elem. + bool parseBool(tinyxml2::XMLElement const* elem, std::string const& text); + + /// xs:boolean child called name; false when there is none. + bool optionalBool(tinyxml2::XMLElement const* parent, char const* name); + + /// xs:boolean attribute; false when absent. + bool boolAttribute(tinyxml2::XMLElement const* elem, char const* name); + + /// xs:hexBinary content of elem. + std::vector hexBinary(tinyxml2::XMLElement const* elem); + + /// parse() applied to every child called name, in document order. + template + auto all(tinyxml2::XMLElement* parent, char const* name, F parse) + { + std::vector out; + for (tinyxml2::XMLElement* child = parent->FirstChildElement(name); child != nullptr; child = child->NextSiblingElement(name)) + { + out.push_back(parse(child)); + } + return out; + } +} + +#endif diff --git a/lib/include/kickcat/utils/xsd.h b/lib/include/kickcat/utils/xsd.h index e135f224..6270ab67 100644 --- a/lib/include/kickcat/utils/xsd.h +++ b/lib/include/kickcat/utils/xsd.h @@ -9,6 +9,9 @@ // Errors throw std::invalid_argument without context: callers add the element they were reading. namespace kickcat::xsd { + /// Strip the XML whitespace (space, tab, CR, LF) around text. + std::string_view trim(std::string_view text); + /// HexDecValue: [+-]?[0-9]+ or #x (0x accepted) followed by hex digits, surrounding spaces ignored. /// Unsigned values up to 2^64 - 1 keep their bit pattern. int64_t parseHexDec(std::string_view text); diff --git a/lib/master/CMakeLists.txt b/lib/master/CMakeLists.txt index 84f5f4ed..1a9eae84 100644 --- a/lib/master/CMakeLists.txt +++ b/lib/master/CMakeLists.txt @@ -1,4 +1,5 @@ target_sources(kickcat PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/AbstractLink.cc ${CMAKE_CURRENT_SOURCE_DIR}/src/Bus.cc ${CMAKE_CURRENT_SOURCE_DIR}/src/CoE.cc ${CMAKE_CURRENT_SOURCE_DIR}/src/CoE/CiA/DS402/Drive.cc diff --git a/lib/master/include/kickcat/AbstractLink.h b/lib/master/include/kickcat/AbstractLink.h index 15c262d8..8b8ea8e9 100644 --- a/lib/master/include/kickcat/AbstractLink.h +++ b/lib/master/include/kickcat/AbstractLink.h @@ -85,6 +85,26 @@ namespace kickcat /// \brief Register a callback to be called when an EtherCAT IRQ event is detected. virtual void attachEcatEventCallback(enum EcatEvent event, std::function callback) = 0; }; + + /// \brief Read size bytes at reg_address of the slave at slave_address (FPRD), waiting for the answer. + /// \details Throws unless exactly one slave answered. + void readRegister(AbstractLink& link, uint16_t slave_address, uint16_t reg_address, void* value, uint16_t size); + + /// \brief Write size bytes at reg_address of the slave at slave_address (FPWR), waiting for the answer. + /// \details Throws unless exactly one slave answered. + void writeRegister(AbstractLink& link, uint16_t slave_address, uint16_t reg_address, void const* value, uint16_t size); + + template + void readRegister(AbstractLink& link, uint16_t slave_address, uint16_t reg_address, T& value) + { + readRegister(link, slave_address, reg_address, &value, sizeof(T)); + } + + template + void writeRegister(AbstractLink& link, uint16_t slave_address, uint16_t reg_address, T const& value) + { + writeRegister(link, slave_address, reg_address, &value, sizeof(T)); + } } #endif diff --git a/lib/master/include/kickcat/DebugHelpers.h b/lib/master/include/kickcat/DebugHelpers.h deleted file mode 100644 index bf6e4d2d..00000000 --- a/lib/master/include/kickcat/DebugHelpers.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef KICKCAT_DEBUGHELPERS_H -#define KICKCAT_DEBUGHELPERS_H - -#include - -#include "kickcat/Link.h" - -namespace kickcat -{ - template - void sendGetRegister(Link& link, uint16_t slave_address, uint16_t reg_address, T& value_read) - { - auto process = [&value_read](DatagramHeader const*, uint8_t const* data, uint16_t wkc) - { - if (wkc != 1) - { - return DatagramState::INVALID_WKC; - } - - std::memcpy(&value_read, data, sizeof(T)); - return DatagramState::OK; - }; - - auto error = [](DatagramState const&) - { - THROW_ERROR("Error while trying to get slave register."); - }; - - link.addDatagram(Command::FPRD, createAddress(slave_address, reg_address), nullptr, process, error); - link.processDatagrams(); - } - - template - void sendWriteRegister(Link& link, uint16_t slave_address, uint16_t reg_address, T value_write) - { - auto process = [&value_write](DatagramHeader const*, uint8_t const*, uint16_t wkc) - { - if (wkc != 1) - { - return DatagramState::INVALID_WKC; - } - return DatagramState::OK; - }; - - auto error = [](DatagramState const&) - { - THROW_ERROR("Error while trying to set slave register."); - }; - - link.addDatagram(Command::FPWR, createAddress(slave_address, reg_address), value_write, process, error); - link.processDatagrams(); - } -} - -#endif diff --git a/lib/master/src/AbstractLink.cc b/lib/master/src/AbstractLink.cc new file mode 100644 index 00000000..9384ba6b --- /dev/null +++ b/lib/master/src/AbstractLink.cc @@ -0,0 +1,48 @@ +#include + +#include "kickcat/AbstractLink.h" +#include "kickcat/Error.h" + +namespace kickcat +{ + void readRegister(AbstractLink& link, uint16_t slave_address, uint16_t reg_address, void* value, uint16_t size) + { + auto process = [value, size](DatagramHeader const*, uint8_t const* data, uint16_t wkc) + { + if (wkc != 1) + { + return DatagramState::INVALID_WKC; + } + std::memcpy(value, data, size); + return DatagramState::OK; + }; + + auto error = [](DatagramState const& state) + { + THROW_ERROR_DATAGRAM("Error while trying to get slave register.", state); + }; + + link.addDatagram(Command::FPRD, createAddress(slave_address, reg_address), nullptr, size, process, error); + link.processDatagrams(); + } + + void writeRegister(AbstractLink& link, uint16_t slave_address, uint16_t reg_address, void const* value, uint16_t size) + { + auto process = [](DatagramHeader const*, uint8_t const*, uint16_t wkc) + { + if (wkc != 1) + { + return DatagramState::INVALID_WKC; + } + return DatagramState::OK; + }; + + auto error = [](DatagramState const& state) + { + THROW_ERROR_DATAGRAM("Error while trying to set slave register.", state); + }; + + link.addDatagram(Command::FPWR, createAddress(slave_address, reg_address), value, size, process, error); + link.processDatagrams(); + } +} diff --git a/lib/src/CoE/OD.cc b/lib/src/CoE/OD.cc index fa2ed164..6ac862bd 100644 --- a/lib/src/CoE/OD.cc +++ b/lib/src/CoE/OD.cc @@ -108,6 +108,55 @@ namespace kickcat::CoE } } + std::optional dataTypeFromLabel(std::string_view label) + { + struct Label + { + std::string_view text; + DataType type; + }; + constexpr std::array LABELS = {{ + {"BOOL", DataType::BOOLEAN }, + {"BYTE", DataType::BYTE }, + {"WORD", DataType::WORD }, + {"DWORD", DataType::DWORD }, + {"SINT", DataType::INTEGER8 }, + {"INT", DataType::INTEGER16 }, + {"INT24", DataType::INTEGER24 }, + {"DINT", DataType::INTEGER32 }, + {"INT40", DataType::INTEGER40 }, + {"INT48", DataType::INTEGER48 }, + {"INT56", DataType::INTEGER56 }, + {"LINT", DataType::INTEGER64 }, + {"USINT", DataType::UNSIGNED8 }, + {"UINT", DataType::UNSIGNED16 }, + {"UINT24", DataType::UNSIGNED24 }, + {"UDINT", DataType::UNSIGNED32 }, + {"UINT40", DataType::UNSIGNED40 }, + {"UINT48", DataType::UNSIGNED48 }, + {"UINT56", DataType::UNSIGNED56 }, + {"ULINT", DataType::UNSIGNED64 }, + {"REAL", DataType::REAL32 }, + {"LREAL", DataType::REAL64 }, + {"BIT2", DataType::BIT2 }, + {"BIT3", DataType::BIT3 }, + {"BIT4", DataType::BIT4 }, + {"BIT5", DataType::BIT5 }, + {"BIT6", DataType::BIT6 }, + {"BIT7", DataType::BIT7 }, + {"BIT8", DataType::BIT8 }, + }}; + + for (auto const& l : LABELS) + { + if (l.text == label) + { + return l.type; + } + } + return std::nullopt; + } + std::string Entry::dataToString() const { if (data == nullptr) diff --git a/lib/src/ESI/Parser.cc b/lib/src/ESI/Parser.cc index eca82619..58178201 100644 --- a/lib/src/ESI/Parser.cc +++ b/lib/src/ESI/Parser.cc @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -8,195 +7,17 @@ #include "kickcat/CoE/protocol.h" #include "kickcat/debug.h" #include "kickcat/ESI/Parser.h" -#include "kickcat/utils/xsd.h" +#include "kickcat/utils/xml.h" using namespace tinyxml2; +using namespace kickcat::xml; +// Signed ESI values are read as BIT_PATTERN: real ETG files write -1 as #xFFFFFFFF. namespace kickcat::ESI { -const std::unordered_map Parser::BASIC_TYPES -{ - {"BOOL", CoE::DataType::BOOLEAN }, - {"BYTE", CoE::DataType::BYTE }, - {"WORD", CoE::DataType::WORD }, - {"DWORD", CoE::DataType::DWORD }, - {"SINT", CoE::DataType::INTEGER8 }, - {"INT", CoE::DataType::INTEGER16 }, - {"INT24", CoE::DataType::INTEGER24 }, - {"DINT", CoE::DataType::INTEGER32 }, - {"INT40", CoE::DataType::INTEGER40 }, - {"INT48", CoE::DataType::INTEGER48 }, - {"INT56", CoE::DataType::INTEGER56 }, - {"LINT", CoE::DataType::INTEGER64 }, - {"USINT", CoE::DataType::UNSIGNED8 }, - {"UINT", CoE::DataType::UNSIGNED16 }, - {"UINT24", CoE::DataType::UNSIGNED24 }, - {"UDINT", CoE::DataType::UNSIGNED32 }, - {"UINT40", CoE::DataType::UNSIGNED40 }, - {"UINT48", CoE::DataType::UNSIGNED48 }, - {"UINT56", CoE::DataType::UNSIGNED56 }, - {"ULINT", CoE::DataType::UNSIGNED64 }, - {"REAL", CoE::DataType::REAL32 }, - {"LREAL", CoE::DataType::REAL64 }, - {"BIT2", CoE::DataType::BIT2 }, - {"BIT3", CoE::DataType::BIT3 }, - {"BIT4", CoE::DataType::BIT4 }, - {"BIT5", CoE::DataType::BIT5 }, - {"BIT6", CoE::DataType::BIT6 }, - {"BIT7", CoE::DataType::BIT7 }, - {"BIT8", CoE::DataType::BIT8 }, -}; - namespace { - XMLElement* requireChild(XMLNode* node, char const* name) - { - auto element = node->FirstChildElement(name); - if (element == nullptr) - { - std::string desc = "Cannot find child element <"; - desc += node->Value(); - desc += "> -> "; - desc += name; - throw std::invalid_argument(desc); - } - return element; - } - - char const* textOrEmpty(XMLElement* node) - { - if (node == nullptr) - { - return ""; - } - char const* text = node->GetText(); - if (text == nullptr) - { - return ""; - } - return text; - } - - // Throws when a mandatory text-bearing element is missing or has empty content. - char const* requireText(XMLElement* elem, char const* child, std::string const& where) - { - if (elem == nullptr) - { - std::string what = "ESI: missing mandatory <"; - what += child; - what += "> in "; - what += where; - throw std::invalid_argument(what); - } - char const* text = elem->GetText(); - if (text == nullptr) - { - std::string what = "ESI: empty <"; - what += child; - what += "> in "; - what += where; - throw std::invalid_argument(what); - } - return text; - } - - // Parent + child-name convenience: fetches the child element, then - // delegates to requireText. Distinct name from requireText to avoid an - // overload-resolution trap where a derived-class pointer (XMLElement*) - // silently binds to the wrong overload. - char const* requireChildText(XMLNode* parent, char const* child, std::string const& where) - { - return requireText(parent->FirstChildElement(child), child, where); - } - - // Nullable read: returns nullptr if the child is missing or has no text. - char const* findText(XMLNode* parent, char const* child) - { - auto* elem = parent->FirstChildElement(child); - if (elem == nullptr) - { - return nullptr; - } - return elem->GetText(); - } - - int64_t parseHexDec(std::string const& text, std::string const& where = {}) - { - try - { - return xsd::parseHexDec(text); - } - catch (std::invalid_argument const& e) - { - std::string what = "ESI: "; - what += e.what(); - if (not where.empty()) - { - what += " in "; - what += where; - } - throw std::invalid_argument(what); - } - } - - // Narrow an int64_t to T with bounds checking. For signed T, also accepts - // the unsigned bit pattern in [0, max-of-make_unsigned_t] so that real - // ETG ESIs using #xFFFFFFFF as a signed-int32 sentinel parse as -1 rather - // than throwing. - template - T narrowChecked(int64_t value, std::string const& text, std::string const& where) - { - static_assert(sizeof(T) <= sizeof(int64_t), "narrowChecked unsupported for T wider than int64"); - auto fail = [&](char const* kind) - { - std::string what = "ESI: value '"; - what += text; - what += "' out of range for "; - what += std::to_string(sizeof(T) * 8); - what += "-bit "; - what += kind; - what += " type in "; - what += where; - throw std::invalid_argument(what); - }; - - if constexpr (std::is_signed_v) - { - using U = std::make_unsigned_t; - int64_t lo = static_cast(std::numeric_limits::min()); - int64_t hi = static_cast(std::numeric_limits::max()); - if (value < lo or value > hi) - { - fail("signed"); - } - return static_cast(static_cast(value)); - } - else - { - int64_t hi = static_cast(std::numeric_limits::max()); - if (value < 0 or value > hi) - { - fail("unsigned"); - } - return static_cast(value); - } - } - - template - T parseHexDec(std::string text, std::string const& where) - { - int64_t value = parseHexDec(text, where); - return narrowChecked(value, text, where); - } - - template - T requireNumber(XMLNode* parent, char const* child, std::string const& where) - { - char const* text = requireChildText(parent, child, where); - return parseHexDec(text, where); - } - std::string objectLabel(uint16_t index) { char buf[24]; @@ -204,120 +25,47 @@ namespace return buf; } - // xs:boolean per the ESI schema. Returns false when the attribute is - // absent (schema default for the ESI attrs we read); throws when present - // but not one of the four canonical values, so typos like Virtual="yes" - // fail loudly instead of silently being treated as false. - bool readBoolAttr(XMLElement* node, char const* name) + // ProductCode, RevisionNo... of the device ; nullopt when absent. + std::optional typeAttribute(XMLElement* device, char const* name) { - if (node == nullptr) - { - return false; - } - char const* raw = node->Attribute(name); - if (raw == nullptr) - { - return false; - } - try - { - return xsd::parseBoolean(raw); - } - catch (std::invalid_argument const& e) - { - std::string what = "ESI: attribute '"; - what += name; - what += "': "; - what += e.what(); - throw std::invalid_argument(what); - } - } - - // xs:int attribute: signed decimal, no #x hex prefix. nullopt when truly - // absent; throws when present but not a valid xs:int (e.g. Chn="abc"), - // so callers can use .value_or(default) for the schema-default case - // without silently swallowing malformed input. - std::optional readIntAttr(XMLElement* node, char const* name) - { - if (node == nullptr) - { - return std::nullopt; - } - int32_t value = 0; - auto result = node->QueryIntAttribute(name, &value); - if (result == tinyxml2::XML_NO_ATTRIBUTE) + XMLElement* type = device->FirstChildElement("Type"); + if (type == nullptr) { return std::nullopt; } - if (result != tinyxml2::XML_SUCCESS) - { - std::string what = "ESI: attribute '"; - what += name; - what += "' is not a valid xs:int"; - throw std::invalid_argument(what); - } - return value; + return numberAttribute(type, name); } - std::vector parseTransitions(XMLElement* parent) + Transitions parseTransitions(XMLElement* parent) { - std::vector out; - for (auto* t = parent->FirstChildElement("Transition"); t != nullptr; t = t->NextSiblingElement("Transition")) + // ETG.2000 InitCmds only run during these, the ENI-only ones are invalid in an ESI. + constexpr Transitions ESI_TRANSITIONS = transition::IP | transition::PS | transition::SO + | transition::SP | transition::OP | transition::OS; + Transitions out = 0; + for (XMLElement* t = parent->FirstChildElement("Transition"); t != nullptr; t = t->NextSiblingElement("Transition")) { - char const* text = t->GetText(); - if (text == nullptr) + std::string text = textOf(t); + transition::Type type = transition::NONE; + try + { + type = transition::fromString(text); + } + catch (std::invalid_argument const& e) { - throw std::invalid_argument("ESI: empty "); + fail(t, e.what()); } - transition::Type type; - fromString(text, type); - out.push_back(type); + if ((type & ESI_TRANSITIONS) == 0) + { + fail(t, "Transition '" + text + "' is not allowed in an ESI InitCmd"); + } + out |= type; } - if (out.empty()) + if (out == 0) { - throw std::invalid_argument("ESI: InitCmd has no child (schema requires at least one)"); + fail(parent, "missing mandatory "); } return out; } - - std::string commentOf(XMLElement* parent) - { - auto* c = parent->FirstChildElement("Comment"); - if (c == nullptr) - { - return {}; - } - char const* text = c->GetText(); - if (text == nullptr) - { - return {}; - } - return text; - } -} - -std::optional Parser::readHexDecAttr(XMLElement* node, char const* name) -{ - if (node == nullptr) - { - return std::nullopt; - } - char const* raw = node->Attribute(name); - if (raw == nullptr) - { - return std::nullopt; - } - return parseHexDec(raw, std::string{"@"} + name); -} - -std::optional Parser::coeTypeFromLabel(std::string const& label) -{ - auto it = BASIC_TYPES.find(label); - if (it == BASIC_TYPES.end()) - { - return std::nullopt; - } - return it->second; } void Parser::openFile(std::string const& file) @@ -353,12 +101,12 @@ void Parser::resolveTopLevel() { throw std::invalid_argument("ESI: document has no root element"); } - vendor_xml_ = requireChild(root_, "Vendor"); - auto desc = requireChild(root_, "Descriptions"); - devices_ = requireChild(desc, "Devices"); - - auto vendor_name = vendor_xml_->FirstChildElement("Name"); - vendor_name_ = textOrEmpty(vendor_name); + withContext("ESI", [&]() + { + vendor_xml_ = require(root_, "Vendor"); + devices_ = require(require(root_, "Descriptions"), "Devices"); + }); + vendor_name_ = optionalText(vendor_xml_, "Name"); } std::vector Parser::listDevices(std::string const& file) @@ -375,23 +123,20 @@ std::vector Parser::listDevicesString(std::string const& xml) std::vector Parser::listDevicesImpl() { - std::vector out; - for (auto* d = devices_->FirstChildElement("Device"); d != nullptr; d = d->NextSiblingElement("Device")) + return withContext("ESI", [&]() { - out.push_back(summarize(d)); - } - return out; + return all(devices_, "Device", [&](XMLElement* d) { return summarize(d); }); + }); } DeviceSummary Parser::summarize(XMLElement* device) { DeviceSummary s; - auto type_node = device->FirstChildElement("Type"); - s.type = textOrEmpty(type_node); - s.product_code = readHexDecAttr(type_node, "ProductCode").value_or(0); - s.revision_no = readHexDecAttr(type_node, "RevisionNo" ).value_or(0); - s.serial_no = readHexDecAttr(type_node, "SerialNo" ).value_or(0); - s.name = textOrEmpty(device->FirstChildElement("Name")); + s.type = optionalText(device, "Type"); + s.product_code = typeAttribute(device, "ProductCode").value_or(0); + s.revision_no = typeAttribute(device, "RevisionNo" ).value_or(0); + s.serial_no = typeAttribute(device, "SerialNo" ).value_or(0); + s.name = optionalText(device, "Name"); return s; } @@ -402,7 +147,7 @@ XMLElement* Parser::selectDevice(DeviceFilter const& filter) if (not any_filter) { std::size_t i = 0; - for (auto* d = devices_->FirstChildElement("Device"); d != nullptr; d = d->NextSiblingElement("Device")) + for (XMLElement* d = devices_->FirstChildElement("Device"); d != nullptr; d = d->NextSiblingElement("Device")) { if (i == filter.index) { @@ -410,40 +155,27 @@ XMLElement* Parser::selectDevice(DeviceFilter const& filter) } ++i; } - throw std::invalid_argument("ESI: device index out of range"); + throw std::invalid_argument("device index out of range"); } - for (auto* d = devices_->FirstChildElement("Device"); d != nullptr; d = d->NextSiblingElement("Device")) + for (XMLElement* d = devices_->FirstChildElement("Device"); d != nullptr; d = d->NextSiblingElement("Device")) { - auto* type_node = d->FirstChildElement("Type"); - if (filter.type) + if (filter.type and (optionalText(d, "Type") != *filter.type)) { - std::string type_text = textOrEmpty(type_node); - if (type_text != *filter.type) - { - continue; - } + continue; } - if (filter.product_code) + if (filter.product_code and (typeAttribute(d, "ProductCode") != filter.product_code)) { - auto pc = readHexDecAttr(type_node, "ProductCode"); - if (not pc or *pc != *filter.product_code) - { - continue; - } + continue; } - if (filter.revision_no) + if (filter.revision_no and (typeAttribute(d, "RevisionNo") != filter.revision_no)) { - auto rev = readHexDecAttr(type_node, "RevisionNo"); - if (not rev or *rev != *filter.revision_no) - { - continue; - } + continue; } return d; } - throw std::invalid_argument("ESI: no device matches filter"); + throw std::invalid_argument("no device matches filter"); } Device Parser::loadDevice(std::string const& file, DeviceFilter const& filter) @@ -460,7 +192,10 @@ Device Parser::loadDeviceString(std::string const& xml, DeviceFilter const& filt Device Parser::loadDeviceImpl(DeviceFilter const& filter) { - return buildDeviceFromElement(selectDevice(filter)); + return withContext("ESI", [&]() + { + return buildDeviceFromElement(selectDevice(filter)); + }); } std::vector Parser::loadAllDevices(std::string const& file, std::vector* errors) @@ -468,18 +203,18 @@ std::vector Parser::loadAllDevices(std::string const& file, std::vector< openFile(file); std::vector out; std::size_t index = 0; - for (auto* d = devices_->FirstChildElement("Device"); d != nullptr; d = d->NextSiblingElement("Device"), ++index) + for (XMLElement* d = devices_->FirstChildElement("Device"); d != nullptr; d = d->NextSiblingElement("Device"), ++index) { try { - out.push_back(buildDeviceFromElement(d)); + out.push_back(withContext("ESI", [&]() { return buildDeviceFromElement(d); })); } catch (std::exception const& e) { if (errors != nullptr) { std::string msg = "device[" + std::to_string(index) + "] ("; - msg += textOrEmpty(d->FirstChildElement("Type")); + msg += optionalText(d, "Type"); msg += "): "; msg += e.what(); errors->push_back(std::move(msg)); @@ -498,33 +233,30 @@ Device Parser::buildDeviceFromElement(XMLElement* device_node) // files (notably some catalog placeholders) ship with empty. // Require the element to be present; allow empty text -> vendor_id = 0 // to preserve compatibility with those files. - auto* id_node = requireChild(vendor_xml_, "Id"); - char const* id_text = id_node->GetText(); - if (id_text != nullptr and *id_text != '\0') + XMLElement* id = require(vendor_xml_, "Id"); + if (not textOf(id).empty()) { - device.vendor_id = parseHexDec(id_text, "Vendor/Id"); + device.vendor_id = numberOf(id); } - auto* type_node = device_node->FirstChildElement("Type"); - device.type = textOrEmpty(type_node); - device.product_code = readHexDecAttr(type_node, "ProductCode").value_or(0); - device.revision_no = readHexDecAttr(type_node, "RevisionNo" ).value_or(0); - device.serial_no = readHexDecAttr(type_node, "SerialNo" ).value_or(0); - device.name = textOrEmpty(device_node->FirstChildElement("Name")); - device.group_type = textOrEmpty(device_node->FirstChildElement("GroupType")); + device.type = optionalText(device_node, "Type"); + device.product_code = typeAttribute(device_node, "ProductCode").value_or(0); + device.revision_no = typeAttribute(device_node, "RevisionNo" ).value_or(0); + device.serial_no = typeAttribute(device_node, "SerialNo" ).value_or(0); + device.name = optionalText(device_node, "Name"); + device.group_type = optionalText(device_node, "GroupType"); // is optional in real-world ESIs — Beckhoff IO terminal entries // and other simple slaves have no CoE dictionary. Accept devices without // ; the dictionary will only contain the synthesised 0x1C00/PDO // mapping objects derived from / declarations. - auto* profile_node = device_node->FirstChildElement("Profile"); + XMLElement* profile_node = device_node->FirstChildElement("Profile"); if (profile_node != nullptr) { - auto* profile_no = profile_node->FirstChildElement("ProfileNo"); - profile_no_ = textOrEmpty(profile_no); + profile_no_ = optionalText(profile_node, "ProfileNo"); if (not profile_no_.empty()) { - device.profile_no = parseHexDec(profile_no_, "Profile/ProfileNo"); + device.profile_no = requireNumber(profile_node, "ProfileNo"); } } @@ -544,104 +276,68 @@ Device Parser::buildDeviceFromElement(XMLElement* device_node) return device; } -namespace transition -{ - char const* toString(Type const& t) - { - switch (t) - { - case IP: { return "IP"; } - case PS: { return "PS"; } - case SO: { return "SO"; } - case SP: { return "SP"; } - case OP: { return "OP"; } - case OS: { return "OS"; } - default: { return "unknown"; } - } - } - - void fromString(std::string_view text, Type& out) - { - if (text == "IP") { out = IP; return; } - if (text == "PS") { out = PS; return; } - if (text == "SO") { out = SO; return; } - if (text == "SP") { out = SP; return; } - if (text == "OP") { out = OP; return; } - if (text == "OS") { out = OS; return; } - - std::string what = "ESI: unknown Transition '"; - what.append(text); - what += "'"; - throw std::invalid_argument(what); - } -} - void Parser::parseMailbox(XMLElement* device, std::optional& out) { // The / block is distinct from // // (which carries request/response timeouts). Iterate children only — never // pick the one nested under . - auto* mbx = device->FirstChildElement("Mailbox"); + XMLElement* mbx = device->FirstChildElement("Mailbox"); if (mbx == nullptr) { return; } out.emplace(); - out->data_link_layer = readBoolAttr(mbx, "DataLinkLayer"); - out->real_time_mode = readBoolAttr(mbx, "RealTimeMode"); + out->data_link_layer = boolAttribute(mbx, "DataLinkLayer"); + out->real_time_mode = boolAttribute(mbx, "RealTimeMode"); - if (auto* coe = mbx->FirstChildElement("CoE")) + if (XMLElement* coe = mbx->FirstChildElement("CoE")) { Mailbox::CoE block; - block.sdo_info = readBoolAttr(coe, "SdoInfo"); - block.pdo_assign = readBoolAttr(coe, "PdoAssign"); - block.pdo_config = readBoolAttr(coe, "PdoConfig"); - block.pdo_upload = readBoolAttr(coe, "PdoUpload"); - block.complete_access = readBoolAttr(coe, "CompleteAccess"); - block.segmented_sdo = readBoolAttr(coe, "SegmentedSdo"); - block.diag_history = readBoolAttr(coe, "DiagHistory"); - block.sdo_upload_with_max_length = readBoolAttr(coe, "SdoUploadWithMaxLength"); - block.time_distribution = readBoolAttr(coe, "TimeDistribution"); - if (char const* eds = coe->Attribute("EdsFile")) - { - block.eds_file = eds; - } + block.sdo_info = boolAttribute(coe, "SdoInfo"); + block.pdo_assign = boolAttribute(coe, "PdoAssign"); + block.pdo_config = boolAttribute(coe, "PdoConfig"); + block.pdo_upload = boolAttribute(coe, "PdoUpload"); + block.complete_access = boolAttribute(coe, "CompleteAccess"); + block.segmented_sdo = boolAttribute(coe, "SegmentedSdo"); + block.diag_history = boolAttribute(coe, "DiagHistory"); + block.sdo_upload_with_max_length = boolAttribute(coe, "SdoUploadWithMaxLength"); + block.time_distribution = boolAttribute(coe, "TimeDistribution"); + block.eds_file = attributeOf(coe, "EdsFile").value_or(""); // CoE/Object (obsolete in the XSD) is ignored; modern ESIs use . - for (auto* ic = coe->FirstChildElement("InitCmd"); ic != nullptr; ic = ic->NextSiblingElement("InitCmd")) + block.init_cmds = all(coe, "InitCmd", [](XMLElement* ic) { Mailbox::CoE::InitCmd cmd; cmd.transitions = parseTransitions(ic); - cmd.index = requireNumber(ic, "Index", "Mailbox/CoE/InitCmd"); - cmd.subindex = requireNumber (ic, "SubIndex", "Mailbox/CoE/InitCmd"); - auto* data = requireChild(ic, "Data"); - cmd.data = loadHexBinary(data); - cmd.adapt_automatically = readBoolAttr(data, "AdaptAutomatically"); - cmd.complete_access = readBoolAttr(ic, "CompleteAccess"); - cmd.overwritten_by_module = readBoolAttr(ic, "OverwrittenByModule"); - cmd.comment = commentOf(ic); - block.init_cmds.push_back(std::move(cmd)); - } + cmd.index = requireNumber(ic, "Index"); + cmd.subindex = requireNumber(ic, "SubIndex"); + XMLElement* data = require(ic, "Data"); + cmd.data = hexBinary(data); + cmd.adapt_automatically = boolAttribute(data, "AdaptAutomatically"); + cmd.complete_access = boolAttribute(ic, "CompleteAccess"); + cmd.overwritten_by_module = boolAttribute(ic, "OverwrittenByModule"); + cmd.comment = optionalText(ic, "Comment"); + return cmd; + }); out->coe = std::move(block); } - if (auto* eoe = mbx->FirstChildElement("EoE")) + if (XMLElement* eoe = mbx->FirstChildElement("EoE")) { Mailbox::EoE block; - block.ip = readBoolAttr(eoe, "IP"); - block.mac = readBoolAttr(eoe, "MAC"); - block.time_stamp = readBoolAttr(eoe, "TimeStamp"); - - for (auto* ic = eoe->FirstChildElement("InitCmd"); ic != nullptr; ic = ic->NextSiblingElement("InitCmd")) + block.ip = boolAttribute(eoe, "IP"); + block.mac = boolAttribute(eoe, "MAC"); + block.time_stamp = boolAttribute(eoe, "TimeStamp"); + block.init_cmds = all(eoe, "InitCmd", [](XMLElement* ic) { Mailbox::EoE::InitCmd cmd; cmd.transitions = parseTransitions(ic); - cmd.type = requireNumber(ic, "Type", "Mailbox/EoE/InitCmd"); - cmd.data = loadHexBinary(requireChild(ic, "Data")); - cmd.comment = commentOf(ic); - block.init_cmds.push_back(std::move(cmd)); - } + cmd.type = requireNumber(ic, "Type", BIT_PATTERN); + cmd.data = hexBinary(require(ic, "Data")); + cmd.comment = optionalText(ic, "Comment"); + return cmd; + }); out->eoe = std::move(block); } @@ -650,40 +346,38 @@ void Parser::parseMailbox(XMLElement* device, std::optional& out) out->foe = Mailbox::FoE{}; } - if (auto* soe = mbx->FirstChildElement("SoE")) + if (XMLElement* soe = mbx->FirstChildElement("SoE")) { Mailbox::SoE block; - block.channel_count = readIntAttr(soe, "ChannelCount"); - block.drive_follows_bit3 = readBoolAttr(soe, "DriveFollowsBit3Support"); - - for (auto* ic = soe->FirstChildElement("InitCmd"); ic != nullptr; ic = ic->NextSiblingElement("InitCmd")) + block.channel_count = numberAttribute(soe, "ChannelCount"); + block.drive_follows_bit3 = boolAttribute(soe, "DriveFollowsBit3Support"); + block.init_cmds = all(soe, "InitCmd", [](XMLElement* ic) { Mailbox::SoE::InitCmd cmd; cmd.transitions = parseTransitions(ic); - cmd.idn = requireNumber(ic, "IDN", "Mailbox/SoE/InitCmd"); - cmd.channel = readIntAttr(ic, "Chn").value_or(0); - cmd.data = loadHexBinary(requireChild(ic, "Data")); - cmd.comment = commentOf(ic); - block.init_cmds.push_back(std::move(cmd)); - } + cmd.idn = requireNumber(ic, "IDN", BIT_PATTERN); + cmd.channel = numberAttribute(ic, "Chn").value_or(0); + cmd.data = hexBinary(require(ic, "Data")); + cmd.comment = optionalText(ic, "Comment"); + return cmd; + }); out->soe = std::move(block); } - if (auto* aoe = mbx->FirstChildElement("AoE")) + if (XMLElement* aoe = mbx->FirstChildElement("AoE")) { Mailbox::AoE block; - block.ads_router = readBoolAttr(aoe, "AdsRouter"); - block.generate_own_net_id = readBoolAttr(aoe, "GenerateOwnNetId"); - block.initialize_own_net_id = readBoolAttr(aoe, "InitializeOwnNetId"); - - for (auto* ic = aoe->FirstChildElement("InitCmd"); ic != nullptr; ic = ic->NextSiblingElement("InitCmd")) + block.ads_router = boolAttribute(aoe, "AdsRouter"); + block.generate_own_net_id = boolAttribute(aoe, "GenerateOwnNetId"); + block.initialize_own_net_id = boolAttribute(aoe, "InitializeOwnNetId"); + block.init_cmds = all(aoe, "InitCmd", [](XMLElement* ic) { Mailbox::AoE::InitCmd cmd; cmd.transitions = parseTransitions(ic); - cmd.data = loadHexBinary(requireChild(ic, "Data")); - cmd.comment = commentOf(ic); - block.init_cmds.push_back(std::move(cmd)); - } + cmd.data = hexBinary(require(ic, "Data")); + cmd.comment = optionalText(ic, "Comment"); + return cmd; + }); out->aoe = std::move(block); } @@ -697,107 +391,63 @@ void Parser::parseMailbox(XMLElement* device, std::optional& out) namespace { - PdoEntry parsePdoEntry(XMLElement* node, std::string const& where) + PdoEntry parsePdoEntry(XMLElement* node) { PdoEntry entry; - entry.index = requireNumber(node, "Index", where); - // SubIndex is optional per the XSD (minOccurs=0); padding entries use 0. - if (char const* sub = findText(node, "SubIndex")) - { - entry.subindex = parseHexDec(sub, where + " SubIndex"); - } - entry.bit_len = requireNumber(node, "BitLen", where); - - if (char const* name = findText(node, "Name")) - { - entry.name = name; - } - if (char const* comment = findText(node, "Comment")) - { - entry.comment = comment; - } - if (char const* data_type = findText(node, "DataType")) - { - entry.data_type = data_type; - } - - entry.fixed = readBoolAttr(node, "Fixed"); - entry.safety_conn_number = readIntAttr (node, "SafetyConnNumber"); - if (char const* type = node->Attribute("SafetyPdoEntryType")) - { - entry.safety_pdo_entry_type = type; - } + entry.index = requireNumber(node, "Index"); + entry.subindex = optionalNumber(node, "SubIndex").value_or(0); // padding entries have none + entry.bit_len = requireNumber(node, "BitLen"); + entry.name = optionalText(node, "Name"); + entry.comment = optionalText(node, "Comment"); + entry.data_type = optionalText(node, "DataType"); + + entry.fixed = boolAttribute(node, "Fixed"); + entry.safety_conn_number = numberAttribute(node, "SafetyConnNumber"); + entry.safety_pdo_entry_type = attributeOf(node, "SafetyPdoEntryType").value_or(""); return entry; } } void Parser::parsePdos(XMLElement* device, char const* element_name, std::vector& out) { - for (auto* pdo_node = device->FirstChildElement(element_name); pdo_node != nullptr; - pdo_node = pdo_node->NextSiblingElement(element_name)) + for (XMLElement* pdo_node = device->FirstChildElement(element_name); pdo_node != nullptr; + pdo_node = pdo_node->NextSiblingElement(element_name)) { - Pdo pdo; - - std::string where = element_name; // Legacy aggregate catalogs declare PDOs by template reference // () with no inline ; skip those rather than // rejecting the whole device (the part is defined fully elsewhere). - if (findText(pdo_node, "Index") == nullptr) + if (optionalText(pdo_node, "Index").empty()) { continue; } + + Pdo pdo; // /@DependOnSlot/@DependOnSlotGroup are modular-device attributes // and are not read. - pdo.index = requireNumber(pdo_node, "Index", where); - - char buf[40]; - std::snprintf(buf, sizeof(buf), "%s 0x%04x", element_name, pdo.index); - where = buf; + pdo.index = requireNumber(pdo_node, "Index"); // is mandatory per PdoType. Multi-language variants (@LcId) are // not distinguished; the first wins. - pdo.name = requireChildText(pdo_node, "Name", where); - - pdo.sm = readIntAttr (pdo_node, "Sm"); - pdo.su = readIntAttr (pdo_node, "Su"); - pdo.fixed = readBoolAttr(pdo_node, "Fixed"); - pdo.mandatory = readBoolAttr(pdo_node, "Mandatory"); - pdo.is_virtual = readBoolAttr(pdo_node, "Virtual"); - pdo.os_fac = readIntAttr (pdo_node, "OSFac"); - pdo.os_min = readIntAttr (pdo_node, "OSMin"); - pdo.os_max = readIntAttr (pdo_node, "OSMax"); - pdo.os_index_inc = readIntAttr (pdo_node, "OSIndexInc"); - pdo.pdo_order = readIntAttr (pdo_node, "PdoOrder"); - pdo.overwritten_by_module = readBoolAttr(pdo_node, "OverwrittenByModule"); - pdo.sra_parameter = readBoolAttr(pdo_node, "SRA_Parameter"); - pdo.safety_conn_number = readIntAttr (pdo_node, "SafetyConnNumber"); - if (char const* type = pdo_node->Attribute("SafetyPdoType")) - { - pdo.safety_pdo_type = type; - } - - for (auto* ex = pdo_node->FirstChildElement("Exclude"); ex != nullptr; ex = ex->NextSiblingElement("Exclude")) - { - char const* text = ex->GetText(); - if (text == nullptr) - { - throw std::invalid_argument("ESI: empty in " + where); - } - pdo.exclude.push_back(parseHexDec(text, where + " Exclude")); - } - for (auto* ex_sm = pdo_node->FirstChildElement("ExcludedSm"); ex_sm != nullptr; ex_sm = ex_sm->NextSiblingElement("ExcludedSm")) - { - char const* text = ex_sm->GetText(); - if (text == nullptr) - { - throw std::invalid_argument("ESI: empty in " + where); - } - pdo.excluded_sm.push_back(parseHexDec(text, where + " ExcludedSm")); - } - for (auto* entry = pdo_node->FirstChildElement("Entry"); entry != nullptr; entry = entry->NextSiblingElement("Entry")) - { - pdo.entries.push_back(parsePdoEntry(entry, where + " Entry")); - } + pdo.name = requireText(pdo_node, "Name"); + + pdo.sm = numberAttribute(pdo_node, "Sm"); + pdo.su = numberAttribute(pdo_node, "Su"); + pdo.fixed = boolAttribute(pdo_node, "Fixed"); + pdo.mandatory = boolAttribute(pdo_node, "Mandatory"); + pdo.is_virtual = boolAttribute(pdo_node, "Virtual"); + pdo.os_fac = numberAttribute(pdo_node, "OSFac"); + pdo.os_min = numberAttribute(pdo_node, "OSMin"); + pdo.os_max = numberAttribute(pdo_node, "OSMax"); + pdo.os_index_inc = numberAttribute(pdo_node, "OSIndexInc"); + pdo.pdo_order = numberAttribute(pdo_node, "PdoOrder"); + pdo.overwritten_by_module = boolAttribute(pdo_node, "OverwrittenByModule"); + pdo.sra_parameter = boolAttribute(pdo_node, "SRA_Parameter"); + pdo.safety_conn_number = numberAttribute(pdo_node, "SafetyConnNumber"); + pdo.safety_pdo_type = attributeOf(pdo_node, "SafetyPdoType").value_or(""); + + pdo.exclude = all(pdo_node, "Exclude", [](XMLElement* e) { return numberOf(e); }); + pdo.excluded_sm = all(pdo_node, "ExcludedSm", [](XMLElement* e) { return numberOf(e, BIT_PATTERN); }); + pdo.entries = all(pdo_node, "Entry", parsePdoEntry); for (auto const& existing : out) { @@ -851,7 +501,7 @@ CoE::Object Parser::buildMappingObject(Pdo const& pdo, bool is_rx) if (pdo.entries.size() > 0xFF) { - throw std::invalid_argument("ESI: PDO has more than 255 entries (cannot synthesize mapping)"); + throw std::invalid_argument("PDO has more than 255 entries (cannot synthesize mapping)"); } // ETG.1000.6: a PDO mapping object is read-write in PRE_OP (the master may @@ -874,7 +524,7 @@ CoE::Object Parser::buildMappingObject(Pdo const& pdo, bool is_rx) auto const& e = pdo.entries[i]; if (e.bit_len > 0xFF) { - throw std::invalid_argument("ESI: PDO entry BitLen > 255 cannot fit in mapping word"); + throw std::invalid_argument("PDO entry BitLen > 255 cannot fit in mapping word"); } CoE::Entry entry; entry.subindex = static_cast(i + 1); @@ -917,7 +567,7 @@ CoE::Object Parser::buildAssignmentObject(std::vector const& pdos, uint16_t if (pdos.size() > 0xFF) { - throw std::invalid_argument("ESI: more than 255 PDOs assigned to a SyncManager"); + throw std::invalid_argument("more than 255 PDOs assigned to a SyncManager"); } // ETG.1000.6: the SM PDO-assignment object is read-write in PRE_OP so the @@ -977,7 +627,7 @@ void Parser::synthesizePdoTargetObjects(Device& device) Field& field = targets[e.index][e.subindex]; field.bitlen = e.bit_len; field.name = e.name; - auto type = coeTypeFromLabel(e.data_type); + auto type = CoE::dataTypeFromLabel(e.data_type); if (type.has_value()) { field.type = *type; @@ -1287,162 +937,148 @@ void Parser::synthesizePdoMappingObjects(Device& device) void Parser::parseEeprom(XMLElement* device, std::optional& out) { - auto* eep = device->FirstChildElement("Eeprom"); + XMLElement* eep = device->FirstChildElement("Eeprom"); if (eep == nullptr) { return; } Eeprom block; - block.assign_to_pdi = readBoolAttr(eep, "AssignToPdi"); + block.assign_to_pdi = boolAttribute(eep, "AssignToPdi"); // EepromType is an xs:choice between raw and the structured form // (++…). Reject documents carrying both so the error // names the real problem. - auto* data_raw = eep->FirstChildElement("Data"); - auto* byte_size = eep->FirstChildElement("ByteSize"); + XMLElement* data_raw = eep->FirstChildElement("Data"); + XMLElement* byte_size = eep->FirstChildElement("ByteSize"); if (data_raw != nullptr and byte_size != nullptr) { - throw std::invalid_argument( - "ESI: has both (raw form) and (structured form); " - "they are mutually exclusive per the schema"); + fail(eep, " (raw form) and (structured form) are mutually exclusive"); } if (data_raw != nullptr) { - block.raw_data = loadHexBinary(data_raw); + block.raw_data = hexBinary(data_raw); out = std::move(block); return; } // ByteSize and ConfigData are mandatory children of the structured form. - block.byte_size = parseHexDec(requireChildText(eep, "ByteSize", "Eeprom"), "Eeprom/ByteSize"); - block.config_data = loadHexBinary(requireChild(eep, "ConfigData")); + block.byte_size = requireNumber(eep, "ByteSize", BIT_PATTERN); + block.config_data = hexBinary(require(eep, "ConfigData")); - if (auto* cfg2 = eep->FirstChildElement("ConfigData2")) + if (XMLElement* cfg2 = eep->FirstChildElement("ConfigData2")) { - block.config_data2 = loadHexBinary(cfg2); + block.config_data2 = hexBinary(cfg2); } - if (auto* boot = eep->FirstChildElement("BootStrap")) + if (XMLElement* boot = eep->FirstChildElement("BootStrap")) { - block.bootstrap = loadHexBinary(boot); + block.bootstrap = hexBinary(boot); } - for (auto* cat = eep->FirstChildElement("Category"); cat != nullptr; cat = cat->NextSiblingElement("Category")) + block.categories = all(eep, "Category", [](XMLElement* cat) { Eeprom::Category c; - c.cat_no = requireNumber(cat, "CatNo", "Eeprom/Category"); - c.preserve_online_data = readBoolAttr(cat, "PreserveOnlineData"); + c.cat_no = requireNumber(cat, "CatNo", BIT_PATTERN); + c.preserve_online_data = boolAttribute(cat, "PreserveOnlineData"); // Payload is an xs:choice; pick by element presence, not text content, // so an empty reads as an empty string rather than absent. // The numeric forms still require a value. - if (auto* d = cat->FirstChildElement("Data")) + if (XMLElement* d = cat->FirstChildElement("Data")) { - c.data = loadHexBinary(d); + c.data = hexBinary(d); } - else if (auto* s = cat->FirstChildElement("DataString")) + else if (XMLElement* s = cat->FirstChildElement("DataString")) { - char const* text = s->GetText(); - if (text != nullptr) - { - c.data_string = text; - } - else - { - c.data_string.emplace(); - } + c.data_string = textOf(s); } else if (cat->FirstChildElement("DataUINT") != nullptr) { - c.data_uint = parseHexDec(requireChildText(cat, "DataUINT", "Eeprom/Category"), "Eeprom/Category/DataUINT"); + c.data_uint = requireNumber(cat, "DataUINT", BIT_PATTERN); } else if (cat->FirstChildElement("DataUDINT") != nullptr) { - c.data_udint = parseHexDec(requireChildText(cat, "DataUDINT", "Eeprom/Category"), "Eeprom/Category/DataUDINT"); + c.data_udint = requireNumber(cat, "DataUDINT", BIT_PATTERN); } else { - throw std::invalid_argument("ESI: in Eeprom/Category missing payload (expected one of Data/DataString/DataUINT/DataUDINT)"); + fail(cat, "missing payload (expected one of , , , )"); } - block.categories.push_back(std::move(c)); - } + return c; + }); out = std::move(block); } namespace { + // An empty or keeps its value at 0. + int32_t syncTimeValue(XMLElement* node) + { + if (textOf(node).empty()) + { + return 0; + } + return numberOf(node, BIT_PATTERN); + } + std::optional parseSyncTime(XMLElement* parent, char const* name) { - auto* node = parent->FirstChildElement(name); + XMLElement* node = parent->FirstChildElement(name); if (node == nullptr) { return std::nullopt; } OpMode::SyncTime st; - char const* text = node->GetText(); - if (text != nullptr) - { - st.value = parseHexDec(text, std::string{"Dc/OpMode/"} + name); - } - st.factor = readIntAttr(node, "Factor"); + st.value = syncTimeValue(node); + st.factor = numberAttribute(node, "Factor"); return st; } std::optional parseShiftTime(XMLElement* parent, char const* name) { - auto* node = parent->FirstChildElement(name); + XMLElement* node = parent->FirstChildElement(name); if (node == nullptr) { return std::nullopt; } OpMode::ShiftTime st; - char const* text = node->GetText(); - if (text != nullptr) + st.value = syncTimeValue(node); + st.factor = numberAttribute(node, "Factor"); + if (attributeOf(node, "Input")) { - st.value = parseHexDec(text, std::string{"Dc/OpMode/"} + name); + st.input = boolAttribute(node, "Input"); } - st.factor = readIntAttr(node, "Factor"); - if (node->Attribute("Input") != nullptr) - { - st.input = readBoolAttr(node, "Input"); - } - st.output_delay_time = readIntAttr(node, "OutputDelayTime"); - st.input_delay_time = readIntAttr(node, "InputDelayTime"); + st.output_delay_time = numberAttribute(node, "OutputDelayTime"); + st.input_delay_time = numberAttribute(node, "InputDelayTime"); return st; } } void Parser::parseDc(XMLElement* device, std::optional& out) { - auto* dc = device->FirstChildElement("Dc"); + XMLElement* dc = device->FirstChildElement("Dc"); if (dc == nullptr) { return; } Dc block; - block.unknown_frmw = readBoolAttr(dc, "UnknownFRMW"); - block.unknown_64bit = readBoolAttr(dc, "Unknown64Bit"); - block.external_ref_clock = readBoolAttr(dc, "ExternalRefClock"); - block.potential_reference_clock = readBoolAttr(dc, "PotentialReferenceClock"); - block.time_loop_control_only = readBoolAttr(dc, "TimeLoopControlOnly"); - block.pdo_oversampling = readBoolAttr(dc, "PdoOversampling"); + block.unknown_frmw = boolAttribute(dc, "UnknownFRMW"); + block.unknown_64bit = boolAttribute(dc, "Unknown64Bit"); + block.external_ref_clock = boolAttribute(dc, "ExternalRefClock"); + block.potential_reference_clock = boolAttribute(dc, "PotentialReferenceClock"); + block.time_loop_control_only = boolAttribute(dc, "TimeLoopControlOnly"); + block.pdo_oversampling = boolAttribute(dc, "PdoOversampling"); - static char const* const CYCLE_NAMES[4] = {"CycleTimeSync0", "CycleTimeSync1", "CycleTimeSync2", "CycleTimeSync3"}; - static char const* const SHIFT_NAMES[4] = {"ShiftTimeSync0", "ShiftTimeSync1", "ShiftTimeSync2", "ShiftTimeSync3"}; - - for (auto* om = dc->FirstChildElement("OpMode"); om != nullptr; om = om->NextSiblingElement("OpMode")) + block.op_modes = all(dc, "OpMode", [](XMLElement* om) { + static char const* const CYCLE_NAMES[4] = {"CycleTimeSync0", "CycleTimeSync1", "CycleTimeSync2", "CycleTimeSync3"}; + static char const* const SHIFT_NAMES[4] = {"ShiftTimeSync0", "ShiftTimeSync1", "ShiftTimeSync2", "ShiftTimeSync3"}; + OpMode mode; - mode.name = requireChildText(om, "Name", "Dc/OpMode"); - if (char const* desc = findText(om, "Desc")) - { - mode.desc = desc; - } - mode.assign_activate = requireNumber(om, "AssignActivate", "Dc/OpMode"); - if (char const* aa = findText(om, "ActivateAdditional")) - { - mode.activate_additional = parseHexDec(aa, "Dc/OpMode/ActivateAdditional"); - } + mode.name = requireText(om, "Name"); + mode.desc = optionalText(om, "Desc"); + mode.assign_activate = requireNumber(om, "AssignActivate"); + mode.activate_additional = optionalNumber(om, "ActivateAdditional"); for (int i = 0; i < 4; ++i) { mode.cycle_time[i] = parseSyncTime (om, CYCLE_NAMES[i]); @@ -1451,97 +1087,101 @@ void Parser::parseDc(XMLElement* device, std::optional& out) // /: @No plus the /@OSFac oversampling map. The // SyncType/CycleTime/ShiftTime children are obsolete in the XSD; skipped. - for (auto* sm = om->FirstChildElement("Sm"); sm != nullptr; sm = sm->NextSiblingElement("Sm")) + mode.sm_configs = all(om, "Sm", [](XMLElement* sm) { OpMode::SmConfig cfg; - auto no = readIntAttr(sm, "No"); + std::optional no = numberAttribute(sm, "No"); if (not no) { - throw std::invalid_argument("ESI: in Dc/OpMode missing required @No"); + fail(sm, "missing mandatory @No"); } cfg.no = *no; - - for (auto* p = sm->FirstChildElement("Pdo"); p != nullptr; p = p->NextSiblingElement("Pdo")) + cfg.pdos = all(sm, "Pdo", [](XMLElement* p) { - char const* text = p->GetText(); - if (text == nullptr) - { - throw std::invalid_argument("ESI: empty in Dc/OpMode/Sm"); - } OpMode::SmConfig::PdoRef ref; - ref.index = parseHexDec(text, "Dc/OpMode/Sm/Pdo"); - ref.os_fac = readIntAttr(p, "OSFac"); - cfg.pdos.push_back(ref); - } - mode.sm_configs.push_back(std::move(cfg)); - } - block.op_modes.push_back(std::move(mode)); - } + ref.index = numberOf(p); + ref.os_fac = numberAttribute(p, "OSFac"); + return ref; + }); + return cfg; + }); + return mode; + }); out = std::move(block); } void Parser::parseSyncManagers(XMLElement* device, std::vector& out) { - for (auto* sm = device->FirstChildElement("Sm"); sm != nullptr; sm = sm->NextSiblingElement("Sm")) + out = all(device, "Sm", [](XMLElement* sm) { SmInfo entry; - - char const* text = sm->GetText(); - if (text != nullptr) + std::string text = textOf(sm); + if (not text.empty()) { - fromString(text, entry.type); + try + { + fromString(text, entry.type); + } + catch (std::invalid_argument const& e) + { + fail(sm, e.what()); + } } - entry.min_size = static_cast(readHexDecAttr(sm, "MinSize" ).value_or(0)); - entry.max_size = static_cast(readHexDecAttr(sm, "MaxSize" ).value_or(0)); - entry.default_size = static_cast(readHexDecAttr(sm, "DefaultSize" ).value_or(0)); - entry.start_address = static_cast(readHexDecAttr(sm, "StartAddress").value_or(0)); - entry.control_byte = static_cast (readHexDecAttr(sm, "ControlByte" ).value_or(0)); - entry.enable = static_cast (readHexDecAttr(sm, "Enable" ).value_or(0)); - entry.is_virtual = readBoolAttr(sm, "Virtual"); - entry.op_only = readBoolAttr(sm, "OpOnly"); - - out.push_back(entry); - } + entry.min_size = numberAttribute(sm, "MinSize" ).value_or(0); + entry.max_size = numberAttribute(sm, "MaxSize" ).value_or(0); + entry.default_size = numberAttribute(sm, "DefaultSize" ).value_or(0); + entry.start_address = numberAttribute(sm, "StartAddress").value_or(0); + entry.control_byte = numberAttribute (sm, "ControlByte" ).value_or(0); + entry.enable = numberAttribute (sm, "Enable" ).value_or(0); + entry.is_virtual = boolAttribute(sm, "Virtual"); + entry.op_only = boolAttribute(sm, "OpOnly"); + return entry; + }); } void Parser::parseSyncUnits(XMLElement* device, std::vector& out) { - for (auto* su = device->FirstChildElement("Su"); su != nullptr; su = su->NextSiblingElement("Su")) + out = all(device, "Su", [](XMLElement* su) { SyncUnit entry; - entry.separate_su = readBoolAttr(su, "SeparateSu"); - entry.separate_frame = readBoolAttr(su, "SeparateFrame"); - entry.frame_repeat_support = readBoolAttr(su, "FrameRepeatSupport"); - out.push_back(entry); - } + entry.separate_su = boolAttribute(su, "SeparateSu"); + entry.separate_frame = boolAttribute(su, "SeparateFrame"); + entry.frame_repeat_support = boolAttribute(su, "FrameRepeatSupport"); + return entry; + }); } void Parser::parseFmmus(XMLElement* device, std::vector& out) { - for (auto* fmmu = device->FirstChildElement("Fmmu"); fmmu != nullptr; fmmu = fmmu->NextSiblingElement("Fmmu")) + out = all(device, "Fmmu", [](XMLElement* fmmu) { Fmmu entry; - - char const* text = fmmu->GetText(); - if (text != nullptr) + std::string text = textOf(fmmu); + if (not text.empty()) { - fromString(text, entry.type); + try + { + fromString(text, entry.type); + } + catch (std::invalid_argument const& e) + { + fail(fmmu, e.what()); + } } - if (auto sm_attr = readHexDecAttr(fmmu, "Sm")) + if (std::optional sm = numberAttribute(fmmu, "Sm")) { - entry.sm = static_cast(*sm_attr); + entry.sm = *sm; } - if (auto su_attr = readHexDecAttr(fmmu, "Su")) + if (std::optional su = numberAttribute(fmmu, "Su")) { - entry.su = static_cast(*su_attr); + entry.su = *su; } - entry.op_only = readBoolAttr(fmmu, "OpOnly"); - - out.push_back(entry); - } + entry.op_only = boolAttribute(fmmu, "OpOnly"); + return entry; + }); } CoE::Dictionary Parser::buildDictionary(XMLElement* profile, std::vector const& sms) @@ -1561,13 +1201,14 @@ CoE::Dictionary Parser::buildDictionary(XMLElement* profile, std::vector } if (dictionary != nullptr) { - dtypes_ = dictionary->FirstChildElement("DataTypes"); + dtypes_ = dictionary->FirstChildElement("DataTypes"); // itself is optional, but when present the XSD requires an // child (DictionaryType sequence). - auto* objects = requireChild(dictionary, "Objects"); - for (auto* node_object = objects->FirstChildElement(); node_object != nullptr; node_object = node_object->NextSiblingElement()) + XMLElement* objects = require(dictionary, "Objects"); + for (XMLElement* node_object = objects->FirstChildElement(); node_object != nullptr; node_object = node_object->NextSiblingElement()) { - out.push_back(createObject(node_object)); + uint16_t index = requireNumber(node_object, "Index"); + out.push_back(withContext(objectLabel(index), [&]() { return createObject(node_object, index); })); } } @@ -1580,7 +1221,7 @@ CoE::Dictionary Parser::buildDictionary(XMLElement* profile, std::vector } if (sms.size() > 0xFF) { - throw std::invalid_argument("ESI: more than 255 entries cannot fit in 0x1C00 SubIndex space"); + throw std::invalid_argument("more than 255 entries cannot fit in 0x1C00 SubIndex space"); } CoE::Object sms_type; @@ -1625,47 +1266,21 @@ CoE::Dictionary Parser::loadString(std::string const& xml) return loadDeviceString(xml, {}).dictionary; } -std::vector Parser::loadHexBinary(XMLElement* node) -{ - if (node == nullptr) - { - return {}; - } - char const* raw = node->GetText(); - if (raw == nullptr) - { - return {}; - } - try - { - return xsd::parseHexBinary(raw); - } - catch (std::invalid_argument const& e) - { - std::string what = "ESI: <"; - what += node->Value(); - what += ">: "; - what += e.what(); - throw std::invalid_argument(what); - } -} - - -void Parser::loadDefaultData(XMLNode* node, CoE::Object& obj, CoE::Entry& entry) +void Parser::loadDefaultData(XMLElement* node, CoE::Object& obj, CoE::Entry& entry) { - auto node_info = node->FirstChildElement("Info"); + XMLElement* node_info = node->FirstChildElement("Info"); if (node_info == nullptr) { return; } - auto node_default_data = node_info->FirstChildElement("DefaultData"); + XMLElement* node_default_data = node_info->FirstChildElement("DefaultData"); if (node_default_data != nullptr) { // VISIBLE_STRING and other binary defaults share the same loader; the // ESI hex-binary content is already in the natural byte // order (ASCII bytes for strings, little-endian for numerics). - std::vector data = loadHexBinary(node_default_data); + std::vector data = hexBinary(node_default_data); if (data.size() != ((entry.bitlen + 7u) / 8u)) { @@ -1685,14 +1300,15 @@ void Parser::loadDefaultData(XMLNode* node, CoE::Object& obj, CoE::Entry& entry) return; } - if (char const* default_value = findText(node_info, "DefaultValue")) + std::string default_value = optionalText(node_info, "DefaultValue"); + if (not default_value.empty()) { uint32_t size = (entry.bitlen + 7u) / 8u; // sub-byte entries occupy 1 byte if (size == 0) { return; } - int64_t value = parseHexDec(default_value, objectLabel(obj.index) + " DefaultValue"); + int64_t value = parseInteger(node_info->FirstChildElement("DefaultValue"), default_value); uint32_t copy_size = std::min(size, sizeof(int64_t)); entry.data = std::malloc(size); if (copy_size < size) @@ -1703,11 +1319,11 @@ void Parser::loadDefaultData(XMLNode* node, CoE::Object& obj, CoE::Entry& entry) } } -uint16_t Parser::loadAccess(XMLNode* node) +uint16_t Parser::loadAccess(XMLElement* node) { uint16_t flags = 0; - auto node_flags = node->FirstChildElement("Flags"); + XMLElement* node_flags = node->FirstChildElement("Flags"); if (node_flags == nullptr) { // A missing means default access, like an empty (access=0 @@ -1715,16 +1331,10 @@ uint16_t Parser::loadAccess(XMLNode* node) return CoE::Access::READ; } - auto node_access = node_flags->FirstChildElement("Access"); + XMLElement* node_access = node_flags->FirstChildElement("Access"); if (node_access != nullptr) { - char const* raw = node_access->GetText(); - std::string access; - if (raw != nullptr) - { - access = raw; - } - + std::string access = textOf(node_access); if (access == "rw" or access == "ro") { flags |= CoE::Access::READ; @@ -1734,14 +1344,14 @@ uint16_t Parser::loadAccess(XMLNode* node) flags |= CoE::Access::WRITE; } - auto parseRestrictions = [](char const* raw_restrictions) -> uint16_t + auto parseRestrictions = [](std::optional raw_restrictions) -> uint16_t { - if (raw_restrictions == nullptr) + if (not raw_restrictions) { return CoE::Access::READ; } - std::string restrictions{raw_restrictions}; + std::string restrictions = *raw_restrictions; std::transform(restrictions.begin(), restrictions.end(), restrictions.begin(), [](char c){ return static_cast(std::tolower(static_cast(c))); }); @@ -1755,8 +1365,8 @@ uint16_t Parser::loadAccess(XMLNode* node) }; uint16_t restrictions_mask = 0; - restrictions_mask |= (parseRestrictions(node_access->Attribute("ReadRestrictions")) << 0); - restrictions_mask |= (parseRestrictions(node_access->Attribute("WriteRestrictions")) << 3); + restrictions_mask |= (parseRestrictions(attributeOf(node_access, "ReadRestrictions")) << 0); + restrictions_mask |= (parseRestrictions(attributeOf(node_access, "WriteRestrictions")) << 3); flags &= restrictions_mask; } @@ -1765,26 +1375,23 @@ uint16_t Parser::loadAccess(XMLNode* node) flags |= CoE::Access::READ; } - if (char const* mapping = findText(node_flags, "PdoMapping")) + for (char c : optionalText(node_flags, "PdoMapping")) { - for (char const* c = mapping; *c != '\0'; ++c) + if (std::tolower(static_cast(c)) == 'r') { - if (std::tolower(static_cast(*c)) == 'r') - { - flags |= CoE::Access::RxPDO; - } - if (std::tolower(static_cast(*c)) == 't') - { - flags |= CoE::Access::TxPDO; - } + flags |= CoE::Access::RxPDO; + } + if (std::tolower(static_cast(c)) == 't') + { + flags |= CoE::Access::TxPDO; } } - if (char const* t = findText(node_flags, "Backup"); t and t[0] == '1') + if (optionalText(node_flags, "Backup") == "1") { flags |= CoE::Access::BACKUP; } - if (char const* t = findText(node_flags, "Setting"); t and t[0] == '1') + if (optionalText(node_flags, "Setting") == "1") { flags |= CoE::Access::SETTING; } @@ -1817,6 +1424,22 @@ namespace } } + // ETG.1000.6 standard array of element, held as a single value (OCTET STRING is ARRAY OF BYTE). + CoE::DataType standardArrayType(CoE::DataType element) + { + switch (element) + { + case CoE::DataType::BYTE: + case CoE::DataType::UNSIGNED8: { return CoE::DataType::OCTET_STRING; } + case CoE::DataType::UNSIGNED16: { return CoE::DataType::UNICODE_STRING; } + case CoE::DataType::INTEGER8: { return CoE::DataType::ARRAY_OF_SINT; } + case CoE::DataType::INTEGER16: { return CoE::DataType::ARRAY_OF_INT; } + case CoE::DataType::INTEGER32: { return CoE::DataType::ARRAY_OF_DINT; } + case CoE::DataType::UNSIGNED32: { return CoE::DataType::ARRAY_OF_UDINT; } + default: { return CoE::DataType::UNKNOWN; } + } + } + // 0 when the type has no fixed width (strings, complex, unknown). uint16_t basicBitSize(CoE::DataType type) { @@ -1860,18 +1483,13 @@ CoE::DataType Parser::resolveType(std::string const& type_name, int depth) { if (depth > MAX_TYPE_DEPTH) { - std::string what = "ESI: recursion exceeds depth "; - what += std::to_string(MAX_TYPE_DEPTH); - what += " resolving '"; - what += type_name; - what += "' (cycle?)"; - throw std::invalid_argument(what); + throw std::invalid_argument(" recursion exceeds depth " + std::to_string(MAX_TYPE_DEPTH) + + " resolving '" + type_name + "' (cycle?)"); } - auto it = BASIC_TYPES.find(type_name); - if (it != BASIC_TYPES.end()) + if (auto basic = CoE::dataTypeFromLabel(type_name)) { - return it->second; + return *basic; } if (type_name.find("STRING") != std::string::npos) @@ -1883,60 +1501,41 @@ CoE::DataType Parser::resolveType(std::string const& type_name, int depth) { return CoE::DataType::UNKNOWN; } - auto dtype = dtypes_->FirstChildElement(); - while (dtype) + for (XMLElement* dtype = dtypes_->FirstChildElement(); dtype != nullptr; dtype = dtype->NextSiblingElement()) { - auto name_elem = dtype->FirstChildElement("Name"); - char const* name_text = nullptr; - if (name_elem != nullptr) + if (optionalText(dtype, "Name") != type_name) { - name_text = name_elem->GetText(); + continue; } - if (name_text != nullptr and type_name == name_text) + if (dtype->FirstChildElement("SubItem") or dtype->FirstChildElement("ArrayInfo")) { - if (dtype->FirstChildElement("SubItem") or dtype->FirstChildElement("ArrayInfo")) - { - return CoE::DataType::UNKNOWN; - } - - auto base = dtype->FirstChildElement("BaseType"); - if (base) - { - char const* base_text = base->GetText(); - if (base_text == nullptr) - { - std::string what = "ESI: empty for DataType '"; - what += type_name; - what += "'"; - throw std::invalid_argument(what); - } - return resolveType(base_text, depth + 1); - } - - // Enum DataType without (EtherCATBase.xsd: BaseType is - // minOccurs=0): the value width is its mandatory . - if (dtype->FirstChildElement("EnumInfo")) - { - uint16_t bitsize = requireNumber(dtype, "BitSize", "DataType '" + type_name + "'"); - return enumTypeFromBitSize(bitsize); - } + return CoE::DataType::UNKNOWN; + } + if (dtype->FirstChildElement("BaseType") != nullptr) + { + return resolveType(requireText(dtype, "BaseType"), depth + 1); + } - break; + // Enum DataType without (EtherCATBase.xsd: BaseType is + // minOccurs=0): the value width is its mandatory . + if (dtype->FirstChildElement("EnumInfo")) + { + return enumTypeFromBitSize(requireNumber(dtype, "BitSize")); } - dtype = dtype->NextSiblingElement(); + break; } return CoE::DataType::UNKNOWN; } -std::tuple Parser::parseType(XMLNode* node) +std::tuple Parser::parseType(XMLElement* node) { - char const* type_text = findText(node, "Type"); - if (type_text == nullptr) + std::string type_text = optionalText(node, "Type"); + if (type_text.empty()) { - type_text = findText(node, "BaseType"); + type_text = optionalText(node, "BaseType"); } - if (type_text == nullptr) + if (type_text.empty()) { return {CoE::DataType::UNKNOWN, 0, 0}; } @@ -1947,85 +1546,73 @@ std::tuple Parser::parseType(XMLNode* node) return {CoE::DataType::UNKNOWN, 0, 0}; } - uint16_t bitlen = requireNumber(node, "BitSize", " reference"); - uint16_t bitoff = 0; - if (char const* bitoff_text = findText(node, "BitOffs")) - { - bitoff = parseHexDec(bitoff_text, ""); - } - + uint16_t bitlen = requireNumber(node, "BitSize"); + uint16_t bitoff = optionalNumber(node, "BitOffs").value_or(0); return {type, bitlen, bitoff}; } -XMLNode* Parser::findNodeType(XMLNode* node, std::string const& where) +XMLElement* Parser::findNodeType(XMLElement* node) { - char const* raw_type = requireText(node->FirstChildElement("Type"), "Type", where); - + std::string type = requireText(node, "Type"); if (dtypes_ == nullptr) { return nullptr; } - auto dtype = dtypes_->FirstChildElement(); - while (dtype) + for (XMLElement* dtype = dtypes_->FirstChildElement(); dtype != nullptr; dtype = dtype->NextSiblingElement()) { - auto name_elem = dtype->FirstChildElement("Name"); - if (name_elem != nullptr) + if (optionalText(dtype, "Name") == type) { - char const* name_text = name_elem->GetText(); - if (name_text != nullptr and std::strcmp(raw_type, name_text) == 0) - { - break; - } + return dtype; } - dtype = dtype->NextSiblingElement(); } - - return dtype; + return nullptr; } -CoE::Object Parser::createObject(XMLNode* node) +CoE::Object Parser::createObject(XMLElement* node, uint16_t index) { CoE::Object object; - object.index = requireNumber(node, "Index", "Object"); - - std::string where = objectLabel(object.index); - object.name = requireChildText(node, "Name", where); + object.index = index; + object.name = requireText(node, "Name"); - auto [type, bitlen, bitoff] = parseType(node); - if (CoE::isBasic(type)) + auto setValue = [&](CoE::DataType type, uint16_t bitlen, uint16_t bitoff) { object.code = CoE::ObjectCode::VAR; object.entries.resize(1); - auto& entry = object.entries.at(0); + CoE::Entry& entry = object.entries.at(0); entry.subindex = 0; entry.bitlen = bitlen; entry.bitoff = bitoff; entry.type = type; entry.access = loadAccess(node); - loadDefaultData(node, object, entry); + }; + auto [type, bitlen, bitoff] = parseType(node); + if (CoE::isBasic(type)) + { + setValue(type, bitlen, bitoff); return object; } - auto node_type = findNodeType(node, where); + XMLElement* node_type = findNodeType(node); if (node_type == nullptr) { if (dtypes_ == nullptr) { - throw std::invalid_argument("ESI: " + where + " references a user-defined " - "but no section is present in "); + fail(node, "references a user-defined but no section is present in "); } - throw std::invalid_argument("ESI: unresolved reference in " + where); + fail(node, "unresolved reference"); } - auto node_subitem = node_type->FirstChildElement("SubItem"); - while (node_subitem) + if ((node_type->FirstChildElement("SubItem") == nullptr) and (node_type->FirstChildElement("ArrayInfo") != nullptr)) + { + setValue(standardArrayType(resolveType(requireText(node_type, "BaseType"))), requireNumber(node, "BitSize"), 0); + return object; + } + for (XMLElement* node_subitem = node_type->FirstChildElement("SubItem"); node_subitem != nullptr; + node_subitem = node_subitem->NextSiblingElement("SubItem")) { CoE::Entry entry; - if (char const* text = findText(node_subitem, "Name")) - { - entry.description = text; - } + entry.description = optionalText(node_subitem, "Name"); auto [subitem_type, subitem_bitlen, subitem_bitoff] = parseType(node_subitem); if (CoE::isBasic(subitem_type)) @@ -2035,7 +1622,7 @@ CoE::Object Parser::createObject(XMLNode* node) entry.type = subitem_type; entry.bitlen = subitem_bitlen; entry.bitoff = subitem_bitoff; - entry.subindex = requireNumber(node_subitem, "SubIdx", where + " SubItem"); + entry.subindex = requireNumber(node_subitem, "SubIdx"); entry.access = loadAccess(node_subitem); object.entries.push_back(std::move(entry)); @@ -2044,11 +1631,10 @@ CoE::Object Parser::createObject(XMLNode* node) { object.code = CoE::ObjectCode::ARRAY; - std::string sub_where = where + " SubItem"; - auto node_array_type = findNodeType(node_subitem, sub_where); + XMLElement* node_array_type = findNodeType(node_subitem); if (node_array_type == nullptr) { - throw std::invalid_argument("ESI: unresolved reference in " + sub_where); + fail(node_subitem, "unresolved reference"); } auto [array_type, array_bitlen, array_bitoff] = parseType(node_array_type); entry.type = array_type; @@ -2056,16 +1642,12 @@ CoE::Object Parser::createObject(XMLNode* node) entry.bitoff = array_bitoff; entry.access = loadAccess(node_subitem); - auto node_array_info = node_array_type->FirstChildElement("ArrayInfo"); - if (node_array_info == nullptr) - { - throw std::invalid_argument("ESI: missing in " + sub_where); - } - uint8_t lbound = requireNumber(node_array_info, "LBound", sub_where); + XMLElement* node_array_info = require(node_array_type, "ArrayInfo"); + uint8_t lbound = requireNumber(node_array_info, "LBound"); if (lbound == 0) { entry.subindex = 1; - entry.bitlen = requireNumber(node_array_type, "BitSize", sub_where); + entry.bitlen = requireNumber(node_array_type, "BitSize"); object.entries.push_back(std::move(entry)); } else @@ -2073,17 +1655,17 @@ CoE::Object Parser::createObject(XMLNode* node) // uint16_t loop counter is required: with an 8-bit counter and // 255 (real ETG examples have this), the // post-increment wraps 255 -> 0 and the loop never terminates. - uint16_t elements = requireNumber(node_array_info, "Elements", sub_where); + uint16_t elements = requireNumber(node_array_info, "Elements"); if (elements > 0xFF) { - throw std::invalid_argument("ESI: > 255 exceeds CoE SubIndex space in " + sub_where); + fail(node_array_info, " > 255 exceeds CoE SubIndex space"); } // An empty array (0) is degenerate but shipped // by real vendors; emit no element entries rather than rejecting. if (elements != 0) { - uint16_t total_bitlen = requireNumber(node_subitem, "BitSize", sub_where); - uint16_t element_bitoff = requireNumber(node_subitem, "BitOffs", sub_where); + uint16_t total_bitlen = requireNumber(node_subitem, "BitSize"); + uint16_t element_bitoff = requireNumber(node_subitem, "BitOffs"); uint16_t count = elements; uint16_t element_bitlen; uint16_t base_bitlen = basicBitSize(array_type); @@ -2100,13 +1682,13 @@ CoE::Object Parser::createObject(XMLNode* node) count = static_cast(total_bitlen / element_bitlen); if (count > 0xFF) { - throw std::invalid_argument("ESI: array element count exceeds CoE SubIndex space in " + sub_where); + fail(node_subitem, "array element count exceeds CoE SubIndex space"); } } else { - throw std::invalid_argument("ESI: array " + std::to_string(total_bitlen) - + " not divisible by " + std::to_string(elements) + " in " + sub_where); + fail(node_subitem, "array " + std::to_string(total_bitlen) + + " not divisible by " + std::to_string(elements)); } for (uint16_t i = 1; i <= count; ++i) @@ -2118,8 +1700,7 @@ CoE::Object Parser::createObject(XMLNode* node) + static_cast(element_bitlen) * (i - 1); if (offset > 0xFFFF) { - throw std::invalid_argument("ESI: array element bitoff " + std::to_string(offset) - + " exceeds uint16 range in " + sub_where); + fail(node_subitem, "array element bitoff " + std::to_string(offset) + " exceeds uint16 range"); } CoE::Entry e; e.type = entry.type; @@ -2133,16 +1714,14 @@ CoE::Object Parser::createObject(XMLNode* node) } } } - - node_subitem = node_subitem->NextSiblingElement("SubItem"); } - auto node_info = node->FirstChildElement("Info"); + XMLElement* node_info = node->FirstChildElement("Info"); if (node_info == nullptr) { return object; } - auto object_subitem = node_info->FirstChildElement("SubItem"); + XMLElement* object_subitem = node_info->FirstChildElement("SubItem"); for (auto& entry : object.entries) { if (object_subitem == nullptr) @@ -2150,9 +1729,10 @@ CoE::Object Parser::createObject(XMLNode* node) break; } - if (char const* text = findText(object_subitem, "Name")) + std::string name = optionalText(object_subitem, "Name"); + if (not name.empty()) { - entry.description = text; + entry.description = name; } loadDefaultData(object_subitem, object, entry); diff --git a/lib/src/ESI/SIIBuilder.cc b/lib/src/ESI/SIIBuilder.cc index db5a1619..78c30f1b 100644 --- a/lib/src/ESI/SIIBuilder.cc +++ b/lib/src/ESI/SIIBuilder.cc @@ -40,7 +40,7 @@ namespace pe.data_type = 0; if (entry.index != 0) // index 0 is a padding gap: no type/name { - auto type = Parser::coeTypeFromLabel(entry.data_type); + auto type = CoE::dataTypeFromLabel(entry.data_type); if (type) { pe.data_type = static_cast(*type); diff --git a/lib/src/protocol.cc b/lib/src/protocol.cc index 6b24b9f5..b2a230a3 100644 --- a/lib/src/protocol.cc +++ b/lib/src/protocol.cc @@ -131,6 +131,80 @@ namespace kickcat } + namespace transition + { + namespace + { + struct Entry + { + Type transition; + char const* name; + State from; + State to; + }; + + constexpr Entry TABLE[] = + { + {IP, "IP", INIT, PRE_OP }, + {PS, "PS", PRE_OP, SAFE_OP }, + {PI, "PI", PRE_OP, INIT }, + {SP, "SP", SAFE_OP, PRE_OP }, + {SO, "SO", SAFE_OP, OPERATIONAL}, + {SI, "SI", SAFE_OP, INIT }, + {OS, "OS", OPERATIONAL, SAFE_OP }, + {OP, "OP", OPERATIONAL, PRE_OP }, + {OI, "OI", OPERATIONAL, INIT }, + {IB, "IB", INIT, BOOT }, + {BI, "BI", BOOT, INIT }, + {II, "II", INIT, INIT }, + {PP, "PP", PRE_OP, PRE_OP }, + {SS, "SS", SAFE_OP, SAFE_OP }, + {PO, "PO", PRE_OP, OPERATIONAL}, + }; + } + + char const* toString(Type t) + { + if (t == NONE) + { + return "NONE"; + } + for (auto const& entry : TABLE) + { + if (entry.transition == t) + { + return entry.name; + } + } + return "unknown"; + } + + Type fromString(std::string_view text) + { + for (auto const& entry : TABLE) + { + if (text == entry.name) + { + return entry.transition; + } + } + throw std::invalid_argument("unknown Transition '" + std::string{text} + "'"); + } + + Type between(State from, State to) + { + for (auto const& entry : TABLE) + { + if ((entry.from == from) and (entry.to == to)) + { + return entry.transition; + } + } + return NONE; + } + } + + char const* toString(Command cmd) { switch (cmd) diff --git a/lib/src/utils/xml.cc b/lib/src/utils/xml.cc new file mode 100644 index 00000000..5f7d3454 --- /dev/null +++ b/lib/src/utils/xml.cc @@ -0,0 +1,155 @@ +#include "kickcat/utils/xml.h" +#include "kickcat/utils/xsd.h" + +using namespace tinyxml2; + +namespace kickcat::xml +{ + std::string pathOf(XMLElement const* elem) + { + std::string path; + for (XMLNode const* node = elem; (node != nullptr) and (node->ToElement() != nullptr); node = node->Parent()) + { + XMLElement const* e = node->ToElement(); + std::string part = e->Name(); + + int index = 0; + for (XMLElement const* s = e->PreviousSiblingElement(e->Name()); s != nullptr; s = s->PreviousSiblingElement(e->Name())) + { + ++index; + } + if ((index > 0) or (e->NextSiblingElement(e->Name()) != nullptr)) + { + part += "[" + std::to_string(index) + "]"; + } + + if (path.empty()) + { + path = part; + } + else + { + path = part + "/" + path; + } + } + return path; + } + + void fail(XMLElement const* where, std::string const& what) + { + throw std::invalid_argument(what + " in " + pathOf(where)); + } + + XMLElement* require(XMLElement* parent, char const* name) + { + XMLElement* child = parent->FirstChildElement(name); + if (child == nullptr) + { + fail(parent, std::string{"missing mandatory <"} + name + ">"); + } + return child; + } + + std::string textOf(XMLElement const* elem) + { + std::string out; + for (XMLNode const* node = elem->FirstChild(); node != nullptr; node = node->NextSibling()) + { + if (XMLText const* text = node->ToText()) + { + out += text->Value(); + } + } + return std::string{xsd::trim(out)}; + } + + std::string optionalText(XMLElement const* parent, char const* name) + { + XMLElement const* child = parent->FirstChildElement(name); + if (child == nullptr) + { + return {}; + } + return textOf(child); + } + + std::string requireText(XMLElement* parent, char const* name) + { + XMLElement* child = require(parent, name); + std::string text = textOf(child); + if (text.empty()) + { + fail(child, std::string{"empty <"} + name + ">"); + } + return text; + } + + std::optional attributeOf(XMLElement const* elem, char const* name) + { + char const* raw = elem->Attribute(name); + if (raw == nullptr) + { + return std::nullopt; + } + return std::string{xsd::trim(raw)}; + } + + int64_t parseInteger(XMLElement const* elem, std::string const& text) + { + try + { + return xsd::parseHexDec(text); + } + catch (std::invalid_argument const& e) + { + fail(elem, e.what()); + } + } + + bool parseBool(XMLElement const* elem, std::string const& text) + { + try + { + return xsd::parseBoolean(text); + } + catch (std::invalid_argument const& e) + { + fail(elem, e.what()); + } + } + + bool optionalBool(XMLElement const* parent, char const* name) + { + XMLElement const* child = parent->FirstChildElement(name); + if (child == nullptr) + { + return false; + } + return parseBool(child, textOf(child)); + } + + bool boolAttribute(XMLElement const* elem, char const* name) + { + std::optional raw = attributeOf(elem, name); + if (not raw) + { + return false; + } + return withContext(std::string{"@"} + name, [&]() + { + return parseBool(elem, *raw); + }); + } + + std::vector hexBinary(XMLElement const* elem) + { + try + { + return xsd::parseHexBinary(textOf(elem)); + } + catch (std::invalid_argument const& e) + { + fail(elem, e.what()); + } + } +} diff --git a/lib/src/utils/xsd.cc b/lib/src/utils/xsd.cc index d2cc29af..b6cc48c1 100644 --- a/lib/src/utils/xsd.cc +++ b/lib/src/utils/xsd.cc @@ -35,19 +35,19 @@ namespace kickcat::xsd } return value; } + } - std::string_view trim(std::string_view text) + std::string_view trim(std::string_view text) + { + while (not text.empty() and isSpace(text.front())) { - while (not text.empty() and isSpace(text.front())) - { - text.remove_prefix(1); - } - while (not text.empty() and isSpace(text.back())) - { - text.remove_suffix(1); - } - return text; + text.remove_prefix(1); + } + while (not text.empty() and isSpace(text.back())) + { + text.remove_suffix(1); } + return text; } int64_t parseHexDec(std::string_view raw) diff --git a/tools/debugUseCase.cc b/tools/debugUseCase.cc index 2d56dd7c..deea662a 100644 --- a/tools/debugUseCase.cc +++ b/tools/debugUseCase.cc @@ -1,6 +1,5 @@ #include "kickcat/Link.h" #include "kickcat/protocol.h" -#include "kickcat/DebugHelpers.h" #include "kickcat/SocketNull.h" #include @@ -67,15 +66,15 @@ int main(int argc, char* argv[]) // This register (0x800) has to be R/W on the device - sendWriteRegister(*link, 0x00, 0x800, 0x0000); + writeRegister(*link, 0x00, 0x800, 0x0000); uint16_t value_read; - sendGetRegister(*link, 0x00, 0x800, value_read); + readRegister(*link, 0x00, 0x800, value_read); printf("Value (initial) : %04x\n", value_read); uint8_t value_write = 0x000f; - sendWriteRegister(*link, 0x00, 0x800, value_write); + writeRegister(*link, 0x00, 0x800, value_write); - sendGetRegister(*link, 0x00, 0x800, value_read); + readRegister(*link, 0x00, 0x800, value_read); printf("Value (modified): %04x\n", value_read); } diff --git a/unit/CMakeLists.txt b/unit/CMakeLists.txt index df3a8d38..1e753cda 100644 --- a/unit/CMakeLists.txt +++ b/unit/CMakeLists.txt @@ -3,7 +3,6 @@ add_executable(kickcat_unit src/adler32_sum-t.cc src/bus-t.cc src/SoftPll-t.cc src/dc-t.cc - src/debughelpers-t.cc src/diagnostics-t.cc src/error-t.cc src/Filesystem-t.cc @@ -30,6 +29,7 @@ add_executable(kickcat_unit src/adler32_sum-t.cc src/ESMStateSafeOP-t.cc src/utils/math-t.cc src/utils/xsd-t.cc + src/utils/xml-t.cc src/Units-t.cc src/Timer-t.cc src/CoE/protocol-t.cc diff --git a/unit/src/CoE/OD-t.cc b/unit/src/CoE/OD-t.cc index a6746468..ccf514e2 100644 --- a/unit/src/CoE/OD-t.cc +++ b/unit/src/CoE/OD-t.cc @@ -33,6 +33,18 @@ TEST(CoE, data_type_to_string) } } +TEST(CoE, data_type_from_label) +{ + ASSERT_EQ(dataTypeFromLabel("BOOL"), DataType::BOOLEAN); + ASSERT_EQ(dataTypeFromLabel("UINT"), DataType::UNSIGNED16); + ASSERT_EQ(dataTypeFromLabel("DINT"), DataType::INTEGER32); + ASSERT_EQ(dataTypeFromLabel("LREAL"), DataType::REAL64); + ASSERT_EQ(dataTypeFromLabel("BIT8"), DataType::BIT8); + ASSERT_FALSE(dataTypeFromLabel("uint").has_value()); + ASSERT_FALSE(dataTypeFromLabel("STRING(8)").has_value()); + ASSERT_FALSE(dataTypeFromLabel("").has_value()); +} + TEST(CoE, access_to_string) { { diff --git a/unit/src/ESI/Parser-t.cc b/unit/src/ESI/Parser-t.cc index b2ec8675..d549f358 100644 --- a/unit/src/ESI/Parser-t.cc +++ b/unit/src/ESI/Parser-t.cc @@ -1,5 +1,6 @@ #include #include +#include #include "kickcat/ESI/Parser.h" #include "kickcat/CoE/EsiParser.h" @@ -779,8 +780,7 @@ TEST(ESIParser, loadDevice_parses_mailbox) ASSERT_EQ(coe.init_cmds.size(), 2u); auto const& coe_ic0 = coe.init_cmds[0]; - ASSERT_EQ(coe_ic0.transitions.size(), 1u); - ASSERT_EQ(coe_ic0.transitions[0], ESI::transition::PS); + ASSERT_EQ(coe_ic0.transitions, transition::PS); ASSERT_EQ(coe_ic0.index, 0x1C12); ASSERT_EQ(coe_ic0.subindex, 0x00); ASSERT_EQ(coe_ic0.data.size(), 2u); @@ -796,9 +796,7 @@ TEST(ESIParser, loadDevice_parses_mailbox) ASSERT_TRUE(eoe.mac); ASSERT_FALSE(eoe.time_stamp); ASSERT_EQ(eoe.init_cmds.size(), 1u); - ASSERT_EQ(eoe.init_cmds[0].transitions.size(), 2u); - ASSERT_EQ(eoe.init_cmds[0].transitions[0], ESI::transition::IP); - ASSERT_EQ(eoe.init_cmds[0].transitions[1], ESI::transition::PS); + ASSERT_EQ(eoe.init_cmds[0].transitions, transition::IP | transition::PS); ASSERT_EQ(eoe.init_cmds[0].type, 5); ASSERT_TRUE(mailbox.aoe.has_value()); @@ -831,6 +829,32 @@ TEST(ESIParser, mailbox_absent_when_block_missing) ASSERT_FALSE(device.mailbox.has_value()); } +TEST(ESIParser, mailbox_throws_on_eni_only_transition) +{ + char const* xml = R"( + + #x1V + + T + + PI#x1000#x0000 + + + )"; + + ESI::Parser parser; + try + { + (void) parser.loadString(xml); + FAIL() << "expected invalid_argument"; + } + catch (std::invalid_argument const& e) + { + std::string msg = e.what(); + ASSERT_NE(msg.find("'PI' is not allowed"), std::string::npos) << msg; + } +} + TEST(ESIParser, mailbox_throws_on_unknown_transition) { char const* xml = R"( @@ -2065,6 +2089,54 @@ TEST(ESIParser, array_subitem_with_inconsistent_arrayinfo_uses_base_type_size) ASSERT_EQ(last->bitoff, 16u + 5u * 8u); } +TEST(ESIParser, object_typed_by_a_standard_array_is_a_value) +{ + // Beckhoff EJ6xxx 0x10F2: the object type is the array itself, not a record holding it. + char const* xml = R"( + + #x1V + + T + 0 + + + + ARRAY [0..3] OF BYTEBYTE32 + 04 + + + ARRAY [0..1] OF INTINT32 + 02 + + + + + #x10F2BackupARRAY [0..3] OF BYTE32 + 01020304 + + #x2000IntsARRAY [0..1] OF INT32 + + + + + )"; + + ESI::Parser parser; + auto dictionary = parser.loadString(xml); + auto [backup, value] = findObject(dictionary, 0x10F2, 0); + ASSERT_NE(value, nullptr); + EXPECT_EQ(backup->code, CoE::ObjectCode::VAR); + EXPECT_EQ(backup->entries.size(), 1u); + EXPECT_EQ(value->type, CoE::DataType::OCTET_STRING); + EXPECT_EQ(value->bitlen, 32u); + ASSERT_NE(value->data, nullptr); + EXPECT_EQ(std::memcmp(value->data, "\x01\x02\x03\x04", 4), 0); + + auto [ints, ints_value] = findObject(dictionary, 0x2000, 0); + ASSERT_NE(ints_value, nullptr); + EXPECT_EQ(ints_value->type, CoE::DataType::ARRAY_OF_INT); +} + TEST(ESIParser, throws_on_eeprom_data_and_byte_size_both_present) { char const* xml = R"( diff --git a/unit/src/debughelpers-t.cc b/unit/src/debughelpers-t.cc deleted file mode 100644 index 0c7ae428..00000000 --- a/unit/src/debughelpers-t.cc +++ /dev/null @@ -1,51 +0,0 @@ -#include - -#include "mocks/Sockets.h" -#include "kickcat/SocketNull.h" -#include "kickcat/DebugHelpers.h" - -using namespace kickcat; - -TEST(DebugHelpers, sendGetRegister) -{ - std::shared_ptr io_nominal{ std::make_shared() }; - std::shared_ptr io_redundancy{ std::make_shared() }; - std::shared_ptr link = std::make_shared(io_nominal, io_redundancy, nullptr); - std::vector> expected(1, {Command::FPRD, 0, false}); - - //Succesful read - uint16_t value_read = 1; - io_nominal->checkSendFrame(expected); - EXPECT_CALL(*io_nominal, setTimeout(testing::_)); - io_nominal->handleReply({0xFF}, 1); - sendGetRegister(*link, 0x00, 0x110, value_read); - ASSERT_EQ(value_read, 0xFF); - - //Invalid WKC - io_nominal->checkSendFrame(expected); - EXPECT_CALL(*io_nominal, setTimeout(testing::_)); - io_nominal->handleReply({0xFF}, 2); - ASSERT_THROW(sendGetRegister(*link, 0x00, 0x110, value_read), Error); -} - -TEST(DebugHelpers, sendWriteRegister) -{ - std::shared_ptr io_nominal{ std::make_shared() }; - std::shared_ptr io_redundancy{ std::make_shared() }; - std::shared_ptr link = std::make_shared(io_nominal, io_redundancy, nullptr); - std::vector> expected(1, {Command::FPWR, 0, false}); - - // Succesful write - uint16_t value_write = 0x00; - io_nominal->checkSendFrame(expected); - EXPECT_CALL(*io_nominal, setTimeout(testing::_)); - io_nominal->handleReply({0}, 1); - sendWriteRegister(*link, 0x00, 0x110, value_write); - ASSERT_EQ(value_write, 0); - - //Invalid WKC - io_nominal->checkSendFrame(expected); - EXPECT_CALL(*io_nominal, setTimeout(testing::_)); - io_nominal->handleReply({0xFF}, 2); - ASSERT_THROW(sendWriteRegister(*link, 0x00, 0x110, value_write), Error); -} diff --git a/unit/src/link-t.cc b/unit/src/link-t.cc index 85257d23..c86fe3fd 100644 --- a/unit/src/link-t.cc +++ b/unit/src/link-t.cc @@ -1,6 +1,7 @@ #include "mocks/Sockets.h" #include "kickcat/Link.h" +#include "kickcat/SocketNull.h" using ::testing::Return; using ::testing::_; @@ -1728,4 +1729,43 @@ TEST_F(LRWMergeTest, wkc_inside_a_contribution_attributes_started_slave_to_prefi 0x28 | 0xB8, 0x29 | 0xB9}; ASSERT_EQ(0, std::memcmp(nominal, expected, sizeof(expected))); } + +TEST(Link, readRegister) +{ + std::shared_ptr io_nominal{ std::make_shared() }; + std::shared_ptr io_redundancy{ std::make_shared() }; + Link link{io_nominal, io_redundancy, nullptr}; + std::vector> expected(1, {Command::FPRD, 0, false}); + + uint16_t value = 1; + io_nominal->checkSendFrame(expected); + EXPECT_CALL(*io_nominal, setTimeout(_)); + io_nominal->handleReply({0xFF}, 1); + readRegister(link, 0x00, 0x110, value); + ASSERT_EQ(value, 0xFF); + + io_nominal->checkSendFrame(expected); + EXPECT_CALL(*io_nominal, setTimeout(_)); + io_nominal->handleReply({0xFF}, 2); + ASSERT_THROW(readRegister(link, 0x00, 0x110, value), Error); +} + +TEST(Link, writeRegister) +{ + std::shared_ptr io_nominal{ std::make_shared() }; + std::shared_ptr io_redundancy{ std::make_shared() }; + Link link{io_nominal, io_redundancy, nullptr}; + std::vector> expected(1, {Command::FPWR, 0, false}); + + uint16_t value = 0; + io_nominal->checkSendFrame(expected); + EXPECT_CALL(*io_nominal, setTimeout(_)); + io_nominal->handleReply({0}, 1); + writeRegister(link, 0x00, 0x110, value); + + io_nominal->checkSendFrame(expected); + EXPECT_CALL(*io_nominal, setTimeout(_)); + io_nominal->handleReply({0xFF}, 2); + ASSERT_THROW(writeRegister(link, 0x00, 0x110, value), Error); +} } diff --git a/unit/src/protocol-t.cc b/unit/src/protocol-t.cc index 83614e2f..5f8b8917 100644 --- a/unit/src/protocol-t.cc +++ b/unit/src/protocol-t.cc @@ -25,6 +25,32 @@ TEST(Protocol, State_to_string) } +TEST(Protocol, transition_string_round_trip) +{ + for (uint16_t bit = 0; bit < 15; ++bit) + { + transition::Type t = static_cast(1 << bit); + ASSERT_EQ(transition::fromString(transition::toString(t)), t); + } + ASSERT_STREQ(transition::toString(transition::SO), "SO"); + ASSERT_STREQ(transition::toString(transition::NONE), "NONE"); + ASSERT_THROW(transition::fromString("XX"), std::invalid_argument); + ASSERT_THROW(transition::fromString(""), std::invalid_argument); +} + + +TEST(Protocol, transition_between_states) +{ + ASSERT_EQ(transition::between(State::INIT, State::PRE_OP), transition::IP); + ASSERT_EQ(transition::between(State::PRE_OP, State::SAFE_OP), transition::PS); + ASSERT_EQ(transition::between(State::SAFE_OP, State::OPERATIONAL), transition::SO); + ASSERT_EQ(transition::between(State::OPERATIONAL, State::INIT), transition::OI); + ASSERT_EQ(transition::between(State::INIT, State::BOOT), transition::IB); + ASSERT_EQ(transition::between(State::BOOT, State::PRE_OP), transition::NONE); + ASSERT_EQ(transition::between(State::INIT, State::OPERATIONAL), transition::NONE); +} + + TEST(Protocol, Command_to_string) { for (uint8_t i = 0; i < UINT8_MAX; ++i) // Command code is defined on 8bits diff --git a/unit/src/utils/xml-t.cc b/unit/src/utils/xml-t.cc new file mode 100644 index 00000000..5696c93c --- /dev/null +++ b/unit/src/utils/xml-t.cc @@ -0,0 +1,104 @@ +#include +#include + +#include "kickcat/utils/xml.h" + +using namespace kickcat; +using namespace tinyxml2; + +namespace +{ + class XML : public ::testing::Test + { + protected: + XMLElement* load(char const* text) + { + EXPECT_EQ(doc_.Parse(text), XML_SUCCESS); + return doc_.RootElement(); + } + + template + std::string errorOf(F parse) + { + try + { + parse(); + } + catch (std::invalid_argument const& e) + { + return e.what(); + } + ADD_FAILURE() << "expected std::invalid_argument"; + return {}; + } + + XMLDocument doc_; + }; +} + +TEST_F(XML, path_counts_same_name_siblings_only) +{ + XMLElement* root = load(""); + XMLElement* d = root->FirstChildElement("C")->NextSiblingElement("C")->FirstChildElement("D"); + EXPECT_EQ(xml::pathOf(d), "A/C[1]/D"); + EXPECT_EQ(xml::pathOf(root->FirstChildElement("B")), "A/B"); + EXPECT_EQ(xml::pathOf(root->FirstChildElement("C")), "A/C[0]"); + EXPECT_EQ(errorOf([&]() { xml::fail(d, "boom"); }), "boom in A/C[1]/D"); +} + +TEST_F(XML, text) +{ + XMLElement* root = load(" 123\n "); + EXPECT_EQ(xml::textOf(root->FirstChildElement("B")), "1234"); + EXPECT_EQ(xml::optionalText(root, "B"), "1234"); + EXPECT_EQ(xml::optionalText(root, "C"), ""); + EXPECT_EQ(xml::requireText(root, "B"), "1234"); + EXPECT_EQ(errorOf([&]() { xml::requireText(root, "C"); }), "missing mandatory in A"); + EXPECT_EQ(errorOf([&]() { xml::requireText(root, "E"); }), "empty in A/E"); +} + +TEST_F(XML, attributes) +{ + XMLElement* root = load(""); + EXPECT_EQ(xml::attributeOf(root, "N"), "#x10"); + EXPECT_FALSE(xml::attributeOf(root, "M").has_value()); + EXPECT_EQ(xml::numberAttribute(root, "N"), 16); + EXPECT_FALSE(xml::numberAttribute(root, "M").has_value()); + EXPECT_TRUE(xml::boolAttribute(root, "B")); + EXPECT_FALSE(xml::boolAttribute(root, "M")); + EXPECT_EQ(errorOf([&]() { xml::boolAttribute(root, "Bad"); }), "@Bad: invalid xs:boolean 'yes' in A"); + EXPECT_EQ(errorOf([&]() { xml::numberAttribute(root, "Big"); }), "@Big: value 256 out of range [0, 255] in A"); +} + +TEST_F(XML, numbers) +{ + XMLElement* root = load("

-1

#xFFFFabc
"); + EXPECT_EQ(xml::requireNumber(root, "Q", xml::BIT_PATTERN), -1); + EXPECT_EQ(xml::requireNumber(root, "P", xml::BIT_PATTERN), 0xFFFF); + EXPECT_EQ(xml::optionalNumber(root, "Q"), 0xFFFF); + EXPECT_FALSE(xml::optionalNumber(root, "S").has_value()); + EXPECT_EQ(errorOf([&]() { xml::requireNumber(root, "P"); }), "value -1 out of range [0, 65535] in A/P"); + EXPECT_EQ(errorOf([&]() { xml::requireNumber(root, "Q"); }), "value 65535 out of range [-32768, 32767] in A/Q"); + EXPECT_EQ(errorOf([&]() { xml::requireNumber(root, "R"); }), "invalid numeric value 'abc' in A/R"); + EXPECT_EQ(errorOf([&]() { xml::requireNumber(root, "S"); }), "missing mandatory in A"); +} + +TEST_F(XML, booleans_and_binary) +{ + XMLElement* root = load("1false01 ff0"); + EXPECT_TRUE(xml::optionalBool(root, "T")); + EXPECT_FALSE(xml::optionalBool(root, "F")); + EXPECT_FALSE(xml::optionalBool(root, "M")); + EXPECT_EQ(xml::hexBinary(root->FirstChildElement("H")), (std::vector{0x01, 0xff})); + EXPECT_NE(errorOf([&]() { xml::hexBinary(root->FirstChildElement("Odd")); }).find("odd length"), std::string::npos); +} + +TEST_F(XML, all_and_context) +{ + XMLElement* root = load("12"); + auto values = xml::all(root, "I", [](XMLElement* e) { return xml::numberOf(e); }); + EXPECT_EQ(values, (std::vector{1, 2})); + + EXPECT_EQ(xml::withContext("ESI", []() { return 3; }), 3); + EXPECT_EQ(errorOf([&]() { xml::withContext("ESI", [&]() { xml::fail(root, "boom"); }); }), "ESI: boom in A"); +} diff --git a/unit/src/utils/xsd-t.cc b/unit/src/utils/xsd-t.cc index 25141f98..d81a921e 100644 --- a/unit/src/utils/xsd-t.cc +++ b/unit/src/utils/xsd-t.cc @@ -5,6 +5,14 @@ using namespace kickcat; +TEST(XSD, trim) +{ + EXPECT_EQ(xsd::trim(" \t\r\nUINT \n"), "UINT"); + EXPECT_EQ(xsd::trim("a b"), "a b"); + EXPECT_EQ(xsd::trim(" \n "), ""); + EXPECT_EQ(xsd::trim("\vx\f"), "\vx\f"); +} + TEST(XSD, hexdec) { EXPECT_EQ(xsd::parseHexDec("42"), 42);