7 minute read

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() and OnWrite() have been added to BasicSlave, 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 throws lely::ev::future_not_ready when the future is not ready.
  • The SdoFuture<T> (and corresponding SdoPromise<T>) convenience definition has been added to <lely/coapp/sdo.hpp> to replace the occurrences of ev::Future<T, ::std::exception_ptr>.
  • A generic Wait() function has been added to the LoopDriver and FiberDriver 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 corresponding uint*_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() and OnTpdo() callbacks have been removed from the driver classes.
  • SdoError now derives from std::system_error. And it no longer contains the network-ID.
  • Async...() functions return a future (SdoFuture<T>) containing an exception (typically SdoError), 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.

Categories:

Updated: