From fccc4d3e6f5fbada727740d1d4b6ded718f456a4 Mon Sep 17 00:00:00 2001 From: Philippe Leduc Date: Mon, 5 Oct 2026 10:03:20 +0200 Subject: [PATCH] ESI, ENI: Extract shared configuration support Extract XML readers, ESM transitions, CoE InitCmds, and data type lookup for use by both ESI and ENI. The ESI parser now uses the shared readers and reports errors with element paths. Move register access from DebugHelpers to AbstractLink for ENI startup. Read standard ESI arrays as typed values and carry PDO entry data types. --- lib/CMakeLists.txt | 1 + lib/include/kickcat/CoE/OD.h | 9 +- lib/include/kickcat/CoE/protocol.h | 13 + lib/include/kickcat/ESI/Device.h | 35 +- lib/include/kickcat/ESI/Parser.h | 21 +- lib/include/kickcat/protocol.h | 28 + lib/include/kickcat/utils/xml.h | 146 +++ lib/include/kickcat/utils/xsd.h | 3 + lib/master/CMakeLists.txt | 1 + lib/master/include/kickcat/AbstractLink.h | 20 + lib/master/include/kickcat/DebugHelpers.h | 55 - lib/master/src/AbstractLink.cc | 48 + lib/src/CoE/OD.cc | 49 + lib/src/ESI/Parser.cc | 1226 +++++++-------------- lib/src/ESI/SIIBuilder.cc | 2 +- lib/src/protocol.cc | 74 ++ lib/src/utils/xml.cc | 155 +++ lib/src/utils/xsd.cc | 20 +- tools/debugUseCase.cc | 9 +- unit/CMakeLists.txt | 2 +- unit/src/CoE/OD-t.cc | 12 + unit/src/ESI/Parser-t.cc | 82 +- unit/src/debughelpers-t.cc | 51 - unit/src/link-t.cc | 40 + unit/src/protocol-t.cc | 26 + unit/src/utils/xml-t.cc | 104 ++ unit/src/utils/xsd-t.cc | 8 + 27 files changed, 1243 insertions(+), 997 deletions(-) create mode 100644 lib/include/kickcat/utils/xml.h delete mode 100644 lib/master/include/kickcat/DebugHelpers.h create mode 100644 lib/master/src/AbstractLink.cc create mode 100644 lib/src/utils/xml.cc delete mode 100644 unit/src/debughelpers-t.cc create mode 100644 unit/src/utils/xml-t.cc 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);