diff --git a/README.md b/README.md index dd1352ac..cb5f2a3f 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ Where to go next: - Simulator and emulator -- [docs/SIMULATION.md](docs/SIMULATION.md) - Tools and GUIs -- [docs/TOOLS.md](docs/TOOLS.md) - Real-time performance tuning -- [docs/PERFORMANCE.md](docs/PERFORMANCE.md) +- ENI network configuration (experimental) -- [docs/ENI.md](docs/ENI.md) ## Platform support diff --git a/docs/ENI.md b/docs/ENI.md new file mode 100644 index 00000000..b5164cf2 --- /dev/null +++ b/docs/ENI.md @@ -0,0 +1,68 @@ +# ENI support + +KickCAT can load an EtherCAT Network Information (ENI) XML file and execute its +startup commands to configure a master bus. This support is **experimental** and +has been tested only with the in-process simulator. + +Enable `eni_parser` to load XML files. The `eni_sequencer` example also needs +`esi_parser` and `master_examples` (all enabled by default). See +[BUILDING.md](BUILDING.md) for prerequisites: + +```bash +source .venv/bin/activate +./scripts/configure.sh build --with=eni_parser --with=esi_parser --with=master_examples +./scripts/setup_build.sh build +cd build && make -j +cd .. +./build/examples/master/eni_sequencer/eni_sequencer \ + -n unit/kickcat_eni_test_drives.xml \ + -e simulation/slave_configs/ecat402-drive.xml +``` + +`eni_sequencer` creates a simulated bus from the ESI file. To inspect an ENI +without starting a bus, run `./build/examples/master/load_eni/load_eni -f FILE`. + +## Using the sequencer + +`ENI::loadFile()` returns a configuration for `ENI::Sequencer`. Keep that +configuration and the link alive for as long as the sequencer uses them. The +sequencer runs the ENI startup commands; do not also call `Bus::init()` or +`Bus::createMapping()`. + +```cpp +ENI::Sequencer sequencer(bus, link, config); +sequencer.requestState(State::SAFE_OP); + +std::vector iomap(sequencer.processImageSize()); +sequencer.mapProcessImage(iomap.data(), iomap.size()); +bus.processDataReadWrite(error); + +sequencer.requestState(State::OPERATIONAL, [&]() +{ + bus.processDataReadWrite(error); +}); +``` + +The process image becomes available at SAFE_OP. Outputs must be exchanged +before requesting OP; the callback keeps them cycling while the transition is +checked. See [`eni_sequencer.cc`](../examples/master/eni_sequencer/eni_sequencer.cc) +for a complete example. `cycleTime()` and `shiftTime()` expose the common DC +timing, when the ENI specifies it, for `Bus::enableDC()`. + +## Current limits + +- The detected slave count and identities must match the ENI and the devices' + SII data. Mailbox and process data layouts are checked against the registers + programmed by the ENI. +- SoE, EoE, FoE, AoE and VoE startup commands are not executed. Bootstrap, + HotConnect and `PreviousPort` checks are not supported. +- The regular `Bus` exchange builds its own frames from the process data + mapping read back from the slaves: ENI `` frames only serve to check + that mapping, and their `CycleTime` is the application's to use. + `` variables are parsed but not used. +- Mailboxes are polled slave by slave: `MailboxStates` and the status FMMUs it + describes are not used. +- Each startup command uses its own round trip; `Requires=frame/cycle` is not + implemented. A failed command throws, without automatic recovery. +- `Validate` only supports the default `EQ` comparison: `NONE` skips the check + and the other comparison types are refused when loading the file. diff --git a/examples/master/CMakeLists.txt b/examples/master/CMakeLists.txt index b897a2ff..f310a226 100644 --- a/examples/master/CMakeLists.txt +++ b/examples/master/CMakeLists.txt @@ -2,6 +2,7 @@ add_subdirectory(common) add_subdirectory(ds402_drive) add_subdirectory(easycat) add_subdirectory(elmo) +add_subdirectory(eni_sequencer) add_subdirectory(freedom-k64f) add_subdirectory(ingenia) add_subdirectory(load_eni) diff --git a/examples/master/eni_sequencer/CMakeLists.txt b/examples/master/eni_sequencer/CMakeLists.txt new file mode 100644 index 00000000..253ffcad --- /dev/null +++ b/examples/master/eni_sequencer/CMakeLists.txt @@ -0,0 +1,5 @@ +if (ENABLE_ENI_PARSER AND ENABLE_ESI_PARSER) + add_executable(eni_sequencer eni_sequencer.cc) + target_link_libraries(eni_sequencer kickcat argparse::argparse) + set_kickcat_properties(eni_sequencer) +endif () diff --git a/examples/master/eni_sequencer/eni_sequencer.cc b/examples/master/eni_sequencer/eni_sequencer.cc new file mode 100644 index 00000000..1320011f --- /dev/null +++ b/examples/master/eni_sequencer/eni_sequencer.cc @@ -0,0 +1,175 @@ +#include +#include +#include + +#include + +#include "kickcat/Bus.h" +#include "kickcat/CoE/mailbox/response.h" +#include "kickcat/ENI/Parser.h" +#include "kickcat/ENI/Sequencer.h" +#include "kickcat/ESC/EmulatedESC.h" +#include "kickcat/ESI/Parser.h" +#include "kickcat/ESI/SIIBuilder.h" +#include "kickcat/Link.h" +#include "kickcat/LoopbackSocket.h" +#include "kickcat/PDO.h" +#include "kickcat/SocketNull.h" +#include "kickcat/slave/Slave.h" + +using namespace kickcat; + +namespace +{ + constexpr uint32_t SLAVE_PD_SIZE = 4096; + + struct SimulatedSlave + { + SimulatedSlave(ESI::Device device) + : dev(std::move(device)) + , pdo(&esc) + , sl(&esc, &pdo) + , inputs(SLAVE_PD_SIZE, 0) + , outputs(SLAVE_PD_SIZE, 0) + { + CoE::materializeStorage(dev.dictionary); + esc.loadEeprom(ESI::buildEepromImage(dev)); + if (dev.mailbox and dev.mailbox->coe) + { + mbx = std::make_unique(&esc, 1024); + mbx->enableCoE(dev.dictionary); + sl.setMailbox(mbx.get()); + sl.setDictionary(&dev.dictionary); + } + pdo.setInput(inputs.data(), SLAVE_PD_SIZE); + pdo.setOutput(outputs.data(), SLAVE_PD_SIZE); + sl.start(); + } + + ESI::Device dev; + EmulatedESC esc; + PDO pdo; + slave::Slave sl; + std::unique_ptr mbx; + std::vector inputs; + std::vector outputs; + }; +} + +int main(int argc, char** argv) +{ + argparse::ArgumentParser program("eni_sequencer"); + + std::string eni_file; + program.add_argument("-n", "--eni") + .help("ENI XML file describing the network") + .required() + .store_into(eni_file); + + std::string esi_file; + program.add_argument("-e", "--esi") + .help("ESI XML file used to emulate every ENI slave (matched by product code)") + .required() + .store_into(esi_file); + + try + { + program.parse_args(argc, argv); + } + catch (std::exception const& e) + { + std::cerr << e.what() << std::endl << program; + return 1; + } + + ENI::Config config; + std::vector> sim; + try + { + config = ENI::loadFile(eni_file); + ESI::Parser parser; + for (auto const& eni_slave : config.slaves) + { + ESI::DeviceFilter filter; + filter.product_code = eni_slave.info.product_code; + sim.push_back(std::make_unique(parser.loadDevice(esi_file, filter))); + } + } + catch (std::exception const& e) + { + std::cerr << "Cannot build the simulated network: " << e.what() << std::endl; + return 1; + } + + bool outputs_valid = false; + auto tick = [&]() + { + for (auto& s : sim) + { + s->sl.routine(); + if (outputs_valid and s->sl.state() == State::SAFE_OP) + { + s->sl.validateOutputData(); + } + } + }; + + std::vector escs; + for (auto& s : sim) + { + escs.push_back(&s->esc); + } + auto link = std::make_shared(std::make_shared(escs, tick), std::make_shared(), [](){}); + Bus bus(link); + + int cyclic_errors = 0; + auto cyclic = [&]() + { + bus.processDataReadWrite([&](DatagramState const&) { ++cyclic_errors; }); + }; + + std::vector iomap; + try + { + ENI::Sequencer sequencer(bus, link, config); + sequencer.requestState(State::SAFE_OP); + + iomap.resize(sequencer.processImageSize()); + sequencer.mapProcessImage(iomap.data(), iomap.size()); + cyclic(); + + outputs_valid = true; + sequencer.requestState(State::OPERATIONAL, cyclic); + } + catch (std::exception const& e) + { + std::cerr << "ENI sequence failed: " << e.what() << std::endl; + return 1; + } + + // The period the ENI frames were planned for, when the configurator gave one. + nanoseconds period = 1ms; + if ((not config.cyclic.empty()) and config.cyclic.front().cycle_time) + { + period = *config.cyclic.front().cycle_time; + } + printf("cycle period: %lld us\n", static_cast(duration_cast(period).count())); + for (int i = 0; i < 100; ++i) + { + cyclic(); + sleep(period); + } + for (std::size_t i = 0; i < bus.slaves().size(); ++i) + { + Slave const& slave = bus.slaves()[i]; + printf("slave %zu: station %u, state 0x%02x, inputs 0x%05x/%d, outputs 0x%05x/%d\n", i, slave.address, + static_cast(sim[i]->sl.state()), slave.input.address, slave.input.bsize, slave.output.address, slave.output.bsize); + } + printf("cyclic errors: %d\n", cyclic_errors); + + if (cyclic_errors != 0) + { + return 1; + } + return 0; +} diff --git a/lib/master/include/kickcat/ENI/Sequencer.h b/lib/master/include/kickcat/ENI/Sequencer.h index 3ebf0363..c463b66f 100644 --- a/lib/master/include/kickcat/ENI/Sequencer.h +++ b/lib/master/include/kickcat/ENI/Sequencer.h @@ -3,6 +3,7 @@ #include #include +#include #include #include "kickcat/AbstractLink.h" @@ -13,7 +14,7 @@ namespace kickcat::ENI { /// \brief Configure a bus through the state transitions described by an ENI. /// \details Startup commands program the slaves; their SII data is checked against the ENI. - /// The mailbox layout is read back from the programmed registers. + /// Mailbox and process data layouts are read back from the programmed registers. class Sequencer { public: @@ -28,6 +29,17 @@ namespace kickcat::ENI void requestState(State target, std::function const& background = [](){}); State state() const { return state_; } + /// \return the process image size, known once the ENI programmed the FMMUs (SAFE_OP and above) + std::size_t processImageSize() const; + + /// \brief Bind slave input/output to iomap, see Bus::mapProcessImage. Needs SAFE_OP or above, + /// and no Bus::configureMailboxStatusCheck(): an ENI does not program its FMMUs. + void mapProcessImage(uint8_t* iomap, std::size_t iomap_size); + + /// \return the DC cycle and shift shared by every DC slave of the ENI, nullopt without DC + std::optional cycleTime() const { return cycle_time_; } + std::optional shiftTime() const { return shift_time_; } + /// \return the ENI slave driven at a bus position ENI::Slave const& slaveAt(std::size_t position) const; @@ -41,6 +53,7 @@ namespace kickcat::ENI void checkIdentity(std::size_t position); void checkMailbox(std::size_t position); void checkIdentification(); + void readProcessDataLayout(); Bus& bus_; std::shared_ptr link_; @@ -48,10 +61,13 @@ namespace kickcat::ENI std::vector eni_of_position_; std::vector position_of_eni_; + std::optional cycle_time_; + std::optional shift_time_; State state_ = State::INIT; bool detected_ = false; bool handlers_attached_ = false; + bool layout_known_ = false; }; } diff --git a/lib/master/src/ENI/Sequencer.cc b/lib/master/src/ENI/Sequencer.cc index de538589..8df9e275 100644 --- a/lib/master/src/ENI/Sequencer.cc +++ b/lib/master/src/ENI/Sequencer.cc @@ -1,6 +1,8 @@ +#include #include #include #include +#include #include "kickcat/ENI/Sequencer.h" #include "kickcat/debug.h" @@ -41,6 +43,79 @@ namespace kickcat::ENI { return std::runtime_error("ENI: " + name + ": " + what); } + + // ETG.2100: a cyclic command carries the process image bytes from its InputOffs/OutputOffs at its logical Addr. + std::optional logicalBit(Config const& config, ProcessDataRange const& range, bool output) + { + for (auto const& cyclic : config.cyclic) + { + for (auto const& frame : cyclic.frames) + { + for (auto const& cmd : frame.cmds) + { + bool carries = (cmd.cmd == Command::LRW) or (cmd.cmd == Command::LRD); + std::optional offset = cmd.input_offs; + if (output) + { + carries = (cmd.cmd == Command::LRW) or (cmd.cmd == Command::LWR); + offset = cmd.output_offs; + } + if (not carries or not offset) + { + continue; + } + uint64_t begin = uint64_t{*offset} * 8; + uint64_t end = begin + cmd.data.size() * 8; + if ((range.bit_start >= begin) and (range.bit_start + range.bit_length <= end)) + { + return uint64_t{cmd.address} * 8 + (range.bit_start - begin); + } + } + } + } + return std::nullopt; + } + + // The ENI ranges of a direction must cover exactly the logical range of its FMMU, through the SM it serves. + void checkPlacement(Config const& config, std::string const& name, std::vector const& ranges, + kickcat::Slave::PIMapping const& mapping, bool output) + { + char const* direction = "inputs"; + if (output) + { + direction = "outputs"; + } + + std::vector> placed; + for (auto const& range : ranges) + { + std::optional bit = logicalBit(config, range, output); + if (not bit) + { + throw slaveError(name, std::string{"ProcessData "} + direction + " are carried by no cyclic command"); + } + if ((range.sm_mask != 0) and ((mapping.bsize == 0) or not (range.sm_mask & (1u << mapping.sync_manager)))) + { + throw slaveError(name, std::string{"programmed "} + direction + " use another SyncManager than the ENI ProcessData"); + } + placed.emplace_back(*bit, range.bit_length); + } + std::sort(placed.begin(), placed.end()); + + uint64_t expected = uint64_t{mapping.address} * 8; + for (auto const& [bit, length] : placed) + { + if (bit != expected) + { + throw slaveError(name, std::string{"programmed "} + direction + " are placed differently than the ENI ProcessData"); + } + expected += length; + } + if (expected != uint64_t{mapping.address} * 8 + static_cast(mapping.size)) + { + throw slaveError(name, std::string{"programmed "} + direction + " sizes differ from the ENI ProcessData"); + } + } } @@ -67,6 +142,23 @@ namespace kickcat::ENI position_of_eni_[i] = position; } + for (auto const& slave : config_.slaves) + { + if (not slave.dc or not slave.dc->cycle_time0) + { + continue; + } + nanoseconds shift = slave.dc->shift_time.value_or(0ns); + if (not cycle_time_) + { + cycle_time_ = slave.dc->cycle_time0; + shift_time_ = shift; + } + if ((*cycle_time_ != *slave.dc->cycle_time0) or (*shift_time_ != shift)) + { + throw std::invalid_argument("ENI: " + slave.info.name + ": DC cycle or shift differs from the other DC slaves"); + } + } } @@ -99,6 +191,35 @@ namespace kickcat::ENI } + std::size_t Sequencer::processImageSize() const + { + if (not layout_known_) + { + throw std::logic_error("ENI: the process data layout is known from SAFE_OP"); + } + std::size_t size = 0; + for (auto const& slave : bus_.slaves()) + { + size += static_cast(slave.input.bsize + slave.output.bsize); + } + return size; + } + + + void Sequencer::mapProcessImage(uint8_t* iomap, std::size_t iomap_size) + { + if (not layout_known_) + { + throw std::logic_error("ENI: the process data layout is known from SAFE_OP"); + } + if (bus_.mailboxStatusFMMUMode() != MailboxStatusFMMU::NONE) + { + throw std::logic_error("ENI: the mailbox status check needs FMMUs an ENI does not program"); + } + bus_.mapProcessImage(iomap, iomap_size); + } + + void Sequencer::detect() { int32_t count = bus_.detectSlaves(); @@ -181,6 +302,10 @@ namespace kickcat::ENI { sendCoE(t, background); } + if (t == transition::PS) + { + readProcessDataLayout(); + } for (auto const& cmd : config_.master.init_cmds) { @@ -191,6 +316,10 @@ namespace kickcat::ENI } state_ = to; + if (to < State::SAFE_OP) + { + layout_known_ = false; + } } @@ -423,4 +552,76 @@ namespace kickcat::ENI } } } + + + void Sequencer::readProcessDataLayout() + { + std::vector& slaves = bus_.slaves(); + for (std::size_t position = 0; position < slaves.size(); ++position) + { + kickcat::Slave& slave = slaves[position]; + ENI::Slave const& eni = slaveAt(position); + std::vector fmmus(slave.esc.fmmus); + std::vector sms(slave.esc.syncManagers); + readRegister(*link_, slave.address, reg::FMMU, fmmus.data(), static_cast(fmmus.size() * sizeof(fmmu::Register))); + readRegister(*link_, slave.address, reg::SYNC_MANAGER, sms.data(), static_cast(sms.size() * sizeof(SyncManager::Register))); + slave.input = {}; + slave.output = {}; + + for (fmmu::Register const& fmmu : fmmus) + { + if (not (fmmu.activate & 1) or (fmmu.length == 0)) + { + continue; + } + + int32_t sm_index = -1; + for (std::size_t n = 0; n < sms.size(); ++n) + { + if ((sms[n].length > 0) and (sms[n].start_address == fmmu.physical_address)) + { + sm_index = static_cast(n); + } + } + if (sm_index < 0) + { + continue; // not a process data FMMU, e.g. a mailbox status bit + } + if ((fmmu.logical_start_bit != 0) or (fmmu.logical_stop_bit != 7) or (fmmu.physical_start_bit != 0)) + { + throw slaveError(eni.info.name, "bit-wise process data mapping is not supported"); + } + + kickcat::Slave::PIMapping* mapping = nullptr; + if (fmmu.type == 1) + { + mapping = &slave.input; + } + if (fmmu.type == 2) + { + mapping = &slave.output; + } + if (mapping == nullptr) + { + throw slaveError(eni.info.name, "read/write process data FMMUs are not supported"); + } + if (mapping->bsize != 0) + { + throw slaveError(eni.info.name, "more than one FMMU per direction is not supported"); + } + mapping->address = fmmu.logical_address; + mapping->bsize = fmmu.length; + mapping->size = fmmu.length * 8; + mapping->sync_manager = sm_index; + } + + if (eni.process_data.send.empty() and eni.process_data.recv.empty()) + { + continue; // ProcessData is optional: the FMMUs are the only description + } + checkPlacement(config_, eni.info.name, eni.process_data.send, slave.output, true); + checkPlacement(config_, eni.info.name, eni.process_data.recv, slave.input, false); + } + layout_known_ = true; + } } diff --git a/unit/src/ENI/Sequencer-t.cc b/unit/src/ENI/Sequencer-t.cc index b2155ab4..c1c0f494 100644 --- a/unit/src/ENI/Sequencer-t.cc +++ b/unit/src/ENI/Sequencer-t.cc @@ -85,6 +85,10 @@ class ENISequencer : public testing::Test for (auto& drive : drives) { drive->sl.routine(); + if (outputs_valid and drive->sl.state() == State::SAFE_OP) + { + drive->sl.validateOutputData(); + } } }; link = std::make_shared(std::make_shared(escs, tick), std::make_shared(), [](){}); @@ -94,6 +98,21 @@ class ENISequencer : public testing::Test return *seq; } + void cycle() + { + bus->processDataReadWrite([this](DatagramState const&) { ++cyclic_errors; }); + } + + void goToOperational() + { + sequencer().requestState(State::SAFE_OP); + iomap.assign(sequencer().processImageSize(), 0); + sequencer().mapProcessImage(iomap.data(), iomap.size()); + cycle(); + outputs_valid = true; + sequencer().requestState(State::OPERATIONAL, [this]() { cycle(); }); + } + void removeInitCmds(ENI::Slave& slave, char const* comment) { std::vector kept; @@ -125,19 +144,52 @@ class ENISequencer : public testing::Test std::shared_ptr link; std::unique_ptr bus; std::unique_ptr seq; + std::vector iomap; + bool outputs_valid = false; + int cyclic_errors = 0; }; -TEST_F(ENISequencer, reaches_safe_op_from_the_eni) +TEST_F(ENISequencer, reaches_operational_and_exchanges_process_data) { - sequencer().requestState(State::SAFE_OP); - ASSERT_EQ(State::SAFE_OP, sequencer().state()); + goToOperational(); + ASSERT_EQ(State::OPERATIONAL, sequencer().state()); for (auto& drive : drives) { - ASSERT_EQ(State::SAFE_OP, drive->sl.state()); + ASSERT_EQ(State::OPERATIONAL, drive->sl.state()); } - ASSERT_EQ(1001, bus->slaves()[0].address); - ASSERT_EQ(1002, bus->slaves()[1].address); + + // Layout programmed by the ENI: outputs then inputs, RxPDO 0x1601 (6 bytes), TxPDO 0x1A00 (11 bytes) + ASSERT_EQ(2 * (6 + 11), iomap.size()); + std::vector& slaves = bus->slaves(); + ASSERT_EQ(0x10000u, slaves[0].output.address); + ASSERT_EQ(0x10006u, slaves[1].output.address); + ASSERT_EQ(0x1000Cu, slaves[0].input.address); + ASSERT_EQ(0x10017u, slaves[1].input.address); + ASSERT_EQ(1001, slaves[0].address); + ASSERT_EQ(1002, slaves[1].address); + + for (std::size_t i = 0; i < drives.size(); ++i) + { + uint16_t status = static_cast(0x1230 + i); + std::memcpy(drives[i]->inputs.data(), &status, sizeof(status)); + uint16_t control = static_cast(0x0F00 + i); + std::memcpy(slaves[i].output.data, &control, sizeof(control)); + } + for (int i = 0; i < 5; ++i) + { + cycle(); + } + for (std::size_t i = 0; i < drives.size(); ++i) + { + uint16_t status = 0; + std::memcpy(&status, slaves[i].input.data, sizeof(status)); + uint16_t control = 0; + std::memcpy(&control, drives[i]->outputs.data(), sizeof(control)); + EXPECT_EQ(0x1230 + i, status); + EXPECT_EQ(0x0F00 + i, control); + } + EXPECT_EQ(0, cyclic_errors); auto [obj, assign] = CoE::findObject(drives[0]->dev.dictionary, 0x1C12, 1); EXPECT_EQ(0x1601, *static_cast(assign->data)); @@ -145,27 +197,30 @@ TEST_F(ENISequencer, reaches_safe_op_from_the_eni) TEST_F(ENISequencer, goes_back_to_init_and_up_again) { - sequencer().requestState(State::SAFE_OP); + goToOperational(); sequencer().requestState(State::INIT); ASSERT_EQ(State::INIT, sequencer().state()); for (auto& drive : drives) { ASSERT_EQ(State::INIT, drive->sl.state()); } + ASSERT_THROW(sequencer().processImageSize(), std::logic_error); std::size_t handlers = bus->slaves()[0].mailbox.to_process.size(); - sequencer().requestState(State::SAFE_OP); - ASSERT_EQ(State::SAFE_OP, sequencer().state()); + outputs_valid = false; + goToOperational(); + ASSERT_EQ(State::OPERATIONAL, sequencer().state()); ASSERT_EQ(handlers, bus->slaves()[0].mailbox.to_process.size()); } TEST_F(ENISequencer, pairs_slaves_by_auto_increment_address) { std::swap(config.slaves[0], config.slaves[1]); - sequencer().requestState(State::SAFE_OP); + goToOperational(); ASSERT_EQ(1001, bus->slaves()[0].address); ASSERT_EQ(1002, bus->slaves()[1].address); ASSERT_EQ("Drive 1 (EVS-NET-01)", sequencer().slaveAt(0).info.name); + ASSERT_EQ(0x10000u, bus->slaves()[0].output.address); } TEST_F(ENISequencer, refuses_inconsistent_configurations) @@ -176,8 +231,23 @@ TEST_F(ENISequencer, refuses_inconsistent_configurations) duplicated.slaves[1].info.auto_inc_addr = 0; ASSERT_THROW(ENI::Sequencer(unused, nullptr, duplicated), std::invalid_argument); - ENI::Sequencer valid(unused, nullptr, config); - ASSERT_THROW(valid.requestState(State::BOOT), std::invalid_argument); + ENI::Config dc = config; + for (auto& slave : dc.slaves) + { + slave.dc = ENI::Dc{}; + slave.dc->cycle_time0 = 1ms; + slave.dc->shift_time = 250us; + } + ENI::Sequencer with_dc(unused, nullptr, dc); + EXPECT_EQ(1ms, with_dc.cycleTime()); + EXPECT_EQ(250us, with_dc.shiftTime()); + + dc.slaves[1].dc->shift_time = 0ns; + ASSERT_THROW(ENI::Sequencer(unused, nullptr, dc), std::invalid_argument); + + ENI::Sequencer without_dc(unused, nullptr, config); + EXPECT_FALSE(without_dc.cycleTime().has_value()); + ASSERT_THROW(without_dc.requestState(State::BOOT), std::invalid_argument); } TEST_F(ENISequencer, refuses_a_slave_count_mismatch) @@ -226,6 +296,42 @@ TEST_F(ENISequencer, accepts_matching_explicit_identification) ASSERT_EQ(State::PRE_OP, sequencer().state()); } +TEST_F(ENISequencer, refuses_process_data_differing_from_the_eni) +{ + config.slaves[0].process_data.recv[0].bit_length = 80; + expectThrowContaining([this]() { sequencer().requestState(State::SAFE_OP); }, "programmed inputs sizes differ from the ENI ProcessData"); +} + +TEST_F(ENISequencer, refuses_process_data_placed_differently_than_the_eni) +{ + // Same size, one byte further in the input image, so one byte further in the logical space + config.slaves[0].process_data.recv[0].bit_start += 8; + expectThrowContaining([this]() { sequencer().requestState(State::SAFE_OP); }, "programmed inputs are placed differently than the ENI ProcessData"); +} + +TEST_F(ENISequencer, refuses_process_data_on_another_sync_manager_than_the_eni) +{ + config.slaves[1].process_data.send[0].sm_mask = 1 << 2; + config.slaves[1].process_data.recv[0].sm_mask = 1 << 2; + expectThrowContaining([this]() { sequencer().requestState(State::SAFE_OP); }, "Drive 2 (EVS-NET-01): programmed inputs use another SyncManager than the ENI ProcessData"); +} + +TEST_F(ENISequencer, refuses_process_data_no_cyclic_command_carries) +{ + config.cyclic.clear(); + expectThrowContaining([this]() { sequencer().requestState(State::SAFE_OP); }, "ProcessData outputs are carried by no cyclic command"); +} + +TEST_F(ENISequencer, process_data_description_is_optional) +{ + for (auto& slave : config.slaves) + { + slave.process_data = {}; + } + goToOperational(); + ASSERT_EQ(2 * (6 + 11), iomap.size()); +} + TEST_F(ENISequencer, reports_a_validate_timeout) { for (auto& cmd : config.slaves[0].init_cmds) @@ -280,6 +386,21 @@ TEST_F(ENISequencer, coe_uploads_objects_larger_than_the_mailbox) ASSERT_EQ(State::SAFE_OP, sequencer().state()); } +TEST_F(ENISequencer, process_image_needs_safe_op) +{ + sequencer().requestState(State::PRE_OP); + uint8_t buffer[64]; + ASSERT_THROW(sequencer().mapProcessImage(buffer, sizeof(buffer)), std::logic_error); +} + +TEST_F(ENISequencer, process_image_refuses_the_mailbox_status_check) +{ + sequencer().requestState(State::SAFE_OP); + bus->configureMailboxStatusCheck(MailboxStatusFMMU::READ_CHECK); + std::vector buffer(sequencer().processImageSize()); + ASSERT_THROW(sequencer().mapProcessImage(buffer.data(), buffer.size()), std::logic_error); +} + TEST_F(ENISequencer, coe_sent_after_the_safe_op_request_is_refused_by_the_slave) { // The PDO assignment of drive 1 arrives one transition late: SAFE_OP is requested with the