From 3306f7eef0d6eee274b6543eea0384fe43335efe Mon Sep 17 00:00:00 2001 From: Philippe Leduc Date: Thu, 1 Oct 2026 12:23:35 +0200 Subject: [PATCH] ENI: Load the process data description of ENI files - Load the process data, cyclic frames and process image of an ENI file. - Add load_eni to dump an ENI file. --- examples/master/CMakeLists.txt | 1 + examples/master/load_eni/CMakeLists.txt | 5 + examples/master/load_eni/load_eni.cc | 153 ++++++++++++++ lib/src/ENI/Parser.cc | 202 +++++++++++++++++++ unit/CMakeLists.txt | 1 + unit/kickcat_eni_test_io.xml | 256 ++++++++++++++++++++++++ unit/src/ENI/Parser-t.cc | 216 ++++++++++++++++++++ 7 files changed, 834 insertions(+) create mode 100644 examples/master/load_eni/CMakeLists.txt create mode 100644 examples/master/load_eni/load_eni.cc create mode 100644 unit/kickcat_eni_test_io.xml diff --git a/examples/master/CMakeLists.txt b/examples/master/CMakeLists.txt index 43742ae6..b897a2ff 100644 --- a/examples/master/CMakeLists.txt +++ b/examples/master/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory(easycat) add_subdirectory(elmo) add_subdirectory(freedom-k64f) add_subdirectory(ingenia) +add_subdirectory(load_eni) add_subdirectory(load_esi) add_subdirectory(simulated_bus) add_subdirectory(wdc_foot) diff --git a/examples/master/load_eni/CMakeLists.txt b/examples/master/load_eni/CMakeLists.txt new file mode 100644 index 00000000..7d22a472 --- /dev/null +++ b/examples/master/load_eni/CMakeLists.txt @@ -0,0 +1,5 @@ +if (ENABLE_ENI_PARSER) + add_executable(load_eni load_eni.cc) + target_link_libraries(load_eni kickcat argparse::argparse) + set_kickcat_properties(load_eni) +endif () diff --git a/examples/master/load_eni/load_eni.cc b/examples/master/load_eni/load_eni.cc new file mode 100644 index 00000000..2f424869 --- /dev/null +++ b/examples/master/load_eni/load_eni.cc @@ -0,0 +1,153 @@ +#include +#include + +#include "kickcat/ENI/Parser.h" +#include "kickcat/OS/Time.h" + +using namespace kickcat; + +namespace +{ + template + void printTransitionCounts(char const* label, std::vector const& cmds) + { + printf(" %-10s %3zu:", label, cmds.size()); + for (uint16_t bit = 0; bit < 15; ++bit) + { + transition::Type t = static_cast(1 << bit); + int count = 0; + for (auto const& cmd : cmds) + { + if (cmd.transitions & t) + { + ++count; + } + } + if (count > 0) + { + printf(" %s=%d", transition::toString(t), count); + } + } + printf("\n"); + } + + std::string statesToString(uint8_t states) + { + std::string out; + if (states & State::INIT) { out += "I"; } + if (states & State::PRE_OP) { out += "P"; } + if (states & State::SAFE_OP) { out += "S"; } + if (states & State::OPERATIONAL) { out += "O"; } + return out; + } + + void printSlave(std::size_t index, ENI::Slave const& slave) + { + ENI::SlaveInfo const& info = slave.info; + printf("[%zu] %s\n", index, info.name.c_str()); + printf(" phys 0x%04x autoinc 0x%04x vendor 0x%08x product 0x%08x revision 0x%08x\n", + info.phys_addr.value_or(0), info.auto_inc_addr.value_or(0), info.vendor_id, info.product_code, info.revision_no); + + uint32_t out_bits = 0; + for (auto const& range : slave.process_data.send) + { + out_bits += range.bit_length; + } + uint32_t in_bits = 0; + for (auto const& range : slave.process_data.recv) + { + in_bits += range.bit_length; + } + printf(" process data: %u output bits, %u input bits, %zu SM, %zu RxPdo, %zu TxPdo\n", + out_bits, in_bits, slave.process_data.sms.size(), slave.process_data.rx_pdos.size(), slave.process_data.tx_pdos.size()); + + if (slave.mailbox) + { + ENI::Mailbox const& mbx = *slave.mailbox; + printf(" mailbox: send 0x%04x/%u recv 0x%04x/%u protocols 0x%02x unsupported InitCmds %u\n", + mbx.send.start, mbx.send.length, mbx.recv.start, mbx.recv.length, mbx.protocols, mbx.unsupported_init_cmds); + printTransitionCounts("CoE", mbx.coe_init_cmds); + } + printTransitionCounts("InitCmds", slave.init_cmds); + + if (slave.dc) + { + printf(" DC: cycle0 %ld ns shift %ld ns\n", + static_cast(slave.dc->cycle_time0.value_or(0ns).count()), static_cast(slave.dc->shift_time.value_or(0ns).count())); + } + } +} + +int main(int argc, char const* argv[]) +{ + argparse::ArgumentParser program("load_eni"); + + std::string eni_file; + program.add_argument("-f", "--file") + .help("ENI XML file") + .required() + .store_into(eni_file); + + try + { + program.parse_args(argc, argv); + } + catch (std::runtime_error const& err) + { + std::cerr << err.what() << std::endl; + std::cerr << program; + return 1; + } + + ENI::Config config; + nanoseconds t1 = now(); + try + { + config = ENI::loadFile(eni_file); + } + catch (std::exception const& e) + { + std::cerr << e.what() << std::endl; + return 1; + } + nanoseconds t2 = now(); + + ENI::Master const& master = config.master; + printf("Master: %s\n", master.name.c_str()); + printTransitionCounts("InitCmds", master.init_cmds); + + printf("%zu slave(s)\n", config.slaves.size()); + for (std::size_t i = 0; i < config.slaves.size(); ++i) + { + printSlave(i, config.slaves[i]); + } + + for (auto const& cyclic : config.cyclic) + { + printf("Cyclic: %zu frame(s)\n", cyclic.frames.size()); + for (auto const& frame : cyclic.frames) + { + for (auto const& cmd : frame.cmds) + { + std::string expected_wkc = "-"; + if (cmd.expected_wkc) + { + expected_wkc = std::to_string(*cmd.expected_wkc); + } + printf(" %-4s %-18s addr 0x%08x len %4zu wkc %s in %d out %d\n", + statesToString(cmd.states).c_str(), toString(cmd.cmd), cmd.address, cmd.data.size(), expected_wkc.c_str(), + static_cast(cmd.input_offs.value_or(-1)), static_cast(cmd.output_offs.value_or(-1))); + } + } + } + + if (config.process_image) + { + printf("ProcessImage: inputs %u bytes (%zu vars), outputs %u bytes (%zu vars)\n", + config.process_image->inputs.byte_size, config.process_image->inputs.variables.size(), + config.process_image->outputs.byte_size, config.process_image->outputs.variables.size()); + } + + printf("Parsed in %fs\n", seconds_f(t2 - t1).count()); + return 0; +} diff --git a/lib/src/ENI/Parser.cc b/lib/src/ENI/Parser.cc index 393f9886..c48b3eca 100644 --- a/lib/src/ENI/Parser.cc +++ b/lib/src/ENI/Parser.cc @@ -43,6 +43,11 @@ namespace kickcat::ENI } } + uint16_t parseIndex(XMLElement* elem) + { + return numberOf(elem); + } + Transitions parseTransitions(XMLElement* parent) { Transitions out = 0; @@ -324,6 +329,116 @@ namespace kickcat::ENI return mbx; } + uint16_t parseSmMask(XMLElement const* node) + { + uint16_t mask = 0; + for (int i = 0; i < 16; ++i) + { + std::string attr = "Sm" + std::to_string(i); + if (boolAttribute(node, attr.c_str())) + { + mask |= static_cast(1 << i); + } + } + return mask; + } + + ProcessDataRange parseRange(XMLElement* node) + { + ProcessDataRange range; + range.bit_start = requireNumber(node, "BitStart"); + range.bit_length = requireNumber(node, "BitLength"); + range.sm_mask = parseSmMask(node); + return range; + } + + SyncManagerSettings parseSm(XMLElement* node, uint8_t index) + { + SyncManagerSettings sm; + sm.index = index; + + XMLElement* type = require(node, "Type"); + try + { + SyncManager::fromString(textOf(type), sm.type); + } + catch (std::exception const& e) + { + fail(type, e.what()); + } + + sm.min_size = optionalNumber(node, "MinSize").value_or(0); + sm.max_size = optionalNumber(node, "MaxSize").value_or(0); + sm.default_size = optionalNumber(node, "DefaultSize").value_or(0); + sm.start_address = requireNumber(node, "StartAddress"); + sm.control_byte = requireNumber(node, "ControlByte"); + XMLElement* enable = require(node, "Enable"); + sm.enable = parseBool(enable, textOf(enable)); + sm.pdos = all(node, "Pdo", parseIndex); + return sm; + } + + CoE::DataType parseDataType(XMLElement* node) + { + std::string text = textOf(node); + std::optional type = CoE::dataTypeFromLabel(text); + if (not type) + { + fail(node, "unknown base data type '" + text + "'"); + } + return *type; + } + + CoE::PdoMappingEntry parsePdoEntry(XMLElement* node) + { + CoE::PdoMappingEntry entry{}; + entry.index = requireNumber(node, "Index"); + entry.subindex = optionalNumber(node, "SubIndex").value_or(0); + entry.bitlen = requireNumber(node, "BitLen"); + if (entry.index != 0) // index 0 is a padding gap: no type + { + entry.type = parseDataType(require(node, "DataType")); + } + return entry; + } + + Pdo parsePdo(XMLElement* node) + { + Pdo pdo; + pdo.index = requireNumber(node, "Index"); + pdo.name = optionalText(node, "Name"); + pdo.fixed = boolAttribute(node, "Fixed"); + pdo.mandatory = boolAttribute(node, "Mandatory"); + pdo.sm = numberAttribute(node, "Sm"); + pdo.exclude = all(node, "Exclude", parseIndex); + pdo.entries = all(node, "Entry", parsePdoEntry); + return pdo; + } + + ProcessData parseProcessData(XMLElement* slave) + { + ProcessData pd; + XMLElement* node = slave->FirstChildElement("ProcessData"); + if (node == nullptr) + { + return pd; + } + + pd.send = all(node, "Send", parseRange); + pd.recv = all(node, "Recv", parseRange); + for (int i = 0; i < 16; ++i) + { + std::string name = "Sm" + std::to_string(i); + if (XMLElement* sm = node->FirstChildElement(name.c_str())) + { + pd.sms.push_back(parseSm(sm, static_cast(i))); + } + } + pd.rx_pdos = all(node, "RxPdo", parsePdo); + pd.tx_pdos = all(node, "TxPdo", parsePdo); + return pd; + } + SlaveInfo parseSlaveInfo(XMLElement* slave) { XMLElement* node = require(slave, "Info"); @@ -411,6 +526,7 @@ namespace kickcat::ENI { Slave slave; slave.info = parseSlaveInfo(node); + slave.process_data = parseProcessData(node); slave.mailbox = parseMailbox(node); slave.init_cmds = parseInitCmds(node); slave.previous_ports = parsePreviousPorts(node); @@ -436,6 +552,84 @@ namespace kickcat::ENI return master; } + uint8_t parseStates(XMLElement* node) + { + uint8_t out = 0; + for (XMLElement* s = node->FirstChildElement("State"); s != nullptr; s = s->NextSiblingElement("State")) + { + std::string text = textOf(s); + if (text == "INIT") { out |= State::INIT; } + else if (text == "PREOP") { out |= State::PRE_OP; } + else if (text == "SAFEOP") { out |= State::SAFE_OP; } + else if (text == "OP") { out |= State::OPERATIONAL; } + else + { + fail(s, "unknown State '" + text + "'"); + } + } + return out; + } + + CyclicCmd parseCyclicCmd(XMLElement* node) + { + CyclicCmd cmd; + parseDatagram(node, cmd); + cmd.states = parseStates(node); + cmd.input_offs = optionalNumber(node, "InputOffs"); + cmd.output_offs = optionalNumber(node, "OutputOffs"); + return cmd; + } + + Cyclic parseCyclic(XMLElement* node) + { + Cyclic cyclic; + cyclic.comment = optionalText(node, "Comment"); + if (std::optional cycle = optionalNumber(node, "CycleTime")) + { + cyclic.cycle_time = microseconds{*cycle}; + } + // Priority and TaskId schedule the master's own tasks: the application owns them. + if (std::optional priority = optionalNumber(node, "Priority")) + { + if ((*priority < 1) or (*priority > 62)) + { + fail(node->FirstChildElement("Priority"), "Priority must be within [1, 62]"); + } + } + + for (XMLElement* f = node->FirstChildElement("Frame"); f != nullptr; f = f->NextSiblingElement("Frame")) + { + Frame frame; + frame.comment = optionalText(f, "Comment"); + frame.cmds = all(f, "Cmd", parseCyclicCmd); + cyclic.frames.push_back(std::move(frame)); + } + return cyclic; + } + + ProcessImageArea parseImageArea(XMLElement* node) + { + ProcessImageArea area; + if (node == nullptr) + { + return area; + } + area.byte_size = requireNumber(node, "ByteSize"); + for (XMLElement* v = node->FirstChildElement("Variable"); v != nullptr; v = v->NextSiblingElement("Variable")) + { + Variable var; + var.name = textOf(require(v, "Name")); + if (XMLElement* type = v->FirstChildElement("DataType")) + { + var.data_type = parseDataType(type); + } + var.bit_size = requireNumber(v, "BitSize"); + var.bit_offs = requireNumber(v, "BitOffs"); + area.variables.push_back(std::move(var)); + } + return area; + } + Config parseDocument(XMLDocument& doc) { XMLElement* root = doc.FirstChildElement("EtherCATConfig"); @@ -448,6 +642,14 @@ namespace kickcat::ENI Config config; config.master = parseMaster(require(node, "Master")); config.slaves = all(node, "Slave", parseSlave); + config.cyclic = all(node, "Cyclic", parseCyclic); + if (XMLElement* image = node->FirstChildElement("ProcessImage")) + { + ProcessImage pi; + pi.inputs = parseImageArea(image->FirstChildElement("Inputs")); + pi.outputs = parseImageArea(image->FirstChildElement("Outputs")); + config.process_image = std::move(pi); + } return config; } } diff --git a/unit/CMakeLists.txt b/unit/CMakeLists.txt index eec9b3ba..e99d8a30 100644 --- a/unit/CMakeLists.txt +++ b/unit/CMakeLists.txt @@ -69,6 +69,7 @@ set(KICKCAT_ESI_FIXTURES ${CMAKE_CURRENT_SOURCE_DIR}/kickcat_esi_test_sm_fmmu.xml ${CMAKE_SOURCE_DIR}/simulation/slave_configs/ecat402-drive.json ${CMAKE_SOURCE_DIR}/simulation/slave_configs/ecat402-drive.xml + ${CMAKE_CURRENT_SOURCE_DIR}/kickcat_eni_test_io.xml ${CMAKE_CURRENT_SOURCE_DIR}/kickcat_eni_test_mailbox.xml ) diff --git a/unit/kickcat_eni_test_io.xml b/unit/kickcat_eni_test_io.xml new file mode 100644 index 00000000..c737c219 --- /dev/null +++ b/unit/kickcat_eni_test_io.xml @@ -0,0 +1,256 @@ + + + + + + + KickCAT test master + ffffffffffff + 020000000001 + a488 + + + + IP + 1 + read slave count + cycle + 7 + 0 + 0 + 0000 + 0 + + + IP + true + clear fmmu + frame + 8 + 0 + #x0600 + 256 + 3 + + + PS + check all in SAFEOP + 7 + 0 + 304 + 0000 + 3 + + 0400 + 1f00 + 2000 + + + + + + + Term 1 (EK1100) + 1001 + 0 + 2 + #x044c2c52 + #x00110000 + 0 + + + + PI + BI + SI + OI + set device state to INIT + cycle + 2 + 0 + 288 + 0100 + 3 + + + IP + set physical address + 2 + 0 + 16 + e903 + 1 + 3 + + + + 1 + true + 1000000 + 250000 + + + + + Term 2 (EL1014) + 1002 + 65535 + 2 + 66465874 + 65536 + + + + 0 + 4 + + + + + IP + set physical address + 2 + 65535 + 16 + ea03 + 1 + 3 + + + PS + set fmmu 0 (inputs) + 5 + 1002 + 1536 + 00000100010000030010000101000000 + 1 + 3 + + + IP + SP + OP + check device state for PREOP + 4 + 1002 + 304 + 0000 + 1 + 3 + + 0200 + 1f00 + 1000 + + + + + B + 1001 + + + + + Term 3 (EL2004) + 1003 + 65534 + 2 + 131346514 + 1048576 + + + + 0 + 4 + + + Outputs + 1 + #x0f00 + #x44 + 1 + #x1600 + 5633 + + + #x1600 + Channel 1 + + #x7000 + 1 + 1 + Output + BOOL + + + 0 + 3 + + + + + B + 1002 + + + D + + + + 1000 + + + SAFEOP + OP + cyclic cmd + 10 + 69632 + 1 + 1 + 26 + + + SAFEOP + OP + 11 + #x12000 + 1 + 1 + 26 + + + PREOP + SAFEOP + OP + 7 + 0 + 304 + 0000 + 3 + 39 + + + + + + 1536 + + Term 2 (EL1014).Channel 1.Input + BOOL + 1 + 208 + + + + 1536 + + Term 3 (EL2004).Channel 1.Output + first output + BOOL + 1 + 208 + + + + + diff --git a/unit/src/ENI/Parser-t.cc b/unit/src/ENI/Parser-t.cc index 2881072c..cbce3fd1 100644 --- a/unit/src/ENI/Parser-t.cc +++ b/unit/src/ENI/Parser-t.cc @@ -48,6 +48,197 @@ TEST(ENIParser, load_errors) expectInvalid("", "missing mandatory "); } +TEST(ENIParser, io_master) +{ + ENI::Config config = ENI::loadFile("kickcat_eni_test_io.xml"); + + ENI::Master const& master = config.master; + EXPECT_EQ(master.name, "KickCAT test master"); + + ASSERT_EQ(master.init_cmds.size(), 3); + + ENI::InitCmd const& count = master.init_cmds[0]; + EXPECT_EQ(count.transitions, transition::IP); + EXPECT_TRUE(count.before_slave); + EXPECT_EQ(count.comment, "read slave count"); + EXPECT_EQ(count.frame_requirement, ENI::requirement::CYCLE); + EXPECT_EQ(count.cmd, Command::BRD); + EXPECT_EQ(count.data, (std::vector{0, 0})); + EXPECT_FALSE(count.expected_wkc.has_value()); + EXPECT_EQ(count.retries, 0); + + ENI::InitCmd const& clear = master.init_cmds[1]; + EXPECT_TRUE(clear.before_slave); + EXPECT_EQ(clear.frame_requirement, ENI::requirement::FRAME); + EXPECT_EQ(clear.cmd, Command::BWR); + EXPECT_EQ(clear.address, createAddress(0, 0x0600)); + EXPECT_EQ(clear.data.size(), 256); + EXPECT_EQ(clear.retries, 3); + + ENI::InitCmd const& check = master.init_cmds[2]; + EXPECT_FALSE(check.before_slave); + EXPECT_EQ(check.transitions, transition::PS); + EXPECT_EQ(check.frame_requirement, ENI::requirement::NONE); + EXPECT_EQ(check.expected_wkc, 3); + ASSERT_TRUE(check.validate.has_value()); + EXPECT_EQ(check.validate->data, (std::vector{0x04, 0x00})); + EXPECT_EQ(check.validate->mask, (std::vector{0x1f, 0x00})); + EXPECT_EQ(check.validate->timeout, 2000ms); +} + +TEST(ENIParser, io_slaves) +{ + ENI::Config config = ENI::loadFile("kickcat_eni_test_io.xml"); + ASSERT_EQ(config.slaves.size(), 3); + + ENI::Slave const& coupler = config.slaves[0]; + EXPECT_EQ(coupler.info.name, "Term 1 (EK1100)"); + EXPECT_EQ(coupler.info.phys_addr, 1001); + EXPECT_EQ(coupler.info.auto_inc_addr, 0); + EXPECT_EQ(coupler.info.vendor_id, 2u); + EXPECT_EQ(coupler.info.product_code, 0x044c2c52u); + EXPECT_EQ(coupler.info.revision_no, 0x00110000u); + EXPECT_EQ(coupler.info.serial_no, 0u); + EXPECT_FALSE(coupler.mailbox.has_value()); + EXPECT_TRUE(coupler.previous_ports.empty()); + ASSERT_EQ(coupler.init_cmds.size(), 2); + EXPECT_EQ(coupler.init_cmds[0].transitions, transition::PI | transition::BI | transition::SI | transition::OI); + EXPECT_EQ(coupler.init_cmds[1].cmd, Command::APWR); + EXPECT_EQ(coupler.init_cmds[1].address, createAddress(0, 0x10)); + EXPECT_EQ(coupler.init_cmds[1].data, (std::vector{0xe9, 0x03})); + EXPECT_EQ(coupler.init_cmds[1].expected_wkc, 1); + + ASSERT_TRUE(coupler.dc.has_value()); + EXPECT_TRUE(coupler.dc->potential_reference_clock); + EXPECT_TRUE(coupler.dc->reference_clock); + EXPECT_EQ(coupler.dc->cycle_time0, 1ms); + EXPECT_FALSE(coupler.dc->cycle_time1.has_value()); + EXPECT_EQ(coupler.dc->shift_time, 250us); + + ENI::Slave const& input = config.slaves[1]; + EXPECT_EQ(input.info.auto_inc_addr, 0xFFFF); + EXPECT_FALSE(input.info.serial_no.has_value()); + ASSERT_EQ(input.process_data.recv.size(), 1); + EXPECT_TRUE(input.process_data.send.empty()); + EXPECT_EQ(input.process_data.recv[0].bit_start, 0u); + EXPECT_EQ(input.process_data.recv[0].bit_length, 4u); + EXPECT_EQ(input.process_data.recv[0].sm_mask, 0); + ASSERT_EQ(input.init_cmds.size(), 3); + EXPECT_EQ(input.init_cmds[0].address, createAddress(0xFFFF, 0x10)); + EXPECT_EQ(input.init_cmds[1].cmd, Command::FPWR); + EXPECT_EQ(input.init_cmds[1].data.size(), 16); + EXPECT_EQ(input.init_cmds[2].transitions, transition::IP | transition::SP | transition::OP); + ASSERT_TRUE(input.init_cmds[2].validate.has_value()); + ASSERT_EQ(input.previous_ports.size(), 1); + EXPECT_TRUE(input.previous_ports[0].selected); + EXPECT_EQ(input.previous_ports[0].port, 1); + EXPECT_EQ(input.previous_ports[0].phys_addr, 1001); + + ENI::Slave const& output = config.slaves[2]; + ASSERT_EQ(output.process_data.send.size(), 1); + EXPECT_EQ(output.process_data.send[0].sm_mask, 1 << 2); + ASSERT_EQ(output.process_data.sms.size(), 1); + ENI::SyncManagerSettings const& sm = output.process_data.sms[0]; + EXPECT_EQ(sm.index, 2); + EXPECT_EQ(sm.type, SyncManager::Output); + EXPECT_EQ(sm.default_size, 1); + EXPECT_EQ(sm.min_size, 0); + EXPECT_EQ(sm.start_address, 0x0f00); + EXPECT_EQ(sm.control_byte, 0x44); + EXPECT_TRUE(sm.enable); + EXPECT_EQ(sm.pdos, (std::vector{0x1600, 0x1601})); + + ASSERT_EQ(output.process_data.rx_pdos.size(), 1); + EXPECT_TRUE(output.process_data.tx_pdos.empty()); + ENI::Pdo const& pdo = output.process_data.rx_pdos[0]; + EXPECT_EQ(pdo.index, 0x1600); + EXPECT_EQ(pdo.name, "Channel 1"); + EXPECT_TRUE(pdo.fixed); + EXPECT_FALSE(pdo.mandatory); + EXPECT_EQ(pdo.sm, 2); + ASSERT_EQ(pdo.entries.size(), 2); + EXPECT_EQ(pdo.entries[0].index, 0x7000); + EXPECT_EQ(pdo.entries[0].subindex, 1); + EXPECT_EQ(pdo.entries[0].bitlen, 1); + EXPECT_EQ(pdo.entries[0].type, CoE::DataType::BOOLEAN); + EXPECT_EQ(pdo.entries[1].index, 0); + EXPECT_EQ(pdo.entries[1].bitlen, 3); + EXPECT_EQ(pdo.entries[1].type, CoE::DataType::UNKNOWN); + + ASSERT_EQ(output.previous_ports.size(), 2); + EXPECT_TRUE(output.previous_ports[0].selected); + EXPECT_FALSE(output.previous_ports[1].selected); + EXPECT_EQ(output.previous_ports[1].port, 3); + EXPECT_FALSE(output.previous_ports[1].phys_addr.has_value()); +} + +TEST(ENIParser, io_cyclic_and_process_image) +{ + ENI::Config config = ENI::loadFile("kickcat_eni_test_io.xml"); + + ASSERT_EQ(config.cyclic.size(), 1); + ENI::Cyclic const& cyclic = config.cyclic[0]; + EXPECT_EQ(cyclic.cycle_time, 1000us); + ASSERT_EQ(cyclic.frames.size(), 1); + ASSERT_EQ(cyclic.frames[0].cmds.size(), 3); + + ENI::CyclicCmd const& lrd = cyclic.frames[0].cmds[0]; + EXPECT_EQ(lrd.states, State::SAFE_OP | State::OPERATIONAL); + EXPECT_EQ(lrd.comment, "cyclic cmd"); + EXPECT_EQ(lrd.cmd, Command::LRD); + EXPECT_EQ(lrd.address, 0x11000u); + EXPECT_EQ(lrd.data.size(), 1); + EXPECT_EQ(lrd.expected_wkc, 1); + EXPECT_EQ(lrd.input_offs, 26u); + EXPECT_FALSE(lrd.output_offs.has_value()); + + ENI::CyclicCmd const& lwr = cyclic.frames[0].cmds[1]; + EXPECT_EQ(lwr.cmd, Command::LWR); + EXPECT_EQ(lwr.address, 0x12000u); + EXPECT_FALSE(lwr.input_offs.has_value()); + EXPECT_EQ(lwr.output_offs, 26u); + + ENI::CyclicCmd const& brd = cyclic.frames[0].cmds[2]; + EXPECT_EQ(brd.states, State::PRE_OP | State::SAFE_OP | State::OPERATIONAL); + EXPECT_EQ(brd.cmd, Command::BRD); + EXPECT_EQ(brd.address, createAddress(0, 0x130)); + EXPECT_EQ(brd.data.size(), 2); + + ASSERT_TRUE(config.process_image.has_value()); + EXPECT_EQ(config.process_image->inputs.byte_size, 1536u); + ASSERT_EQ(config.process_image->inputs.variables.size(), 1); + EXPECT_EQ(config.process_image->inputs.variables[0].name, "Term 2 (EL1014).Channel 1.Input"); + EXPECT_EQ(config.process_image->inputs.variables[0].bit_offs, 208u); + ASSERT_EQ(config.process_image->outputs.variables.size(), 1); + EXPECT_EQ(config.process_image->outputs.variables[0].data_type, CoE::DataType::BOOLEAN); + EXPECT_EQ(config.process_image->outputs.variables[0].bit_size, 1u); +} + +TEST(ENIParser, data_types) +{ + auto pdo = [](std::string const& entry) + { + return wrapSlave("#x1a00p" + entry + ""); + }; + expectInvalid(pdo("#x600016"), "missing mandatory "); + expectInvalid(pdo("#x600016WORD16"), "unknown base data type 'WORD16'"); + expectInvalid(pdo("#x6000256UINT"), "out of range"); + + ENI::Config config = ENI::loadString(pdo("04")); + EXPECT_EQ(config.slaves.at(0).process_data.tx_pdos.at(0).entries.at(0).type, CoE::DataType::UNKNOWN); + + auto variable = [](std::string const& type) + { + return wrapConfig("1v" + type + + "80"); + }; + config = ENI::loadString(variable("")); + EXPECT_EQ(config.process_image->inputs.variables.at(0).data_type, CoE::DataType::UNKNOWN); + config = ENI::loadString(variable("USINT")); + EXPECT_EQ(config.process_image->inputs.variables.at(0).data_type, CoE::DataType::UNSIGNED8); + expectInvalid(variable("STRING(1)"), "unknown base data type"); +} + TEST(ENIParser, mailbox_slave) { ENI::Config config = ENI::loadFile("kickcat_eni_test_mailbox.xml"); @@ -129,6 +320,7 @@ TEST(ENIParser, negative_values) expectInvalid(wrapInitCmd("10-1"), "value -1 out of range [0, 65535]"); expectInvalid(wrapInitCmd("1000-1"), "out of range"); expectInvalid(wrapInitCmd("1-16"), "out of range"); + expectInvalid(wrapSlave("0-8"), "out of range"); expectInvalid(wrapSlave("-1000000"), "out of range"); // xs:int identifiers and addresses written with the top bit set @@ -202,6 +394,8 @@ TEST(ENIParser, malformed_slave_and_master) expectInvalid(wrapSlave("0101" "0100"), "missing mandatory "); + expectInvalid(wrapSlave("Sideways001"), + "ProcessData/Sm2/Type"); expectInvalid("ffff000000000000", "MAC address must be 6 bytes"); @@ -230,10 +424,24 @@ TEST(ENIParser, error_path_names_the_element) TEST(ENIParser, xsd_types) { ENI::Config config = ENI::loadString(wrapSlave( + "Outputs#x1800#x64true" + "Inputs#x1c00#x20false" "1000000-100000-100000")); ENI::Slave const& slave = config.slaves.at(0); + ASSERT_EQ(slave.process_data.sms.size(), 2); + EXPECT_TRUE(slave.process_data.sms[0].enable); + EXPECT_FALSE(slave.process_data.sms[1].enable); ASSERT_TRUE(slave.dc.has_value()); EXPECT_EQ(slave.dc->cycle_time1, -100us); + + auto cyclic = [](std::string const& task) + { + return wrapConfig("" + task + "OP12655362"); + }; + ENI::Config config_task = ENI::loadString(cyclic("1PlcTask")); + EXPECT_FALSE(config_task.cyclic.at(0).cycle_time.has_value()); + EXPECT_EQ(config_task.cyclic.at(0).frames.at(0).cmds.size(), 1); + expectInvalid(cyclic("63"), "Priority must be within [1, 62] in EtherCATConfig/Config/Cyclic/Priority"); } TEST(ENIParser, datagram_must_fit_in_a_frame) @@ -243,6 +451,9 @@ TEST(ENIParser, datagram_must_fit_in_a_frame) EXPECT_EQ(ENI::loadString(wrapInitCmd(fits)).slaves.at(0).init_cmds.at(0).data.size(), MAX_DATA); expectInvalid(wrapInitCmd("80" + std::to_string(MAX_DATA + 1) + ""), "does not fit in a frame"); expectInvalid(wrapInitCmd("80" + std::string(2 * (MAX_DATA + 1), '0') + ""), "does not fit in a frame"); + + expectInvalid(wrapConfig("OP12655366000"), + "does not fit in a frame"); } TEST(ENIParser, strict_numbers) @@ -272,9 +483,14 @@ TEST(ENIParser, text_spans_comments_and_cdata) TEST(ENIParser, attributes_are_trimmed) { ENI::Config config = ENI::loadString(wrapSlave( + "08" + "#x1600p" "1000" "001")); ENI::Slave const& slave = config.slaves.at(0); + EXPECT_EQ(slave.process_data.send.at(0).sm_mask, 1 << 2); + EXPECT_EQ(slave.process_data.rx_pdos.at(0).sm, 2); + EXPECT_TRUE(slave.process_data.rx_pdos.at(0).fixed); EXPECT_FALSE(slave.init_cmds.at(0).validate.has_value()); }