diff --git a/examples/master/load_esi/load_esi.cc b/examples/master/load_esi/load_esi.cc index 89a071ba..8a769193 100644 --- a/examples/master/load_esi/load_esi.cc +++ b/examples/master/load_esi/load_esi.cc @@ -1,4 +1,4 @@ -#include "kickcat/CoE/EsiParser.h" +#include "kickcat/ESI/Parser.h" #include "kickcat/OS/Time.h" #include @@ -25,7 +25,7 @@ int main(int argc, char const* argv[]) return 1; } - CoE::EsiParser parser; + ESI::Parser parser; nanoseconds t1 = since_epoch(); CoE::Dictionary coe_dict = parser.loadFile(esi_file); diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 170a7001..176960a5 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -61,7 +61,7 @@ endif() if (ENABLE_ESI_PARSER) find_package(tinyxml2 CONFIG REQUIRED) - list(APPEND KICKCAT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/CoE/EsiParser.cc) + list(APPEND KICKCAT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/ESI/Parser.cc) list(APPEND OS_LIBRARIES tinyxml2::tinyxml2) endif() diff --git a/lib/include/kickcat/CoE/EsiParser.h b/lib/include/kickcat/CoE/EsiParser.h index cb95fd9e..5f5f52be 100644 --- a/lib/include/kickcat/CoE/EsiParser.h +++ b/lib/include/kickcat/CoE/EsiParser.h @@ -1,73 +1,11 @@ #ifndef KICKCAT_COE_ESI_PARSER_H #define KICKCAT_COE_ESI_PARSER_H -#include -#include - -#include "kickcat/CoE/OD.h" +#include "kickcat/ESI/Parser.h" namespace kickcat::CoE { - class EsiParser - { - public: - EsiParser() = default; - ~EsiParser() = default; - - CoE::Dictionary loadFile (std::string const& file); - CoE::Dictionary loadString(std::string const& xml); - - char const* vendor() const { return vendor_->FirstChildElement("Name")->GetText(); } - char const* profile() const { return profile_->FirstChildElement("ProfileNo")->GetText(); } - - private: - template - T toNumber(tinyxml2::XMLElement* node) - { - std::string field = node->GetText(); - if (field.rfind("#x", 0) == 0) - { - field[0] = '0'; - } - return std::stoi(field, nullptr, 0); - } - - CoE::Dictionary parse(); // main method - std::vector loadHexBinary(tinyxml2::XMLElement* node); - std::vector loadString(tinyxml2::XMLElement* node); - - void loadDefaultData(tinyxml2::XMLNode* node, Object& obj, Entry& entry); - - uint16_t loadAccess(tinyxml2::XMLNode* node); - - std::tuple parseType(tinyxml2::XMLNode* node); - - DataType resolveType(std::string const& type_name); - - tinyxml2::XMLNode* findNodeType(tinyxml2::XMLNode* node); - - Object create(tinyxml2::XMLNode* node); - - - // Manage XML entry point - tinyxml2::XMLDocument doc_; - tinyxml2::XMLElement* root_; - - // second level - tinyxml2::XMLElement* vendor_; - tinyxml2::XMLElement* desc_; - - // jump on profile and associated dictionnary - tinyxml2::XMLElement* profile_; - tinyxml2::XMLElement* devices_; - tinyxml2::XMLElement* device_; - tinyxml2::XMLElement* dictionary_; - tinyxml2::XMLElement* dtypes_; - tinyxml2::XMLElement* objects_; - - static const std::unordered_map BASIC_TYPES; - static const std::unordered_map SM_CONF; - }; + using EsiParser = ::kickcat::ESI::Parser; } #endif diff --git a/lib/include/kickcat/ESI/Device.h b/lib/include/kickcat/ESI/Device.h new file mode 100644 index 00000000..07d9a676 --- /dev/null +++ b/lib/include/kickcat/ESI/Device.h @@ -0,0 +1,48 @@ +#ifndef KICKCAT_ESI_DEVICE_H +#define KICKCAT_ESI_DEVICE_H + +#include +#include +#include +#include +#include + +#include "kickcat/CoE/OD.h" + +namespace kickcat::ESI +{ + struct DeviceSummary + { + std::string type; + uint32_t product_code = 0; + uint32_t revision_no = 0; + uint32_t serial_no = 0; + std::string name; + }; + + struct DeviceFilter + { + std::optional type; + std::optional product_code; + std::optional revision_no; + std::size_t index = 0; + }; + + struct Device + { + std::string type; + uint32_t product_code = 0; + uint32_t revision_no = 0; + uint32_t serial_no = 0; + std::string name; + std::string group_type; + uint16_t profile_no = 0; + + std::string vendor_name; + uint32_t vendor_id = 0; + + CoE::Dictionary dictionary; + }; +} + +#endif diff --git a/lib/include/kickcat/ESI/Parser.h b/lib/include/kickcat/ESI/Parser.h new file mode 100644 index 00000000..d67024a6 --- /dev/null +++ b/lib/include/kickcat/ESI/Parser.h @@ -0,0 +1,77 @@ +#ifndef KICKCAT_ESI_PARSER_H +#define KICKCAT_ESI_PARSER_H + +#include +#include +#include +#include +#include + +#include "kickcat/CoE/OD.h" +#include "kickcat/ESI/Device.h" + +namespace kickcat::ESI +{ + class Parser + { + public: + Parser() = default; + ~Parser() = default; + + CoE::Dictionary loadFile (std::string const& file); + CoE::Dictionary loadString(std::string const& xml); + + char const* vendor() const { return vendor_name_.c_str(); } + char const* profile() const { return profile_no_.c_str(); } + + std::vector listDevices (std::string const& file); + std::vector listDevicesString(std::string const& xml); + + Device loadDevice (std::string const& file, DeviceFilter const& filter = {}); + Device loadDeviceString(std::string const& xml, DeviceFilter const& filter = {}); + + 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(); + + std::vector listDevicesImpl(); + Device loadDeviceImpl(DeviceFilter const& filter); + + tinyxml2::XMLElement* selectDevice(DeviceFilter const& filter); + DeviceSummary summarize (tinyxml2::XMLElement* device); + + CoE::Dictionary buildDictionary(tinyxml2::XMLElement* device, tinyxml2::XMLElement* profile); + + std::vector loadHexBinary(tinyxml2::XMLElement* node); + std::vector loadStringData(tinyxml2::XMLElement* node); + + void loadDefaultData(tinyxml2::XMLNode* node, CoE::Object& obj, CoE::Entry& entry); + uint16_t loadAccess(tinyxml2::XMLNode* node); + + static constexpr int MAX_TYPE_DEPTH = 16; + + std::tuple parseType(tinyxml2::XMLNode* node); + CoE::DataType resolveType (std::string const& type_name, int depth = 0); + tinyxml2::XMLNode* findNodeType(tinyxml2::XMLNode* node, std::string const& where); + + CoE::Object createObject(tinyxml2::XMLNode* node); + + tinyxml2::XMLDocument doc_; + tinyxml2::XMLElement* root_ = nullptr; + tinyxml2::XMLElement* vendor_xml_ = nullptr; + tinyxml2::XMLElement* devices_ = nullptr; + + tinyxml2::XMLElement* dtypes_ = nullptr; + + std::string vendor_name_; + std::string profile_no_; + + static const std::unordered_map BASIC_TYPES; + static const std::unordered_map SM_CONF; + }; +} + +#endif diff --git a/lib/src/CoE/EsiParser.cc b/lib/src/CoE/EsiParser.cc deleted file mode 100644 index 04e062b3..00000000 --- a/lib/src/CoE/EsiParser.cc +++ /dev/null @@ -1,526 +0,0 @@ -#include -#include -#include "kickcat/debug.h" - -#include "kickcat/CoE/EsiParser.h" - -using namespace tinyxml2; - -namespace kickcat::CoE -{ - - const std::unordered_map EsiParser::BASIC_TYPES - { - {"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 }, - }; - - const std::unordered_map EsiParser::SM_CONF - { - {"MBoxOut", 1}, - {"MBoxIn", 2}, - {"Outputs", 3}, - {"Inputs", 4}, - }; - - Dictionary EsiParser::loadFile(std::string const& file) - { - XMLError result = doc_.LoadFile(file.c_str()); - if (result != XML_SUCCESS) - { - throw std::runtime_error(doc_.ErrorIDToName(result)); - } - - return parse(); - } - - Dictionary EsiParser::loadString(std::string const& xml) - { - XMLError result = doc_.Parse(xml.c_str()); - if (result != XML_SUCCESS) - { - throw std::runtime_error(doc_.ErrorIDToName(result)); - } - - return parse(); - } - - Dictionary EsiParser::parse() - { - root_ = doc_.RootElement(); - - // Helper to find and check a child element, throw if not found - auto firstChildElement = [](XMLNode* node, char const* name) -> XMLElement* - { - 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; - }; - - // Position handler on main entry points - vendor_ = firstChildElement(root_, "Vendor"); - desc_ = firstChildElement(root_, "Descriptions"); - - // jump on profile and associated dictionnary - devices_ = firstChildElement(desc_, "Devices"); - device_ = firstChildElement(devices_, "Device"); - profile_ = firstChildElement(device_, "Profile"); - dictionary_ = firstChildElement(profile_, "Dictionary"); - dtypes_ = firstChildElement(dictionary_, "DataTypes"); - objects_ = firstChildElement(dictionary_, "Objects"); - - // Load dictionary - Dictionary dictionary; - - // loop over dictionnary - auto node_object = objects_->FirstChildElement(); - while (node_object) - { - CoE::Object obj = create(node_object); - dictionary.push_back(std::move(obj)); - node_object = node_object->NextSiblingElement(); - } - - // load sync managers type object - CoE::Object sms_type; - sms_type.index = 0x1c00; - sms_type.code = ObjectCode::ARRAY; - sms_type.name = "Sync manager type"; - - // create first entry (array size) - sms_type.entries.push_back(CoE::Entry{0, 8, 0, Access::READ, DataType::UNSIGNED8, "Subindex 0"}); - - auto sm = firstChildElement(device_, "Sm"); - while (sm) - { - CoE::Entry entry; - entry.subindex = sms_type.entries.size(); - entry.access = Access::READ; - entry.bitlen = 8; - entry.bitoff = sms_type.entries.size() * 8 + 8; // + 8 for padding of the first entry - entry.description = "Subindex " + std::to_string(sms_type.entries.size()); - entry.type = DataType::UNSIGNED8; - entry.data = malloc(1); - - uint8_t sm_type = SM_CONF.at(sm->GetText()); - std::memcpy(entry.data, &sm_type, 1); - - sms_type.entries.push_back(std::move(entry)); - sm = sm->NextSiblingElement("Sm"); - } - auto& subindex0 = sms_type.entries.at(0); - subindex0.data = malloc(1); - uint8_t array_size = sms_type.entries.size() - 1; - std::memcpy(subindex0.data, &array_size, 1); - dictionary.push_back(std::move(sms_type)); - - return dictionary; - } - - std::vector EsiParser::loadHexBinary(XMLElement* node) - { - std::string field = node->GetText(); - std::vector data; - data.reserve(field.size() / 2); // 2 ascii character for one byte - - // Extract hex, data is already LE - for (std::size_t i = 0; i < field.size(); i += 2) - { - std::string hex = field.substr(i, 2); - uint8_t byte = std::stoi(hex, nullptr, 16); - data.push_back(byte); - } - - return data; - } - - std::vector EsiParser::loadString(XMLElement* node) - { - auto data = loadHexBinary(node); - std::reverse(data.begin(), data.end()); - return data; - } - - void EsiParser::loadDefaultData(XMLNode* node, Object& obj, Entry& entry) - { - auto node_info = node->FirstChildElement("Info"); - if (node_info == nullptr) - { - return; - } - - auto node_default_data = node_info->FirstChildElement("DefaultData"); - if (node_default_data != nullptr) - { - std::vector data; - if(entry.type == DataType::VISIBLE_STRING) - { - data = loadString(node_default_data); - } - else - { - data = loadHexBinary(node_default_data); - } - - if (data.size() != (entry.bitlen / 8)) - { - esi_warning("Cannot load default data for 0x%04x.%d, expected size mismatch.\n" - "-> Got %ld bits, expected: %d bit\n" - "==> Skipping entry\n", - obj.index, entry.subindex, - data.size() * 8, entry.bitlen); - return; - } - entry.data = malloc(entry.bitlen / 8); - std::memcpy(entry.data, data.data(), data.size()); - return; - } - - auto node_default_value = node_info->FirstChildElement("DefaultValue"); - if (node_default_value != nullptr) - { - std::string text = node_default_value->GetText(); - int64_t value; - if (text.rfind("#x", 0) == 0) - { - text[0] = '0'; - value = std::stoll(text, nullptr, 16); - } - else - { - value = std::stoll(text, nullptr, 10); - } - - uint32_t size = entry.bitlen / 8; - entry.data = malloc(size); - std::memcpy(entry.data, &value, size); - } - } - - uint16_t EsiParser::loadAccess(XMLNode* node) - { - uint16_t flags = 0; - - auto node_flags = node->FirstChildElement("Flags"); - if (node_flags == nullptr) - { - return flags; - } - - auto node_access = node_flags->FirstChildElement("Access"); - if (node_access != nullptr) - { - std::string access = node_access->GetText(); - - // global read rule - if (access == "rw" or access == "ro") - { - flags |= Access::READ; - } - - // global write rule - if (access == "rw" or access == "wo") - { - flags |= Access::WRITE; - } - - auto parseRestrictions = [](char const* raw_restrictions) -> uint16_t - { - if (raw_restrictions == nullptr) - { - return Access::READ; - } - - // lower the string - std::string restrictions{raw_restrictions}; - std::transform(restrictions.begin(), restrictions.end(), restrictions.begin(), - [](char c){ return std::tolower(c); }); - - uint16_t result = 0; - if (restrictions.find("preop") != std::string::npos) { result |= Access::READ_PREOP; } - if (restrictions.find("safeop") != std::string::npos) { result |= Access::READ_SAFEOP; } - if (restrictions.find("_op") != std::string::npos) { result |= Access::READ_OP; } - if (restrictions.find("op") == 0) { result |= Access::READ_OP; } - - return result; - }; - - // restrictions - uint16_t restrictions_mask = 0; - restrictions_mask |= (parseRestrictions(node_access->Attribute("ReadRestrictions")) << 0); - restrictions_mask |= (parseRestrictions(node_access->Attribute("WriteRestrictions")) << 3); - - flags &= restrictions_mask; - } - else - { - flags |= Access::READ; // Default value - } - - auto node_pdo_mapping = node_flags->FirstChildElement("PdoMapping"); - if (node_pdo_mapping != nullptr) - { - std::string mapping = node_pdo_mapping->GetText(); - for (auto const& c : mapping) - { - if (std::tolower(c) == 'r') { flags |= Access::RxPDO; } - if (std::tolower(c) == 't') { flags |= Access::TxPDO; } - } - } - - auto node_backup = node_flags->FirstChildElement("Backup"); - if (node_backup != nullptr) - { - if (node_backup->GetText()[0] == '1') { flags |= Access::BACKUP; } - } - - auto node_setting = node_flags->FirstChildElement("Setting"); - if (node_setting != nullptr) - { - if (node_setting->GetText()[0] == '1') { flags |= Access::SETTING; } - } - - return flags; - } - - DataType EsiParser::resolveType(std::string const& type_name) - { - auto it = BASIC_TYPES.find(type_name); - if (it != BASIC_TYPES.end()) - { - return it->second; - } - - if (type_name.find("STRING") != std::string::npos) - { - return DataType::VISIBLE_STRING; - } - - auto dtype = dtypes_->FirstChildElement(); - while (dtype) - { - auto name_elem = dtype->FirstChildElement("Name"); - if (name_elem and type_name == name_elem->GetText()) - { - if (dtype->FirstChildElement("SubItem") or dtype->FirstChildElement("ArrayInfo")) - { - return DataType::UNKNOWN; - } - - auto base = dtype->FirstChildElement("BaseType"); - if (base) - { - return resolveType(base->GetText()); - } - - break; - } - dtype = dtype->NextSiblingElement(); - } - - return DataType::UNKNOWN; - } - - std::tuple EsiParser::parseType(XMLNode* node) - { - auto node_type = node->FirstChildElement("Type"); - if (not node_type) - { - node_type = node->FirstChildElement("BaseType"); - } - - if (not node_type) - { - return {DataType::UNKNOWN, 0, 0}; - } - - DataType type = resolveType(node_type->GetText()); - if (type == DataType::UNKNOWN) - { - return {DataType::UNKNOWN, 0, 0}; - } - - uint16_t bitlen = toNumber(node->FirstChildElement("BitSize")); - uint16_t bitoff = 0; - auto node_bitoff = node->FirstChildElement("BitOffs"); - if (node_bitoff) - { - bitoff = toNumber(node_bitoff); - } - - return {type, bitlen, bitoff}; - } - - - XMLNode* EsiParser::findNodeType(XMLNode* node) - { - std::string raw_type = node->FirstChildElement("Type")->GetText(); - - auto dtype = dtypes_->FirstChildElement(); - while (dtype) - { - if (raw_type == dtype->FirstChildElement("Name")->GetText()) - { - break; - } - dtype = dtype->NextSiblingElement(); - } - - return dtype; - } - - - Object EsiParser::create(XMLNode* node) - { - Object object; - object.index = toNumber(node->FirstChildElement("Index")); - object.name = node->FirstChildElement("Name")->GetText(); - auto [type, bitlen, bitoff] = parseType(node); - if (isBasic(type)) - { - // Basic type: no subindex in the ESI file because it is defined directly in the object node. - object.code = ObjectCode::VAR; - object.entries.resize(1); - auto& 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); - - return object; - } - - auto node_type = findNodeType(node); - auto node_subitem = node_type->FirstChildElement("SubItem"); - while (node_subitem) - { - Entry entry; - auto node_name = node_subitem->FirstChildElement("Name"); - if (node_name) - { - entry.description = node_name->GetText(); - } - - auto [subitem_type, subitem_bitlen, subitem_bitoff] = parseType(node_subitem); - if (isBasic(subitem_type)) - { - object.code = ObjectCode::RECORD; - - entry.type = subitem_type; - entry.bitlen = subitem_bitlen; - entry.bitoff = subitem_bitoff; - entry.subindex = toNumber(node_subitem->FirstChildElement("SubIdx")); - entry.access = loadAccess(node_subitem); - - object.entries.push_back(std::move(entry)); - } - else - { - object.code = ObjectCode::ARRAY; - - auto node_array_type = findNodeType(node_subitem); - auto [array_type, array_bitlen, array_bitoff] = parseType(node_array_type); - entry.type = array_type; - entry.bitlen = array_bitlen; - entry.bitoff = array_bitoff; - entry.access = loadAccess(node_subitem); - - auto node_array_info = node_array_type->FirstChildElement("ArrayInfo"); - uint8_t lbound = toNumber(node_array_info->FirstChildElement("LBound")); - if (lbound == 0) - { - // one big entry which is an array: - // - bitlen shall be updated accordingly - // - elements cannot be used because it represents the internal elements, not the elements accessible - // through subindex - entry.subindex = 1; - entry.bitlen = toNumber(node_array_type->FirstChildElement("BitSize")); - object.entries.push_back(std::move(entry)); - } - else - { - // array entries are the subindex starting from 1, 0 is the array size - uint8_t elements = toNumber(node_array_info->FirstChildElement("Elements")); - uint16_t element_bitlen = toNumber(node_subitem->FirstChildElement("BitSize")) / elements; - uint16_t element_bitoff = toNumber(node_subitem->FirstChildElement("BitOffs")); - - for (uint8_t i = 1; i <= elements; ++i) - { - entry.bitlen = element_bitlen; - entry.bitoff = element_bitoff + element_bitlen * (i - 1); - entry.subindex = i; - object.entries.push_back(std::move(entry)); - } - } - } - - node_subitem = node_subitem->NextSiblingElement("SubItem"); - } - - - // Set default data value - // Update name if possible by using the object node - auto node_info = node->FirstChildElement("Info"); - if (node_info == nullptr) - { - return object; - } - auto object_subitem = node_info->FirstChildElement("SubItem"); - auto entry = object.entries.begin(); - for (auto& object_entry : object.entries) - { - if (object_subitem) - { - auto object_subitem_name = object_subitem->FirstChildElement("Name"); - if (object_subitem_name) - { - object_entry.description = object_subitem_name->GetText(); - } - - loadDefaultData(object_subitem, object, *entry); - - entry++; - object_subitem = object_subitem->NextSiblingElement(); - } - } - - return object; - } -} diff --git a/lib/src/ESI/Parser.cc b/lib/src/ESI/Parser.cc new file mode 100644 index 00000000..9d277920 --- /dev/null +++ b/lib/src/ESI/Parser.cc @@ -0,0 +1,810 @@ +#include +#include +#include +#include + +#include "kickcat/debug.h" +#include "kickcat/ESI/Parser.h" + +using namespace tinyxml2; + +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 }, +}; + +const std::unordered_map Parser::SM_CONF +{ + {"MBoxOut", 1}, + {"MBoxIn", 2}, + {"Outputs", 3}, + {"Inputs", 4}, +}; + +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 overload: fetches then validates. + char const* requireText(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 text) + { + if (text.empty()) + { + throw std::invalid_argument("ESI: empty numeric value"); + } + if (text.rfind("#x", 0) == 0) + { + if (text.size() == 2) + { + throw std::invalid_argument("ESI: '#x' with no hex digits"); + } + text[0] = '0'; + return std::stoll(text, nullptr, 16); + } + if (text.rfind("0x", 0) == 0 or text.rfind("0X", 0) == 0) + { + if (text.size() == 2) + { + throw std::invalid_argument("ESI: '0x' with no hex digits"); + } + return std::stoll(text, nullptr, 16); + } + return std::stoll(text, nullptr, 10); + } + + template + T requireNumber(XMLNode* parent, char const* child, std::string const& where) + { + char const* text = requireText(parent, child, where); + return static_cast(parseHexDec(text)); + } + + std::string objectLabel(uint16_t index) + { + char buf[24]; + std::snprintf(buf, sizeof(buf), "Object 0x%04x", index); + return buf; + } +} + +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 static_cast(parseHexDec(raw)); +} + +void Parser::openFile(std::string const& file) +{ + XMLError result = doc_.LoadFile(file.c_str()); + if (result != XML_SUCCESS) + { + throw std::runtime_error(doc_.ErrorIDToName(result)); + } + resolveTopLevel(); +} + +void Parser::openString(std::string const& xml) +{ + XMLError result = doc_.Parse(xml.c_str()); + if (result != XML_SUCCESS) + { + throw std::runtime_error(doc_.ErrorIDToName(result)); + } + resolveTopLevel(); +} + +void Parser::resolveTopLevel() +{ + root_ = doc_.RootElement(); + 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); +} + +std::vector Parser::listDevices(std::string const& file) +{ + openFile(file); + return listDevicesImpl(); +} + +std::vector Parser::listDevicesString(std::string const& xml) +{ + openString(xml); + return listDevicesImpl(); +} + +std::vector Parser::listDevicesImpl() +{ + std::vector out; + for (auto* d = devices_->FirstChildElement("Device"); d != nullptr; d = d->NextSiblingElement("Device")) + { + out.push_back(summarize(d)); + } + return out; +} + +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")); + return s; +} + +XMLElement* Parser::selectDevice(DeviceFilter const& filter) +{ + bool any_filter = filter.type or filter.product_code or filter.revision_no; + + if (not any_filter) + { + std::size_t i = 0; + for (auto* d = devices_->FirstChildElement("Device"); d != nullptr; d = d->NextSiblingElement("Device")) + { + if (i == filter.index) + { + return d; + } + ++i; + } + throw std::invalid_argument("ESI: device index out of range"); + } + + for (auto* d = devices_->FirstChildElement("Device"); d != nullptr; d = d->NextSiblingElement("Device")) + { + auto* type_node = d->FirstChildElement("Type"); + if (filter.type) + { + std::string type_text = textOrEmpty(type_node); + if (type_text != *filter.type) + { + continue; + } + } + if (filter.product_code) + { + auto pc = readHexDecAttr(type_node, "ProductCode"); + if (not pc or *pc != *filter.product_code) + { + continue; + } + } + if (filter.revision_no) + { + auto rev = readHexDecAttr(type_node, "RevisionNo"); + if (not rev or *rev != *filter.revision_no) + { + continue; + } + } + return d; + } + + throw std::invalid_argument("ESI: no device matches filter"); +} + +Device Parser::loadDevice(std::string const& file, DeviceFilter const& filter) +{ + openFile(file); + return loadDeviceImpl(filter); +} + +Device Parser::loadDeviceString(std::string const& xml, DeviceFilter const& filter) +{ + openString(xml); + return loadDeviceImpl(filter); +} + +Device Parser::loadDeviceImpl(DeviceFilter const& filter) +{ + auto* device_node = selectDevice(filter); + auto* profile_node = requireChild(device_node, "Profile"); + + Device device; + device.vendor_name = vendor_name_; + if (auto* id = vendor_xml_->FirstChildElement("Id"); id != nullptr) + { + char const* text = id->GetText(); + if (text != nullptr and *text != '\0') + { + device.vendor_id = static_cast(parseHexDec(text)); + } + } + + 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")); + + auto* profile_no = profile_node->FirstChildElement("ProfileNo"); + profile_no_ = textOrEmpty(profile_no); + if (not profile_no_.empty()) + { + device.profile_no = static_cast(parseHexDec(profile_no_)); + } + + device.dictionary = buildDictionary(device_node, profile_node); + return device; +} + +CoE::Dictionary Parser::buildDictionary(XMLElement* device, XMLElement* profile) +{ + auto* dictionary = requireChild(profile, "Dictionary"); + dtypes_ = requireChild(dictionary, "DataTypes"); + auto* objects = requireChild(dictionary, "Objects"); + + CoE::Dictionary out; + + for (auto* node_object = objects->FirstChildElement(); node_object != nullptr; node_object = node_object->NextSiblingElement()) + { + out.push_back(createObject(node_object)); + } + + CoE::Object sms_type; + sms_type.index = 0x1c00; + sms_type.code = CoE::ObjectCode::ARRAY; + sms_type.name = "Sync manager type"; + sms_type.entries.push_back(CoE::Entry{0, 8, 0, CoE::Access::READ, CoE::DataType::UNSIGNED8, "Subindex 0"}); + + for (auto* sm = device->FirstChildElement("Sm"); sm != nullptr; sm = sm->NextSiblingElement("Sm")) + { + CoE::Entry entry; + entry.subindex = static_cast(sms_type.entries.size()); + entry.access = CoE::Access::READ; + entry.bitlen = 8; + entry.bitoff = static_cast(sms_type.entries.size() * 8 + 8); + entry.description = "Subindex " + std::to_string(sms_type.entries.size()); + entry.type = CoE::DataType::UNSIGNED8; + entry.data = std::malloc(1); + + char const* sm_text = textOrEmpty(sm); + auto it = SM_CONF.find(sm_text); + uint8_t sm_type = 0; + if (it != SM_CONF.end()) + { + sm_type = it->second; + } + std::memcpy(entry.data, &sm_type, 1); + + sms_type.entries.push_back(std::move(entry)); + } + auto& subindex0 = sms_type.entries.at(0); + subindex0.data = std::malloc(1); + uint8_t array_size = static_cast(sms_type.entries.size() - 1); + std::memcpy(subindex0.data, &array_size, 1); + out.push_back(std::move(sms_type)); + + return out; +} + +CoE::Dictionary Parser::loadFile(std::string const& file) +{ + return loadDevice(file, {}).dictionary; +} + +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 {}; + } + std::string field = raw; + std::vector data; + data.reserve(field.size() / 2); + + for (std::size_t i = 0; i < field.size(); i += 2) + { + std::string hex = field.substr(i, 2); + uint8_t byte = static_cast(std::stoi(hex, nullptr, 16)); + data.push_back(byte); + } + + return data; +} + +std::vector Parser::loadStringData(XMLElement* node) +{ + auto data = loadHexBinary(node); + std::reverse(data.begin(), data.end()); + return data; +} + +void Parser::loadDefaultData(XMLNode* node, CoE::Object& obj, CoE::Entry& entry) +{ + auto node_info = node->FirstChildElement("Info"); + if (node_info == nullptr) + { + return; + } + + auto node_default_data = node_info->FirstChildElement("DefaultData"); + if (node_default_data != nullptr) + { + std::vector data; + if (entry.type == CoE::DataType::VISIBLE_STRING) + { + data = loadStringData(node_default_data); + } + else + { + data = loadHexBinary(node_default_data); + } + + if (data.size() != (entry.bitlen / 8)) + { + esi_warning("Cannot load default data for 0x%04x.%d, expected size mismatch.\n" + "-> Got %ld bits, expected: %d bit\n" + "==> Skipping entry\n", + obj.index, entry.subindex, + data.size() * 8, entry.bitlen); + return; + } + entry.data = std::malloc(entry.bitlen / 8); + std::memcpy(entry.data, data.data(), data.size()); + return; + } + + if (char const* default_value = findText(node_info, "DefaultValue")) + { + int64_t value = parseHexDec(default_value); + uint32_t size = entry.bitlen / 8; + uint32_t copy_size = std::min(size, sizeof(int64_t)); + entry.data = std::malloc(size); + if (copy_size < size) + { + std::memset(entry.data, 0, size); + } + std::memcpy(entry.data, &value, copy_size); + } +} + +uint16_t Parser::loadAccess(XMLNode* node) +{ + uint16_t flags = 0; + + auto node_flags = node->FirstChildElement("Flags"); + if (node_flags == nullptr) + { + return flags; + } + + auto node_access = node_flags->FirstChildElement("Access"); + if (node_access != nullptr) + { + char const* raw = node_access->GetText(); + std::string access; + if (raw != nullptr) + { + access = raw; + } + + if (access == "rw" or access == "ro") + { + flags |= CoE::Access::READ; + } + if (access == "rw" or access == "wo") + { + flags |= CoE::Access::WRITE; + } + + auto parseRestrictions = [](char const* raw_restrictions) -> uint16_t + { + if (raw_restrictions == nullptr) + { + return CoE::Access::READ; + } + + std::string restrictions{raw_restrictions}; + std::transform(restrictions.begin(), restrictions.end(), restrictions.begin(), + [](char c){ return std::tolower(c); }); + + uint16_t result = 0; + if (restrictions.find("preop") != std::string::npos) { result |= CoE::Access::READ_PREOP; } + if (restrictions.find("safeop") != std::string::npos) { result |= CoE::Access::READ_SAFEOP; } + if (restrictions.find("_op") != std::string::npos) { result |= CoE::Access::READ_OP; } + if (restrictions.find("op") == 0) { result |= CoE::Access::READ_OP; } + + return result; + }; + + uint16_t restrictions_mask = 0; + restrictions_mask |= (parseRestrictions(node_access->Attribute("ReadRestrictions")) << 0); + restrictions_mask |= (parseRestrictions(node_access->Attribute("WriteRestrictions")) << 3); + + flags &= restrictions_mask; + } + else + { + flags |= CoE::Access::READ; + } + + if (char const* mapping = findText(node_flags, "PdoMapping")) + { + for (char const* c = mapping; *c != '\0'; ++c) + { + if (std::tolower(*c) == 'r') + { + flags |= CoE::Access::RxPDO; + } + if (std::tolower(*c) == 't') + { + flags |= CoE::Access::TxPDO; + } + } + } + + if (char const* t = findText(node_flags, "Backup"); t and t[0] == '1') + { + flags |= CoE::Access::BACKUP; + } + if (char const* t = findText(node_flags, "Setting"); t and t[0] == '1') + { + flags |= CoE::Access::SETTING; + } + + return flags; +} + +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); + } + + auto it = BASIC_TYPES.find(type_name); + if (it != BASIC_TYPES.end()) + { + return it->second; + } + + if (type_name.find("STRING") != std::string::npos) + { + return CoE::DataType::VISIBLE_STRING; + } + + auto dtype = dtypes_->FirstChildElement(); + while (dtype) + { + auto name_elem = dtype->FirstChildElement("Name"); + char const* name_text = nullptr; + if (name_elem != nullptr) + { + name_text = name_elem->GetText(); + } + if (name_text != nullptr and type_name == name_text) + { + 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); + } + + break; + } + dtype = dtype->NextSiblingElement(); + } + + return CoE::DataType::UNKNOWN; +} + +std::tuple Parser::parseType(XMLNode* node) +{ + char const* type_text = findText(node, "Type"); + if (type_text == nullptr) + { + type_text = findText(node, "BaseType"); + } + if (type_text == nullptr) + { + return {CoE::DataType::UNKNOWN, 0, 0}; + } + + CoE::DataType type = resolveType(type_text); + if (type == CoE::DataType::UNKNOWN) + { + 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 = static_cast(parseHexDec(bitoff_text)); + } + + return {type, bitlen, bitoff}; +} + +XMLNode* Parser::findNodeType(XMLNode* node, std::string const& where) +{ + char const* raw_type = requireText(node->FirstChildElement("Type"), "Type", where); + + auto dtype = dtypes_->FirstChildElement(); + while (dtype) + { + auto name_elem = dtype->FirstChildElement("Name"); + if (name_elem != nullptr) + { + char const* name_text = name_elem->GetText(); + if (name_text != nullptr and std::strcmp(raw_type, name_text) == 0) + { + break; + } + } + dtype = dtype->NextSiblingElement(); + } + + return dtype; +} + +CoE::Object Parser::createObject(XMLNode* node) +{ + CoE::Object object; + object.index = requireNumber(node, "Index", "Object"); + + std::string where = objectLabel(object.index); + object.name = requireText(node, "Name", where); + + auto [type, bitlen, bitoff] = parseType(node); + if (CoE::isBasic(type)) + { + object.code = CoE::ObjectCode::VAR; + object.entries.resize(1); + auto& 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); + + return object; + } + + auto node_type = findNodeType(node, where); + if (node_type == nullptr) + { + throw std::invalid_argument("ESI: unresolved reference in " + where); + } + auto node_subitem = node_type->FirstChildElement("SubItem"); + while (node_subitem) + { + CoE::Entry entry; + if (char const* text = findText(node_subitem, "Name")) + { + entry.description = text; + } + + auto [subitem_type, subitem_bitlen, subitem_bitoff] = parseType(node_subitem); + if (CoE::isBasic(subitem_type)) + { + object.code = CoE::ObjectCode::RECORD; + + entry.type = subitem_type; + entry.bitlen = subitem_bitlen; + entry.bitoff = subitem_bitoff; + entry.subindex = requireNumber(node_subitem, "SubIdx", where + " SubItem"); + entry.access = loadAccess(node_subitem); + + object.entries.push_back(std::move(entry)); + } + else + { + object.code = CoE::ObjectCode::ARRAY; + + std::string sub_where = where + " SubItem"; + auto node_array_type = findNodeType(node_subitem, sub_where); + if (node_array_type == nullptr) + { + throw std::invalid_argument("ESI: unresolved reference in " + sub_where); + } + auto [array_type, array_bitlen, array_bitoff] = parseType(node_array_type); + entry.type = array_type; + entry.bitlen = array_bitlen; + 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); + if (lbound == 0) + { + entry.subindex = 1; + entry.bitlen = requireNumber(node_array_type, "BitSize", sub_where); + object.entries.push_back(std::move(entry)); + } + else + { + uint8_t elements = requireNumber (node_array_info, "Elements", sub_where); + if (elements == 0) + { + throw std::invalid_argument("ESI: is zero in " + sub_where); + } + uint16_t element_bitlen = static_cast(requireNumber(node_subitem, "BitSize", sub_where) / elements); + uint16_t element_bitoff = requireNumber(node_subitem, "BitOffs", sub_where); + + for (uint8_t i = 1; i <= elements; ++i) + { + entry.bitlen = element_bitlen; + entry.bitoff = static_cast(element_bitoff + element_bitlen * (i - 1)); + entry.subindex = i; + object.entries.push_back(std::move(entry)); + } + } + } + + node_subitem = node_subitem->NextSiblingElement("SubItem"); + } + + auto node_info = node->FirstChildElement("Info"); + if (node_info == nullptr) + { + return object; + } + auto object_subitem = node_info->FirstChildElement("SubItem"); + for (auto& entry : object.entries) + { + if (object_subitem == nullptr) + { + break; + } + + if (char const* text = findText(object_subitem, "Name")) + { + entry.description = text; + } + loadDefaultData(object_subitem, object, entry); + + object_subitem = object_subitem->NextSiblingElement(); + } + + return object; +} + +} diff --git a/simulation/network_simulator.cc b/simulation/network_simulator.cc index f7e534ea..9572bb54 100644 --- a/simulation/network_simulator.cc +++ b/simulation/network_simulator.cc @@ -6,7 +6,7 @@ #include #include -#include "kickcat/CoE/EsiParser.h" +#include "kickcat/ESI/Parser.h" #include "kickcat/CoE/mailbox/response.h" #include "kickcat/ESC/EmulatedESC.h" #include "kickcat/Frame.h" @@ -105,7 +105,7 @@ int main(int argc, char* argv[]) output_pdo.reserve(slave_count); constexpr uint32_t PDO_MAX_SIZE = 32; - CoE::EsiParser parser; + ESI::Parser parser; for (const auto& config_path : expanded_slave_configs) { diff --git a/tools/od_generator.cc b/tools/od_generator.cc index d3643c53..03ddf38e 100644 --- a/tools/od_generator.cc +++ b/tools/od_generator.cc @@ -5,7 +5,7 @@ #include "kickcat/CoE/OD.h" #include "kickcat/Prints.h" -#include "kickcat/CoE/EsiParser.h" +#include "kickcat/ESI/Parser.h" namespace kickcat { @@ -146,7 +146,7 @@ namespace kickcat CoE::Dictionary loadOD(std::string esiFileName) { - CoE::EsiParser parser; + ESI::Parser parser; return parser.loadFile(esiFileName); } diff --git a/unit/CMakeLists.txt b/unit/CMakeLists.txt index 3d0ad267..111409db 100644 --- a/unit/CMakeLists.txt +++ b/unit/CMakeLists.txt @@ -23,8 +23,8 @@ add_executable(kickcat_unit src/adler32_sum-t.cc src/ESMStateSafeOP-t.cc src/Units-t.cc src/CoE/protocol-t.cc - src/CoE/EsiParser-t.cc src/CoE/OD-t.cc + src/ESI/Parser-t.cc src/CoE/DS402StateMachine-t.cc src/mailbox/request-t.cc src/mailbox/response-t.cc @@ -37,10 +37,11 @@ add_executable(kickcat_unit src/adler32_sum-t.cc src/Time.cc ) -# Use for ESI as a dataset for EsiParser +# ESI fixtures used by Parser-t.cc file(COPY ${CMAKE_SOURCE_DIR}/examples/slave/nuttx/xmc4800/boards/wdc_foot/foot.xml DESTINATION ${CMAKE_BINARY_DIR}) file(COPY ${CMAKE_SOURCE_DIR}/unit/kickcat_esi_test_basic.xml DESTINATION ${CMAKE_BINARY_DIR}) file(COPY ${CMAKE_SOURCE_DIR}/unit/kickcat_esi_test_complex.xml DESTINATION ${CMAKE_BINARY_DIR}) +file(COPY ${CMAKE_SOURCE_DIR}/unit/kickcat_esi_test_multi_device.xml DESTINATION ${CMAKE_BINARY_DIR}) target_link_libraries(kickcat_unit kickcat GTest::gmock_main) target_include_directories(kickcat_unit PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/unit/kickcat_esi_test_multi_device.xml b/unit/kickcat_esi_test_multi_device.xml new file mode 100644 index 00000000..7df4dd1f --- /dev/null +++ b/unit/kickcat_esi_test_multi_device.xml @@ -0,0 +1,98 @@ + + + + #x0CAFE + KickCAT + + + + + KickCAT Test Devices + KickCAT Test Devices + + + + + kickcat_multi_dev_alpha + KickCAT Multi Device Alpha + KickCAT Test Devices + + 5101 + + + + UDINT + 32 + + + + + #x1000 + Device Type + UDINT + 32 + + ro + + + + + + + + kickcat_multi_dev_beta + KickCAT Multi Device Beta + KickCAT Test Devices + + 5102 + + + + UDINT + 32 + + + + + #x1000 + Device Type + UDINT + 32 + + ro + + + + + + + + kickcat_multi_dev_beta + KickCAT Multi Device Beta r2 + KickCAT Test Devices + + 5103 + + + + UDINT + 32 + + + + + #x1000 + Device Type + UDINT + 32 + + ro + + + + + + + + + diff --git a/unit/src/CoE/EsiParser-t.cc b/unit/src/ESI/Parser-t.cc similarity index 53% rename from unit/src/CoE/EsiParser-t.cc rename to unit/src/ESI/Parser-t.cc index e2df4229..4d06cc96 100644 --- a/unit/src/CoE/EsiParser-t.cc +++ b/unit/src/ESI/Parser-t.cc @@ -1,30 +1,31 @@ #include #include +#include "kickcat/ESI/Parser.h" #include "kickcat/CoE/EsiParser.h" -// This test is not a real unit test but an integration test: it could be rework later -// if the benefit/cost is OK +// This test is not a real unit test but an integration test: it could be reworked later +// if the benefit/cost is OK. using namespace kickcat; -TEST(EsiParser, load_error_not_an_xml) +TEST(ESIParser, load_error_not_an_xml) { - CoE::EsiParser parser; + ESI::Parser parser; ASSERT_THROW((void) parser.loadFile(""), std::runtime_error); ASSERT_THROW((void) parser.loadString(""), std::runtime_error); } -TEST(EsiParser, load_error_xml_not_an_esi) +TEST(ESIParser, load_error_xml_not_an_esi) { - CoE::EsiParser parser; + ESI::Parser parser; ASSERT_THROW((void) parser.loadString(""), std::invalid_argument); } -TEST(EsiParser, load) +TEST(ESIParser, load) { - CoE::EsiParser parser; + ESI::Parser parser; auto dictionary = parser.loadFile("foot.xml"); ASSERT_EQ(dictionary.size(), 22); @@ -137,9 +138,9 @@ TEST(EsiParser, load) } } -TEST(EsiParser, load_complex_with_enums_and_default_value) +TEST(ESIParser, load_complex_with_enums_and_default_value) { - CoE::EsiParser parser; + ESI::Parser parser; auto dictionary = parser.loadFile("kickcat_esi_test_complex.xml"); ASSERT_STREQ(parser.profile(), "5002"); @@ -148,7 +149,6 @@ TEST(EsiParser, load_complex_with_enums_and_default_value) // 5 objects + 1 SM type object = 6 ASSERT_EQ(dictionary.size(), 6); - // 0x1000 - standard VAR with DefaultData { auto [object, entry] = findObject(dictionary, 0x1000, 0); ASSERT_NE(object, nullptr); @@ -164,7 +164,6 @@ TEST(EsiParser, load_complex_with_enums_and_default_value) ASSERT_EQ(data, 0x1389); } - // 0x6010 - VAR using enum type (DT_OperationMode -> USINT) with DefaultValue (decimal) { auto [object, entry] = findObject(dictionary, 0x6010, 0); ASSERT_NE(object, nullptr); @@ -181,7 +180,6 @@ TEST(EsiParser, load_complex_with_enums_and_default_value) ASSERT_EQ(data, 2); } - // 0x6020 - VAR using leaf alias type (DT_Temperature -> INT) with DefaultValue (hex) { auto [object, entry] = findObject(dictionary, 0x6020, 0); ASSERT_NE(object, nullptr); @@ -198,7 +196,6 @@ TEST(EsiParser, load_complex_with_enums_and_default_value) ASSERT_EQ(data, 0x00E1); } - // 0x2000 - RECORD with enum and alias SubItems, DefaultValue in SubItems { auto [object, entry] = findObject(dictionary, 0x2000, 0); ASSERT_NE(object, nullptr); @@ -208,7 +205,6 @@ TEST(EsiParser, load_complex_with_enums_and_default_value) ASSERT_EQ(object->name, "Drive Parameters"); ASSERT_EQ(object->entries.size(), 3); - // SubIndex 0: USINT ASSERT_EQ(entry->type, CoE::DataType::UNSIGNED8); ASSERT_EQ(entry->bitlen, 8); ASSERT_NE(entry->data, nullptr); @@ -216,7 +212,6 @@ TEST(EsiParser, load_complex_with_enums_and_default_value) std::memcpy(&sub0, entry->data, 1); ASSERT_EQ(sub0, 2); - // SubIndex 1: DT_OperationMode -> USINT, DefaultValue=1 auto [obj1, e1] = findObject(dictionary, 0x2000, 1); ASSERT_NE(e1, nullptr); ASSERT_EQ(e1->type, CoE::DataType::UNSIGNED8); @@ -227,7 +222,6 @@ TEST(EsiParser, load_complex_with_enums_and_default_value) std::memcpy(&mode, e1->data, 1); ASSERT_EQ(mode, 1); - // SubIndex 2: DT_Temperature -> INT, DefaultValue=#x0019 (25) auto [obj2, e2] = findObject(dictionary, 0x2000, 2); ASSERT_NE(e2, nullptr); ASSERT_EQ(e2->type, CoE::DataType::INTEGER16); @@ -239,7 +233,6 @@ TEST(EsiParser, load_complex_with_enums_and_default_value) ASSERT_EQ(temp, 25); } - // SM type object { auto [object, entry] = findObject(dictionary, 0x1C00, 0); ASSERT_NE(object, nullptr); @@ -248,18 +241,17 @@ TEST(EsiParser, load_complex_with_enums_and_default_value) } } -TEST(EsiParser, load_basic_with_bit_types_and_no_info) +TEST(ESIParser, load_basic_with_bit_types_and_no_info) { - CoE::EsiParser parser; + ESI::Parser parser; auto dictionary = parser.loadFile("kickcat_esi_test_basic.xml"); ASSERT_STREQ(parser.profile(), "5001"); ASSERT_STREQ(parser.vendor(), "KickCAT"); - // 8 objects in the dictionary + 1 SM type object generated by parser = 9 + // 8 objects + 1 SM type object = 9 ASSERT_EQ(dictionary.size(), 9); - // 0x1000 - basic VAR with DefaultData { auto [object, entry] = findObject(dictionary, 0x1000, 0); ASSERT_NE(object, nullptr); @@ -272,7 +264,6 @@ TEST(EsiParser, load_basic_with_bit_types_and_no_info) ASSERT_NE(entry->data, nullptr); } - // 0x1008 - STRING type { auto [object, entry] = findObject(dictionary, 0x1008, 0); ASSERT_NE(object, nullptr); @@ -283,7 +274,6 @@ TEST(EsiParser, load_basic_with_bit_types_and_no_info) ASSERT_EQ(entry->bitlen, 64); } - // 0x10F8 - VAR without Info element (must not crash) { auto [object, entry] = findObject(dictionary, 0x10F8, 0); ASSERT_NE(object, nullptr); @@ -296,7 +286,6 @@ TEST(EsiParser, load_basic_with_bit_types_and_no_info) ASSERT_EQ(entry->data, nullptr); } - // 0x6000 - RECORD with BIT6 SubItem { auto [object, entry] = findObject(dictionary, 0x6000, 0); ASSERT_NE(object, nullptr); @@ -305,33 +294,28 @@ TEST(EsiParser, load_basic_with_bit_types_and_no_info) ASSERT_EQ(object->code, CoE::ObjectCode::RECORD); ASSERT_EQ(object->entries.size(), 5); - // SubIndex 0: USINT auto [obj0, e0] = findObject(dictionary, 0x6000, 0); ASSERT_EQ(e0->type, CoE::DataType::UNSIGNED8); ASSERT_EQ(e0->bitlen, 8); - // SubIndex 1: UINT (SensorValue) auto [obj1, e1] = findObject(dictionary, 0x6000, 1); ASSERT_NE(e1, nullptr); ASSERT_EQ(e1->type, CoE::DataType::UNSIGNED16); ASSERT_EQ(e1->bitlen, 16); ASSERT_EQ(e1->bitoff, 16); - // SubIndex 2: BOOL (DigitalIn) auto [obj2, e2] = findObject(dictionary, 0x6000, 2); ASSERT_NE(e2, nullptr); ASSERT_EQ(e2->type, CoE::DataType::BOOLEAN); ASSERT_EQ(e2->bitlen, 1); ASSERT_EQ(e2->bitoff, 32); - // SubIndex 3: BIT6 (Padding) auto [obj3, e3] = findObject(dictionary, 0x6000, 3); ASSERT_NE(e3, nullptr); ASSERT_EQ(e3->type, CoE::DataType::BIT6); ASSERT_EQ(e3->bitlen, 6); ASSERT_EQ(e3->bitoff, 33); - // SubIndex 4: BOOL (StatusBit) auto [obj4, e4] = findObject(dictionary, 0x6000, 4); ASSERT_NE(e4, nullptr); ASSERT_EQ(e4->type, CoE::DataType::BOOLEAN); @@ -339,7 +323,6 @@ TEST(EsiParser, load_basic_with_bit_types_and_no_info) ASSERT_EQ(e4->bitoff, 39); } - // 0x7010 - RECORD with BIT6 SubItem (outputs) { auto [object, entry] = findObject(dictionary, 0x7010, 0); ASSERT_NE(object, nullptr); @@ -354,7 +337,6 @@ TEST(EsiParser, load_basic_with_bit_types_and_no_info) ASSERT_EQ(e3->bitlen, 6); } - // SM type object generated by parser (4 Sm elements) { auto [object, entry] = findObject(dictionary, 0x1C00, 0); ASSERT_NE(object, nullptr); @@ -365,3 +347,245 @@ TEST(EsiParser, load_basic_with_bit_types_and_no_info) ASSERT_EQ(object->entries.size(), 5); } } + +TEST(ESIParser, loadDevice_populates_aggregate) +{ + ESI::Parser parser; + ESI::Device device = parser.loadDevice("kickcat_esi_test_basic.xml"); + + ASSERT_EQ(device.vendor_name, "KickCAT"); + ASSERT_EQ(device.vendor_id, 0x0CAFE); + ASSERT_EQ(device.type, "kickcat_esi_test_basic"); + ASSERT_EQ(device.product_code, 0x00001234u); + ASSERT_EQ(device.revision_no, 0x1u); + ASSERT_EQ(device.name, "KickCAT ESI Test Basic"); + ASSERT_EQ(device.group_type, "KickCAT Test Devices"); + ASSERT_EQ(device.profile_no, 5001u); + ASSERT_EQ(device.dictionary.size(), 9u); +} + +TEST(ESIParser, listDevices_enumerates_all) +{ + ESI::Parser parser; + auto summaries = parser.listDevices("kickcat_esi_test_multi_device.xml"); + + ASSERT_EQ(summaries.size(), 3u); + + ASSERT_EQ(summaries[0].type, "kickcat_multi_dev_alpha"); + ASSERT_EQ(summaries[0].product_code, 0x00001111u); + ASSERT_EQ(summaries[0].revision_no, 0x1u); + + ASSERT_EQ(summaries[1].type, "kickcat_multi_dev_beta"); + ASSERT_EQ(summaries[1].product_code, 0x00002222u); + ASSERT_EQ(summaries[1].revision_no, 0x1u); + + ASSERT_EQ(summaries[2].type, "kickcat_multi_dev_beta"); + ASSERT_EQ(summaries[2].product_code, 0x00002222u); + ASSERT_EQ(summaries[2].revision_no, 0x2u); +} + +TEST(ESIParser, loadDevice_default_picks_first) +{ + ESI::Parser parser; + ESI::Device device = parser.loadDevice("kickcat_esi_test_multi_device.xml"); + + ASSERT_EQ(device.type, "kickcat_multi_dev_alpha"); + ASSERT_EQ(device.product_code, 0x00001111u); + ASSERT_EQ(device.profile_no, 5101u); +} + +TEST(ESIParser, loadDevice_filter_by_index) +{ + ESI::Parser parser; + ESI::DeviceFilter filter; + filter.index = 2; + ESI::Device device = parser.loadDevice("kickcat_esi_test_multi_device.xml", filter); + + ASSERT_EQ(device.product_code, 0x00002222u); + ASSERT_EQ(device.revision_no, 0x2u); + ASSERT_EQ(device.profile_no, 5103u); +} + +TEST(ESIParser, loadDevice_filter_by_product_and_revision) +{ + ESI::Parser parser; + ESI::DeviceFilter filter; + filter.product_code = 0x00002222u; + filter.revision_no = 0x2u; + ESI::Device device = parser.loadDevice("kickcat_esi_test_multi_device.xml", filter); + + ASSERT_EQ(device.type, "kickcat_multi_dev_beta"); + ASSERT_EQ(device.product_code, 0x00002222u); + ASSERT_EQ(device.revision_no, 0x2u); + ASSERT_EQ(device.profile_no, 5103u); +} + +TEST(ESIParser, loadDevice_filter_by_type) +{ + ESI::Parser parser; + ESI::DeviceFilter filter; + filter.type = "kickcat_multi_dev_beta"; + ESI::Device device = parser.loadDevice("kickcat_esi_test_multi_device.xml", filter); + + // first match wins (revision 1) + ASSERT_EQ(device.product_code, 0x00002222u); + ASSERT_EQ(device.revision_no, 0x1u); +} + +TEST(ESIParser, loadDevice_filter_no_match_throws) +{ + ESI::Parser parser; + ESI::DeviceFilter filter; + filter.product_code = 0xDEADBEEFu; + ASSERT_THROW((void) parser.loadDevice("kickcat_esi_test_multi_device.xml", filter), + std::invalid_argument); +} + +TEST(ESIParser, throws_with_context_when_object_missing_name) +{ + char const* xml = R"( + + #x1V + + T + 0 + + UDINT32 + + + #x1000 + UDINT + 32 + + + + + + )"; + + 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("Name"), std::string::npos) << msg; + ASSERT_NE(msg.find("Object 0x1000"), std::string::npos) << msg; + } +} + +TEST(ESIParser, throws_with_context_when_object_missing_index) +{ + char const* xml = R"( + + #x1V + + T + 0 + + UDINT32 + + + NoIndex + UDINT + 32 + + + + + + )"; + + ESI::Parser parser; + ASSERT_THROW((void) parser.loadString(xml), std::invalid_argument); +} + +TEST(ESIParser, throws_on_basetype_cycle) +{ + char const* xml = R"( + + #x1V + + T + 0 + + + DT_ADT_B32 + DT_BDT_A32 + + + + #x1000 + Cycle + DT_A + 32 + + + + + + )"; + + 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("recursion"), std::string::npos) << msg; + } +} + +TEST(ESIParser, throws_when_subitem_type_unresolved) +{ + // RECORD references DT_Missing which is not in . + char const* xml = R"( + + #x1V + + T + 0 + + + USINT8 + + + + #x2000 + Bad + DT_Missing + 16 + + + + + + )"; + + 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("Object 0x2000"), std::string::npos) << msg; + } +} + +TEST(ESIParser, CoE_alias_is_backwards_compatible) +{ + // The CoE::EsiParser alias must still resolve to ESI::Parser. + CoE::EsiParser parser; + auto dictionary = parser.loadFile("kickcat_esi_test_basic.xml"); + ASSERT_EQ(dictionary.size(), 9u); + ASSERT_STREQ(parser.vendor(), "KickCAT"); +}