Merge branch 'add_fake_library' into 'stable-1.6'

Add fake library

See merge request etherlab.org/ethercat!136
This commit is contained in:
Florian Pose 2024-07-26 11:18:49 +00:00
commit 419f62161b
16 changed files with 3189 additions and 987 deletions

1
.gitignore vendored
View File

@ -33,6 +33,7 @@ ethercat.spec
examples/dc_user/ec_dc_user_example
examples/user/ec_user_example
examples/user/build/
fake_lib/libfakeethercat.la
lib/*.cmake
lib/libethercat.la
lib/libethercat.pc

View File

@ -1,5 +1,5 @@
image:
registry.gitlab.com/etherlab.org/build-container-factory/leap-15.3:linux-syms
registry.gitlab.com/etherlab.org/build-container-factory/leap-15.4:linux-syms
stages:
- build
@ -60,14 +60,14 @@ build:
script:
- ./bootstrap
- ./configure --with-linux-dir=/usr/src/linux-obj/$(uname -i)/default --enable-tty --with-devices=2 --enable-ccat
- ./configure --with-linux-dir=/usr/src/linux-obj/$(uname -i)/default --enable-tty --with-devices=2 --enable-ccat --enable-fakeuserlib
- make -j8 all modules
- make -C lib symbol-version-check
- make DISTCHECK_CONFIGURE_FLAGS="--with-linux-dir=/usr/src/linux-obj/$(uname -i)/default" distcheck
- make dist
- mkdir test_dist && cd test_dist && tar xzf ../ethercat-*.tar.gz && cd ethercat-*/
- mkdir build && cd build
- ../configure --with-linux-dir=/usr/src/linux-obj/$(uname -i)/default --disable-8139too --enable-tty --with-devices=2 --enable-ccat
- ../configure --with-linux-dir=/usr/src/linux-obj/$(uname -i)/default --disable-8139too --enable-tty --with-devices=2 --enable-ccat --enable-fakeuserlib
- make -j8 all modules
artifacts:
@ -124,8 +124,11 @@ doxygen:
# It must be named 'pdf' and must produce a 'pdf' directory as an artifact.
# All PDF files in this directory will be collected.
pdf:
needs:
- job: commands
artifacts: true
stage: doc
image: registry.gitlab.com/etherlab.org/build-container-factory/leap-15.3:texlive
image: registry.gitlab.com/etherlab.org/build-container-factory/leap-15.4:texlive
script:
- cd documentation
- git show -s --format="\def\revision{%h}\def\gitversion{%(describe)}\def\gittag{%(describe:abbrev=0)}\def\gitauthor{%an}\def\isodate#1-#2-#3x{\day=#3 \month=#2 \year=#1}\isodate %csx" HEAD > git.tex

File diff suppressed because it is too large Load Diff

View File

@ -47,6 +47,11 @@ SUBDIRS += \
examples
endif
if ENABLE_FAKEUSERLIB
SUBDIRS += \
fake_lib
endif
if ENABLE_TTY
SUBDIRS += tty
endif

4
NEWS
View File

@ -4,6 +4,10 @@ vim: spelllang=en spell tw=78
-------------------------------------------------------------------------------
Changes in 1.6.1:
* Added libfakeethercat to simulate Process Data of EtherCAT Slaves.
Changes in 1.6.0:
* Added all native Ethernet drivers for kernels 5.14 and 6.1.

View File

@ -15,7 +15,7 @@ Contents:
This is an open-source EtherCAT master implementation for Linux 2.6 or newer.
See the [features file](FEATURES.md) for a list of features. For more
information, see http://etherlab.org/en/ethercat.
information, see https://etherlab.org/ethercat.
or contact
@ -54,7 +54,7 @@ make doc
```
An up-to-date Doxygen output can be found on
[docs.etherlab.org](https://docs.etherlab.org/ethercat/1.5/doxygen/index.html).
[docs.etherlab.org](https://docs.etherlab.org/ethercat/1.6/doxygen/index.html).
# Requirements
@ -65,13 +65,20 @@ the EtherCAT master.
## Hardware requirements
A table of supported hardware can be found at
http://etherlab.org/en/ethercat/hardware.php.
A table of supported hardware can be found at:
https://docs.etherlab.org/ethercat/1.6/doxygen/devicedrivers.html
# Building and installing
See the [install file](INSTALL.md).
# Dry-run and Field Simulation
A limited set of the userspace API is available in `libfakeethercat`,
a library which can be used to run an userspace application
without an EtherCAT master or with emulated EtherCAT slaves.
Please find some details [here](fake_lib/README.md).
# Realtime and Tuning
Realtime patches for the Linux kernel are supported, but not required. The

View File

@ -31,7 +31,7 @@ AC_PREREQ(2.59)
# - Update version number below
# - make dist-bzip2
#
AC_INIT([ethercat],[1.6.0],[fp@igh.de])
AC_INIT([ethercat],[1.6.1],[fp@igh.de])
AC_CONFIG_AUX_DIR([autoconf])
AM_INIT_AUTOMAKE([-Wall -Wno-override -Werror dist-bzip2 subdir-objects foreign])
m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])])
@ -1168,6 +1168,37 @@ fi
AM_CONDITIONAL(ENABLE_USERLIB, test "x$userlib" = "x1")
#-----------------------------------------------------------------------------
# Fake Userspace library generation
#-----------------------------------------------------------------------------
AC_MSG_CHECKING([whether to build the fake userspace library])
AC_ARG_ENABLE([fakeuserlib],
AS_HELP_STRING([--enable-fakeuserlib],
[Generation of the userspace library (default: no)]),
[
case "${enableval}" in
yes) fakeuserlib=1
;;
no) fakeuserlib=0
;;
*) AC_MSG_ERROR([Invalid value for --enable-fakeuserlib])
;;
esac
],
[fakeuserlib=0]
)
if test "x${fakeuserlib}" = "x1"; then
AC_MSG_RESULT([yes])
PKG_CHECK_MODULES([RTIPC], [librtipc])
else
AC_MSG_RESULT([no])
fi
AM_CONDITIONAL(ENABLE_FAKEUSERLIB, test "x$fakeuserlib" = "x1")
#-----------------------------------------------------------------------------
# TTY driver
#-----------------------------------------------------------------------------
@ -1389,6 +1420,7 @@ AC_CONFIG_FILES([
examples/user/Makefile
examples/xenomai/Makefile
examples/xenomai_posix/Makefile
fake_lib/Makefile
include/Makefile
lib/Makefile
lib/libethercat.pc

View File

@ -351,7 +351,7 @@ Public License (GPL \cite{gpl})\index{GPL}, version 2. Other developers, that
want to use EtherCAT with Linux systems, are invited to use the master code or
even participate on development.
To allow static linking of userspace application against the master's
To allow dynamic linking of userspace application against the master's
application interface (see \autoref{chap:api}), the userspace library (see
\autoref{sec:userlib}) is licensed under the terms and conditions of the GNU
Lesser General Public License (LGPL \cite{lgpl})\index{LGPL}, version 2.1.
@ -962,8 +962,8 @@ be found.
The application interface of the EtherCAT master is defined in the header file
\textit{include/ecrt.h} (acronym for ``EtherCAT Real-Time'') which is listed
in this section. The calling conventions of all methods are documented in the
comments of this header. There is also a Doxygen-generated online version at
\url{https://docs.etherlab.org}.
comments of this header. There is also a Doxygen-generated \cite{doxygen}
online version at \url{https://docs.etherlab.org}.
\lstinputlisting[
basicstyle=\ttfamily\scriptsize,
@ -2709,6 +2709,10 @@ installation prefix as \textit{libethercat.a} (for static linking),
\textit{libethercat.la} (for the use with \textit{libtool}) and
\textit{libethercat.so} (for dynamic linking).
For running an application without actual EtherCAT hardware or for simulation
purposes, there is a special library called \textit{libfakeethercat} (see
\autoref{sec:fakelib}).
\subsection{Using the Library}
The application interface header \textit{ecrt.h} (see \autoref{sec:ecrt}) can
@ -2750,6 +2754,9 @@ gcc -static ectest.c -o ectest -I/opt/etherlab/include \
/opt/etherlab/lib/libethercat.a
\end{lstlisting}
Please keep in mind, that your application has to be licensed under GPLv2
then, because the LGPL does only allow dynamic linking.
\subsection{Implementation}
\label{sec:userimp}
@ -2836,6 +2843,33 @@ The test results show, that for this configuration, the userspace API causes
about \unit{1}{\micro\second} additional delay for each function, compared to
the kernel API.
\subsection{Simulation / Fake Library}
\label{sec:fakelib}
Sometimes is is handy to run your EtherCAT realtime application without an
actual EtherCAT network connected, for example for test purposes.
Though it is possible to spin up an EtherCAT master and to connect it to a
loopback device, this step is not always wanted.
The EtherCAT master (since version 1.6.1) comes with a library
\textit{libfakeethercat} that comes with a reasonable subset of the EtherCAT
application interface (see \autoref{chap:api}).
The \texttt{ecrt} method implementation in the fake library will just accept
your input and behave as if everything would be fine. Without further steps,
the process data will be all-zero then.
As a special feature, the \textit{libfakeethercat} will create RtIPC
\cite{rtipc} endpoints for registered PDO entries to enable a simulation
interface. Another application that either uses RtIPC directly or another
(inverted) instance of \textit{libfakeethercat} will then connect to these
endpoints and thus create the possibility to provide simulated values to your
pristine application.
The fake library functions an usage is documented in Doxygen~\cite{doxygen}
and the most recent version can be found online:
\url{https://docs.etherlab.org/ethercat/1.6/doxygen/libfakeethercat.html}
%------------------------------------------------------------------------------
\section{RTDM Interface}
@ -3547,6 +3581,9 @@ Applications.
\url{http://svn.gna.org/svn/xenomai/tags/v2.4.0/doc/nodist/pdf/RTDM-and-Applications.pdf},
2013.
\bibitem{rtipc} Real-Time Inter-Process-Communication library. Part of the
EtherLab toolkit. \url{https://gitlab.com/etherlab.org/rtipc}, 2024.
\end{thebibliography}
\printnomenclature

View File

@ -3440,7 +3440,7 @@ Il y a plusieurs mani\`eres d'obtenir le logiciel du ma\^itre:
\item Une version officielle (par exemple \masterversion) peut \^etre
t\'el\'echarg\'ee depuis le site web du
ma\^itre\footnote{\url{http://etherlab.org/en/ethercat/index.php}}
ma\^itre\footnote{\url{https://etherlab.org/ethercat}}
dans le projet EtherLab~\cite{etherlab} sous forme d'archive tar.
\item La r\'evision de d\'eveloppement la plus r\'ecente (mais aussi
@ -3793,7 +3793,7 @@ KERNEL=="EtherCAT[0-9]*", MODE="0664", GROUP="users"
\bibitem{etherlab} Ingenieurgemeinschaft IgH: EtherLab -- Open Source Toolkit
for rapid realtime code generation under Linux with Simulink/RTW and EtherCAT
technology. \url{http://etherlab.org/en}, 2008.
technology. \url{https://etherlab.org}, 2024.
\bibitem{dlspec} IEC 61158-4-12: Data-link Protocol Specification.
International Electrotechnical Commission (IEC), 2005.

View File

@ -26,7 +26,7 @@ Version: @VERSION@
Release: 1
License: GPL
URL: http://etherlab.org/en/ethercat
URL: https://etherlab.org/ethercat
Provides: @PACKAGE@
Source: %{name}-%{version}.tar.bz2
@ -44,7 +44,7 @@ Group: EtherLab
%description
This is an open-source EtherCAT master implementation for Linux 2.6. See the
FEATURES file for a list of features. For more information, see
http://etherlab.org/en/ethercat.
https://etherlab.org/ethercat.
%kernel_module_package
@ -60,7 +60,7 @@ Group: EtherLab
%description devel
This is an open-source EtherCAT master implementation for Linux 2.6. See the
FEATURES file for a list of features. For more information, see
http://etherlab.org/en/ethercat.
https://etherlab.org/ethercat.
#-----------------------------------------------------------------------------

46
fake_lib/Makefile.am Normal file
View File

@ -0,0 +1,46 @@
#------------------------------------------------------------------------------
#
# Copyright (C) 2006-2024 Florian Pose, Ingenieurgemeinschaft IgH
#
# This file is part of the IgH EtherCAT master userspace library.
#
# The IgH EtherCAT master userspace library is free software; you can
# redistribute it and/or modify it under the terms of the GNU Lesser General
# Public License as published by the Free Software Foundation; version 2.1 of
# the License.
#
# The IgH EtherCAT master userspace library is distributed in the hope that
# it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with the IgH EtherCAT master userspace library. If not, see
# <http://www.gnu.org/licenses/>.
#
#------------------------------------------------------------------------------
lib_LTLIBRARIES = libfakeethercat.la
#------------------------------------------------------------------------------
libfakeethercat_la_SOURCES = \
fakeethercat.cpp
noinst_HEADERS = \
fakeethercat.h
libfakeethercat_la_CXXFLAGS = \
-fno-strict-aliasing \
-Wall \
-I$(top_srcdir)/include \
-Dethercat_EXPORTS \
-fvisibility=hidden \
$(RTIPC_CFLAGS)
libfakeethercat_la_LDFLAGS = -version-info 3:0:2 \
$(RTIPC_LIBS) \
-Wl,--version-script=$(srcdir)/../lib/libethercat.map \
-fvisibility=hidden
libfakeethercat_la_DEPENDENCIES = ../lib/libethercat.map

133
fake_lib/README.md Normal file
View File

@ -0,0 +1,133 @@
FakeEtherCAT Library {#libfakeethercat}
====================
Libfakeethercat is a userspace library which has the same API as
the EtherCAT master interface library libethercat.
Libfakeethercat can be used to spin up your RT application in a dry-run mode,
without any master configured or slaves attached.
Furthermore, it is possible to emulate EtherCAT slaves on process data level
by running two applications back to back.
## Supported features
Currently, only a very limited subset of libethercat functionality is supported:
- Creating master and domain instances
- Activating master, `send`/`receive`.
- Processing and queuing domains
- Configuring PDOs
- Configuring SDOs using `ecrt_slave_config_sdo*`
The SDO config does not do anything,
but when activating the master the SDO config will
be dumped into a JSON file.
ecrt_master_state() and ecrt_domain_state() both return states as if
the bus works without errors.
So currenty, a bus error cannot be simulated.
## How to build
[RtIPC](https://gitlab.com/etherlab.org/rtipc) is needed.
Simply pass `--enable-fakeuserlib` to your `./configure` call
and the library will be built for you.
## How to set up dry run mode
### Redirect library loading
To avoid recompiling your application,
we will use `LD_LIBRARY_PATH` to load `libfakeethercat` instead of `libethercat`.
```sh
# pick a location for an empty directory
export MY_LIB_LOCATION=$HOME/fake_lib64
# create that directory
mkdir -p $MY_LIB_LOCATION
# create a symlink to libfakeethercat
# assuming SOVERSION is 1
# debian users, please use /usr/lib/x86_64-linux-gnu/libfakeethercat.so.1
ln -s /usr/lib64/libfakeethercat.so.1 $MY_LIB_LOCATION/libethercat.so.1
# use MY_LIB_LOCATION to load libraries first
export LD_LIBRARY_PATH=$MY_LIB_LOCATION
# check whether everything is done right
ldd my_application | grep ethercat
libethercat.so.1 => /home/vh/fake_lib64/libethercat.so.1 (0x00007fa5a5c59000)
```
### Set up FakeEtherCAT Home
RtIPC needs a place to store its configuration.
Set `FAKE_EC_HOMEDIR` environment variabe to a path to an empty directory,
for instance `/tmp/FakeEtherCAT`.
`FAKE_EC_NAME` can be set to a useful name of your application,
default is `FakeEtherCAT`.
```sh
export FAKE_EC_HOMEDIR=/tmp/FakeEtherCAT
rm -rf $FAKE_EC_HOMEDIR
mkdir -p $FAKE_EC_HOMEDIR
```
### Spin up your application
Now it's time to simply launch your application.
You will notice that the PDO configuration will be dumped at stderr.
The path displayed is the path of the RtIPC variable in the following format:
`$FAKE_EC_PREFIX/$MASTER_ID/$DOMAIN_ID/$ALIAS$POSITION/$PDO`.
## How to emulate EtherCAT slaves
Let's say you'd like to build virtual EtherCAT slaves to emulate your field.
Libfakeethercat makes that possible with the help of RtIPC.
### Building a simulator
Your field emulator simply has to swap the sync manager direction setting
(EC_DIR_INPUT and EC_DIR_OUTPUT) in ec_sync_info_t.
Libfakeethercat instances use shared memory, provided by RtIPC,
to exchange process data.
The direction setting decides which instance writes and which reads from
shared memory.
For instance, emulating a digial output works in the following way:
Create another real time application with the same slave information
as your control application.
Then, replace all EC_DIR_INPUT with EC_DIR_OUTPUT and vice versa.
Now, remember to read process data instead of writing it, and vice versa.
So, your EL2004 digital out will be read by your simulating application
and has PDO object 0x1600 ff. configured with Sync Manager 2 as EC_DIR_INPUT.
[EtherLab](https://gitlab.com/etherlab.org/etherlab) provides
a convenient way to build an entire simulator using SIMULINK since
Version 2.4.0.
Run `web(etherlab_help_path('swap_io.html'), '-helpbrowser')`
in your MATLAB shell to read more about how to set up a
Process Data simulator.
### Start your application
First, do all the steps explained above to run your
control application in dry run mode.
Then, in another shell, do the same thing with your simulator,
but do not remove the `FAKE_EC_HOMEDIR` directory and
pick another `FAKE_EC_NAME`.
Carefully watch the PDO configuration on stderr and compare them.
All paths configured as Output on the control application have to
be configured as Input on your simulator and vice versa.
If you use multiple domains and there is a mismatch of the domain IDs,
set `FAKE_EC_DOMAIN_PERMUTATION` to a space-separated list of integers to
permutate the domain IDs of one application.
`FAKE_EC_DOMAIN_PERMUTATION="0 1"` swaps domains 0 and 1, for instance.
Finally, your control application needs to be restarted
so it can find the RtIPC variables which contains the process data of the
simulator.
## Environment variables
- FAKE_EC_DOMAIN_PERMUTATION: Permutate the domain IDs, useful to match control and simulation applications.
- FAKE_EC_HOMEDIR: Directory for RtIPC builletin board and SDO json files
- FAKE_EC_NAME: Will be used for naming RtIPC config and SDO json file
- FAKE_EC_PREFIX: Prefix for RtIPC variables, useful to run multiple simulators side by side.

693
fake_lib/fakeethercat.cpp Normal file
View File

@ -0,0 +1,693 @@
/*****************************************************************************
*
* Copyright (C) 2024 Bjarne von Horn, Ingenieurgemeinschaft IgH
*
* This file is part of the IgH EtherCAT master userspace library.
*
* The IgH EtherCAT master userspace library is free software; you can
* redistribute it and/or modify it under the terms of the GNU Lesser General
* Public License as published by the Free Software Foundation; version 2.1
* of the License.
*
* The IgH EtherCAT master userspace library is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the IgH EtherCAT master userspace library. If not, see
* <http://www.gnu.org/licenses/>.
*
****************************************************************************/
#include "fakeethercat.h"
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
#include <utility>
#include <unordered_set>
#include <iterator>
#include <ios>
static std::vector<size_t> getPermutationVector(size_t count);
static std::ostream &operator<<(std::ostream &os, const sdo_address &a)
{
os << std::setfill('0') << std::hex << std::setw(6) << a.getCombined();
return os;
}
static std::ostream &operator<<(std::ostream &os, const ec_address &a)
{
os << std::setfill('0') << std::hex << std::setw(8) << a.getCombined();
return os;
}
static void add_spaces(std::ostream &out, int const num)
{
for (int i = 0; i < num; ++i)
{
out << ' ';
}
}
template <typename Map, typename Func>
static void map2Json(std::ostream &out, const Map &map, Func &&print_func, int indent = 0)
{
indent += 4;
out << "{";
bool is_first = true;
for (const auto &kv : map)
{
if (is_first)
{
out << '\n';
is_first = false;
}
else
{
out << ",\n";
}
add_spaces(out, indent);
out << "\"0x" << std::hex << std::setfill('0') << std::setw(2 * sizeof(typename Map::key_type));
out << kv.first << "\": ";
print_func(out, kv.second);
}
out << '\n';
add_spaces(out, indent - 4);
out << "}";
}
size_t pdo::sizeInBytes() const
{
size_t ans = 0;
for (const auto &entry : entries)
{
ans += entry.bit_length;
}
return (ans + 7) / 8;
}
Offset pdo::findEntry(uint16_t idx, uint8_t subindex) const
{
size_t offset_bits = 0;
for (const auto &entry : entries)
{
if (entry.index == idx && entry.subindex == subindex)
{
return Offset(offset_bits / 8, offset_bits % 8);
}
offset_bits += entry.bit_length;
}
return NotFound;
}
ec_domain::ec_domain(rtipc *rtipc, const char *prefix, ec_master_t *master) : rt_group(rtipc_create_group(rtipc, 1.0)), prefix(prefix), master(master)
{
}
int ec_domain::activate(int domain_id)
{
std::unordered_set<uint32_t> slaves;
connected.resize(mapped_pdos.size());
size_t idx = 0;
for (const auto &pdo : mapped_pdos)
{
slaves.insert(pdo.slave_address.getCombined());
void *rt_pdo = nullptr;
char buf[512];
const auto fmt = snprintf(buf, sizeof(buf), "%s/%d/%d/%08X/%04X", prefix, master->getId(), domain_id, pdo.slave_address.getCombined(), pdo.pdo_index);
if (fmt < 0 || fmt >= (int)sizeof(buf))
{
return -ENOBUFS;
}
switch (pdo.dir)
{
case EC_DIR_OUTPUT:
rt_pdo = rtipc_txpdo(rt_group, buf, rtipc_uint8_T, data.data() + pdo.offset, pdo.size_bytes);
std::cerr << "Registering " << buf << " as Output\n";
break;
case EC_DIR_INPUT:
rt_pdo = rtipc_rxpdo(rt_group, buf, rtipc_uint8_T, data.data() + pdo.offset, pdo.size_bytes, connected.data() + idx);
std::cerr << "Registering " << buf << " as Input\n";
break;
default:
std::cerr << "Unknown direction " << pdo.dir << '\n';
return -1;
}
if (!rt_pdo)
{
std::cerr << "Failed to register RtIPC PDO\n";
return -1;
}
++idx;
}
activated_ = true;
numSlaves = slaves.size();
return 0;
}
int ec_domain::process()
{
rtipc_rx(rt_group);
return 0;
}
int ec_domain::queue()
{
rtipc_tx(rt_group);
return 0;
}
ssize_t ec_domain::map(ec_slave_config const &config, unsigned int syncManager,
uint16_t pdo_index)
{
if (activated_)
return -1;
for (const auto &pdo : mapped_pdos)
{
if (pdo.slave_address == config.address && syncManager == pdo.syncManager && pdo_index == pdo.pdo_index)
{
// already mapped;
return pdo.offset;
}
}
const auto ans = data.size();
const auto size = config.sync_managers.at(syncManager).pdos.at(pdo_index).sizeInBytes();
mapped_pdos.emplace_back(ans, size, config.address, syncManager, pdo_index, config.sync_managers.at(syncManager).dir);
data.resize(ans + size);
return ans;
}
uint8_t *ecrt_domain_data(
const ec_domain_t *domain)
{
return domain->getData();
}
int ecrt_domain_process(
ec_domain_t *domain)
{
return domain->process();
}
int ecrt_domain_queue(
ec_domain_t *domain)
{
return domain->queue();
}
int ecrt_domain_state(
const ec_domain_t *domain, /**< Domain. */
ec_domain_state_t *state /**< Pointer to a state object to store the
information. */
)
{
state->working_counter = domain->getNumSlaves();
state->redundancy_active = 0;
state->wc_state = EC_WC_COMPLETE;
return 0;
}
int ec_master::activate()
{
const auto permutate = getPermutationVector(domains.size());
int i = 0;
for (auto &domain : domains)
{
if (domain.activate(permutate[i]))
return -1;
++i;
}
{
std::ofstream out(rt_ipc_dir + "/" + rt_ipc_name + "_slaves.json");
if (!out.is_open())
{
std::cerr << "could not dump json.\n";
return -1;
}
out << "{\n \"slaves\": ";
map2Json(out, slaves, [](std::ostream &out, const ec_slave_config &slave)
{ slave.dumpJson(out, 8); }, 4);
out << "\n}\n";
}
return rtipc_prepare(rt_ipc.get());
}
int ecrt_master_activate(
ec_master_t *master /**< EtherCAT master. */
)
{
try
{
return master->activate();
}
catch (const std::exception &e)
{
std::cerr << "Could not activate: " << e.what() << '\n';
return -1;
}
}
int ecrt_master_application_time(
ec_master_t *master, /**< EtherCAT master. */
uint64_t app_time /**< Application time. */
)
{
return 0;
}
ec_domain_t *ecrt_master_create_domain(
ec_master_t *master /**< EtherCAT master. */
)
{
return master->createDomain();
}
static const char *getPrefix()
{
if (const auto ans = getenv("FAKE_EC_PREFIX"))
return ans;
return "/FakeEtherCAT";
}
ec_domain *ec_master::createDomain()
{
domains.emplace_back(rt_ipc.get(), getPrefix(), this);
return &domains.back();
}
int ecrt_master_link_state(
const ec_master_t *master, /**< EtherCAT master. */
unsigned int dev_idx, /**< Index of the device (0 = main device, 1 =
first backup device, ...). */
ec_master_link_state_t *state /**< Structure to store the information.
*/
)
{
state->slaves_responding = master->getNoSlaves();
state->al_states = 4;
state->link_up = 1;
return 0;
}
int ecrt_master_receive(
ec_master_t *master /**< EtherCAT master. */
)
{
return 0;
}
int ecrt_master_reset(
ec_master_t *master /**< EtherCAT master. */
)
{
return 0;
}
int ecrt_master_scan_progress(
ec_master_t *master, /**< EtherCAT master */
ec_master_scan_progress_t *progress /**< Structure that will output
the progress information. */
)
{
progress->scan_index = progress->slave_count = master->getNoSlaves();
return 0;
}
int ecrt_master_send(
ec_master_t *master /**< EtherCAT master. */
)
{
return 0;
}
ec_slave_config_t *ecrt_master_slave_config(
ec_master_t *master, /**< EtherCAT master */
uint16_t alias, /**< Slave alias. */
uint16_t position, /**< Slave position. */
uint32_t vendor_id, /**< Expected vendor ID. */
uint32_t product_code /**< Expected product code. */
)
{
return master->slave_config(alias, position, vendor_id, product_code);
}
ec_slave_config_t *ec_master::slave_config(
uint16_t alias, /**< Slave alias. */
uint16_t position, /**< Slave position. */
uint32_t vendor_id, /**< Expected vendor ID. */
uint32_t product_code /**< Expected product code. */
)
{
const ec_address address{alias, position};
const auto it = slaves.find(address);
if (it != slaves.end())
{
if (it->second.vendor_id == vendor_id && it->second.product_code == product_code)
return &it->second;
else
{
std::cerr << "Attempted to reconfigure slave (" << alias << "," << position << ")!\n";
return nullptr;
}
}
else
{
return &slaves.insert(std::make_pair<ec_address, ec_slave_config>(ec_address{address}, ec_slave_config{address, vendor_id, product_code})).first->second;
}
}
int ecrt_master_state(
const ec_master_t *master, /**< EtherCAT master. */
ec_master_state_t *state /**< Structure to store the information. */
)
{
state->slaves_responding = master->getNoSlaves();
state->link_up = 1;
state->al_states = 8;
return 0;
}
int ecrt_master_sync_monitor_queue(
ec_master_t *master /**< EtherCAT master. */
)
{
return 0;
}
uint32_t ecrt_master_sync_monitor_process(
const ec_master_t *master /**< EtherCAT master. */
)
{
return 32;
}
int ecrt_master_sync_reference_clock(
ec_master_t *master /**< EtherCAT master. */
)
{
return 0;
}
int ecrt_master_sync_reference_clock_to(
ec_master_t *master, /**< EtherCAT master. */
uint64_t sync_time /**< Sync reference clock to this time. */
)
{
return 0;
}
int ecrt_master_sync_slave_clocks(
ec_master_t *master /**< EtherCAT master. */
)
{
return 0;
}
void ecrt_release_master(ec_master_t *master)
{
delete master;
}
ec_master_t *ecrt_request_master(
unsigned int master_index /**< Index of the master to request. */
)
{
return new ec_master(master_index);
}
static const char *getName()
{
if (const auto ans = getenv("FAKE_EC_NAME"))
{
return ans;
}
return "FakeEtherCAT";
}
static const char *getRtIpcDir()
{
if (const auto ans = getenv("FAKE_EC_HOMEDIR"))
{
return ans;
}
return "/tmp/FakeEtherCAT";
}
static std::vector<size_t> getPermutationVector(size_t count)
{
std::vector<size_t> ans;
for (size_t i = 0; i < count; ++i)
{
ans.push_back(i);
}
const auto spec = getenv("FAKE_EC_DOMAIN_PERMUTATION");
if (!spec)
return ans;
std::istringstream is(spec);
std::istream_iterator<int> begin(is), end;
std::vector<int> values(begin, end);
if (values.size() % 2)
{
throw std::invalid_argument("Specify an even number of indices to permutate.\n");
}
for (size_t i = 0; i < values.size() / 2; ++i)
{
std::swap(ans.at(values[2 * i]), ans.at(values[2 * i + 1]));
}
return ans;
}
ec_master::ec_master(int id) : rt_ipc_dir(getRtIpcDir()), rt_ipc_name(getName()), rt_ipc(rtipc_create(rt_ipc_name.c_str(), rt_ipc_dir.c_str())), id_(id)
{
}
int ecrt_slave_config_complete_sdo(
ec_slave_config_t *sc, /**< Slave configuration. */
uint16_t index, /**< Index of the SDO to configure. */
const uint8_t *data, /**< Pointer to the data. */
size_t size /**< Size of the \a data. */
)
{
return ecrt_slave_config_sdo(sc, index, 0, data, size);
}
ec_sdo_request_t *ecrt_slave_config_create_sdo_request(
ec_slave_config_t *sc, /**< Slave configuration. */
uint16_t index, /**< SDO index. */
uint8_t subindex, /**< SDO subindex. */
size_t size /**< Data size to reserve. */
)
{
return nullptr;
}
int ecrt_slave_config_dc(
ec_slave_config_t *sc, /**< Slave configuration. */
uint16_t assign_activate, /**< AssignActivate word. */
uint32_t sync0_cycle, /**< SYNC0 cycle time [ns]. */
int32_t sync0_shift, /**< SYNC0 shift time [ns]. */
uint32_t sync1_cycle, /**< SYNC1 cycle time [ns]. */
int32_t sync1_shift /**< SYNC1 shift time [ns]. */
)
{
return 0;
}
int ecrt_slave_config_idn(
ec_slave_config_t *sc, /**< Slave configuration. */
uint8_t drive_no, /**< Drive number. */
uint16_t idn, /**< SoE IDN. */
ec_al_state_t state, /**< AL state in which to write the IDN (PREOP or
SAFEOP). */
const uint8_t *data, /**< Pointer to the data. */
size_t size /**< Size of the \a data. */
)
{
return 0;
}
int ecrt_slave_config_pdos(
ec_slave_config_t *sc, /**< Slave configuration. */
unsigned int n_syncs, /**< Number of sync manager configurations in
\a syncs. */
const ec_sync_info_t syncs[] /**< Array of sync manager
configurations. */
)
{
if (!syncs)
return 0;
for (unsigned int sync_idx = 0; sync_idx < n_syncs; ++sync_idx)
{
if (syncs[sync_idx].index == 0xff)
{
return 0;
}
auto &manager = sc->sync_managers[syncs[sync_idx].index];
manager.dir = syncs[sync_idx].dir;
for (unsigned int i = 0; i < syncs[sync_idx].n_pdos; ++i)
{
const auto &in_pdo = syncs[sync_idx].pdos[i];
if (in_pdo.n_entries == 0 || !in_pdo.entries)
{
std::cerr << "Default mapping not supported.";
return -1;
}
auto &out_pdo = manager.pdos[in_pdo.index];
for (unsigned int pdo_entry_idx = 0; pdo_entry_idx < in_pdo.n_entries; ++pdo_entry_idx)
{
out_pdo.entries.push_back(in_pdo.entries[pdo_entry_idx]);
}
}
}
return 0;
}
int ecrt_domain_reg_pdo_entry_list(
ec_domain_t *domain, /**< Domain. */
const ec_pdo_entry_reg_t *pdo_entry_regs /**< Array of PDO
registrations. */
)
{
const ec_pdo_entry_reg_t *reg;
ec_slave_config_t *sc;
int ret;
for (reg = pdo_entry_regs; reg->index; reg++)
{
if (!(sc = ecrt_master_slave_config(domain->getMaster(), reg->alias,
reg->position, reg->vendor_id, reg->product_code)))
return -ENOENT;
if ((ret = ecrt_slave_config_reg_pdo_entry(sc, reg->index,
reg->subindex, domain, reg->bit_position)) < 0)
return ret;
*reg->offset = ret;
}
return 0;
}
int ecrt_slave_config_reg_pdo_entry(
ec_slave_config_t *sc, /**< Slave configuration. */
uint16_t entry_index, /**< Index of the PDO entry to register. */
uint8_t entry_subindex, /**< Subindex of the PDO entry to register. */
ec_domain_t *domain, /**< Domain. */
unsigned int *bit_position /**< Optional address if bit addressing
is desired */
)
{
for (auto sync_it : sc->sync_managers)
{
for (auto pdo_it : sync_it.second.pdos)
{
const auto offset = pdo_it.second.findEntry(entry_index, entry_subindex);
if (offset != NotFound)
{
const auto domain_offset = domain->map(*sc, sync_it.first, pdo_it.first);
if (domain_offset != -1)
{
if (bit_position)
*bit_position = offset.bits;
else if (!offset.bits)
{
std::cerr << "Pdo Entry is not byte aligned but bit offset is ignored!\n";
return -1;
}
return domain_offset + offset.bytes;
}
else
{
return -1;
}
}
}
}
return -1; // offset
}
int ecrt_slave_config_sdo8(
ec_slave_config_t *sc, /**< Slave configuration */
uint16_t sdo_index, /**< Index of the SDO to configure. */
uint8_t sdo_subindex, /**< Subindex of the SDO to configure. */
uint8_t value /**< Value to set. */
)
{
return ecrt_slave_config_sdo(sc, sdo_index, sdo_subindex, &value, 1);
}
int ecrt_slave_config_sdo16(
ec_slave_config_t *sc, /**< Slave configuration */
uint16_t sdo_index, /**< Index of the SDO to configure. */
uint8_t sdo_subindex, /**< Subindex of the SDO to configure. */
uint16_t const value /**< Value to set. */
)
{
uint8_t buf[sizeof(value)];
memcpy(&buf, &value, sizeof(value));
return ecrt_slave_config_sdo(sc, sdo_index, sdo_subindex, buf, sizeof(buf));
}
int ecrt_slave_config_sdo32(
ec_slave_config_t *sc, /**< Slave configuration */
uint16_t sdo_index, /**< Index of the SDO to configure. */
uint8_t sdo_subindex, /**< Subindex of the SDO to configure. */
uint32_t const value /**< Value to set. */
)
{
uint8_t buf[sizeof(value)];
memcpy(&buf, &value, sizeof(value));
return ecrt_slave_config_sdo(sc, sdo_index, sdo_subindex, buf, sizeof(buf));
}
int ecrt_slave_config_sdo(
ec_slave_config_t *sc, /**< Slave configuration. */
uint16_t index, /**< Index of the SDO to configure. */
uint8_t subindex, /**< Subindex of the SDO to configure. */
const uint8_t *data, /**< Pointer to the data. */
size_t size /**< Size of the \a data. */
)
{
sc->sdos[sdo_address{index, subindex}] = std::basic_string<uint8_t>(data, data + size);
return 0;
}
void ecrt_write_lreal(void *data, double const value)
{
memcpy(data, &value, sizeof(value));
}
void ecrt_write_real(void *data, float const value)
{
memcpy(data, &value, sizeof(value));
}
float ecrt_read_real(const void *data)
{
float ans;
memcpy(&ans, data, sizeof(ans));
return ans;
}
double ecrt_read_lreal(const void *data)
{
double ans;
memcpy(&ans, data, sizeof(ans));
return ans;
}
void ec_slave_config::dumpJson(std::ostream &out, int indent) const
{
out << "{\n";
indent += 4;
add_spaces(out, indent);
out << "\"vendor_id\": " << std::dec << vendor_id << ",\n";
add_spaces(out, indent);
out << "\"product_id\": " << product_code << ",\n";
add_spaces(out, indent);
out << "\"sdos\": ";
map2Json(out, sdos, [](std::ostream &out, const std::basic_string<uint8_t> &value)
{
out << "\"0x";
for (const auto s : value) {
out << std::hex << std::setfill('0') << std::setw(2) << (unsigned)s;
}
out << '"'; }, indent);
out << '\n';
add_spaces(out, indent - 4);
out << "}";
}

228
fake_lib/fakeethercat.h Normal file
View File

@ -0,0 +1,228 @@
/*****************************************************************************
*
* Copyright (C) 2024 Bjarne von Horn, Ingenieurgemeinschaft IgH
*
* This file is part of the IgH EtherCAT master userspace library.
*
* The IgH EtherCAT master userspace library is free software; you can
* redistribute it and/or modify it under the terms of the GNU Lesser General
* Public License as published by the Free Software Foundation; version 2.1
* of the License.
*
* The IgH EtherCAT master userspace library is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the IgH EtherCAT master userspace library. If not, see
* <http://www.gnu.org/licenses/>.
*
****************************************************************************/
#pragma once
#include <ecrt.h>
#include <rtipc.h>
#include <iostream>
#include <iomanip>
#include <list>
#include <map>
#include <memory>
#include <string>
#include <vector>
struct Offset
{
int bytes;
int bits;
constexpr Offset(int bytes,
int bits) : bytes(bytes), bits(bits) {}
constexpr bool operator!=(const Offset &other) const noexcept
{
return bytes != other.bytes || bits != other.bits;
}
};
constexpr Offset NotFound(-1, -1);
struct pdo
{
std::vector<ec_pdo_entry_info_t> entries;
size_t sizeInBytes() const;
Offset findEntry(uint16_t idx, uint8_t subindex) const;
};
struct syncManager
{
ec_direction_t dir;
std::map<uint16_t /* address */, pdo> pdos;
};
class ec_address
{
uint32_t value;
public:
ec_address(uint16_t alias, /**< Slave alias. */
uint16_t position /**< Slave position. */)
: value(static_cast<uint32_t>(alias) << 16 | position)
{
}
uint16_t getAlias() const { return value >> 16; }
uint16_t getPosition() const { return value & 0xFFFF; }
uint32_t getCombined() const { return value; }
bool operator<(const ec_address &other) const noexcept
{
return value < other.value;
}
bool operator==(const ec_address &other) const noexcept
{
return value == other.value;
}
};
class sdo_address
{
uint32_t value;
public:
sdo_address(uint16_t index, /**< Slave alias. */
uint8_t subindex /**< Slave position. */)
: value(static_cast<uint32_t>(index) << 8 | subindex)
{
}
uint16_t getIndex() const { return value >> 8; }
uint8_t getSubIndex() const { return value & 0xFF; }
uint32_t getCombined() const { return value; }
bool operator<(const sdo_address &other) const noexcept
{
return value < other.value;
}
bool operator==(const sdo_address &other) const noexcept
{
return value == other.value;
}
};
struct ec_slave_config
{
ec_address address;
uint32_t vendor_id; /**< Expected vendor ID. */
uint32_t product_code; /**< Expected product code. */
std::map<unsigned int, syncManager> sync_managers;
std::map<sdo_address, std::basic_string<uint8_t>> sdos;
ec_slave_config(
ec_address address,
uint32_t vendor_id, /**< Expected vendor ID. */
uint32_t product_code /**< Expected product code. */)
: address(address), vendor_id(vendor_id), product_code(product_code)
{
}
void dumpJson(std::ostream &out, int indent) const;
};
struct ec_domain
{
private:
struct PdoMap
{
size_t offset;
size_t size_bytes;
ec_address slave_address;
unsigned int syncManager;
uint16_t pdo_index;
ec_direction_t dir;
PdoMap(
size_t offset,
size_t size_bytes,
ec_address slave_address,
unsigned int syncManager,
uint16_t pdo_index,
ec_direction_t dir)
: offset(offset), size_bytes(size_bytes), slave_address(slave_address), syncManager(syncManager), pdo_index(pdo_index), dir(dir)
{
}
};
std::vector<uint8_t> data;
std::vector<unsigned char> connected;
std::vector<PdoMap> mapped_pdos;
rtipc_group *rt_group;
const char *prefix;
ec_master_t *master;
bool activated_ = false;
size_t numSlaves = 0;
public:
explicit ec_domain(struct rtipc *rtipc, const char *prefix, ec_master_t *master);
uint8_t *getData() const
{
if (!activated_)
return nullptr;
return const_cast<uint8_t *>(data.data());
}
int activate(int domain_id);
int process();
int queue();
size_t getNumSlaves() const { return numSlaves; }
ssize_t map(ec_slave_config const &config, unsigned int syncManager,
uint16_t pdo_index);
ec_master_t *getMaster() const { return master; }
};
struct ec_master
{
private:
struct RtIpcDeleter
{
void operator()(struct rtipc *r) const
{
rtipc_exit(r);
}
};
std::string rt_ipc_dir;
std::string rt_ipc_name;
std::list<ec_domain> domains;
std::map<ec_address, ec_slave_config> slaves;
std::unique_ptr<struct rtipc, RtIpcDeleter> rt_ipc;
int id_;
public:
explicit ec_master(int id);
int activate();
ec_domain *createDomain();
int getNoSlaves() const { return slaves.size(); }
int getId() const { return id_; }
ec_slave_config_t *slave_config(
uint16_t alias, /**< Slave alias. */
uint16_t position, /**< Slave position. */
uint32_t vendor_id, /**< Expected vendor ID. */
uint32_t product_code /**< Expected product code. */
);
};

View File

@ -82,6 +82,9 @@ LIBETHERCAT_1.5.2 {
ecrt_voe_handler_received_header;
ecrt_voe_handler_send_header;
ecrt_voe_handler_write;
local:
# Hide all C++ symbols in libfakeethercat
_Z*;
};
LIBETHERCAT_1.5.3 {

View File

@ -40,6 +40,9 @@
A list of all native network card drivers can be found
<a href="devicedrivers.html">here</a>.
A <a href="libfakeethercat.html">second userspace library</a> can be used for a dry-run mode
or simulating Process Data.
For information how to build and install, see the INSTALL file in the source
root.