New release: v2.1.0
v2.1.0 has just been released. This is a major release containing several new features and a few minor API changes (see below).
Stackful coroutines
This release adds support for stackful coroutines, called fibers to prevent confusion with the existing stackless coroutines.
The fiber implementation on Windows is based on
CreateFiber()
/SwitchToFiber()
and allows fibers to be migrated between
threads. All other platforms use mkjmp()
/setjmp()
/longjmp()
.
Since using longjmp()
to restore an environment saved by setjmp()
in a
different thread is undefined behavior (according to paragrapg 7.13.2.1 in
ISO/IEC 9899:2011), fibers can only be resumed by the thread on which they were
created.
The API allows the user to specify per fiber whether the signal mask,
floating-point environment and the last value of errno
/GetLastError()
are
preserved. Additionally, on POSIX platforms, it is possible to add guard pages
to the fiber stack to detect stack overflow.
The event library provides a fiber executor, which ensures that each task submitted to it runs in a fiber. To limit fiber creation overhead, the fibers are reused once they finish executing a task. The implementation maintains a list of unused fibers, up to a user-defined limit.
If a task runs on a fiber executor, it can invoke ev_fiber_await()
or
lely::ev::fiber_await()
and lely::ev_fiber_yield()
to yield the current task
or suspend it until a future becomes ready (or is canceled).
The C++ CANopen application library uses the fiber executor to implement the
FiberDriver
; a driver similar to LoopDriver
, but using coroutines instead of
a thread to allow blocking functions in tasks. To ease switching between these
drivers, they both provide a generic Wait()
function to wait for a future to
become ready.
The current Run...()
functions in LoopDriver
are deprecated and have no
equivalent in FiberDriver
. But since they are equivalent to
Wait(Async...())
, the migration is trivial.
CANopen event callbacks in C++
Custom callbacks
Up until now, responding to a CANopen event required the user to override one of
the On...()
methods in a master, driver or slave class. Especially in the case
of a master, it is often more convenient to be able to register a callback
function instead of having to create a derived class. This is now possible for
every CANopen event. For every protected virtual On...()
method there is a
public On...(::std::function<void(...)>)
method that allows the user to
register a single callback function. This function is invoked after the
virtual method.
PDO event callbacks
Since PDO events are not always traceable to individual nodes (although see
below), every driver needs to be notified of every
event. This is quite inefficient, especially since many drivers do not even
overload OnRpdo()
, OnRpdoError()
or OnTpdo()
. These methods have therefore
been removed from the driver classes in favor of custom callbacks registered
with the master class, or the new OnRpdoWrite()
event (see below).
SDO write indications
The OnWrite()
event has been added to the Device
class. It is invoked after
every successful SDO write (download) access to objects 2000..BFFF in the local
object dictionary.
If remote PDO mapping is enabled (see below) and the
object is mapped to an RPDO, the OnRpdoWrite()
event callback will be invoked
with the remote object index and sub-index (and node-ID). This callback is
also available in the driver classes.
PDO mapping
The PDO mapping logic has been made more robust and now allows and ignores empty
mapping entries. Strictly speaking, empty entries are not allowed by the
standard, but they tend to occur when CompactSubObj
is used to define them in
EDS/DCF files.
Remote PDO mapping in C++
The PDO mapping in the object dictionary records how local objects are mapped to RPDOs and TPDOs. When writing a driver, however, it would be more convenient to refer to the remote object (and node-ID) mapped to a PDO. But this information is normally not available.
This release adds support for remote PDO mapping in the local object dictionary. Objects 5800..59FF and 5A00..5BFF record the remote node-ID and TPDO or RPDO number (this can be omitted for PDOs using one of the pre-defined COB-IDs). Objects 5C00..5DFF and 5E00..5FFF contain copies of, respectively, the TPDO and RPDO mapping on the remote node. For now, this information has to be added by hand. We’re working on generating it automatically with a tool.
If the remote PDO mapping is available in the object dictionary, applications
can use the rpdo_mapped
and tpdo_mapped
members in the master, driver and
slave classes to refer to objects in the local object dictionary by their remote
object index and sub-index.
For example, the following statement in a method in a driver retrieves a value from the local object dictionary that is mapped to an RPDO and originates from object 6000:01 on the remote node:
uint8_t val = rpdo_mapped[0x6000][1];
Similarly,
tpdo_mapped[0x6000][1] = val;
writes a value to an object in the local object dictionary that will be sent to object 6000:01 on the remote node by a TPDO.
TPDO event tracking
This release adds TPDO event tracking to the local object dictionary. With the
co_dev_tpdo_event()
or Device::WriteEvent()
/Device::TpdoWriteEvent()
functions, the application can indicate that an event occurred for an object in
the local object dictionary. The stack will then check if the object is mapped
to an acyclic or event-driven TPDO and, if so, trigger that TPDO.
Sometimes it is useful to batch events before triggering a TPDO. This can be
achieved by locking/unlocking TPDO event handling with
co_nmt_on_tpdo_event_lock()
/co_nmt_on_tpdo_event_unlock()
or the
tpdo_event_mutex
member in the master and driver classes. The TPDO event
“mutex” is recursive and can be “locked” multiple times. Only when the last lock
is released are the TPDOs triggered.
NMT boot slave process
Heartbeat errors are now ignored during the NMT boot slave process, as they
should be. Unexpected boot-up messages are also ignored by the boot process, but
the user is notified through the co_nmt_st_ind_t
or OnState()
event
callback. Additionally, while the master waits for the boot-up message from
optional slaves before beginning the boot process, mandatory slaves are now
booted immediately. Otherwise the user will never be notified about a missing
mandatory slave.
Automatic node configuration
The boot process now supports automatic configuration of the slaves via
object 1F20 (Store DCF) and 1F22 (Concise DCF). During the configuration step of
the boot process, if the corresponding sub-index in object 1F20 contains a DCF
text file, the master will update all writable objects on the slave for which
ParameterValue
does not equal DefaultValue
.
This does not work for PDO configuration, since a PDO has to be disabled, updated and re-enabled, in that order, and the master performs a single SDO write per sub-object.
Additionally, or alternatively, if a binary (concise) DCF is present in the corresponding sub-index in object 1F22, the master will execute all SDO writes specified in that file.
Any user-specific configuration implemented in the co_nmt_cfg_ind_t
or
OnConfig()
event callbacks is executed after the automatic configuration.
Virtual CAN bus
The I/O library now contains a software-defined virtual CAN bus. This can help to create platform-independent tests of CAN(open) applications.
To use the virtual CAN bus, include <lely/io2/vcan.hpp>
and create a single
instance of VirtualCanController
and one instance of VirtualCanChannel
for
each virtual device. CAN frames sent by one channel are received by all others,
but not by the sending channel, just like a real CAN bus. VirtualCanController
also allows state changes and the insertion of error frames. This may help to
write tests for error handling code.
Static code analysis
Two static code analysis tools have been added: LGTM and SonarCloud. To allow these tools to access the repository, we have created a GitHub mirror of the master branch at https://github.com/lely-industries/lely-core. This mirror is manually synchronized with the GitLab repository.
You can find the results of the analyses at https://lgtm.com/projects/gl/lely_industries/lely-core?mode=list and https://sonarcloud.io/dashboard?id=lely-industries_lely-core, respectively.
Other
- The record/array object callbacks
OnRead()
andOnWrite()
have been added toBasicSlave
, in addition to the existing callbacks for sub-objects. - A lock-free single-producer, single-consumer ring buffer is available in
<lely/util/spscring.h>
and is now used for the receive buffer in CAN channels. Future<T, E>::get()
now throwslely::ev::future_not_ready
when the future is not ready.- The
SdoFuture<T>
(and correspondingSdoPromise<T>
) convenience definition has been added to<lely/coapp/sdo.hpp>
to replace the occurrences ofev::Future<T, ::std::exception_ptr>
. - A generic
Wait()
function has been added to theLoopDriver
andFiberDriver
classes. This allows the user to write pseudo-blocking code by waiting on any function returning a future. - All instances of
uint8_t
and other exact-width integers have been replaced by the correspondinguint*_least_t
. The only exception is the C++ CANopen application library, since it is not designed to run on embedded platforms.
API changes
- The (deprecated)
__likely()
and__unlikely()
macros have been removed from<lely/features.h>
. - The
OnRpdo()
,OnRpdoError()
andOnTpdo()
callbacks have been removed from the driver classes. SdoError
now derives fromstd::system_error
. And it no longer contains the network-ID.Async...()
functions return a future (SdoFuture<T>
) containing an exception (typicallySdoError
), instead of an error code, on error.- The
LoopDriver
class declaration has been moved from<lely/coapp/driver.hpp>
to<lely/coapp/loop_driver.hpp>
.
Download
You can download the source from GitLab or the Ubuntu packages from our PPA.